Add pre-cut layer and marker planning
This commit is contained in:
@@ -168,6 +168,9 @@ const Cutting = () => {
|
|||||||
const [previewPage, setPreviewPage] = useState(1);
|
const [previewPage, setPreviewPage] = useState(1);
|
||||||
const [previewItemsPerPage, setPreviewItemsPerPage] = useState(10);
|
const [previewItemsPerPage, setPreviewItemsPerPage] = useState(10);
|
||||||
const [selectedPreviewRowIds, setSelectedPreviewRowIds] = useState<string[]>([]);
|
const [selectedPreviewRowIds, setSelectedPreviewRowIds] = useState<string[]>([]);
|
||||||
|
const [previewQuantityOverrides, setPreviewQuantityOverrides] = useState<Record<string, string>>({});
|
||||||
|
const [previewLayers, setPreviewLayers] = useState<Record<string, string>>({});
|
||||||
|
const [previewMarkerReferences, setPreviewMarkerReferences] = useState<Record<string, string>>({});
|
||||||
const [lotSelections, setLotSelections] = useState<Record<string, { lotId: string; quantity: string }>>({});
|
const [lotSelections, setLotSelections] = useState<Record<string, { lotId: string; quantity: string }>>({});
|
||||||
const [productionPlanForm, setProductionPlanForm] = useState({ name: '', responsibleUserId: null as number | null, responsibleName: '', expectedDate: '', notes: '' });
|
const [productionPlanForm, setProductionPlanForm] = useState({ name: '', responsibleUserId: null as number | null, responsibleName: '', expectedDate: '', notes: '' });
|
||||||
const [operationalUsers, setOperationalUsers] = useState<OperationalUser[]>([]);
|
const [operationalUsers, setOperationalUsers] = useState<OperationalUser[]>([]);
|
||||||
@@ -391,13 +394,18 @@ const Cutting = () => {
|
|||||||
};
|
};
|
||||||
}, [cutPlan.needRows, materialReadinessByProductId]);
|
}, [cutPlan.needRows, materialReadinessByProductId]);
|
||||||
|
|
||||||
|
const getPreviewQuantity = (row: CutPlanSkuRow) => {
|
||||||
|
const override = Number((previewQuantityOverrides[row.id] || '').replace(',', '.'));
|
||||||
|
return Number.isFinite(override) && override > 0 ? Math.round(override) : row.suggestedCutQuantity;
|
||||||
|
};
|
||||||
|
|
||||||
const selectedPreviewRows = useMemo(() => {
|
const selectedPreviewRows = useMemo(() => {
|
||||||
const selectedIds = new Set(selectedPreviewRowIds);
|
const selectedIds = new Set(selectedPreviewRowIds);
|
||||||
return rowsAvailableForOrder.filter(row => selectedIds.has(row.id));
|
return rowsAvailableForOrder.filter(row => selectedIds.has(row.id));
|
||||||
}, [rowsAvailableForOrder, selectedPreviewRowIds]);
|
}, [rowsAvailableForOrder, selectedPreviewRowIds]);
|
||||||
const orderPreviewTotalUnits = useMemo(() => (
|
const orderPreviewTotalUnits = useMemo(() => (
|
||||||
selectedPreviewRows.reduce((total, row) => total + row.suggestedCutQuantity, 0)
|
selectedPreviewRows.reduce((total, row) => total + getPreviewQuantity(row), 0)
|
||||||
), [selectedPreviewRows]);
|
), [previewQuantityOverrides, selectedPreviewRows]);
|
||||||
const selectedResponsibleUser = useMemo(() => (
|
const selectedResponsibleUser = useMemo(() => (
|
||||||
operationalUsers.find((user) => user.id === productionPlanForm.responsibleUserId && user.name === productionPlanForm.responsibleName) || null
|
operationalUsers.find((user) => user.id === productionPlanForm.responsibleUserId && user.name === productionPlanForm.responsibleName) || null
|
||||||
), [operationalUsers, productionPlanForm.responsibleName, productionPlanForm.responsibleUserId]);
|
), [operationalUsers, productionPlanForm.responsibleName, productionPlanForm.responsibleUserId]);
|
||||||
@@ -421,12 +429,12 @@ const Cutting = () => {
|
|||||||
unit: component.unit || 'UN',
|
unit: component.unit || 'UN',
|
||||||
quantity: 0
|
quantity: 0
|
||||||
};
|
};
|
||||||
current.quantity += row.suggestedCutQuantity * component.quantityPerUnit;
|
current.quantity += getPreviewQuantity(row) * component.quantityPerUnit;
|
||||||
requirements.set(key, current);
|
requirements.set(key, current);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return Array.from(requirements.values()).sort((a, b) => a.label.localeCompare(b.label, 'pt-BR'));
|
return Array.from(requirements.values()).sort((a, b) => a.label.localeCompare(b.label, 'pt-BR'));
|
||||||
}, [compositionsByProductId, selectedPreviewRows]);
|
}, [compositionsByProductId, previewQuantityOverrides, selectedPreviewRows]);
|
||||||
|
|
||||||
const selectedLotReservations = useMemo(() => (
|
const selectedLotReservations = useMemo(() => (
|
||||||
selectedMaterialRequirements.flatMap(requirement => {
|
selectedMaterialRequirements.flatMap(requirement => {
|
||||||
@@ -675,6 +683,9 @@ const Cutting = () => {
|
|||||||
setGenerationMessage('');
|
setGenerationMessage('');
|
||||||
setPreviewPage(1);
|
setPreviewPage(1);
|
||||||
setSelectedPreviewRowIds(rowsAvailableForOrder.map(row => row.id));
|
setSelectedPreviewRowIds(rowsAvailableForOrder.map(row => row.id));
|
||||||
|
setPreviewQuantityOverrides({});
|
||||||
|
setPreviewLayers({});
|
||||||
|
setPreviewMarkerReferences({});
|
||||||
setLotSelections({});
|
setLotSelections({});
|
||||||
setProductionPlanForm({ name: cutPlanReference, responsibleUserId: null, responsibleName: '', expectedDate: '', notes: '' });
|
setProductionPlanForm({ name: cutPlanReference, responsibleUserId: null, responsibleName: '', expectedDate: '', notes: '' });
|
||||||
setIsPlanDetailsOpen(false);
|
setIsPlanDetailsOpen(false);
|
||||||
@@ -729,7 +740,7 @@ const Cutting = () => {
|
|||||||
issueDate,
|
issueDate,
|
||||||
productSku: row.id,
|
productSku: row.id,
|
||||||
productDescription: row.name,
|
productDescription: row.name,
|
||||||
quantity: row.suggestedCutQuantity,
|
quantity: getPreviewQuantity(row),
|
||||||
unit: 'UN',
|
unit: 'UN',
|
||||||
integrationStatus: 'Local',
|
integrationStatus: 'Local',
|
||||||
components: (compositionsByProductId.get(normalizeMaterialKey(row.id))?.components || [])
|
components: (compositionsByProductId.get(normalizeMaterialKey(row.id))?.components || [])
|
||||||
@@ -739,12 +750,14 @@ const Cutting = () => {
|
|||||||
componentSku: component.componentSku,
|
componentSku: component.componentSku,
|
||||||
componentName: component.componentName,
|
componentName: component.componentName,
|
||||||
quantityPerUnit: component.quantityPerUnit,
|
quantityPerUnit: component.quantityPerUnit,
|
||||||
totalQuantity: row.suggestedCutQuantity * component.quantityPerUnit,
|
totalQuantity: getPreviewQuantity(row) * component.quantityPerUnit,
|
||||||
unit: component.unit
|
unit: component.unit
|
||||||
})),
|
})),
|
||||||
markers: [
|
markers: [
|
||||||
{ label: row.family.materialLabel, color: '#38bdf8' },
|
{ label: row.family.materialLabel, color: '#38bdf8' },
|
||||||
...(row.color ? [{ label: row.color, color: '#52DFA0' }] : []),
|
...(row.color ? [{ label: row.color, color: '#52DFA0' }] : []),
|
||||||
|
...(Number(previewLayers[row.id]) > 0 ? [{ label: `${previewLayers[row.id]} camadas`, color: '#c084fc' }] : []),
|
||||||
|
...(previewMarkerReferences[row.id]?.trim() ? [{ label: `Risco ${previewMarkerReferences[row.id].trim()}`, color: '#38bdf8' }] : []),
|
||||||
{ label: 'Material confirmado', color: '#52DFA0' },
|
{ label: 'Material confirmado', color: '#52DFA0' },
|
||||||
...(row.issues.length ? [{ label: 'Dados pendentes', color: '#f59e0b' }] : [])
|
...(row.issues.length ? [{ label: 'Dados pendentes', color: '#f59e0b' }] : [])
|
||||||
],
|
],
|
||||||
@@ -760,6 +773,9 @@ const Cutting = () => {
|
|||||||
stock: row.stock,
|
stock: row.stock,
|
||||||
openProductionQuantity: row.openProductionQuantity,
|
openProductionQuantity: row.openProductionQuantity,
|
||||||
availableQuantity: row.availableQuantity,
|
availableQuantity: row.availableQuantity,
|
||||||
|
plannedLayers: Number(previewLayers[row.id]) || null,
|
||||||
|
markerReference: previewMarkerReferences[row.id]?.trim() || '',
|
||||||
|
manuallyAdjustedQuantity: getPreviewQuantity(row) !== row.suggestedCutQuantity,
|
||||||
issues: row.issues
|
issues: row.issues
|
||||||
}
|
}
|
||||||
})), selectedLotReservations, {
|
})), selectedLotReservations, {
|
||||||
@@ -992,7 +1008,7 @@ const Cutting = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="mt-3 divide-y divide-dark-border overflow-hidden rounded-lg border border-dark-border bg-dark-card">
|
<div className="mt-3 divide-y divide-dark-border overflow-hidden rounded-lg border border-dark-border bg-dark-card">
|
||||||
{selectedPreviewRows.map(row => (
|
{selectedPreviewRows.map(row => (
|
||||||
<div key={row.id} className="flex items-center justify-between gap-4 px-3 py-2.5">
|
<div key={row.id} className="grid gap-3 px-3 py-3 lg:grid-cols-[minmax(0,1fr)_100px_100px_190px] lg:items-center">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="truncate text-sm font-bold text-dark-text" title={row.name}>{row.name}</p>
|
<p className="truncate text-sm font-bold text-dark-text" title={row.name}>{row.name}</p>
|
||||||
<div className="mt-1 flex flex-wrap items-center gap-1.5">
|
<div className="mt-1 flex flex-wrap items-center gap-1.5">
|
||||||
@@ -1002,10 +1018,19 @@ const Cutting = () => {
|
|||||||
<span className="text-[10px] font-semibold text-dark-muted">{row.size || 'Sem tamanho'}</span>
|
<span className="text-[10px] font-semibold text-dark-muted">{row.size || 'Sem tamanho'}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span className="shrink-0 text-sm font-bold text-brand-primary">{formatNumber(row.suggestedCutQuantity)} un.</span>
|
<label className="text-[10px] font-bold uppercase tracking-wider text-dark-muted">Cortar
|
||||||
|
<input inputMode="numeric" value={previewQuantityOverrides[row.id] ?? ''} onChange={(event) => setPreviewQuantityOverrides(current => ({ ...current, [row.id]: event.target.value }))} className="mt-1 h-9 w-full rounded-lg border border-dark-border bg-dark-input px-2 text-right text-sm font-bold text-brand-primary outline-none focus:border-brand-primary" placeholder={formatNumber(row.suggestedCutQuantity)} aria-label={`Quantidade planejada para ${row.name}`} />
|
||||||
|
</label>
|
||||||
|
<label className="text-[10px] font-bold uppercase tracking-wider text-dark-muted">Camadas
|
||||||
|
<input inputMode="numeric" value={previewLayers[row.id] ?? ''} onChange={(event) => setPreviewLayers(current => ({ ...current, [row.id]: event.target.value }))} className="mt-1 h-9 w-full rounded-lg border border-dark-border bg-dark-input px-2 text-right text-sm font-bold text-dark-text outline-none focus:border-brand-primary" placeholder="Opcional" aria-label={`Camadas planejadas para ${row.name}`} />
|
||||||
|
</label>
|
||||||
|
<label className="text-[10px] font-bold uppercase tracking-wider text-dark-muted">Risco / encaixe
|
||||||
|
<input value={previewMarkerReferences[row.id] ?? ''} onChange={(event) => setPreviewMarkerReferences(current => ({ ...current, [row.id]: event.target.value }))} className="mt-1 h-9 w-full rounded-lg border border-dark-border bg-dark-input px-2 text-sm font-bold text-dark-text outline-none placeholder:text-dark-muted focus:border-brand-primary" placeholder="Código ou arquivo" aria-label={`Risco ou encaixe para ${row.name}`} />
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
<p className="mt-3 text-xs font-semibold text-dark-muted">A quantidade sugerida vem da cobertura. Ajustes manuais ficam marcados na OP; camadas e risco são referências de preparação, e o resultado real é confirmado durante a execução.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>}
|
</div>}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user