Upload source code

This commit is contained in:
2026-03-12 19:36:34 +00:00
parent 783b6cb7e8
commit c7fb0c8561
158 changed files with 22553 additions and 0 deletions
@@ -0,0 +1,374 @@
import React from 'react';
import { Modal } from '../Modal';
import { Input } from '../Input';
import { Button } from '../Button';
import { Select } from '../Select';
import api from '../../services/api';
import { useAuth } from '../../context/useAuth';
import { useToast } from '../../hooks/useToast';
import type { PaintingScheme, TechnicalDataSheet } from '../../types';
interface CreatePaintingSchemeModalProps {
isOpen: boolean;
onClose: () => void;
onSuccess: () => void;
projectId?: string;
initialData?: PaintingScheme;
}
export const CreatePaintingSchemeModal: React.FC<CreatePaintingSchemeModalProps> = ({ isOpen, onClose, onSuccess, projectId, initialData }) => {
const { isGuest } = useAuth();
const { showGuestWarning } = useToast();
const [loading, setLoading] = React.useState(false);
const [projects, setProjects] = React.useState<{ id: string, name: string }[]>([]);
const [dataSheets, setDataSheets] = React.useState<TechnicalDataSheet[]>([]);
const [selectedProjectId, setSelectedProjectId] = React.useState(projectId || '');
const [formData, setFormData] = React.useState({
name: '',
type: '',
coat: '',
solidsVolume: '',
yieldTheoretical: '',
epsMin: '',
epsMax: '',
dilution: '',
manufacturer: '',
color: '',
notes: '',
paintConsumption: '',
thinnerConsumption: '',
paintId: '',
thinnerId: '',
thinnerSymbol: '',
colorHex: '#ffffff'
});
React.useEffect(() => {
if (!projectId) {
api.get('/projects').then(response => {
setProjects(response.data);
}).catch(err => console.error("Error loading projects", err));
} else {
setSelectedProjectId(projectId);
}
api.get('/datasheets').then(response => {
console.log('Frontend: Datasheets received:', response.data.length);
setDataSheets(response.data);
}).catch(err => {
console.error("Frontend: Error loading datasheets:", err);
});
}, [projectId, isOpen]);
React.useEffect(() => {
if (initialData) {
setFormData({
name: initialData.name || '',
type: initialData.type || '',
coat: initialData.coat || '',
solidsVolume: initialData.solidsVolume?.toString() || '',
yieldTheoretical: initialData.yieldTheoretical?.toString() || '',
epsMin: initialData.epsMin?.toString() || '',
epsMax: initialData.epsMax?.toString() || '',
dilution: initialData.dilution?.toString() || '',
manufacturer: initialData.manufacturer || '',
color: initialData.color || '',
notes: initialData.notes || '',
paintConsumption: initialData.paintConsumption?.toString() || '',
thinnerConsumption: initialData.thinnerConsumption?.toString() || '',
paintId: typeof initialData.paintId === 'object' ? (initialData.paintId?._id || '') : (initialData.paintId as string) || '',
thinnerId: typeof initialData.thinnerId === 'object' ? (initialData.thinnerId?._id || '') : (initialData.thinnerId as string) || '',
thinnerSymbol: initialData.thinnerSymbol || '',
colorHex: initialData.colorHex || '#ffffff'
});
if (initialData.projectId) setSelectedProjectId(initialData.projectId);
} else {
setFormData({
name: '', type: '', coat: '', solidsVolume: '', yieldTheoretical: '', epsMin: '', epsMax: '',
dilution: '', manufacturer: '', color: '', notes: '',
paintConsumption: '', thinnerConsumption: '', paintId: '', thinnerId: '',
thinnerSymbol: '',
colorHex: '#ffffff'
});
if (projectId) setSelectedProjectId(projectId);
}
}, [initialData, isOpen, projectId]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
// Mantém a atualização básica do estado
setFormData(prev => {
const newState = { ...prev, [name]: value };
// Lógica Proativa: Se mudar o NOME do produto no topo, tenta preencher tudo
if (name === 'name' && value) {
const ds = dataSheets.find(d => d.name === value);
if (ds) {
let mappedType = '';
const dsTypeNormalized = ds.type?.toLowerCase() || '';
if (dsTypeNormalized.includes('epóxi')) mappedType = 'epoxy';
else if (dsTypeNormalized.includes('poliuretano')) mappedType = 'polyurethane';
else if (dsTypeNormalized.includes('zinco')) mappedType = 'silicate-zinc';
else if (dsTypeNormalized.includes('acríl')) mappedType = 'acrylic';
else if (dsTypeNormalized.includes('alquíd')) mappedType = 'alkyd';
// Se a tinta tem um redutor na ficha, tenta achar o ID dele na biblioteca
let thinnerId = ''; // Resetar redutor ao trocar a tinta
if (ds.reducer) {
const reducerCode = ds.reducer.trim().toLowerCase();
console.log(`Auto-fill: Looking for reducer "${reducerCode}" for paint "${ds.name}"`);
// Busca flexível: exata ou contém
const matchingReducer = dataSheets.find(d =>
d.name.toLowerCase() === reducerCode ||
d.name.toLowerCase().includes(reducerCode) ||
reducerCode.includes(d.name.toLowerCase())
);
if (matchingReducer) {
thinnerId = matchingReducer._id || matchingReducer.id || '';
console.log(`Auto-fill: Found matching reducer: ${matchingReducer.name}`);
} else {
console.log(`Auto-fill: No matching reducer found in library for "${reducerCode}"`);
}
}
return {
...newState,
type: mappedType || newState.type,
solidsVolume: ds.solidsVolume?.toString() || newState.solidsVolume,
yieldTheoretical: ds.yieldTheoretical?.toString() || newState.yieldTheoretical,
epsMin: ds.dftMin?.toString() || newState.epsMin,
epsMax: ds.dftMax?.toString() || newState.epsMax,
manufacturer: ds.manufacturer || newState.manufacturer,
dilution: ds.dilution?.toString() || newState.dilution,
paintId: ds._id || ds.id || newState.paintId,
thinnerId: thinnerId,
thinnerSymbol: ds.reducer || ''
};
}
}
return newState;
});
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
console.log("Submitting form with data:", formData);
if (isGuest()) {
showGuestWarning();
return;
}
if (!selectedProjectId) {
alert("Selecione um projeto");
return;
}
setLoading(true);
try {
const payload = {
...formData,
projectId: selectedProjectId,
solidsVolume: parseInt(formData.solidsVolume),
yieldTheoretical: parseFloat(formData.yieldTheoretical),
epsMin: parseFloat(formData.epsMin),
epsMax: parseFloat(formData.epsMax),
dilution: parseInt(formData.dilution),
paintConsumption: parseFloat(formData.paintConsumption),
thinnerConsumption: parseFloat(formData.thinnerConsumption),
paintId: formData.paintId || null,
thinnerId: formData.thinnerId || null,
thinnerSymbol: formData.thinnerSymbol
};
if (initialData) {
await api.put(`/painting-schemes/${initialData.id}`, payload);
} else {
await api.post('/painting-schemes', payload);
}
onSuccess();
onClose();
} catch (error) {
console.error('Error saving scheme', error);
alert('Erro ao salvar esquema');
} finally {
setLoading(false);
}
};
return (
<Modal isOpen={isOpen} onClose={onClose} title={initialData ? "Editar Esquema" : "Novo Esquema / Demão"}>
<form onSubmit={handleSubmit} className="space-y-4">
{!projectId && (
<Select
name="projectId"
label="Projeto"
options={projects.map(p => ({ label: p.name, value: p.id }))}
value={selectedProjectId}
onChange={(e) => setSelectedProjectId(e.target.value)}
required
/>
)}
<Select
name="name"
label="Nome/Descrição (Produto)"
options={[
{ label: 'Outro (Manual)', value: '' },
...dataSheets.map(ds => ({ label: ds.name, value: ds.name }))
]}
value={formData.name}
onChange={handleChange}
required
/>
{formData.name === '' && (
<Input name="name" label="Descrição Manual" placeholder="Ex: Pintura Interna" value={formData.name} onChange={handleChange} required />
)}
<Select
name="coat"
label="Demão (Etapa)"
options={[
{ label: 'Primer / Selador', value: 'Primer' },
{ label: 'Stripe Coat', value: 'Stripe Coat' },
{ label: 'Intermediário', value: 'Intermediario' },
{ label: 'Acabamento', value: 'Acabamento' },
{ label: 'Retoque', value: 'Retoque' }
]}
value={formData.coat}
onChange={handleChange}
required
/>
<div className="grid grid-cols-2 gap-4">
<Select
name="type"
label="Tipo de Tinta"
options={[
{ label: 'Epóxi', value: 'epoxy' },
{ label: 'Poliuretano', value: 'polyurethane' },
{ label: 'Silicato Zinco', value: 'silicate-zinc' },
{ label: 'Acrílica', value: 'acrylic' },
{ label: 'Alquídica', value: 'alkyd' }
]}
value={formData.type}
onChange={handleChange}
/>
<Input name="solidsVolume" label="Sólidos Vol. (%)" type="number" value={formData.solidsVolume} onChange={handleChange} />
</div>
<div className="grid grid-cols-2 gap-4">
<Input name="yieldTheoretical" label="Rendimento (m²/L)" type="number" step="0.01" value={formData.yieldTheoretical} onChange={handleChange} />
</div>
<div className="grid grid-cols-2 gap-4">
<Input name="epsMin" label="EPS Mín (μm)" type="number" value={formData.epsMin} onChange={handleChange} />
<Input name="epsMax" label="EPS Máx (μm)" type="number" value={formData.epsMax} onChange={handleChange} />
</div>
<div className="grid grid-cols-2 gap-4">
<Input name="dilution" label="Diluição (%)" type="number" value={formData.dilution} onChange={handleChange} />
<Input name="manufacturer" label="Fabricante" value={formData.manufacturer} onChange={handleChange} />
</div>
<div className="grid grid-cols-2 gap-4">
<Input name="color" label="Cor (Munsell/RAL)" placeholder="Ex: N6.5 ou RAL 7035" value={formData.color} onChange={handleChange} />
<div className="flex flex-col gap-1">
<label className="text-[10px] font-bold text-primary uppercase tracking-[0.15em] ml-1 mb-1">Cor Representativa</label>
<div className="flex items-center gap-3 bg-surface-soft/50 p-2 rounded-xl border border-border/40">
<input
type="color"
name="colorHex"
value={formData.colorHex}
onChange={handleChange}
title="Cor Representativa"
className="w-10 h-10 rounded-lg cursor-pointer bg-transparent"
/>
<span className="text-xs font-mono font-bold text-text-muted">{formData.colorHex}</span>
</div>
</div>
</div>
<div className="flex flex-col gap-1 w-full">
<label className="text-[10px] font-bold text-primary uppercase tracking-[0.15em] ml-1 mb-1">Observações</label>
<textarea
name="notes"
aria-label="Observações"
className="flex min-h-[80px] w-full rounded-xl border bg-[var(--input-bg)] border-[var(--input-border)] text-[var(--input-text)] px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary disabled:opacity-50 transition-all font-medium placeholder:text-[var(--input-placeholder)]"
value={formData.notes}
onChange={handleChange}
/>
</div>
<div className="bg-surface-soft p-4 rounded-xl border border-border/40 mt-4">
<h3 className="text-sm font-bold text-text-main mb-3">Planejamento de Consumo (Opcional)</h3>
<div className="grid grid-cols-2 gap-4">
<Select
name="paintId"
label="Produto (Tinta)"
options={[
{ label: 'Selecione...', value: '' },
...dataSheets.map(ds => ({ label: ds.name, value: ds._id || ds.id || '' }))
]}
value={formData.paintId}
onChange={(e) => {
const selectedPaintId = e.target.value;
setFormData(prev => {
const ds = dataSheets.find(d => (d._id || d.id) === selectedPaintId);
let thinnerId = ''; // Resetar ao trocar tinta
if (ds && ds.reducer) {
const reducerClean = ds.reducer.trim().toLowerCase();
console.log(`Consumption: Looking for reducer "${reducerClean}" for paint ID ${selectedPaintId}`);
// Busca exata ou por inclusão
const matchingThinner = dataSheets.find(d =>
d.name.toLowerCase() === reducerClean ||
d.name.toLowerCase().includes(reducerClean) ||
reducerClean.includes(d.name.toLowerCase())
);
if (matchingThinner) {
thinnerId = matchingThinner._id || matchingThinner.id || '';
console.log(`Consumption: Found reducer match: ${matchingThinner.name}`);
}
}
return { ...prev, paintId: selectedPaintId, thinnerId, thinnerSymbol: ds?.reducer || '' };
});
}}
/>
<Input
name="thinnerSymbol"
label="Redutor (diluente)"
value={formData.thinnerSymbol}
readOnly
placeholder="Preenchido pela tinta"
className="bg-surface-soft/50 font-bold text-primary"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4 mt-2">
<Input
name="paintConsumption"
label="Consumo Tinta (L/Kg)"
type="number"
step="0.001"
value={formData.paintConsumption}
onChange={handleChange}
/>
<Input
name="thinnerConsumption"
label="Consumo Diluente (L/Kg)"
type="number"
step="0.001"
value={formData.thinnerConsumption}
onChange={handleChange}
/>
</div>
<div className="flex justify-end gap-2 mt-6">
<Button type="button" variant="ghost" onClick={onClose} disabled={loading}>Cancelar</Button>
<Button type="submit" disabled={loading}>{loading ? 'Salvando...' : (initialData ? 'Salvar Alterações' : 'Adicionar Demão')}</Button>
</div>
</form>
</Modal>
);
};