- Update SpoolPresetRepository to include user_id in the update query. - Expand .gitignore to include various environment and temporary files. - Revise agent documentation for better clarity and formatting. - Implement pull-to-refresh functionality in the inventory list. - Integrate API calls for deleting and updating filaments, ensuring state synchronization. - Add custom color picker for filament color selection with hex validation. - Update AndroidManifest and Gradle files for improved configuration and permissions. - Refactor MainActivity and MainApplication for better splash screen handling. - Update styles and colors for a cohesive UI experience. - Replace splash screen logos and icons with new assets.
361 lines
14 KiB
TypeScript
361 lines
14 KiB
TypeScript
import React, { useState } from 'react';
|
|
import {
|
|
View, Text, ScrollView, TouchableOpacity, StyleSheet, Alert, Share,
|
|
} from 'react-native';
|
|
import { useLocalSearchParams, useRouter } from 'expo-router';
|
|
import { Ionicons } from '@expo/vector-icons';
|
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
import { useFilamentStore } from '@store/filamentStore';
|
|
import { usePresetStore } from '@store/presetStore';
|
|
import { useAuthStore } from '@store/authStore';
|
|
import { deleteFilamentUseCase } from '@infrastructure/container';
|
|
import { calcFilamentPercentage } from '@domain/Filament';
|
|
import { colors, typography, spacing, radius, getStockColor, getStockBgColor } from '@shared/theme';
|
|
import { formatWeight } from '@shared/utils/filament';
|
|
import { Card } from '@presentation/components/ui/Card';
|
|
import { Button } from '@presentation/components/ui/Button';
|
|
import { StockBar } from '@presentation/components/filament/StockBar';
|
|
|
|
/**
|
|
* Tela de Detalhe do Filamento — 6L-0
|
|
*/
|
|
export default function FilamentDetailScreen(): React.ReactElement {
|
|
const { id } = useLocalSearchParams<{ id: string }>();
|
|
const router = useRouter();
|
|
const { filaments, removeFilament } = useFilamentStore();
|
|
const { presets } = usePresetStore();
|
|
|
|
const filament = filaments.find((f) => f.id === id);
|
|
const preset = filament ? presets.find((p) => p.id === filament.spoolPresetId) : undefined;
|
|
|
|
if (!filament) {
|
|
return (
|
|
<SafeAreaView style={styles.safe}>
|
|
<View style={styles.notFound}>
|
|
<Text style={styles.notFoundText}>Filamento não encontrado</Text>
|
|
<TouchableOpacity onPress={() => router.back()}>
|
|
<Text style={styles.backLink}>Voltar</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
const pct = calcFilamentPercentage(filament);
|
|
const stockColor = getStockColor(pct);
|
|
const slug = `${filament.brand.toLowerCase()}-${(filament.model ?? '').toLowerCase()}-${filament.colorHex.replace('#', '').toLowerCase()}`.replace(/\s+/g, '-');
|
|
|
|
function handleDelete(): void {
|
|
Alert.alert(
|
|
'Excluir filamento',
|
|
`Deseja excluir "${filament!.brand}${filament!.model ? ` ${filament!.model}` : ''}"? Esta ação não pode ser desfeita.`,
|
|
[
|
|
{ text: 'Cancelar', style: 'cancel' },
|
|
{
|
|
text: 'Excluir',
|
|
style: 'destructive',
|
|
onPress: async () => {
|
|
try {
|
|
const { user } = useAuthStore.getState();
|
|
await deleteFilamentUseCase.execute(filament!.id, user?.id || '');
|
|
removeFilament(filament!.id);
|
|
router.back();
|
|
} catch (err) {
|
|
console.error('delete filament error', err);
|
|
Alert.alert('Erro', 'Não foi possível deletar o filamento.');
|
|
}
|
|
},
|
|
},
|
|
],
|
|
);
|
|
}
|
|
|
|
return (
|
|
<SafeAreaView style={styles.safe}>
|
|
<ScrollView showsVerticalScrollIndicator={false}>
|
|
{/* Header */}
|
|
<View style={styles.header}>
|
|
<TouchableOpacity onPress={() => router.back()}>
|
|
<Ionicons name="chevron-back" size={24} color={colors.textPrimary} />
|
|
</TouchableOpacity>
|
|
<Text style={styles.headerTitle}>
|
|
{filament.brand}{filament.model ? ` ${filament.model}` : ''}
|
|
</Text>
|
|
<View style={styles.headerActions}>
|
|
<TouchableOpacity
|
|
onPress={() => router.push(`/(app)/inventory/${id}/edit` as never)}
|
|
style={styles.headerBtn}
|
|
>
|
|
<Ionicons name="pencil-outline" size={20} color={colors.textPrimary} />
|
|
</TouchableOpacity>
|
|
<TouchableOpacity onPress={handleDelete} style={styles.headerBtn}>
|
|
<Ionicons name="trash-outline" size={20} color={colors.textPrimary} />
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
|
|
<View style={styles.content}>
|
|
{/* Identity Card */}
|
|
<Card style={styles.identityCard}>
|
|
<View style={[styles.colorSwatch, { backgroundColor: filament.colorHex }]} />
|
|
<View style={styles.identityInfo}>
|
|
<Text style={styles.filamentName}>
|
|
{filament.brand}{filament.model ? ` ${filament.model}` : ''}
|
|
</Text>
|
|
<Text style={styles.filamentBrandLine}>
|
|
{filament.brand} · {filament.colorHex.toLowerCase().startsWith('#') ? '' : '#'}{filament.colorHex}
|
|
</Text>
|
|
<View style={styles.badgeRow}>
|
|
<View style={styles.materialBadge}>
|
|
<Text style={styles.materialBadgeText}>{filament.material}</Text>
|
|
</View>
|
|
<View style={styles.hexBadge}>
|
|
<Text style={styles.hexBadgeText}>{filament.colorHex}</Text>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
</Card>
|
|
|
|
{/* Filamento Disponível */}
|
|
<Card>
|
|
<View style={styles.stockHeader}>
|
|
<Text style={styles.sectionLabel}>FILAMENTO DISPONÍVEL</Text>
|
|
<View style={[styles.pctBadge, { backgroundColor: getStockBgColor(pct) }]}>
|
|
<Text style={[styles.pctText, { color: stockColor }]}>{pct}%</Text>
|
|
</View>
|
|
</View>
|
|
<View style={styles.weightRow}>
|
|
<Text style={[styles.netWeight, { color: stockColor }]}>
|
|
{formatWeight(filament.netWeightG)}
|
|
</Text>
|
|
<Text style={styles.totalWeight}>de {formatWeight(filament.totalWeightG)}</Text>
|
|
</View>
|
|
<StockBar percentage={pct} />
|
|
<View style={styles.stockMeta}>
|
|
<Text style={styles.stockMetaText}>
|
|
Carretel: {preset?.name ?? '—'} · {preset?.spoolWeightG ?? 0}g
|
|
</Text>
|
|
<Text style={styles.stockMetaText}>
|
|
Total pesado: {formatWeight(filament.totalWeightG)}
|
|
</Text>
|
|
</View>
|
|
</Card>
|
|
|
|
{/* Parâmetros de Impressão */}
|
|
<Card>
|
|
<Text style={styles.sectionLabel}>PARÂMETROS DE IMPRESSÃO</Text>
|
|
<View style={styles.paramsGrid}>
|
|
<View style={styles.paramItem}>
|
|
<Ionicons name="thermometer-outline" size={20} color={colors.stockLow} />
|
|
<Text style={styles.paramValue}>{filament.tempHotendC ?? '—'}°C</Text>
|
|
<Text style={styles.paramLabel}>Hotend</Text>
|
|
</View>
|
|
<View style={styles.paramItem}>
|
|
<Ionicons name="flag-outline" size={20} color={colors.stockMedium} />
|
|
<Text style={styles.paramValue}>{filament.tempBedC ?? '—'}°C</Text>
|
|
<Text style={styles.paramLabel}>Mesa</Text>
|
|
</View>
|
|
<View style={styles.paramItem}>
|
|
<Ionicons name="time-outline" size={20} color={colors.accent} />
|
|
<Text style={styles.paramValue}>{filament.flowFactorPct ?? '—'}%</Text>
|
|
<Text style={styles.paramLabel}>Fluxo</Text>
|
|
</View>
|
|
</View>
|
|
</Card>
|
|
|
|
{/* Identificação */}
|
|
<Card>
|
|
<View style={styles.idSection}>
|
|
{/* QR Code placeholder */}
|
|
<View style={styles.qrPreview}>
|
|
<Ionicons name="qr-code-outline" size={48} color={colors.textSecondary} />
|
|
</View>
|
|
<View style={styles.idInfo}>
|
|
<Text style={styles.idTitle}>Identificação</Text>
|
|
<Text style={styles.idSlug}>{slug}</Text>
|
|
<View style={styles.idButtons}>
|
|
<TouchableOpacity
|
|
style={styles.idBtn}
|
|
onPress={() => router.push(`/(app)/filaments/${id}/label` as never)}
|
|
>
|
|
<Ionicons name="add-outline" size={14} color={colors.accent} />
|
|
<Text style={styles.idBtnText}>Exportar SVG</Text>
|
|
</TouchableOpacity>
|
|
<TouchableOpacity
|
|
style={[styles.idBtn, styles.idBtnSecondary]}
|
|
onPress={() => router.push(`/(app)/filaments/${id}/qrcode` as never)}
|
|
>
|
|
<Ionicons name="qr-code-outline" size={14} color={colors.textSecondary} />
|
|
<Text style={styles.idBtnTextSecondary}>Ver QR</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
</Card>
|
|
|
|
{/* Observações */}
|
|
{filament.notes ? (
|
|
<Card>
|
|
<Text style={styles.sectionLabel}>OBSERVAÇÕES</Text>
|
|
<Text style={styles.notesText}>{filament.notes}</Text>
|
|
</Card>
|
|
) : null}
|
|
</View>
|
|
</ScrollView>
|
|
|
|
{/* CTA fixo */}
|
|
<View style={styles.footer}>
|
|
<Button
|
|
label="Pesar Novamente"
|
|
leftIcon={<Ionicons name="scale-outline" size={18} color={colors.bgBase} />}
|
|
onPress={() => router.push(`/(app)/inventory/${id}/edit` as never)}
|
|
/>
|
|
</View>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
safe: { flex: 1, backgroundColor: colors.bgBase },
|
|
notFound: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: spacing[4] },
|
|
notFoundText: { fontFamily: typography.fontFamily.ui, fontSize: typography.fontSize.base, color: colors.textSecondary },
|
|
backLink: { fontFamily: typography.fontFamily.ui, fontSize: typography.fontSize.base, color: colors.accent },
|
|
|
|
header: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
paddingHorizontal: spacing[5],
|
|
paddingVertical: spacing[4],
|
|
},
|
|
headerTitle: {
|
|
fontFamily: typography.fontFamily.ui,
|
|
fontSize: typography.fontSize.md,
|
|
fontWeight: typography.fontWeight.semibold,
|
|
color: colors.textPrimary,
|
|
flex: 1,
|
|
textAlign: 'center',
|
|
},
|
|
headerActions: { flexDirection: 'row', gap: spacing[2] },
|
|
headerBtn: { padding: spacing[1] },
|
|
|
|
content: { paddingHorizontal: spacing[5], gap: spacing[4], paddingBottom: spacing[10] },
|
|
|
|
identityCard: { flexDirection: 'row', alignItems: 'center', gap: spacing[4] },
|
|
colorSwatch: { width: 72, height: 72, borderRadius: radius.lg },
|
|
identityInfo: { flex: 1, gap: spacing[1] },
|
|
filamentName: {
|
|
fontFamily: typography.fontFamily.ui,
|
|
fontSize: typography.fontSize.lg,
|
|
fontWeight: typography.fontWeight.bold,
|
|
color: colors.textPrimary,
|
|
},
|
|
filamentBrandLine: {
|
|
fontFamily: typography.fontFamily.ui,
|
|
fontSize: typography.fontSize.sm,
|
|
color: colors.textSecondary,
|
|
},
|
|
badgeRow: { flexDirection: 'row', gap: spacing[2], marginTop: spacing[1] },
|
|
materialBadge: {
|
|
paddingHorizontal: spacing[3],
|
|
paddingVertical: 3,
|
|
backgroundColor: colors.bgHover,
|
|
borderRadius: radius.full,
|
|
},
|
|
materialBadgeText: {
|
|
fontFamily: typography.fontFamily.ui,
|
|
fontSize: typography.fontSize.xs,
|
|
fontWeight: typography.fontWeight.medium,
|
|
color: colors.textSecondary,
|
|
},
|
|
hexBadge: {
|
|
paddingHorizontal: spacing[3],
|
|
paddingVertical: 3,
|
|
backgroundColor: colors.bgHover,
|
|
borderRadius: radius.full,
|
|
},
|
|
hexBadgeText: {
|
|
fontFamily: typography.fontFamily.mono,
|
|
fontSize: typography.fontSize.xs,
|
|
color: colors.textSecondary,
|
|
},
|
|
|
|
sectionLabel: {
|
|
fontFamily: typography.fontFamily.ui,
|
|
fontSize: typography.fontSize.xs,
|
|
fontWeight: typography.fontWeight.bold,
|
|
color: colors.textSecondary,
|
|
letterSpacing: 0.8,
|
|
textTransform: 'uppercase',
|
|
marginBottom: spacing[3],
|
|
},
|
|
|
|
stockHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: spacing[2] },
|
|
pctBadge: { paddingHorizontal: spacing[3], paddingVertical: 4, borderRadius: radius.sm },
|
|
pctText: { fontFamily: typography.fontFamily.ui, fontSize: typography.fontSize.sm, fontWeight: typography.fontWeight.bold },
|
|
weightRow: { flexDirection: 'row', alignItems: 'baseline', gap: spacing[2], marginBottom: spacing[3] },
|
|
netWeight: { fontFamily: typography.fontFamily.ui, fontSize: typography.fontSize['3xl'], fontWeight: typography.fontWeight.bold },
|
|
totalWeight: { fontFamily: typography.fontFamily.ui, fontSize: typography.fontSize.sm, color: colors.textSecondary },
|
|
stockMeta: { flexDirection: 'row', justifyContent: 'space-between', marginTop: spacing[3] },
|
|
stockMetaText: { fontFamily: typography.fontFamily.ui, fontSize: typography.fontSize.xs, color: colors.textSecondary },
|
|
|
|
paramsGrid: { flexDirection: 'row', justifyContent: 'space-around' },
|
|
paramItem: { alignItems: 'center', gap: spacing[1] },
|
|
paramValue: {
|
|
fontFamily: typography.fontFamily.ui,
|
|
fontSize: typography.fontSize.xl,
|
|
fontWeight: typography.fontWeight.bold,
|
|
color: colors.textPrimary,
|
|
},
|
|
paramLabel: { fontFamily: typography.fontFamily.ui, fontSize: typography.fontSize.xs, color: colors.textSecondary },
|
|
|
|
idSection: { flexDirection: 'row', gap: spacing[4], alignItems: 'flex-start' },
|
|
qrPreview: {
|
|
width: 80,
|
|
height: 80,
|
|
backgroundColor: colors.bgHover,
|
|
borderRadius: radius.md,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
idInfo: { flex: 1, gap: spacing[2] },
|
|
idTitle: {
|
|
fontFamily: typography.fontFamily.ui,
|
|
fontSize: typography.fontSize.base,
|
|
fontWeight: typography.fontWeight.semibold,
|
|
color: colors.textPrimary,
|
|
},
|
|
idSlug: {
|
|
fontFamily: typography.fontFamily.mono,
|
|
fontSize: typography.fontSize.xs,
|
|
color: colors.textSecondary,
|
|
},
|
|
idButtons: { flexDirection: 'row', gap: spacing[2], flexWrap: 'wrap' },
|
|
idBtn: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: 4,
|
|
backgroundColor: colors.accentMuted,
|
|
paddingHorizontal: spacing[3],
|
|
paddingVertical: 6,
|
|
borderRadius: radius.sm,
|
|
},
|
|
idBtnText: { fontFamily: typography.fontFamily.ui, fontSize: typography.fontSize.xs, color: colors.accent },
|
|
idBtnSecondary: { backgroundColor: colors.bgHover },
|
|
idBtnTextSecondary: { fontFamily: typography.fontFamily.ui, fontSize: typography.fontSize.xs, color: colors.textSecondary },
|
|
|
|
notesText: {
|
|
fontFamily: typography.fontFamily.ui,
|
|
fontSize: typography.fontSize.base,
|
|
color: colors.textPrimary,
|
|
lineHeight: typography.fontSize.base * typography.lineHeight.relaxed,
|
|
},
|
|
|
|
footer: {
|
|
padding: spacing[5],
|
|
borderTopWidth: 1,
|
|
borderTopColor: colors.border,
|
|
backgroundColor: colors.bgBase,
|
|
},
|
|
});
|