Add status tabs to production plans
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 48s

This commit is contained in:
Cauê Faleiros
2026-08-28 09:26:02 -03:00
parent 6b3f84a666
commit 56ce81850b

View File

@@ -13,6 +13,18 @@ const statusOptions: Array<{ value: ProductionOrderStatus; label: string; classN
{ value: 'canceled', label: 'Cancelada', className: 'border-zinc-500/35 bg-zinc-500/10 text-zinc-400' }
];
type ProductionPlanStatusTab = 'all' | 'open' | 'cutting' | 'sewing' | 'review' | 'finished' | 'canceled';
const statusTabs: Array<{ key: ProductionPlanStatusTab; label: string; dotClass: string }> = [
{ key: 'all', label: 'Todos', dotClass: 'bg-dark-muted' },
{ key: 'open', label: 'Em aberto', dotClass: 'bg-amber-400' },
{ key: 'cutting', label: 'Em corte', dotClass: 'bg-amber-300' },
{ key: 'sewing', label: 'Em costura', dotClass: 'bg-sky-400' },
{ key: 'review', label: 'Em revisão', dotClass: 'bg-violet-400' },
{ key: 'finished', label: 'Finalizada', dotClass: 'bg-emerald-400' },
{ key: 'canceled', label: 'Cancelada', dotClass: 'bg-zinc-500' }
];
const statusStyle = (status: string) => statusOptions.find(option => option.value === status)?.className || 'border-dark-border bg-dark-input text-dark-muted';
const formatNumber = (value: number) => new Intl.NumberFormat('pt-BR', { maximumFractionDigits: 2 }).format(value);
const formatDate = (value: string | null) => value ? new Intl.DateTimeFormat('pt-BR').format(new Date(`${value.slice(0, 10)}T00:00:00`)) : '-';
@@ -32,6 +44,7 @@ const ProductionPlans = () => {
const [responsiblePickerPlanId, setResponsiblePickerPlanId] = useState<number | null>(null);
const [reviewingPlanId, setReviewingPlanId] = useState<number | null>(null);
const [isReviewUserPickerOpen, setIsReviewUserPickerOpen] = useState(false);
const [statusFilter, setStatusFilter] = useState<ProductionPlanStatusTab>('all');
const [reviewForm, setReviewForm] = useState({
decision: 'pending' as ProductionPlanReviewDecision,
materialsReserved: false,
@@ -77,6 +90,9 @@ const ProductionPlans = () => {
const activePlans = useMemo(() => plans.filter(plan => !['finished', 'canceled'].includes(plan.status)), [plans]);
const totalOpenQuantity = useMemo(() => activePlans.reduce((total, plan) => total + plan.totalQuantity, 0), [activePlans]);
const reviewingPlan = useMemo(() => plans.find(plan => plan.id === reviewingPlanId) || null, [plans, reviewingPlanId]);
const filteredPlans = useMemo(() => (
statusFilter === 'all' ? plans : plans.filter(plan => plan.status === statusFilter)
), [plans, statusFilter]);
const refreshPlans = async () => {
setIsRefreshing(true);
@@ -234,10 +250,10 @@ const ProductionPlans = () => {
{message && <div className="rounded-xl border border-dark-border bg-dark-card px-4 py-3 text-sm font-bold text-dark-muted">{message}</div>}
<div className="rounded-2xl border border-dark-border bg-dark-card shadow-sm">
<div className="flex flex-col gap-2 border-b border-dark-border px-5 py-4 sm:flex-row sm:items-center sm:justify-between"><div><h2 className="font-bold text-dark-text">Fila de planos</h2><p className="mt-1 text-xs font-semibold text-dark-muted">Mover um plano atualiza as OPs locais ativas. OPs finalizadas ou canceladas preservam o histórico.</p></div><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">{plans.length} {plans.length === 1 ? 'plano' : 'planos'}</span></div>
{isLoading ? <div className="flex min-h-56 items-center justify-center text-brand-primary"><Loader2 className="h-7 w-7 animate-spin" /></div> : plans.length ? (
<div className="border-b border-dark-border px-5 py-4"><div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between"><div><h2 className="font-bold text-dark-text">Fila de planos</h2><p className="mt-1 text-xs font-semibold text-dark-muted">Mover um plano atualiza as OPs locais ativas. OPs finalizadas ou canceladas preservam o histórico.</p></div><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">{filteredPlans.length} {filteredPlans.length === 1 ? 'plano' : 'planos'}</span></div><div className="mt-4 flex gap-1 overflow-x-auto" role="tablist" aria-label="Status dos planos">{statusTabs.map(tab => { const count = tab.key === 'all' ? plans.length : plans.filter(plan => plan.status === tab.key).length; const isSelected = statusFilter === tab.key; return <button key={tab.key} type="button" role="tab" aria-selected={isSelected} onClick={() => setStatusFilter(tab.key)} className={`shrink-0 border-b-2 px-3 py-2 text-left text-xs font-bold transition-colors cursor-pointer ${isSelected ? 'border-brand-primary text-dark-text' : 'border-transparent text-dark-muted hover:text-dark-text'}`}><span className="flex items-center gap-2"><span className={`h-2 w-2 rounded-full ${tab.dotClass}`} />{tab.label}</span><span className="mt-1 block text-sm text-dark-text">{count}</span></button>; })}</div></div>
{isLoading ? <div className="flex min-h-56 items-center justify-center text-brand-primary"><Loader2 className="h-7 w-7 animate-spin" /></div> : filteredPlans.length ? (
<div className="divide-y divide-dark-border">
{plans.map(plan => (
{filteredPlans.map(plan => (
<div key={plan.id} className="p-5">
<div className="min-w-0"><div className="flex flex-wrap items-center gap-2"><span className="font-mono text-xs font-bold text-brand-primary">{plan.number}</span><span className={`rounded-full border px-2.5 py-1 text-xs font-bold ${statusStyle(plan.status)}`}>{plan.statusLabel}</span>{plan.review && <span className={`rounded-full border px-2 py-1 text-[10px] font-bold ${reviewDecisionStyle(plan.review.decision)}`}>{reviewDecisionLabel[plan.review.decision]}</span>}</div><p className="mt-2 text-base font-bold text-dark-text">{plan.name}</p>{plan.notes && <p className="mt-1 max-w-3xl text-xs font-semibold leading-5 text-dark-muted">{plan.notes}</p>}</div>
<div className="mt-4 grid gap-3 border-t border-dark-border pt-4 sm:grid-cols-2 xl:grid-cols-4">
@@ -250,7 +266,7 @@ const ProductionPlans = () => {
</div>
))}
</div>
) : <div className="flex min-h-56 flex-col items-center justify-center p-8 text-center"><CheckCircle2 className="h-8 w-8 text-brand-primary" /><h3 className="mt-3 font-bold text-dark-text">Nenhum plano confirmado</h3><p className="mt-1 text-sm font-semibold text-dark-muted">Gere OPs a partir do Plano de Corte para criar o primeiro plano PCP.</p></div>}
) : <div className="flex min-h-56 flex-col items-center justify-center p-8 text-center"><CheckCircle2 className="h-8 w-8 text-brand-primary" /><h3 className="mt-3 font-bold text-dark-text">{plans.length ? 'Nenhum plano neste status' : 'Nenhum plano confirmado'}</h3><p className="mt-1 text-sm font-semibold text-dark-muted">{plans.length ? 'Escolha outro status para visualizar os demais planos.' : 'Gere OPs a partir do Plano de Corte para criar o primeiro plano PCP.'}</p></div>}
</div>
{reviewingPlan && <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 p-4 backdrop-blur-sm" role="dialog" aria-modal="true" aria-label={`Checklist do plano ${reviewingPlan.number}`} onClick={() => busyPlanId !== reviewingPlan.id && setReviewingPlanId(null)}>