Improve production plan operations
This commit is contained in:
@@ -900,9 +900,9 @@ const updateProductionPlanStatus = async (id, status) => {
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
const planResult = await client.query(`
|
||||
UPDATE production_plans
|
||||
SET status = $1,
|
||||
completed_at = CASE WHEN $1 = 'finished' THEN COALESCE(completed_at, CURRENT_TIMESTAMP) ELSE NULL END,
|
||||
UPDATE production_plans
|
||||
SET status = $1::text,
|
||||
completed_at = CASE WHEN $1::text = 'finished' THEN COALESCE(completed_at, CURRENT_TIMESTAMP) ELSE NULL END,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $2
|
||||
RETURNING id;
|
||||
@@ -913,9 +913,9 @@ const updateProductionPlanStatus = async (id, status) => {
|
||||
throw error;
|
||||
}
|
||||
await client.query(`
|
||||
UPDATE production_orders
|
||||
SET status = $1,
|
||||
completed_date = CASE WHEN $1 = 'finished' THEN COALESCE(completed_date, CURRENT_DATE) ELSE completed_date END,
|
||||
UPDATE production_orders
|
||||
SET status = $1::text,
|
||||
completed_date = CASE WHEN $1::text = 'finished' THEN COALESCE(completed_date, CURRENT_DATE) ELSE completed_date END,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE production_plan_id = $2
|
||||
AND status NOT IN ('finished', 'canceled');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ArrowLeft, Check, CheckCircle2, ChevronDown, ClipboardCheck, ClipboardList, Factory, Loader2, PlayCircle, UserRound, X, type LucideIcon } from 'lucide-react';
|
||||
import { ArrowLeft, CalendarDays, Check, CheckCircle2, ChevronDown, ClipboardCheck, ClipboardList, Factory, Loader2, PlayCircle, RefreshCw, UserRound, X, type LucideIcon } from 'lucide-react';
|
||||
import { fetchOperationalUsers, fetchProductionPlans, saveProductionPlanReview, updateProductionPlan, updateProductionPlanStatus } from '../dataService';
|
||||
import type { OperationalUser, ProductionOrderStatus, ProductionPlan, ProductionPlanReviewDecision } from '../types';
|
||||
|
||||
@@ -25,6 +25,8 @@ const planMetrics: Array<{ label: string; value: string | number; icon: LucideIc
|
||||
const ProductionPlans = () => {
|
||||
const [plans, setPlans] = useState<ProductionPlan[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [isLoadingOperationalUsers, setIsLoadingOperationalUsers] = useState(false);
|
||||
const [busyPlanId, setBusyPlanId] = useState<number | null>(null);
|
||||
const [operationalUsers, setOperationalUsers] = useState<OperationalUser[]>([]);
|
||||
const [responsiblePickerPlanId, setResponsiblePickerPlanId] = useState<number | null>(null);
|
||||
@@ -52,7 +54,12 @@ const ProductionPlans = () => {
|
||||
}, []);
|
||||
|
||||
const loadOperationalUsers = useCallback(async () => {
|
||||
setOperationalUsers(await fetchOperationalUsers());
|
||||
setIsLoadingOperationalUsers(true);
|
||||
try {
|
||||
setOperationalUsers(await fetchOperationalUsers());
|
||||
} finally {
|
||||
setIsLoadingOperationalUsers(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -63,6 +70,20 @@ 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 refreshPlans = async () => {
|
||||
setIsRefreshing(true);
|
||||
try {
|
||||
await Promise.all([loadPlans(), loadOperationalUsers()]);
|
||||
} finally {
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleResponsiblePicker = (planId: number) => {
|
||||
setResponsiblePickerPlanId(current => current === planId ? null : planId);
|
||||
if (!operationalUsers.length) void loadOperationalUsers();
|
||||
};
|
||||
|
||||
const movePlan = async (plan: ProductionPlan, status: ProductionOrderStatus) => {
|
||||
if (status === plan.status) return;
|
||||
setBusyPlanId(plan.id);
|
||||
@@ -150,14 +171,15 @@ const ProductionPlans = () => {
|
||||
: 'border-dark-border bg-dark-input text-dark-muted';
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-5">
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
|
||||
<div>
|
||||
<Link to="/production-orders" className="mb-3 inline-flex items-center gap-2 text-sm font-bold text-zinc-500 transition-colors hover:text-zinc-900 dark:text-dark-muted dark:hover:text-dark-text"><ArrowLeft className="h-4 w-4" />Ordens de Produção</Link>
|
||||
<h1 className="text-2xl font-bold text-zinc-900 dark:text-dark-text">Planos de Produção</h1>
|
||||
<p className="mt-2 font-medium text-zinc-500 dark:text-dark-muted">Controle PCP acima das fichas: cada plano agrupa OPs, materiais reservados e a movimentação da operação.</p>
|
||||
<p className="text-[11px] font-bold uppercase tracking-[0.16em] text-brand-primary">PCP</p>
|
||||
<h1 className="mt-1 text-2xl font-bold text-zinc-900 dark:text-dark-text">Planos de Produção</h1>
|
||||
<p className="mt-2 max-w-3xl font-medium text-zinc-500 dark:text-dark-muted">Acompanhe cada frente de produção, o responsável e o estágio das OPs sem perder o histórico do plano.</p>
|
||||
</div>
|
||||
<button type="button" onClick={() => void loadPlans()} className="inline-flex h-10 items-center justify-center rounded-xl border border-dark-border bg-dark-card px-4 text-sm font-bold text-dark-text transition-colors hover:border-brand-primary cursor-pointer">Atualizar</button>
|
||||
<button type="button" onClick={() => void refreshPlans()} disabled={isRefreshing} title="Atualizar planos" aria-label="Atualizar planos" className="inline-flex h-10 w-10 items-center justify-center rounded-xl border border-dark-border bg-dark-card text-dark-muted transition-colors hover:border-brand-primary hover:text-brand-primary disabled:cursor-not-allowed disabled:opacity-60 cursor-pointer"><RefreshCw className={`h-4 w-4 ${isRefreshing ? 'animate-spin' : ''}`} /></button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
@@ -168,25 +190,30 @@ const ProductionPlans = () => {
|
||||
? activePlans.reduce((total, plan) => total + plan.orderCount, 0)
|
||||
: formatNumber(totalOpenQuantity);
|
||||
const MetricIcon = metric.icon;
|
||||
return <div key={metric.label} className="rounded-2xl border border-dark-border bg-dark-card p-5 shadow-sm"><div className="flex items-center justify-between"><p className="text-sm font-bold text-dark-muted">{metric.label}</p><MetricIcon className={`h-5 w-5 ${metric.tone}`} /></div><p className="mt-3 text-3xl font-bold text-dark-text">{value}</p></div>;
|
||||
const description = metric.label === 'Planos ativos' ? 'Frentes em acompanhamento' : metric.label === 'OPs planejadas' ? 'Ordens vinculadas aos planos ativos' : 'Quantidade ainda em operação';
|
||||
return <div key={metric.label} className="rounded-2xl border border-dark-border bg-dark-card p-5 shadow-sm"><div className="flex items-center justify-between"><p className="text-sm font-bold text-dark-muted">{metric.label}</p><span className={`rounded-lg border border-dark-border bg-dark-input p-2 ${metric.tone}`}><MetricIcon className="h-4 w-4" /></span></div><p className="mt-3 text-3xl font-bold text-dark-text">{value}</p><p className="mt-1 text-xs font-semibold text-dark-muted">{description}</p></div>;
|
||||
})}
|
||||
</div>
|
||||
|
||||
{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="overflow-hidden rounded-2xl border border-dark-border bg-dark-card shadow-sm">
|
||||
<div className="border-b border-dark-border px-5 py-4"><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 todas as OPs locais que continuam ativas; OPs concluídas e canceladas preservam seu histórico.</p></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="divide-y divide-dark-border">
|
||||
{plans.map(plan => (
|
||||
<div key={plan.id} className="p-5">
|
||||
<div className="grid gap-4 xl:grid-cols-[minmax(240px,1.3fr)_minmax(150px,.7fr)_minmax(160px,.7fr)_minmax(190px,.75fr)_180px] xl:items-center">
|
||||
<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 truncate font-bold text-dark-text" title={plan.name}>{plan.name}</p><p className="mt-1 text-xs font-semibold text-dark-muted">{plan.responsibleName ? `Responsável: ${plan.responsibleName}` : plan.contractor ? `Responsável: ${plan.contractor}` : 'Sem responsável definido'} · previsão {formatDate(plan.expectedDate)}</p>{plan.notes && <p className="mt-1 truncate text-xs font-semibold text-dark-muted" title={plan.notes}>{plan.notes}</p>}</div>
|
||||
<div className="text-sm"><p className="text-xs font-bold uppercase tracking-wider text-dark-muted">Escopo</p><p className="mt-1 font-bold text-dark-text">{plan.orderCount} {plan.orderCount === 1 ? 'OP' : 'OPs'} · {formatNumber(plan.totalQuantity)} un.</p><p className="mt-1 text-xs font-semibold text-dark-muted">Cortado {formatNumber(plan.actualCutQuantity)} · final {formatNumber(plan.finishedQuantity)}</p></div>
|
||||
<div className="flex flex-wrap gap-1.5">{statusOptions.filter(option => Number(plan.statusCounts[option.value] || 0) > 0).map(option => <span key={option.value} className={`rounded-full border px-2 py-1 text-xs font-bold ${statusStyle(option.value)}`}>{plan.statusCounts[option.value]} {option.label.toLowerCase()}</span>)}</div>
|
||||
<div className="relative text-xs font-bold text-dark-muted"><p>Responsável</p><button type="button" disabled={busyPlanId === plan.id} onClick={() => setResponsiblePickerPlanId(current => current === plan.id ? null : plan.id)} className="mt-1 flex h-10 w-full items-center justify-between gap-2 rounded-lg border border-dark-border bg-dark-input px-3 text-left text-sm font-semibold text-dark-text transition-colors hover:border-brand-primary disabled:cursor-not-allowed disabled:opacity-60 cursor-pointer"><span className="flex min-w-0 items-center gap-2"><UserRound className="h-4 w-4 shrink-0 text-brand-primary" /><span className="truncate">{plan.responsibleName || 'Selecionar responsável'}</span></span><ChevronDown className={`h-4 w-4 shrink-0 text-dark-muted transition-transform ${responsiblePickerPlanId === plan.id ? 'rotate-180' : ''}`} /></button>{responsiblePickerPlanId === plan.id && <div className="absolute right-0 z-20 mt-2 w-full min-w-[270px] overflow-hidden rounded-xl border border-dark-border bg-dark-card shadow-xl"><div className="border-b border-dark-border px-3 py-2 text-[10px] font-bold uppercase tracking-wider text-dark-muted">Usuários ativos do Graphs</div><div className="max-h-56 overflow-y-auto p-1.5">{operationalUsers.map(user => <button key={user.id ?? 'super-admin'} type="button" onClick={() => void changeResponsible(plan, user)} className="flex w-full items-center justify-between gap-3 rounded-lg px-2.5 py-2 text-left text-sm font-semibold text-dark-text transition-colors hover:bg-dark-input cursor-pointer"><span className="truncate">{user.name}</span>{plan.responsibleUserId === user.id && plan.responsibleName === user.name && <Check className="h-4 w-4 shrink-0 text-brand-primary" />}</button>)}{!operationalUsers.length && <p className="px-2 py-3 text-xs font-semibold text-dark-muted">Nenhum usuário ativo encontrado.</p>}</div>{plan.responsibleName && <button type="button" onClick={() => void changeResponsible(plan, null)} className="w-full border-t border-dark-border px-3 py-2 text-left text-xs font-bold text-dark-muted transition-colors hover:bg-dark-input hover:text-dark-text cursor-pointer">Remover responsável</button>}</div>}</div>
|
||||
<div className="flex gap-2 xl:flex-col"><button type="button" disabled={busyPlanId === plan.id} onClick={() => openReview(plan)} className="inline-flex h-10 flex-1 items-center justify-center gap-2 rounded-lg border border-dark-border bg-dark-input px-3 text-xs font-bold text-dark-text transition-colors hover:border-brand-primary disabled:opacity-60 cursor-pointer"><ClipboardCheck className="h-4 w-4 text-brand-primary" />Checklist</button><label className="flex-1 text-xs font-bold text-dark-muted">Mover plano<select value={plan.status} disabled={busyPlanId === plan.id} onChange={(event) => void movePlan(plan, event.target.value)} className="mt-1 h-10 w-full rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-bold text-dark-text outline-none focus:border-brand-primary disabled:opacity-60 cursor-pointer"><option value={plan.status}>{busyPlanId === plan.id ? 'Atualizando...' : plan.statusLabel}</option>{statusOptions.filter(option => option.value !== plan.status).map(option => <option key={option.value} value={option.value}>{option.label}</option>)}</select></label></div>
|
||||
<div className="flex flex-col gap-4 xl:flex-row xl:items-start xl:justify-between">
|
||||
<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="flex shrink-0 flex-wrap gap-2"><button type="button" disabled={busyPlanId === plan.id} onClick={() => openReview(plan)} className="inline-flex h-10 items-center justify-center gap-2 rounded-lg border border-dark-border bg-dark-input px-3 text-xs font-bold text-dark-text transition-colors hover:border-brand-primary disabled:cursor-not-allowed disabled:opacity-60 cursor-pointer"><ClipboardCheck className="h-4 w-4 text-brand-primary" />Checklist</button><label className="min-w-[156px] text-[10px] font-bold uppercase tracking-wider text-dark-muted">Mover plano<select value={plan.status} disabled={busyPlanId === plan.id} onChange={(event) => void movePlan(plan, event.target.value as ProductionOrderStatus)} className="mt-1 h-10 w-full rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-bold normal-case tracking-normal text-dark-text outline-none focus:border-brand-primary disabled:opacity-60 cursor-pointer"><option value={plan.status}>{busyPlanId === plan.id ? 'Atualizando...' : plan.statusLabel}</option>{statusOptions.filter(option => option.value !== plan.status).map(option => <option key={option.value} value={option.value}>{option.label}</option>)}</select></label></div>
|
||||
</div>
|
||||
<div className="mt-4 grid gap-3 border-t border-dark-border pt-4 sm:grid-cols-2 xl:grid-cols-4">
|
||||
<div className="rounded-xl border border-dark-border bg-dark-input/45 p-3"><p className="text-[10px] font-bold uppercase tracking-wider text-dark-muted">Escopo</p><p className="mt-1 text-sm font-bold text-dark-text">{plan.orderCount} {plan.orderCount === 1 ? 'OP' : 'OPs'} · {formatNumber(plan.totalQuantity)} un.</p><p className="mt-1 text-xs font-semibold text-dark-muted">Corte previsto pelo plano</p></div>
|
||||
<div className="rounded-xl border border-dark-border bg-dark-input/45 p-3"><p className="text-[10px] font-bold uppercase tracking-wider text-dark-muted">Andamento</p><p className="mt-1 text-sm font-bold text-dark-text">Cortado {formatNumber(plan.actualCutQuantity)} · final {formatNumber(plan.finishedQuantity)}</p><div className="mt-2 flex flex-wrap gap-1.5">{statusOptions.filter(option => Number(plan.statusCounts[option.value] || 0) > 0).map(option => <span key={option.value} className={`rounded-full border px-2 py-0.5 text-[10px] font-bold ${statusStyle(option.value)}`}>{plan.statusCounts[option.value]} {option.label.toLowerCase()}</span>)}</div></div>
|
||||
<div className="rounded-xl border border-dark-border bg-dark-input/45 p-3"><p className="flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-wider text-dark-muted"><CalendarDays className="h-3.5 w-3.5" />Prazo</p><p className="mt-1 text-sm font-bold text-dark-text">{formatDate(plan.expectedDate)}</p><p className="mt-1 text-xs font-semibold text-dark-muted">Início: {formatDate(plan.plannedStartDate)}</p></div>
|
||||
<div className="rounded-xl border border-dark-border bg-dark-input/45 p-3"><p className="text-[10px] font-bold uppercase tracking-wider text-dark-muted">Responsável</p><button type="button" disabled={busyPlanId === plan.id} onClick={() => toggleResponsiblePicker(plan.id)} className="mt-1 flex h-9 w-full items-center justify-between gap-2 rounded-lg border border-dark-border bg-dark-card px-2.5 text-left text-sm font-semibold text-dark-text transition-colors hover:border-brand-primary disabled:cursor-not-allowed disabled:opacity-60 cursor-pointer"><span className="flex min-w-0 items-center gap-2"><UserRound className="h-4 w-4 shrink-0 text-brand-primary" /><span className="truncate">{plan.responsibleName || 'Selecionar responsável'}</span></span><ChevronDown className={`h-4 w-4 shrink-0 text-dark-muted transition-transform ${responsiblePickerPlanId === plan.id ? 'rotate-180' : ''}`} /></button><p className="mt-1 text-xs font-semibold text-dark-muted">{plan.contractor || 'Sem facção definida'}</p></div>
|
||||
</div>
|
||||
{responsiblePickerPlanId === plan.id && <div className="mt-3 rounded-xl border border-brand-primary/25 bg-brand-primary/[0.045] p-3"><div className="flex items-center justify-between gap-3"><div><p className="text-sm font-bold text-dark-text">Atribuir responsável</p><p className="mt-0.5 text-xs font-semibold text-dark-muted">Selecione um usuário ativo do Graphs para acompanhar este plano.</p></div><button type="button" onClick={() => setResponsiblePickerPlanId(null)} className="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-dark-border bg-dark-card text-dark-muted transition-colors hover:text-dark-text cursor-pointer" aria-label="Fechar responsáveis"><X className="h-4 w-4" /></button></div><div className="mt-3 grid gap-2 sm:grid-cols-2 xl:grid-cols-3">{isLoadingOperationalUsers ? <div className="flex items-center gap-2 px-2 py-3 text-xs font-bold text-dark-muted"><Loader2 className="h-4 w-4 animate-spin text-brand-primary" />Carregando usuários...</div> : operationalUsers.map(user => <button key={user.id ?? 'super-admin'} type="button" disabled={busyPlanId === plan.id} onClick={() => void changeResponsible(plan, user)} className="flex min-h-10 items-center justify-between gap-3 rounded-lg border border-dark-border bg-dark-card px-3 py-2 text-left text-sm font-semibold text-dark-text transition-colors hover:border-brand-primary hover:bg-dark-input disabled:opacity-60 cursor-pointer"><span className="truncate">{user.name}</span>{plan.responsibleUserId === user.id && plan.responsibleName === user.name && <Check className="h-4 w-4 shrink-0 text-brand-primary" />}</button>)}{!isLoadingOperationalUsers && !operationalUsers.length && <p className="px-2 py-3 text-xs font-semibold text-dark-muted">Nenhum usuário ativo encontrado. Cadastre ou reative um usuário para atribuir o plano.</p>}</div>{plan.responsibleName && <button type="button" disabled={busyPlanId === plan.id} onClick={() => void changeResponsible(plan, null)} className="mt-3 inline-flex h-9 items-center rounded-lg border border-dark-border bg-dark-card px-3 text-xs font-bold text-dark-muted transition-colors hover:border-red-400/40 hover:text-red-300 disabled:opacity-60 cursor-pointer">Remover responsável</button>}</div>}
|
||||
{reviewingPlanId === plan.id && <div className="mt-5 rounded-xl border border-brand-primary/25 bg-brand-primary/[0.045] p-4"><div className="flex items-start justify-between gap-4"><div><h3 className="text-sm font-bold text-dark-text">Liberação operacional</h3><p className="mt-1 text-xs font-semibold text-dark-muted">Registre a decisão antes de avançar o plano. Este registro não muda o estoque nem substitui o recebimento.</p></div><button type="button" onClick={() => setReviewingPlanId(null)} className="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-dark-border bg-dark-input text-dark-muted hover:text-dark-text cursor-pointer" aria-label="Fechar checklist"><X className="h-4 w-4" /></button></div><div className="mt-4 grid gap-3 md:grid-cols-2 xl:grid-cols-4">{[{ key: 'materialsReserved', label: 'Materiais reservados' }, { key: 'cutSpecificationReady', label: 'Ficha e corte definidos' }, { key: 'sizeAndColorReviewed', label: 'Tamanhos e cores revisados' }, { key: 'responsibleAndDateDefined', label: 'Responsável e data definidos' }].map(item => <label key={item.key} className="flex items-center gap-2 rounded-lg border border-dark-border bg-dark-card px-3 py-2.5 text-xs font-bold text-dark-text"><input type="checkbox" checked={reviewForm[item.key as keyof Pick<typeof reviewForm, 'materialsReserved' | 'cutSpecificationReady' | 'sizeAndColorReviewed' | 'responsibleAndDateDefined'>]} onChange={(event) => setReviewForm(current => ({ ...current, [item.key]: event.target.checked }))} className="h-4 w-4 accent-brand-primary" />{item.label}</label>)}</div><div className="mt-4 grid gap-3 md:grid-cols-[180px_minmax(0,1fr)_minmax(0,1fr)_auto]"><label className="text-xs font-bold text-dark-muted">Decisão<select value={reviewForm.decision} onChange={(event) => setReviewForm(current => ({ ...current, decision: event.target.value as ProductionPlanReviewDecision }))} className="mt-1 h-10 w-full rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-bold text-dark-text outline-none focus:border-brand-primary"><option value="pending">Em revisão</option><option value="approved">Liberar para operação</option><option value="adjustments_required">Solicitar ajustes</option></select></label><label className="text-xs font-bold text-dark-muted">Revisado por<input value={reviewForm.reviewedBy} onChange={(event) => setReviewForm(current => ({ ...current, reviewedBy: 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" placeholder="Nome do usuário" /></label><label className="text-xs font-bold text-dark-muted">Observação<input value={reviewForm.notes} onChange={(event) => setReviewForm(current => ({ ...current, notes: 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" placeholder="Ajuste, orientação ou motivo" /></label><button type="button" disabled={busyPlanId === plan.id} onClick={() => void submitReview(plan)} className="mt-5 inline-flex h-10 items-center justify-center gap-2 rounded-lg bg-brand-primary px-4 text-xs font-bold text-brand-contrast disabled:opacity-60 cursor-pointer"><Check className="h-4 w-4" />Salvar</button></div></div>}
|
||||
</div>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user