Upload source code
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user