Add Tiny production order detail sync
This commit is contained in:
@@ -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>
|
||||
|
||||
29
src/types.ts
29
src/types.ts
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user