import React, { useState, useEffect } from 'react'; import { View, Text, ScrollView, TouchableOpacity, TextInput, StyleSheet, Alert, Modal, KeyboardAvoidingView, Platform, } from 'react-native'; import { useRouter } from 'expo-router'; import { useForm, Controller } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { Ionicons } from '@expo/vector-icons'; import { SafeAreaView } from 'react-native-safe-area-context'; import { usePresetStore } from '@store/presetStore'; import { useFilamentStore } from '@store/filamentStore'; import { useAuthStore } from '@store/authStore'; import { Input } from '@presentation/components/ui/Input'; import { Button } from '@presentation/components/ui/Button'; import { Card } from '@presentation/components/ui/Card'; import { calcNetWeight } from '@domain/Filament'; import { colors, typography, spacing, radius } from '@shared/theme'; import { MATERIALS, type Material } from '@shared/constants'; import { formatWeight } from '@shared/utils/filament'; import { createFilamentUseCase } from '@infrastructure/container'; import { isValidHex, normalizeHex } from '@shared/utils/filament'; const schema = z.object({ brand: z.string().min(1, 'Marca obrigatória'), model: z.string().optional(), totalWeightG: z.coerce.number().min(1, 'Informe o peso total'), tempHotendC: z.coerce.number().optional(), tempBedC: z.coerce.number().optional(), flowFactorPct: z.coerce.number().optional(), notes: z.string().optional(), }); type FormData = z.infer; const PRESET_COLORS = ['#E05533', '#2563EB', '#FFFFFF', '#1E1B18', '#22C55E', '#EAB308', '#EC4899', '#A855F7', '#F97316']; /** * Tela de Cadastro de Filamento — 2X-0 */ export default function NewFilamentScreen(): React.ReactElement { const router = useRouter(); const { presets, systemPresets } = usePresetStore(); const { addFilament } = useFilamentStore(); const { user } = useAuthStore(); const [selectedColor, setSelectedColor] = useState('#E05533'); const [hexInput, setHexInput] = useState('#E05533'); const [selectedMaterial, setSelectedMaterial] = useState('PLA'); const [selectedPresetId, setSelectedPresetId] = useState(systemPresets[0]?.id ?? null); const [isLoading, setIsLoading] = useState(false); const [isColorPickerOpen, setIsColorPickerOpen] = useState(false); const [colorInputValue, setColorInputValue] = useState(''); // Quando os presets carregam (via layout), seleciona o primeiro sistema automaticamente useEffect(() => { if (selectedPresetId === null && presets.length > 0) { setSelectedPresetId(presets.find((p) => p.isSystem)?.id ?? presets[0].id); } }, [presets, selectedPresetId]); const { control, handleSubmit, watch, formState: { errors } } = useForm({ resolver: zodResolver(schema), defaultValues: { brand: '', model: '', totalWeightG: 0, tempHotendC: 210, tempBedC: 60, flowFactorPct: 100 }, }); const totalWeight = watch('totalWeightG'); const selectedPreset = presets.find((p) => p.id === selectedPresetId); const netWeight = selectedPreset && totalWeight ? calcNetWeight(Number(totalWeight), selectedPreset.spoolWeightG) : 0; const pct = Math.min(100, Math.round((netWeight / 1000) * 100)); const handleOpenColorPicker = (): void => { setColorInputValue(selectedColor); setIsColorPickerOpen(true); }; const handleConfirmColor = (): void => { if (!colorInputValue) { Alert.alert('Erro', 'Digite um código hexadecimal'); return; } const normalized = normalizeHex(colorInputValue.trim()); if (isValidHex(normalized)) { setSelectedColor(normalized); setHexInput(normalized); setIsColorPickerOpen(false); setColorInputValue(''); } else { Alert.alert('Erro', 'Código hexadecimal inválido. Use o formato #RRGGBB ou #RGB.'); } }; const handleCancelColor = (): void => { setIsColorPickerOpen(false); setColorInputValue(''); }; async function onSubmit(data: FormData): Promise { if (!selectedPresetId) { Alert.alert('Atenção', 'Selecione um preset de carretel.'); return; } if (!user) { Alert.alert('Erro', 'Usuário não autenticado.'); return; } setIsLoading(true); try { const filament = await createFilamentUseCase.execute(user.id, { material: selectedMaterial, brand: data.brand, model: data.model ?? null, colorHex: selectedColor, spoolPresetId: selectedPresetId, totalWeightG: data.totalWeightG, tempHotendC: data.tempHotendC ?? null, tempBedC: data.tempBedC ?? null, flowFactorPct: data.flowFactorPct ?? null, notes: data.notes ?? null, }); addFilament(filament); router.back(); } catch (err) { console.error('create filament error', err); Alert.alert('Erro', 'Não foi possível salvar o filamento.'); } finally { setIsLoading(false); } } return ( <> Código Hexadecimal Cancelar Confirmar {/* Header */} router.back()}> Novo Filamento {/* Cor */} COR DO FILAMENTO {hexInput} {PRESET_COLORS.map((c) => ( { setSelectedColor(c); setHexInput(c); }} style={[styles.colorDot, { backgroundColor: c }, selectedColor === c && styles.colorDotActive]} /> ))} {/* Material */} MATERIAL {MATERIALS.map((m) => ( setSelectedMaterial(m)} style={[styles.chip, selectedMaterial === m && styles.chipActive]} > {m} ))} {/* Marca e Modelo */} ( )} /> ( )} /> {/* Parâmetros de impressão */} PARÂMETROS DE IMPRESSÃO ( } /> )} /> ( } /> )} /> ( } /> )} /> {/* Preset */} PRESET DO CARRETEL router.push('/(app)/config/presets/new')}> + Personalizado {presets.map((p) => ( setSelectedPresetId(p.id)} style={[styles.presetItem, selectedPresetId === p.id && styles.presetItemActive]} > {p.name} {selectedPresetId === p.id && Carretel: {p.spoolWeightG}g} {p.spoolWeightG}g ))} {/* Calculadora */} CALCULADORA DE PESO Peso Total na Balança (g) ( g )} /> {netWeight > 0 && ( Filamento Disponível {totalWeight}g – {selectedPreset?.spoolWeightG}g {formatWeight(netWeight)} {pct}% )} {/* CTA fixo */}