Compare commits
9 Commits
1c97f03b3e
...
db5cdcba24
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db5cdcba24 | ||
|
|
8293c0e91c | ||
|
|
081cd36160 | ||
|
|
f9456924c0 | ||
|
|
8d5fef6d2a | ||
|
|
5a8e2326d8 | ||
|
|
f69e9b2af5 | ||
|
|
d70841a09f | ||
|
|
bcf7787f37 |
@@ -57,7 +57,7 @@ const correctionFilterOptions: Array<{ value: CorrectionIssueFilter; label: stri
|
|||||||
];
|
];
|
||||||
|
|
||||||
const familyStyles: Record<CutFamilyKey, string> = {
|
const familyStyles: Record<CutFamilyKey, string> = {
|
||||||
BLCS: 'border-emerald-400/30 bg-emerald-400/10 text-emerald-300',
|
BLCS: 'border-fuchsia-400/30 bg-fuchsia-400/10 text-fuchsia-300',
|
||||||
BLOS: 'border-sky-400/30 bg-sky-400/10 text-sky-300',
|
BLOS: 'border-sky-400/30 bg-sky-400/10 text-sky-300',
|
||||||
BLMC: 'border-amber-400/30 bg-amber-400/10 text-amber-300',
|
BLMC: 'border-amber-400/30 bg-amber-400/10 text-amber-300',
|
||||||
BLPM: 'border-purple-400/30 bg-purple-400/10 text-purple-300',
|
BLPM: 'border-purple-400/30 bg-purple-400/10 text-purple-300',
|
||||||
@@ -159,6 +159,8 @@ const Cutting = () => {
|
|||||||
const [itemsPerPage, setItemsPerPage] = useState(10);
|
const [itemsPerPage, setItemsPerPage] = useState(10);
|
||||||
const [isGeneratingOrders, setIsGeneratingOrders] = useState(false);
|
const [isGeneratingOrders, setIsGeneratingOrders] = useState(false);
|
||||||
const [isOrderPreviewOpen, setIsOrderPreviewOpen] = useState(false);
|
const [isOrderPreviewOpen, setIsOrderPreviewOpen] = useState(false);
|
||||||
|
const [previewPage, setPreviewPage] = useState(1);
|
||||||
|
const [previewItemsPerPage, setPreviewItemsPerPage] = useState(10);
|
||||||
const [editingCutRow, setEditingCutRow] = useState<CutPlanSkuRow | null>(null);
|
const [editingCutRow, setEditingCutRow] = useState<CutPlanSkuRow | null>(null);
|
||||||
const [quickCutEdit, setQuickCutEdit] = useState<CutProductOverride>({});
|
const [quickCutEdit, setQuickCutEdit] = useState<CutProductOverride>({});
|
||||||
const [isQuickCutEditSaving, setIsQuickCutEditSaving] = useState(false);
|
const [isQuickCutEditSaving, setIsQuickCutEditSaving] = useState(false);
|
||||||
@@ -364,6 +366,14 @@ const Cutting = () => {
|
|||||||
rowsAvailableForOrder.reduce((total, row) => total + row.suggestedCutQuantity, 0)
|
rowsAvailableForOrder.reduce((total, row) => total + row.suggestedCutQuantity, 0)
|
||||||
), [rowsAvailableForOrder]);
|
), [rowsAvailableForOrder]);
|
||||||
|
|
||||||
|
const previewRowsWithIssues = useMemo(() => (
|
||||||
|
rowsAvailableForOrder.filter(row => row.issues.length).length
|
||||||
|
), [rowsAvailableForOrder]);
|
||||||
|
const previewTotalPages = Math.ceil(rowsAvailableForOrder.length / previewItemsPerPage);
|
||||||
|
const safePreviewPage = Math.min(previewPage, previewTotalPages || 1);
|
||||||
|
const previewStartIndex = (safePreviewPage - 1) * previewItemsPerPage;
|
||||||
|
const paginatedPreviewRows = rowsAvailableForOrder.slice(previewStartIndex, previewStartIndex + previewItemsPerPage);
|
||||||
|
|
||||||
const totalPages = Math.ceil(filteredRows.length / itemsPerPage);
|
const totalPages = Math.ceil(filteredRows.length / itemsPerPage);
|
||||||
const safeCurrentPage = Math.min(currentPage, totalPages || 1);
|
const safeCurrentPage = Math.min(currentPage, totalPages || 1);
|
||||||
const startIndex = (safeCurrentPage - 1) * itemsPerPage;
|
const startIndex = (safeCurrentPage - 1) * itemsPerPage;
|
||||||
@@ -587,6 +597,7 @@ const Cutting = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setGenerationMessage('');
|
setGenerationMessage('');
|
||||||
|
setPreviewPage(1);
|
||||||
setIsOrderPreviewOpen(true);
|
setIsOrderPreviewOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -777,60 +788,51 @@ const Cutting = () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-3 border-b border-dark-border p-4 md:grid-cols-3">
|
<div className="grid grid-cols-2 divide-x divide-y divide-dark-border border-b border-dark-border md:grid-cols-4 md:divide-y-0">
|
||||||
<div className="rounded-xl border border-dark-border bg-dark-input/60 p-3">
|
{[
|
||||||
<p className="text-[10px] font-bold uppercase tracking-widest text-dark-muted">OPs previstas</p>
|
['OPs a criar', formatNumber(rowsAvailableForOrder.length), 'text-dark-text'],
|
||||||
<p className="mt-2 text-2xl font-bold text-dark-text">{formatNumber(rowsAvailableForOrder.length)}</p>
|
['Unidades para cortar', `${formatNumber(orderPreviewTotalUnits)} un.`, 'text-brand-primary'],
|
||||||
</div>
|
['Cobertura alvo', `${formatNumber(targetCoverageDays)} dias`, 'text-dark-text'],
|
||||||
<div className="rounded-xl border border-dark-border bg-dark-input/60 p-3">
|
['Revisar dados', formatNumber(previewRowsWithIssues), previewRowsWithIssues ? 'text-amber-300' : 'text-dark-text']
|
||||||
<p className="text-[10px] font-bold uppercase tracking-widest text-dark-muted">Unidades totais</p>
|
].map(([label, value, tone]) => <div key={label} className="p-4"><p className="text-[10px] font-bold uppercase tracking-widest text-dark-muted">{label}</p><p className={`mt-1 text-xl font-bold ${tone}`}>{value}</p></div>)}
|
||||||
<p className="mt-2 text-2xl font-bold text-red-300">{formatNumber(orderPreviewTotalUnits)} un.</p>
|
|
||||||
</div>
|
|
||||||
<div className="rounded-xl border border-dark-border bg-dark-input/60 p-3">
|
|
||||||
<p className="text-[10px] font-bold uppercase tracking-widest text-dark-muted">Cobertura alvo</p>
|
|
||||||
<p className="mt-2 text-2xl font-bold text-brand-primary">{formatNumber(targetCoverageDays)} dias</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="overflow-auto">
|
<div className="overflow-auto">
|
||||||
<table className="w-full min-w-[980px] text-left text-sm">
|
<table className="w-full min-w-[900px] table-fixed text-left text-sm">
|
||||||
|
<colgroup>
|
||||||
|
<col className="w-[44%]" />
|
||||||
|
<col className="w-[16%]" />
|
||||||
|
<col className="w-[14%]" />
|
||||||
|
<col className="w-[14%]" />
|
||||||
|
<col className="w-[12%]" />
|
||||||
|
</colgroup>
|
||||||
<thead className="border-b border-dark-border bg-dark-header text-dark-muted">
|
<thead className="border-b border-dark-border bg-dark-header text-dark-muted">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-4 py-3 text-[10px] font-bold uppercase tracking-wider">SKU</th>
|
|
||||||
<th className="px-4 py-3 text-[10px] font-bold uppercase tracking-wider">Produto</th>
|
<th className="px-4 py-3 text-[10px] font-bold uppercase tracking-wider">Produto</th>
|
||||||
<th className="px-4 py-3 text-[10px] font-bold uppercase tracking-wider">Família</th>
|
<th className="px-4 py-3 text-[10px] font-bold uppercase tracking-wider">Cortar agora</th>
|
||||||
<th className="px-4 py-3 text-[10px] font-bold uppercase tracking-wider">Variação</th>
|
<th className="px-4 py-3 text-[10px] font-bold uppercase tracking-wider">Demanda</th>
|
||||||
<th className="px-4 py-3 text-[10px] font-bold uppercase tracking-wider">Necessidade</th>
|
<th className="px-4 py-3 text-[10px] font-bold uppercase tracking-wider">Disponível</th>
|
||||||
<th className="px-4 py-3 text-[10px] font-bold uppercase tracking-wider">Dados</th>
|
<th className="px-4 py-3 text-[10px] font-bold uppercase tracking-wider">Revisar</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-dark-border">
|
<tbody className="divide-y divide-dark-border">
|
||||||
{rowsAvailableForOrder.map(row => (
|
{paginatedPreviewRows.map(row => (
|
||||||
<tr key={row.id} className="hover:bg-dark-input/50">
|
<tr key={row.id} className="hover:bg-dark-input/50">
|
||||||
<td className="px-4 py-3 font-mono text-[11px] text-dark-muted">#{row.id}</td>
|
|
||||||
<td className="max-w-0 px-4 py-3">
|
<td className="max-w-0 px-4 py-3">
|
||||||
<div className="truncate font-bold text-dark-text" title={row.name}>{row.name}</div>
|
<div className="truncate font-bold text-dark-text" title={row.name}>{row.name}</div>
|
||||||
<div className="mt-1 text-[10px] font-semibold text-dark-muted">
|
<div className="mt-1 flex flex-wrap items-center gap-1.5">
|
||||||
Média: {formatNumber(row.dailySales, 2)} un./dia · Estoque: {formatNumber(row.stock)} un.
|
<span className="font-mono text-[10px] font-medium text-dark-muted">#{row.id}</span>
|
||||||
</div>
|
<span className={`inline-flex whitespace-nowrap rounded-full border px-2 py-0.5 text-[10px] font-bold ${familyStyles[row.family.key]}`}>{row.family.materialLabel}</span>
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3">
|
|
||||||
<span className={`inline-flex whitespace-nowrap rounded-full border px-2.5 py-1 text-xs font-bold ${familyStyles[row.family.key]}`}>
|
|
||||||
{row.family.materialLabel}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3">
|
|
||||||
<div className="flex min-w-0 items-center gap-1.5">
|
|
||||||
<ProductColorBadge label={row.color} className="max-w-[110px] px-2" />
|
<ProductColorBadge label={row.color} className="max-w-[110px] px-2" />
|
||||||
<span className="inline-flex items-center gap-1 rounded-full border border-emerald-400/25 bg-emerald-400/10 px-2 py-1 text-xs font-bold text-emerald-300">
|
<span className="inline-flex items-center gap-1 rounded-full border border-emerald-400/25 bg-emerald-400/10 px-2 py-0.5 text-[10px] font-bold text-emerald-300">
|
||||||
<Ruler className="h-3 w-3" />
|
<Ruler className="h-3 w-3" />
|
||||||
{row.size || '-'}
|
{row.size || '-'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 font-bold whitespace-nowrap text-red-300">
|
<td className="px-4 py-3 font-bold whitespace-nowrap text-brand-primary">{formatNumber(row.suggestedCutQuantity)} un.</td>
|
||||||
{formatNumber(row.suggestedCutQuantity)} un.
|
<td className="px-4 py-3 font-bold whitespace-nowrap text-dark-text">{formatNumber(row.projectedDemand, 1)} un.</td>
|
||||||
</td>
|
<td className="px-4 py-3 whitespace-nowrap"><div className="font-bold text-dark-text">{formatNumber(row.availableQuantity)} un.</div>{row.openProductionQuantity ? <div className="mt-1 text-[10px] font-semibold text-dark-muted">{formatNumber(row.openProductionQuantity)} em OP</div> : null}</td>
|
||||||
<td className="px-4 py-3">{renderIssueBadge(row)}</td>
|
<td className="px-4 py-3">{renderIssueBadge(row)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
@@ -838,6 +840,21 @@ const Cutting = () => {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<PaginationControls
|
||||||
|
totalItems={rowsAvailableForOrder.length}
|
||||||
|
currentPage={safePreviewPage}
|
||||||
|
totalPages={previewTotalPages}
|
||||||
|
pageSize={previewItemsPerPage}
|
||||||
|
pageSizeOptions={[10, 20, 50]}
|
||||||
|
itemLabel="OPs"
|
||||||
|
pageSizeLabel="por página"
|
||||||
|
startIndex={previewStartIndex}
|
||||||
|
endIndex={Math.min(previewStartIndex + previewItemsPerPage, rowsAvailableForOrder.length)}
|
||||||
|
onPageChange={setPreviewPage}
|
||||||
|
onPageSizeChange={(pageSize) => { setPreviewItemsPerPage(pageSize); setPreviewPage(1); }}
|
||||||
|
className="border-t border-dark-border px-4 py-3"
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="flex flex-col gap-3 border-t border-dark-border p-4 sm:flex-row sm:items-center sm:justify-between">
|
<div className="flex flex-col gap-3 border-t border-dark-border p-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<p className="text-xs font-semibold text-dark-muted">
|
<p className="text-xs font-semibold text-dark-muted">
|
||||||
OPs abertas já existentes para o mesmo plano e SKU serão ignoradas.
|
OPs abertas já existentes para o mesmo plano e SKU serão ignoradas.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Fragment, type FormEvent, useCallback, useEffect, useMemo, useState } from 'react';
|
import { Fragment, type FormEvent, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { Link, useOutletContext } from 'react-router-dom';
|
import { Link, useOutletContext } from 'react-router-dom';
|
||||||
import { ArrowLeft, CalendarDays, CheckCircle2, ClipboardList, Clock3, Download, PackageCheck, Search } from 'lucide-react';
|
import { ArrowLeft, CheckCircle2, ChevronDown, ChevronUp, ClipboardList, Clock3, Download, PackageCheck, PlayCircle, Search, XCircle } from 'lucide-react';
|
||||||
import DateRangePicker from '../components/DateRangePicker';
|
import DateRangePicker from '../components/DateRangePicker';
|
||||||
import PaginationControls from '../components/PaginationControls';
|
import PaginationControls from '../components/PaginationControls';
|
||||||
import RefreshStatus from '../components/RefreshStatus';
|
import RefreshStatus from '../components/RefreshStatus';
|
||||||
@@ -22,9 +22,12 @@ const statusTabs: Array<{ key: ProductionOrderStatusTab; label: string; dotClass
|
|||||||
{ key: 'canceled', label: 'Cancelada', dotClass: 'bg-zinc-500' }
|
{ key: 'canceled', label: 'Cancelada', dotClass: 'bg-zinc-500' }
|
||||||
];
|
];
|
||||||
|
|
||||||
const editableStatusOptions: Array<{ value: ProductionOrderStatusTab; label: string }> = statusTabs
|
const editableStatusOptions: Array<{ value: Exclude<ProductionOrderStatusTab, 'all'>; label: string; description: string }> = [
|
||||||
.filter(tab => tab.key !== 'all')
|
{ value: 'open', label: 'Em aberto', description: 'Aguardando o início da produção.' },
|
||||||
.map(tab => ({ value: tab.key, label: tab.label }));
|
{ value: 'in_progress', label: 'Em andamento', description: 'A produção já foi iniciada.' },
|
||||||
|
{ value: 'finished', label: 'Finalizada', description: 'A produção desta OP foi concluída.' },
|
||||||
|
{ value: 'canceled', label: 'Cancelada', description: 'Esta OP não seguirá para produção.' }
|
||||||
|
];
|
||||||
|
|
||||||
const statusStyles: Record<string, { label: string; className: string; dotClass: string }> = {
|
const statusStyles: Record<string, { label: string; className: string; dotClass: string }> = {
|
||||||
open: {
|
open: {
|
||||||
@@ -63,16 +66,6 @@ const formatQuantity = (value: number) => (
|
|||||||
}).format(value)
|
}).format(value)
|
||||||
);
|
);
|
||||||
|
|
||||||
const formatOptionalQuantity = (value: number | null) => (
|
|
||||||
value === null ? '-' : formatQuantity(value)
|
|
||||||
);
|
|
||||||
|
|
||||||
const hasProductionOrderDetails = (order: ProductionOrderItem) => (
|
|
||||||
order.components.length > 0 ||
|
|
||||||
order.steps.length > 0 ||
|
|
||||||
Boolean(order.notes || order.supplier || order.lotCode || order.rollQuantity || order.fabricKg || order.ribKg || order.yieldPiecesPerKg)
|
|
||||||
);
|
|
||||||
|
|
||||||
const getStatusStyle = (status: ProductionOrderStatus, fallbackLabel: string) => (
|
const getStatusStyle = (status: ProductionOrderStatus, fallbackLabel: string) => (
|
||||||
statusStyles[String(status)] || {
|
statusStyles[String(status)] || {
|
||||||
label: fallbackLabel || 'Em aberto',
|
label: fallbackLabel || 'Em aberto',
|
||||||
@@ -137,10 +130,11 @@ const ProductionOrders = () => {
|
|||||||
const [supplyMessage, setSupplyMessage] = useState('');
|
const [supplyMessage, setSupplyMessage] = useState('');
|
||||||
const [busyStatusOrderId, setBusyStatusOrderId] = useState<number | null>(null);
|
const [busyStatusOrderId, setBusyStatusOrderId] = useState<number | null>(null);
|
||||||
const [statusMessage, setStatusMessage] = useState('');
|
const [statusMessage, setStatusMessage] = useState('');
|
||||||
|
const [selectedOrderId, setSelectedOrderId] = useState<number | null>(null);
|
||||||
|
const [statusEditorOrderId, setStatusEditorOrderId] = useState<number | null>(null);
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [itemsPerPage, setItemsPerPage] = useState(20);
|
const [itemsPerPage, setItemsPerPage] = useState(20);
|
||||||
const [exitForm, setExitForm] = useState({
|
const [exitForm, setExitForm] = useState({
|
||||||
orderId: '',
|
|
||||||
lotId: '',
|
lotId: '',
|
||||||
quantity: '',
|
quantity: '',
|
||||||
reason: '',
|
reason: '',
|
||||||
@@ -201,7 +195,7 @@ const ProductionOrders = () => {
|
|||||||
const progressCount = summary.counts.in_progress || 0;
|
const progressCount = summary.counts.in_progress || 0;
|
||||||
const finishedCount = summary.counts.finished || 0;
|
const finishedCount = summary.counts.finished || 0;
|
||||||
const totalQuantity = summary.orders.reduce((total, order) => total + order.quantity, 0);
|
const totalQuantity = summary.orders.reduce((total, order) => total + order.quantity, 0);
|
||||||
const activeOrders = summary.orders.filter(order => order.status !== 'finished' && order.status !== 'canceled');
|
const selectedOrder = summary.orders.find(order => order.id === selectedOrderId) || null;
|
||||||
|
|
||||||
const handleManualRefresh = () => {
|
const handleManualRefresh = () => {
|
||||||
void loadProductionOrders({ force: true });
|
void loadProductionOrders({ force: true });
|
||||||
@@ -221,6 +215,7 @@ const ProductionOrders = () => {
|
|||||||
setStatusMessage(error instanceof Error ? error.message : 'Não foi possível atualizar o status da OP.');
|
setStatusMessage(error instanceof Error ? error.message : 'Não foi possível atualizar o status da OP.');
|
||||||
} finally {
|
} finally {
|
||||||
setBusyStatusOrderId(null);
|
setBusyStatusOrderId(null);
|
||||||
|
setStatusEditorOrderId(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -229,7 +224,6 @@ const ProductionOrders = () => {
|
|||||||
setSupplyLots(supplySummary.lots);
|
setSupplyLots(supplySummary.lots);
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectedOrder = summary.orders.find(order => `${order.id}` === exitForm.orderId);
|
|
||||||
const selectedLot = supplyLots.find(lot => `${lot.id}` === exitForm.lotId);
|
const selectedLot = supplyLots.find(lot => `${lot.id}` === exitForm.lotId);
|
||||||
|
|
||||||
const handleProductionExit = async (event: FormEvent<HTMLFormElement>) => {
|
const handleProductionExit = async (event: FormEvent<HTMLFormElement>) => {
|
||||||
@@ -247,7 +241,7 @@ const ProductionOrders = () => {
|
|||||||
reason: exitForm.reason.trim() || selectedOrder.productDescription,
|
reason: exitForm.reason.trim() || selectedOrder.productDescription,
|
||||||
});
|
});
|
||||||
await refreshSupplyLots();
|
await refreshSupplyLots();
|
||||||
setExitForm({ orderId: '', lotId: '', quantity: '', reason: '' });
|
setExitForm({ lotId: '', quantity: '', reason: '' });
|
||||||
setSupplyMessage('Saída de material registrada no estoque.');
|
setSupplyMessage('Saída de material registrada no estoque.');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setSupplyMessage(error instanceof Error ? error.message : 'Não foi possível baixar o material.');
|
setSupplyMessage(error instanceof Error ? error.message : 'Não foi possível baixar o material.');
|
||||||
@@ -273,6 +267,43 @@ const ProductionOrders = () => {
|
|||||||
exportToCSV(exportData, `ordens_producao_${new Date().toISOString().split('T')[0]}.csv`);
|
exportToCSV(exportData, `ordens_producao_${new Date().toISOString().split('T')[0]}.csv`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderOrderDetails = (order: ProductionOrderItem) => (
|
||||||
|
<div className="space-y-6 rounded-xl border border-dark-border bg-dark-input/25 p-5">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">Detalhes da OP</p>
|
||||||
|
<p className="mt-1 text-sm font-bold text-dark-text">{order.number || `OP #${order.id}`} · {order.productDescription}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-3 md:grid-cols-4">
|
||||||
|
<div className="rounded-xl border border-dark-border bg-dark-card p-3"><p className="text-[10px] font-bold uppercase tracking-wider text-dark-muted">Quantidade</p><p className="mt-1 text-lg font-bold text-dark-text">{formatQuantity(order.quantity)} <span className="text-xs text-dark-muted">{order.unit}</span></p></div>
|
||||||
|
<div className="relative rounded-xl border border-dark-border bg-dark-card p-3"><p className="text-[10px] font-bold uppercase tracking-wider text-dark-muted">Status</p><button type="button" onClick={() => setStatusEditorOrderId(current => current === order.id ? null : order.id)} aria-expanded={statusEditorOrderId === order.id} className={`mt-2 inline-flex cursor-pointer items-center gap-2 rounded-lg border px-2.5 py-1 text-xs font-bold ${getStatusStyle(order.status, order.statusLabel).className}`}><span className={`h-2 w-2 rounded-full ${getStatusStyle(order.status, order.statusLabel).dotClass}`} />{getStatusStyle(order.status, order.statusLabel).label}{statusEditorOrderId === order.id ? <ChevronUp className="h-3.5 w-3.5" /> : <ChevronDown className="h-3.5 w-3.5" />}</button>{statusEditorOrderId === order.id && <div className="absolute left-0 top-full z-20 mt-2 w-64 overflow-hidden rounded-xl border border-dark-border bg-dark-card shadow-xl"><p className="border-b border-dark-border px-3 py-2 text-[10px] font-bold uppercase tracking-wider text-dark-muted">Alterar status</p>{editableStatusOptions.map(option => { const style = getStatusStyle(option.value, option.label); const isCurrent = order.status === option.value; const Icon = option.value === 'finished' ? CheckCircle2 : option.value === 'canceled' ? XCircle : option.value === 'in_progress' ? PlayCircle : Clock3; return <button key={option.value} type="button" disabled={isCurrent || busyStatusOrderId === order.id} onClick={() => void handleStatusChange(order, option.value)} className={`flex w-full cursor-pointer items-center gap-3 px-3 py-2.5 text-left transition-colors disabled:cursor-not-allowed disabled:opacity-60 ${isCurrent ? style.className : 'hover:bg-dark-input/70'}`}><Icon className="h-4 w-4 shrink-0" /><span className="text-xs font-bold text-dark-text">{option.label}{isCurrent && ' · Atual'}</span></button>; })}</div>}</div>
|
||||||
|
<div className="rounded-xl border border-dark-border bg-dark-card p-3"><p className="text-[10px] font-bold uppercase tracking-wider text-dark-muted">Emissão</p><p className="mt-1 text-sm font-bold text-dark-text">{formatDate(order.issueDate)}</p></div>
|
||||||
|
<div className="rounded-xl border border-dark-border bg-dark-card p-3"><p className="text-[10px] font-bold uppercase tracking-wider text-dark-muted">Previsão</p><p className="mt-1 text-sm font-bold text-dark-text">{formatDate(order.expectedDate)}</p></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h3 className="text-xs font-bold uppercase tracking-widest text-dark-muted">Produto e marcadores</h3>
|
||||||
|
<div className="mt-3 rounded-xl border border-dark-border bg-dark-card p-4">
|
||||||
|
<p className="font-mono text-xs font-bold text-dark-muted">{order.productSku || 'Sem SKU'}</p>
|
||||||
|
{order.markers.length > 0 && <div className="mt-3 flex flex-wrap gap-1.5">{order.markers.map(marker => <span key={`${order.id}-${marker.label}`} className="inline-flex items-center gap-1.5 rounded-full border border-dark-border bg-dark-input px-2.5 py-1 text-[10px] font-bold text-dark-muted"><span className="h-1.5 w-1.5 rounded-full" style={{ backgroundColor: marker.color || 'var(--color-dark-muted)' }} />{marker.label}</span>)}</div>}
|
||||||
|
<div className="mt-4 flex flex-wrap gap-x-5 gap-y-2 text-xs font-semibold text-dark-muted"><span>Pedido: {order.orderReference || '-'}</span><span>Integração: {order.integrationStatus || 'Local'}</span></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h3 className="text-xs font-bold uppercase tracking-widest text-dark-muted">Composição</h3>
|
||||||
|
{order.components.length ? <div className="mt-3 overflow-hidden rounded-xl border border-dark-border"><table className="w-full text-xs"><thead className="bg-dark-header text-dark-muted"><tr><th className="px-3 py-2.5 text-left font-bold">Material</th><th className="px-3 py-2.5 text-right font-bold">Por unidade</th><th className="px-3 py-2.5 text-right font-bold">Total</th></tr></thead><tbody className="divide-y divide-dark-border">{order.components.map(component => <tr key={component.id}><td className="px-3 py-3"><p className="font-bold text-dark-text">{component.componentName}</p><p className="mt-0.5 font-mono text-[10px] font-semibold text-dark-muted">{component.componentSku || 'Sem SKU'}</p></td><td className="px-3 py-3 text-right font-semibold text-dark-muted">{formatQuantity(component.quantityPerUnit)} {component.unit}</td><td className="px-3 py-3 text-right font-bold text-dark-text">{formatQuantity(component.totalQuantity)} {component.unit}</td></tr>)}</tbody></table></div> : <p className="mt-3 rounded-xl border border-dark-border bg-dark-card px-4 py-3 text-sm font-semibold text-dark-muted">Sem composição sincronizada.</p>}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h3 className="text-xs font-bold uppercase tracking-widest text-dark-muted">Baixa de material</h3>
|
||||||
|
{order.status === 'in_progress' ? <form onSubmit={handleProductionExit} className="mt-3 rounded-xl border border-dark-border bg-dark-card p-4"><p className="text-sm font-semibold text-dark-muted">Registre uma saída somente quando o material for realmente consumido pela OP.</p><div className="mt-4 grid grid-cols-1 gap-3 sm:grid-cols-2"><label className="text-xs font-bold text-dark-muted">Lote de material<select value={exitForm.lotId} onChange={(event) => setExitForm(current => ({ ...current, lotId: event.target.value }))} className="mt-1.5 h-11 w-full rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-semibold text-dark-text outline-none focus:border-brand-primary"><option value="">Selecione...</option>{supplyLots.map(lot => <option key={lot.id} value={lot.id}>#{lot.id} · {lot.product} · {formatQuantity(lot.quantity)} {lot.unit}</option>)}</select></label><label className="text-xs font-bold text-dark-muted">Quantidade<input inputMode="decimal" value={exitForm.quantity} onChange={(event) => setExitForm(current => ({ ...current, quantity: event.target.value }))} className="mt-1.5 h-11 w-full rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-semibold text-dark-text outline-none placeholder:text-dark-muted focus:border-brand-primary" placeholder="kg" /></label><label className="sm:col-span-2 text-xs font-bold text-dark-muted">Motivo (opcional)<input value={exitForm.reason} onChange={(event) => setExitForm(current => ({ ...current, reason: event.target.value }))} className="mt-1.5 h-11 w-full rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-semibold text-dark-text outline-none placeholder:text-dark-muted focus:border-brand-primary" placeholder="Ex.: corte do pedido" /></label></div>{selectedLot && <p className="mt-3 text-xs font-bold text-dark-muted">Saldo do lote #{selectedLot.id}: {formatQuantity(selectedLot.quantity)} {selectedLot.unit}</p>}<button type="submit" disabled={isSupplyBusy || !supplyLots.length || !exitForm.lotId || !exitForm.quantity} className="mt-4 inline-flex h-11 items-center gap-2 rounded-lg border border-brand-primary/35 bg-brand-primary/10 px-4 text-sm font-bold text-brand-primary transition-colors hover:bg-brand-primary/20 disabled:cursor-not-allowed disabled:opacity-50"><PackageCheck className="h-4 w-4" />{isSupplyBusy ? 'Registrando...' : 'Registrar baixa'}</button>{supplyMessage && <p className="mt-3 rounded-lg border border-dark-border bg-dark-input px-3 py-2 text-sm font-bold text-dark-muted">{supplyMessage}</p>}</form> : <div className="mt-3 rounded-xl border border-dark-border bg-dark-card px-4 py-3 text-sm font-semibold text-dark-muted">Altere a OP para <span className="font-bold text-sky-300">Em andamento</span> antes de registrar a baixa de materiais.</div>}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{(order.steps.length > 0 || order.notes || order.supplier || order.lotCode || order.rollQuantity || order.fabricKg || order.ribKg || order.yieldPiecesPerKg) && <section><h3 className="text-xs font-bold uppercase tracking-widest text-dark-muted">Informações adicionais</h3><div className="mt-3 grid grid-cols-1 gap-3 sm:grid-cols-2">{(order.supplier || order.lotCode || order.rollQuantity || order.fabricKg || order.ribKg || order.yieldPiecesPerKg) && <dl className="grid grid-cols-2 gap-3 rounded-xl border border-dark-border bg-dark-card p-4 text-xs sm:col-span-2">{order.supplier && <div><dt className="font-bold text-dark-muted">Fornecedor</dt><dd className="mt-1 font-bold text-dark-text">{order.supplier}</dd></div>}{order.lotCode && <div><dt className="font-bold text-dark-muted">Lote</dt><dd className="mt-1 font-bold text-dark-text">{order.lotCode}</dd></div>}{order.rollQuantity !== null && <div><dt className="font-bold text-dark-muted">Rolos</dt><dd className="mt-1 font-bold text-dark-text">{formatQuantity(order.rollQuantity)}</dd></div>}{order.fabricKg !== null && <div><dt className="font-bold text-dark-muted">Malha</dt><dd className="mt-1 font-bold text-dark-text">{formatQuantity(order.fabricKg)} kg</dd></div>}{order.ribKg !== null && <div><dt className="font-bold text-dark-muted">Ribana</dt><dd className="mt-1 font-bold text-dark-text">{formatQuantity(order.ribKg)} kg</dd></div>}{order.yieldPiecesPerKg !== null && <div><dt className="font-bold text-dark-muted">Rendimento</dt><dd className="mt-1 font-bold text-dark-text">{formatQuantity(order.yieldPiecesPerKg)} un./kg</dd></div>}</dl>}{order.steps.length > 0 && <div className="rounded-xl border border-dark-border bg-dark-card p-4 sm:col-span-2"><p className="text-xs font-bold text-dark-text">Etapas</p><div className="mt-3 divide-y divide-dark-border">{order.steps.map(step => <div key={step.id} className="flex items-center gap-3 py-2 text-xs"><span className="h-2.5 w-2.5 rounded-full" style={{ backgroundColor: step.color || 'var(--color-brand-primary)' }} /><span className="font-bold text-dark-text">{step.name}</span><span className="ml-auto font-semibold text-dark-muted">{formatDate(step.startDate)} - {formatDate(step.endDate)}</span></div>)}</div></div>}{order.notes && <p className="whitespace-pre-wrap rounded-xl border border-dark-border bg-dark-card p-4 text-sm font-semibold text-dark-muted sm:col-span-2">{order.notes}</p>}</div></section>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex flex-col gap-4 xl:flex-row xl:items-center xl:justify-between">
|
<div className="flex flex-col gap-4 xl:flex-row xl:items-center xl:justify-between">
|
||||||
@@ -384,86 +415,6 @@ const ProductionOrders = () => {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{activeOrders.length > 0 && (
|
|
||||||
<form onSubmit={handleProductionExit} className="rounded-2xl border border-dark-border bg-dark-card p-5 shadow-sm">
|
|
||||||
<div className="flex flex-col gap-2 lg:flex-row lg:items-start lg:justify-between">
|
|
||||||
<div>
|
|
||||||
<h2 className="text-base font-bold text-dark-text">Baixa de material da OP</h2>
|
|
||||||
<p className="mt-1 text-sm font-semibold text-dark-muted">Consome um lote do estoque e registra a saída nas movimentações.</p>
|
|
||||||
</div>
|
|
||||||
{selectedLot && (
|
|
||||||
<span className="w-fit rounded-full border border-dark-border bg-dark-input px-2.5 py-1 text-xs font-bold text-dark-muted">
|
|
||||||
Saldo lote #{selectedLot.id}: {formatQuantity(selectedLot.quantity)} {selectedLot.unit}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="mt-4 grid grid-cols-1 gap-3 xl:grid-cols-[1.4fr_1.4fr_120px_1fr_auto]">
|
|
||||||
<label className="text-xs font-bold text-dark-muted">
|
|
||||||
Ordem de produção
|
|
||||||
<select
|
|
||||||
value={exitForm.orderId}
|
|
||||||
onChange={(event) => setExitForm(current => ({ ...current, orderId: event.target.value }))}
|
|
||||||
className="mt-1 h-10 w-full rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-semibold text-dark-text outline-none focus:border-brand-primary"
|
|
||||||
>
|
|
||||||
<option value="">Selecione...</option>
|
|
||||||
{activeOrders.map(order => (
|
|
||||||
<option key={order.id} value={order.id}>
|
|
||||||
{order.number || `#${order.id}`} · {order.productDescription}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label className="text-xs font-bold text-dark-muted">
|
|
||||||
Lote de material
|
|
||||||
<select
|
|
||||||
value={exitForm.lotId}
|
|
||||||
onChange={(event) => setExitForm(current => ({ ...current, lotId: event.target.value }))}
|
|
||||||
className="mt-1 h-10 w-full rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-semibold text-dark-text outline-none focus:border-brand-primary"
|
|
||||||
>
|
|
||||||
<option value="">Selecione...</option>
|
|
||||||
{supplyLots.map(lot => (
|
|
||||||
<option key={lot.id} value={lot.id}>
|
|
||||||
#{lot.id} · {lot.product} · {formatQuantity(lot.quantity)} {lot.unit}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label className="text-xs font-bold text-dark-muted">
|
|
||||||
Quantidade
|
|
||||||
<input
|
|
||||||
inputMode="decimal"
|
|
||||||
value={exitForm.quantity}
|
|
||||||
onChange={(event) => setExitForm(current => ({ ...current, quantity: event.target.value }))}
|
|
||||||
className="mt-1 h-10 w-full rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-semibold text-dark-text outline-none placeholder:text-dark-muted focus:border-brand-primary"
|
|
||||||
placeholder="kg"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label className="text-xs font-bold text-dark-muted">
|
|
||||||
Motivo
|
|
||||||
<input
|
|
||||||
value={exitForm.reason}
|
|
||||||
onChange={(event) => setExitForm(current => ({ ...current, reason: event.target.value }))}
|
|
||||||
className="mt-1 h-10 w-full rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-semibold text-dark-text outline-none placeholder:text-dark-muted focus:border-brand-primary"
|
|
||||||
placeholder="ex: corte do pedido"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={isSupplyBusy || !supplyLots.length}
|
|
||||||
className="mt-5 inline-flex h-10 items-center justify-center gap-2 rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-bold text-dark-text transition-colors hover:border-brand-primary disabled:cursor-not-allowed disabled:opacity-50"
|
|
||||||
>
|
|
||||||
<PackageCheck className="h-4 w-4 text-brand-primary" />
|
|
||||||
Baixar
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{supplyMessage && (
|
|
||||||
<div className="mt-3 rounded-lg border border-dark-border bg-dark-input px-3 py-2 text-sm font-bold text-dark-muted">
|
|
||||||
{supplyMessage}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="overflow-hidden rounded-2xl border border-dark-border bg-dark-card shadow-sm">
|
<div className="overflow-hidden rounded-2xl border border-dark-border bg-dark-card shadow-sm">
|
||||||
<div className="border-b border-dark-border p-5">
|
<div className="border-b border-dark-border p-5">
|
||||||
{statusMessage && (
|
{statusMessage && (
|
||||||
@@ -527,152 +478,59 @@ const ProductionOrders = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full min-w-[1180px] text-left text-sm">
|
<table className="w-full min-w-[820px] text-left text-sm">
|
||||||
<thead className="border-b border-dark-border bg-dark-header text-dark-muted">
|
<thead className="border-b border-dark-border bg-dark-header text-dark-muted">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-6 py-4 text-[10px] font-bold uppercase tracking-wider">Número</th>
|
<th className="px-6 py-4 text-[10px] font-bold uppercase tracking-wider">Número</th>
|
||||||
<th className="px-6 py-4 text-[10px] font-bold uppercase tracking-wider">Pedidos</th>
|
<th className="px-6 py-4 text-[10px] font-bold uppercase tracking-wider">Produto</th>
|
||||||
<th className="px-6 py-4 text-[10px] font-bold uppercase tracking-wider">Data</th>
|
|
||||||
<th className="px-6 py-4 text-[10px] font-bold uppercase tracking-wider">Data Prevista</th>
|
|
||||||
<th className="px-6 py-4 text-[10px] font-bold uppercase tracking-wider">SKU / Produto</th>
|
|
||||||
<th className="px-6 py-4 text-right text-[10px] font-bold uppercase tracking-wider">Quantidade</th>
|
<th className="px-6 py-4 text-right text-[10px] font-bold uppercase tracking-wider">Quantidade</th>
|
||||||
<th className="px-6 py-4 text-[10px] font-bold uppercase tracking-wider">Marcadores</th>
|
|
||||||
<th className="px-6 py-4 text-[10px] font-bold uppercase tracking-wider">Integrações</th>
|
|
||||||
<th className="px-6 py-4 text-[10px] font-bold uppercase tracking-wider">Status</th>
|
<th className="px-6 py-4 text-[10px] font-bold uppercase tracking-wider">Status</th>
|
||||||
|
<th className="px-6 py-4 text-right text-[10px] font-bold uppercase tracking-wider">Ações</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-dark-border">
|
<tbody className="divide-y divide-dark-border">
|
||||||
{paginatedOrders.map((order: ProductionOrderItem) => {
|
{paginatedOrders.map((order: ProductionOrderItem) => {
|
||||||
const statusStyle = getStatusStyle(order.status, order.statusLabel);
|
const statusStyle = getStatusStyle(order.status, order.statusLabel);
|
||||||
const showDetails = hasProductionOrderDetails(order);
|
|
||||||
return (
|
return (
|
||||||
<Fragment key={order.id}>
|
<Fragment key={order.id}>
|
||||||
<tr className="transition-colors hover:bg-dark-input/50">
|
<tr className="transition-colors hover:bg-dark-input/50">
|
||||||
<td className="px-6 py-3 font-mono text-xs font-bold text-dark-text">{order.number || '-'}</td>
|
<td className="px-6 py-3 font-mono text-xs font-bold text-dark-text">{order.number || '-'}</td>
|
||||||
<td className="px-6 py-3 text-xs font-semibold text-dark-muted">{order.orderReference || '-'}</td>
|
|
||||||
<td className="px-6 py-3 text-xs font-semibold text-dark-muted">
|
|
||||||
<span className="inline-flex items-center gap-1.5">
|
|
||||||
<CalendarDays className="h-3.5 w-3.5" />
|
|
||||||
{formatDate(order.issueDate)}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-3 text-xs font-semibold text-dark-muted">{formatDate(order.expectedDate)}</td>
|
|
||||||
<td className="px-6 py-3">
|
<td className="px-6 py-3">
|
||||||
<div className="font-bold text-dark-text">{order.productDescription}</div>
|
<div className="font-bold text-dark-text">{order.productDescription}</div>
|
||||||
<div className="mt-1 font-mono text-[10px] font-semibold text-dark-muted">{order.productSku || 'Sem SKU'}</div>
|
<div className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-[10px] font-semibold text-dark-muted">
|
||||||
|
<span className="font-mono">{order.productSku || 'Sem SKU'}</span>
|
||||||
|
<span>{formatDate(order.issueDate)}</span>
|
||||||
|
{order.orderReference && <span>{order.orderReference}</span>}
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-3 text-right">
|
<td className="px-6 py-3 text-right">
|
||||||
<span className="font-bold text-dark-text">{formatQuantity(order.quantity)}</span>
|
<span className="font-bold text-dark-text">{formatQuantity(order.quantity)}</span>
|
||||||
<span className="ml-1 text-xs font-semibold text-dark-muted">{order.unit}</span>
|
<span className="ml-1 text-xs font-semibold text-dark-muted">{order.unit}</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-3">
|
<td className="px-6 py-3">
|
||||||
{order.markers.length ? (
|
<span className={`inline-flex min-h-9 items-center gap-2 rounded-lg border px-3 text-xs font-bold ${statusStyle.className}`}>
|
||||||
<div className="flex max-w-52 flex-wrap gap-1.5">
|
<span className={`h-2 w-2 rounded-full ${statusStyle.dotClass}`} />
|
||||||
{order.markers.map(marker => (
|
{statusStyle.label}
|
||||||
<span
|
|
||||||
key={`${order.id}-${marker.label}`}
|
|
||||||
className="inline-flex items-center gap-1 rounded-full border border-dark-border bg-dark-input px-2 py-0.5 text-[10px] font-bold text-dark-muted"
|
|
||||||
>
|
|
||||||
<span className="h-1.5 w-1.5 rounded-full" style={{ backgroundColor: marker.color || 'var(--color-dark-muted)' }} />
|
|
||||||
{marker.label}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<span className="text-xs font-semibold text-dark-muted">-</span>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-3">
|
|
||||||
<span className="inline-flex items-center gap-2 text-xs font-semibold text-dark-muted">
|
|
||||||
<span className="h-2 w-2 rounded-full bg-sky-400" />
|
|
||||||
{order.integrationStatus || 'Tiny'}
|
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-3">
|
<td className="px-6 py-3 text-right">
|
||||||
<label className="sr-only" htmlFor={`production-order-status-${order.id}`}>Status da OP</label>
|
<button
|
||||||
<select
|
type="button"
|
||||||
id={`production-order-status-${order.id}`}
|
onClick={() => {
|
||||||
value={order.status}
|
setSelectedOrderId(current => current === order.id ? null : order.id);
|
||||||
disabled={busyStatusOrderId === order.id}
|
setSupplyMessage('');
|
||||||
onChange={(event) => void handleStatusChange(order, event.target.value as ProductionOrderStatusTab)}
|
setExitForm({ lotId: '', quantity: '', reason: '' });
|
||||||
className={`h-8 rounded-lg border bg-dark-input px-2 text-[10px] font-bold uppercase tracking-wide outline-none transition-colors focus:border-brand-primary disabled:cursor-not-allowed disabled:opacity-50 ${statusStyle.className}`}
|
}}
|
||||||
|
title={selectedOrderId === order.id ? 'Fechar detalhes da OP' : 'Abrir detalhes da OP'}
|
||||||
|
aria-label={`${selectedOrderId === order.id ? 'Fechar' : 'Abrir'} detalhes da OP ${order.number || order.id}`}
|
||||||
|
aria-expanded={selectedOrderId === order.id}
|
||||||
|
className={`inline-flex h-9 w-9 cursor-pointer items-center justify-center rounded-lg border transition-colors hover:border-brand-primary hover:text-brand-primary ${selectedOrderId === order.id ? 'border-brand-primary bg-brand-primary/10 text-brand-primary' : 'border-dark-border bg-dark-input text-dark-muted'}`}
|
||||||
>
|
>
|
||||||
{editableStatusOptions.map(option => (
|
{selectedOrderId === order.id ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
||||||
<option key={option.value} value={option.value}>{option.label}</option>
|
</button>
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{showDetails && (
|
{selectedOrderId === order.id && <tr className="bg-dark-input/20"><td colSpan={5} className="px-6 pb-5 pt-3">{renderOrderDetails(order)}</td></tr>}
|
||||||
<tr className="bg-dark-input/20">
|
|
||||||
<td colSpan={9} className="px-6 pb-5 pt-1">
|
|
||||||
<div className="grid grid-cols-1 gap-4 rounded-xl border border-dark-border bg-dark-card/80 p-4 xl:grid-cols-[1.4fr_1fr_1fr]">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-xs font-bold uppercase tracking-widest text-dark-muted">Composição</h3>
|
|
||||||
{order.components.length ? (
|
|
||||||
<div className="mt-3 overflow-hidden rounded-lg border border-dark-border">
|
|
||||||
<table className="w-full text-xs">
|
|
||||||
<thead className="bg-dark-header text-dark-muted">
|
|
||||||
<tr>
|
|
||||||
<th className="px-3 py-2 text-left font-bold">Produto</th>
|
|
||||||
<th className="px-3 py-2 text-left font-bold">SKU</th>
|
|
||||||
<th className="px-3 py-2 text-right font-bold">Qtd.</th>
|
|
||||||
<th className="px-3 py-2 text-right font-bold">Total</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="divide-y divide-dark-border">
|
|
||||||
{order.components.map(component => (
|
|
||||||
<tr key={component.id}>
|
|
||||||
<td className="px-3 py-2 font-semibold text-dark-text">{component.componentName}</td>
|
|
||||||
<td className="px-3 py-2 font-mono text-dark-muted">{component.componentSku || '-'}</td>
|
|
||||||
<td className="px-3 py-2 text-right font-semibold text-dark-muted">{formatQuantity(component.quantityPerUnit)} {component.unit}</td>
|
|
||||||
<td className="px-3 py-2 text-right font-bold text-dark-text">{formatQuantity(component.totalQuantity)} {component.unit}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<p className="mt-3 text-xs font-semibold text-dark-muted">Sem composição sincronizada.</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h3 className="text-xs font-bold uppercase tracking-widest text-dark-muted">Etapas</h3>
|
|
||||||
{order.steps.length ? (
|
|
||||||
<div className="mt-3 divide-y divide-dark-border rounded-lg border border-dark-border">
|
|
||||||
{order.steps.map(step => (
|
|
||||||
<div key={step.id} className="grid grid-cols-[32px_1fr_auto] items-center gap-3 px-3 py-2 text-xs">
|
|
||||||
<span className="font-mono font-bold text-dark-muted">{step.stepNumber ?? '-'}</span>
|
|
||||||
<div>
|
|
||||||
<p className="font-bold text-dark-text">{step.name}</p>
|
|
||||||
<p className="mt-0.5 font-semibold text-dark-muted">{formatDate(step.startDate)} - {formatDate(step.endDate)}</p>
|
|
||||||
</div>
|
|
||||||
<span className="h-2.5 w-2.5 rounded-full" style={{ backgroundColor: step.color || 'var(--color-brand-primary)' }} title={step.status || 'Sem status'} />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<p className="mt-3 text-xs font-semibold text-dark-muted">Sem etapas sincronizadas.</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h3 className="text-xs font-bold uppercase tracking-widest text-dark-muted">Observações</h3>
|
|
||||||
<dl className="mt-3 grid grid-cols-2 gap-2 text-xs">
|
|
||||||
<div><dt className="font-bold text-dark-muted">Fornecedor</dt><dd className="mt-0.5 font-semibold text-dark-text">{order.supplier || '-'}</dd></div>
|
|
||||||
<div><dt className="font-bold text-dark-muted">Lote</dt><dd className="mt-0.5 font-semibold text-dark-text">{order.lotCode || '-'}</dd></div>
|
|
||||||
<div><dt className="font-bold text-dark-muted">Rolos</dt><dd className="mt-0.5 font-semibold text-dark-text">{formatOptionalQuantity(order.rollQuantity)}</dd></div>
|
|
||||||
<div><dt className="font-bold text-dark-muted">Malha kg</dt><dd className="mt-0.5 font-semibold text-dark-text">{formatOptionalQuantity(order.fabricKg)}</dd></div>
|
|
||||||
<div><dt className="font-bold text-dark-muted">Ribana kg</dt><dd className="mt-0.5 font-semibold text-dark-text">{formatOptionalQuantity(order.ribKg)}</dd></div>
|
|
||||||
<div><dt className="font-bold text-dark-muted">Rendimento</dt><dd className="mt-0.5 font-semibold text-dark-text">{formatOptionalQuantity(order.yieldPiecesPerKg)}</dd></div>
|
|
||||||
</dl>
|
|
||||||
{order.notes && <p className="mt-3 whitespace-pre-wrap rounded-lg border border-dark-border bg-dark-input p-3 text-xs font-semibold text-dark-muted">{order.notes}</p>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -713,6 +571,7 @@ const ProductionOrders = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user