Add local production order workflow
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m40s

This commit is contained in:
Cauê Faleiros
2026-07-20 12:57:28 -03:00
parent 9af5f296a7
commit 88089343a8
6 changed files with 384 additions and 13 deletions

View File

@@ -4,7 +4,7 @@ import { ArrowLeft, CalendarDays, CheckCircle2, ClipboardList, Clock3, Download,
import DateRangePicker from '../components/DateRangePicker';
import PaginationControls from '../components/PaginationControls';
import RefreshStatus from '../components/RefreshStatus';
import { consumeSupplyLotForProduction, exportToCSV, fetchProductionOrders, fetchSupplySummary } from '../dataService';
import { consumeSupplyLotForProduction, exportToCSV, fetchProductionOrders, fetchSupplySummary, updateProductionOrderStatus } from '../dataService';
import type { DateRange, ProductionOrderItem, ProductionOrderStatus, ProductionOrderSummary, SupplyLot } from '../types';
type ProductionOrderStatusTab = 'all' | 'open' | 'in_progress' | 'finished' | 'canceled';
@@ -22,6 +22,10 @@ const statusTabs: Array<{ key: ProductionOrderStatusTab; label: string; dotClass
{ key: 'canceled', label: 'Cancelada', dotClass: 'bg-zinc-500' }
];
const editableStatusOptions: Array<{ value: ProductionOrderStatusTab; label: string }> = statusTabs
.filter(tab => tab.key !== 'all')
.map(tab => ({ value: tab.key, label: tab.label }));
const statusStyles: Record<string, { label: string; className: string; dotClass: string }> = {
open: {
label: 'Em aberto',
@@ -121,6 +125,8 @@ const ProductionOrders = () => {
const [isLoading, setIsLoading] = useState(true);
const [isSupplyBusy, setIsSupplyBusy] = useState(false);
const [supplyMessage, setSupplyMessage] = useState('');
const [busyStatusOrderId, setBusyStatusOrderId] = useState<number | null>(null);
const [statusMessage, setStatusMessage] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(20);
const [exitForm, setExitForm] = useState({
@@ -190,6 +196,23 @@ const ProductionOrders = () => {
void loadProductionOrders({ force: true });
};
const handleStatusChange = async (order: ProductionOrderItem, status: ProductionOrderStatusTab) => {
if (status === 'all' || status === order.status) return;
setBusyStatusOrderId(order.id);
setStatusMessage('');
try {
await updateProductionOrderStatus(order.id, status);
const nextSummary = await fetchProductionOrders(dateRange, { search: searchTerm }, { force: true });
setSummary(nextSummary);
setStatusMessage(`OP ${order.number || `#${order.id}`} atualizada para ${statusStyles[status]?.label || status}.`);
} catch (error) {
setStatusMessage(error instanceof Error ? error.message : 'Não foi possível atualizar o status da OP.');
} finally {
setBusyStatusOrderId(null);
}
};
const refreshSupplyLots = async () => {
const supplySummary = await fetchSupplySummary();
setSupplyLots(supplySummary.lots);
@@ -248,7 +271,7 @@ const ProductionOrders = () => {
Suprimentos
</Link>
<h1 className="text-2xl font-bold text-dark-text">Ordens de Produção</h1>
<p className="mt-2 text-dark-muted font-medium">Acompanhe as ordens de produção sincronizadas do Tiny.</p>
<p className="mt-2 text-dark-muted font-medium">Acompanhe as ordens locais geradas pelo plano de corte.</p>
</div>
<DateRangePicker
dateRange={dateRange}
@@ -399,6 +422,11 @@ const ProductionOrders = () => {
<div className="overflow-hidden rounded-2xl border border-dark-border bg-dark-card shadow-sm">
<div className="border-b border-dark-border p-5">
{statusMessage && (
<div className="mb-4 rounded-xl border border-dark-border bg-dark-input px-3 py-2 text-sm font-bold text-dark-muted">
{statusMessage}
</div>
)}
<div className="flex flex-col gap-4 xl:flex-row xl:items-center xl:justify-between">
<div className="relative w-full xl:max-w-xl">
<Search className="absolute left-3 top-1/2 h-5 w-5 -translate-y-1/2 text-dark-muted" />
@@ -515,10 +543,18 @@ const ProductionOrders = () => {
</span>
</td>
<td className="px-6 py-3">
<span className={`inline-flex items-center gap-2 rounded-full border px-2.5 py-1 text-[10px] font-bold uppercase tracking-wide ${statusStyle.className}`}>
<span className={`h-1.5 w-1.5 rounded-full ${statusStyle.dotClass}`} />
{statusStyle.label}
</span>
<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>
);
@@ -534,7 +570,7 @@ const ProductionOrders = () => {
</div>
<p className="mt-4 text-sm font-bold text-dark-text">Nenhuma ordem de produção encontrada.</p>
<p className="mt-1 max-w-md text-sm font-medium text-dark-muted">
Quando a sincronização com o Tiny estiver ativa, as ordens aparecerão aqui com status, produto, quantidade e marcadores.
Gere ordens pelo Plano de Corte para acompanhar status, produto, quantidade e baixa de material dentro do Graphs.
</p>
</div>
)}