Add Tiny production order detail sync

This commit is contained in:
Cauê Faleiros
2026-07-23 10:42:53 -03:00
parent 615e07ad32
commit c07938c94e
6 changed files with 667 additions and 101 deletions

View File

@@ -1,4 +1,4 @@
import { 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 { ArrowLeft, CalendarDays, CheckCircle2, ClipboardList, Clock3, Download, PackageCheck, Search } from 'lucide-react';
import DateRangePicker from '../components/DateRangePicker';
@@ -63,6 +63,16 @@ const formatQuantity = (value: number) => (
}).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) => (
statusStyles[String(status)] || {
label: fallbackLabel || 'Em aberto',
@@ -500,63 +510,136 @@ const ProductionOrders = () => {
<tbody className="divide-y divide-dark-border">
{paginatedOrders.map((order: ProductionOrderItem) => {
const statusStyle = getStatusStyle(order.status, order.statusLabel);
const showDetails = hasProductionOrderDetails(order);
return (
<tr key={order.id} 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 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">
<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>
</td>
<td className="px-6 py-3 text-right">
<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>
</td>
<td className="px-6 py-3">
{order.markers.length ? (
<div className="flex max-w-52 flex-wrap gap-1.5">
{order.markers.map(marker => (
<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>
<Fragment key={order.id}>
<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 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">
<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>
</td>
<td className="px-6 py-3 text-right">
<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>
</td>
<td className="px-6 py-3">
{order.markers.length ? (
<div className="flex max-w-52 flex-wrap gap-1.5">
{order.markers.map(marker => (
<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>
</td>
<td className="px-6 py-3">
<label className="sr-only" htmlFor={`production-order-status-${order.id}`}>Status da OP</label>
<select
id={`production-order-status-${order.id}`}
value={order.status}
disabled={busyStatusOrderId === order.id}
onChange={(event) => void handleStatusChange(order, event.target.value as ProductionOrderStatusTab)}
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}`}
>
{editableStatusOptions.map(option => (
<option key={option.value} value={option.value}>{option.label}</option>
))}
</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>
</td>
<td className="px-6 py-3">
<label className="sr-only" htmlFor={`production-order-status-${order.id}`}>Status da OP</label>
<select
id={`production-order-status-${order.id}`}
value={order.status}
disabled={busyStatusOrderId === order.id}
onChange={(event) => void handleStatusChange(order, event.target.value as ProductionOrderStatusTab)}
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}`}
>
{editableStatusOptions.map(option => (
<option key={option.value} value={option.value}>{option.label}</option>
))}
</select>
</td>
</tr>
</select>
</td>
</tr>
{showDetails && (
<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>
);
})}
</tbody>

View File

@@ -34,6 +34,26 @@ export interface ProductionOrderMarker {
color?: string | null;
}
export interface ProductionOrderComponent {
id: number;
componentTinyId: string;
componentSku: string;
componentName: string;
quantityPerUnit: number;
totalQuantity: number;
unit: string;
}
export interface ProductionOrderStep {
id: number;
stepNumber: number | null;
name: string;
startDate: string | null;
endDate: string | null;
status: string;
color: string;
}
export interface ProductionOrderItem {
id: number;
tinyId: string;
@@ -48,7 +68,16 @@ export interface ProductionOrderItem {
quantity: number;
unit: string;
integrationStatus: string;
notes: string;
supplier: string;
lotCode: string;
rollQuantity: number | null;
fabricKg: number | null;
ribKg: number | null;
yieldPiecesPerKg: number | null;
markers: ProductionOrderMarker[];
components: ProductionOrderComponent[];
steps: ProductionOrderStep[];
createdAt: string | null;
updatedAt: string | null;
}