Files
MeowSpool/mobile/app/(app)/inventory/[id].tsx
T
Felipe 9c0b3d584c feat: enhance filament management and UI improvements
- 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.
2026-03-14 14:14:06 -03:00

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,
},
});