feat: mapa termico de kits de corte e liberacao balanceada para solda
This commit is contained in:
@@ -0,0 +1,53 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import { MapaTermicoKitsView } from './MapaTermicoKitsView';
|
||||||
|
import { PecaKitInfo } from '@/hooks/useKitsProducao';
|
||||||
|
import { Flame } from 'lucide-react';
|
||||||
|
|
||||||
|
interface MapaTermicoKitsModalProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
ofNumber?: string;
|
||||||
|
fase?: string;
|
||||||
|
onSelectPecaParaApontar?: (peca: PecaKitInfo) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MapaTermicoKitsModal: React.FC<MapaTermicoKitsModalProps> = ({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
ofNumber,
|
||||||
|
fase,
|
||||||
|
onSelectPecaParaApontar,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto p-4 sm:p-6">
|
||||||
|
<DialogHeader className="pb-2 border-b border-slate-200 dark:border-slate-800">
|
||||||
|
<DialogTitle className="flex items-center gap-2 text-lg sm:text-xl font-black text-slate-900 dark:text-slate-100 uppercase tracking-tight">
|
||||||
|
<Flame className="w-5 h-5 text-orange-500 fill-orange-500 animate-pulse" />
|
||||||
|
<span>Mapa Térmico de Kits de Montagem</span>
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="py-2">
|
||||||
|
<MapaTermicoKitsView
|
||||||
|
initialOF={ofNumber}
|
||||||
|
initialFase={fase || 'all'}
|
||||||
|
readOnlyOF={Boolean(ofNumber)}
|
||||||
|
onSelectPecaParaApontar={(peca) => {
|
||||||
|
if (onSelectPecaParaApontar) {
|
||||||
|
onSelectPecaParaApontar(peca);
|
||||||
|
onOpenChange(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,574 @@
|
|||||||
|
import React, { useState, useMemo } from 'react';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import {
|
||||||
|
Flame,
|
||||||
|
CheckCircle2,
|
||||||
|
AlertTriangle,
|
||||||
|
Clock,
|
||||||
|
Layers,
|
||||||
|
Search,
|
||||||
|
RefreshCw,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronUp,
|
||||||
|
PackageCheck,
|
||||||
|
ShieldCheck,
|
||||||
|
Wrench,
|
||||||
|
Scissors
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { useKitsProducao, PecaKitInfo } from '@/hooks/useKitsProducao';
|
||||||
|
import { useOFs } from '@/hooks/useOFs';
|
||||||
|
|
||||||
|
interface MapaTermicoKitsViewProps {
|
||||||
|
initialOF?: string;
|
||||||
|
initialFase?: string;
|
||||||
|
readOnlyOF?: boolean;
|
||||||
|
onSelectPecaParaApontar?: (peca: PecaKitInfo) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MapaTermicoKitsView: React.FC<MapaTermicoKitsViewProps> = ({
|
||||||
|
initialOF = '',
|
||||||
|
initialFase = 'all',
|
||||||
|
readOnlyOF = false,
|
||||||
|
onSelectPecaParaApontar,
|
||||||
|
}) => {
|
||||||
|
const { ofs, loading: loadingOFs } = useOFs();
|
||||||
|
const [selectedOF, setSelectedOF] = useState<string>(initialOF || (ofs[0]?.of_number || ''));
|
||||||
|
const [selectedFase, setSelectedFase] = useState<string>(initialFase);
|
||||||
|
const [busca, setBusca] = useState<string>('');
|
||||||
|
const [filtroStatus, setFiltroStatus] = useState<'todos' | 'liberados' | 'parciais' | 'travados'>('todos');
|
||||||
|
const [pecasExpandidas, setPecasExpandidas] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
|
// Atualizar OF padrão se carregar depois
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!selectedOF && ofs.length > 0) {
|
||||||
|
setSelectedOF(ofs[0].of_number);
|
||||||
|
}
|
||||||
|
}, [ofs, selectedOF]);
|
||||||
|
|
||||||
|
const { listaKits, resumoKits, loading, refetch } = useKitsProducao(selectedOF, selectedFase);
|
||||||
|
|
||||||
|
// Extrair fases únicas da lista de kits carregada
|
||||||
|
const fasesDisponiveis = useMemo(() => {
|
||||||
|
const s = new Set<string>();
|
||||||
|
listaKits.forEach((k) => {
|
||||||
|
if (k.etapa_fase) s.add(k.etapa_fase);
|
||||||
|
});
|
||||||
|
return Array.from(s).sort((a, b) => a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' }));
|
||||||
|
}, [listaKits]);
|
||||||
|
|
||||||
|
// Alternar expansão de componentes de uma peça
|
||||||
|
const toggleExpand = (pecaId: string) => {
|
||||||
|
setPecasExpandidas((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(pecaId)) {
|
||||||
|
next.delete(pecaId);
|
||||||
|
} else {
|
||||||
|
next.add(pecaId);
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const expandirTodas = () => {
|
||||||
|
setPecasExpandidas(new Set(listaKits.map((k) => k.peca_id)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const recolherTodas = () => {
|
||||||
|
setPecasExpandidas(new Set());
|
||||||
|
};
|
||||||
|
|
||||||
|
// Filtragem
|
||||||
|
const pecasFiltradas = useMemo(() => {
|
||||||
|
return listaKits.filter((item) => {
|
||||||
|
// Filtro de status
|
||||||
|
if (filtroStatus === 'liberados') {
|
||||||
|
if (item.kits_completos_cortados <= 0) return false;
|
||||||
|
} else if (filtroStatus === 'parciais') {
|
||||||
|
if (
|
||||||
|
item.kits_completos_cortados >= item.quantidade_total ||
|
||||||
|
item.status_termico === 'vermelho'
|
||||||
|
)
|
||||||
|
return false;
|
||||||
|
} else if (filtroStatus === 'travados') {
|
||||||
|
if (item.status_termico !== 'vermelho') return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtro de busca textual
|
||||||
|
if (busca.trim()) {
|
||||||
|
const t = busca.toLowerCase().trim();
|
||||||
|
const bateMarca = item.marca.toLowerCase().includes(t);
|
||||||
|
const bateDesc = (item.descricao || '').toLowerCase().includes(t);
|
||||||
|
const batePerfil = (item.perfil_principal || '').toLowerCase().includes(t);
|
||||||
|
const bateComp = item.componentes.some((c) =>
|
||||||
|
c.marca_componente.toLowerCase().includes(t) || (c.perfil || '').toLowerCase().includes(t)
|
||||||
|
);
|
||||||
|
if (!bateMarca && !bateDesc && !batePerfil && !bateComp) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}, [listaKits, filtroStatus, busca]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4 w-full">
|
||||||
|
{/* ──────────────── Barra de Controles e Seletores ──────────────── */}
|
||||||
|
<Card className="border border-slate-200 dark:border-slate-800 bg-white/90 dark:bg-slate-900/90 shadow-sm">
|
||||||
|
<CardContent className="p-4 space-y-3">
|
||||||
|
<div className="flex items-center justify-between gap-3 flex-wrap">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="p-2 rounded-xl bg-orange-100 text-orange-600 dark:bg-orange-950/60 dark:text-orange-400">
|
||||||
|
<Flame className="w-5 h-5 animate-pulse" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-black text-slate-900 dark:text-slate-100 tracking-tight text-base sm:text-lg flex items-center gap-2">
|
||||||
|
<span>Mapa Térmico de Kits de Produção</span>
|
||||||
|
<Badge variant="outline" className="border-orange-300 text-orange-700 dark:border-orange-800 dark:text-orange-400 text-xs font-bold">
|
||||||
|
Corte ➔ Solda
|
||||||
|
</Badge>
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs text-slate-500 dark:text-slate-400">
|
||||||
|
Prontidão física de montagem: kits cortados disponíveis para liberar a Solda
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => refetch()}
|
||||||
|
disabled={loading}
|
||||||
|
className="text-xs gap-1.5 h-8 font-semibold"
|
||||||
|
>
|
||||||
|
<RefreshCw className={`w-3.5 h-3.5 ${loading ? 'animate-spin' : ''}`} />
|
||||||
|
<span>Atualizar</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 pt-1">
|
||||||
|
{/* Seletor de OF */}
|
||||||
|
<div>
|
||||||
|
<label className="text-xs font-bold text-slate-600 dark:text-slate-400 block mb-1">
|
||||||
|
Ordem de Fabricação (OF)
|
||||||
|
</label>
|
||||||
|
{readOnlyOF ? (
|
||||||
|
<div className="h-9 px-3 rounded-lg border border-slate-200 dark:border-slate-800 bg-slate-100 dark:bg-slate-800 flex items-center font-bold text-xs">
|
||||||
|
OF {selectedOF}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Select value={selectedOF} onValueChange={setSelectedOF} disabled={loadingOFs}>
|
||||||
|
<SelectTrigger className="h-9 text-xs font-semibold">
|
||||||
|
<SelectValue placeholder="Selecione uma OF..." />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{ofs.map((o) => (
|
||||||
|
<SelectItem key={o.id} value={o.of_number}>
|
||||||
|
OF {o.of_number} {o.cliente ? `• ${o.cliente}` : ''}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Seletor de Fase */}
|
||||||
|
<div>
|
||||||
|
<label className="text-xs font-bold text-slate-600 dark:text-slate-400 block mb-1">
|
||||||
|
Fase da OF
|
||||||
|
</label>
|
||||||
|
<Select value={selectedFase} onValueChange={setSelectedFase}>
|
||||||
|
<SelectTrigger className="h-9 text-xs font-semibold">
|
||||||
|
<SelectValue placeholder="Todas as fases" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">Todas as fases</SelectItem>
|
||||||
|
{fasesDisponiveis.map((f) => (
|
||||||
|
<SelectItem key={f} value={f}>
|
||||||
|
{f.toLowerCase().startsWith('fase') ? f : `Fase ${f}`}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Busca Rápida */}
|
||||||
|
<div>
|
||||||
|
<label className="text-xs font-bold text-slate-600 dark:text-slate-400 block mb-1">
|
||||||
|
Buscar Marca / Componente
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="w-3.5 h-3.5 absolute left-3 top-3 text-slate-400" />
|
||||||
|
<Input
|
||||||
|
placeholder="Ex: V1, p2, W200..."
|
||||||
|
value={busca}
|
||||||
|
onChange={(e) => setBusca(e.target.value)}
|
||||||
|
className="h-9 pl-9 text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* ──────────────── Cards de KPIs do Mapa Térmico ──────────────── */}
|
||||||
|
{resumoKits && (
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-2.5 sm:gap-3">
|
||||||
|
{/* KPI 1: 100% Liberadas */}
|
||||||
|
<div className="p-3.5 rounded-2xl bg-emerald-50/80 dark:bg-emerald-950/40 border border-emerald-200 dark:border-emerald-800 shadow-sm flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div className="text-[11px] font-black uppercase text-emerald-700 dark:text-emerald-400">
|
||||||
|
100% Liberadas
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-black text-emerald-900 dark:text-emerald-100 mt-0.5">
|
||||||
|
{resumoKits.totalKits100Porcento}
|
||||||
|
</div>
|
||||||
|
<div className="text-[10px] text-emerald-700/80 dark:text-emerald-400/80 font-medium">
|
||||||
|
peças com todos os kits cortados
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-2.5 rounded-xl bg-emerald-100 text-emerald-700 dark:bg-emerald-900/60 dark:text-emerald-300">
|
||||||
|
<CheckCircle2 className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* KPI 2: Kits Montáveis Liberados */}
|
||||||
|
<div className="p-3.5 rounded-2xl bg-blue-50/80 dark:bg-blue-950/40 border border-blue-200 dark:border-blue-800 shadow-sm flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div className="text-[11px] font-black uppercase text-blue-700 dark:text-blue-400">
|
||||||
|
Unidades Liberadas
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-black text-blue-900 dark:text-blue-100 mt-0.5">
|
||||||
|
{resumoKits.totalUnidadesLiberadasSolda} <span className="text-xs text-blue-600 font-bold">/ {resumoKits.totalUnidadesTotal} un</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-[10px] text-blue-700/80 dark:text-blue-400/80 font-medium">
|
||||||
|
kits prontos na bancada de solda
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-2.5 rounded-xl bg-blue-100 text-blue-700 dark:bg-blue-900/60 dark:text-blue-300">
|
||||||
|
<PackageCheck className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* KPI 3: Em Andamento / Quase Prontos */}
|
||||||
|
<div className="p-3.5 rounded-2xl bg-amber-50/80 dark:bg-amber-950/40 border border-amber-200 dark:border-amber-800 shadow-sm flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div className="text-[11px] font-black uppercase text-amber-700 dark:text-amber-400">
|
||||||
|
Corte em Andamento
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-black text-amber-900 dark:text-amber-100 mt-0.5">
|
||||||
|
{resumoKits.totalKitsParciais}
|
||||||
|
</div>
|
||||||
|
<div className="text-[10px] text-amber-700/80 dark:text-amber-400/80 font-medium">
|
||||||
|
kits parciais (prioridade de corte)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-2.5 rounded-xl bg-amber-100 text-amber-700 dark:bg-amber-900/60 dark:text-amber-300">
|
||||||
|
<Clock className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* KPI 4: Travados / 0% */}
|
||||||
|
<div className="p-3.5 rounded-2xl bg-rose-50/80 dark:bg-rose-950/40 border border-rose-200 dark:border-rose-800 shadow-sm flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div className="text-[11px] font-black uppercase text-rose-700 dark:text-rose-400">
|
||||||
|
Não Iniciados
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-black text-rose-900 dark:text-rose-100 mt-0.5">
|
||||||
|
{resumoKits.totalKitsNaoIniciados}
|
||||||
|
</div>
|
||||||
|
<div className="text-[10px] text-rose-700/80 dark:text-rose-400/80 font-medium">
|
||||||
|
sem nenhum componente cortado
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-2.5 rounded-xl bg-rose-100 text-rose-700 dark:bg-rose-900/60 dark:text-rose-300">
|
||||||
|
<AlertTriangle className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ──────────────── Filtros por Status Térmico & Ações em Massa ──────────────── */}
|
||||||
|
<div className="flex items-center justify-between gap-2 flex-wrap text-xs">
|
||||||
|
<div className="flex items-center gap-1.5 flex-wrap">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setFiltroStatus('todos')}
|
||||||
|
className={`px-3 py-1.5 rounded-xl font-bold transition-colors ${
|
||||||
|
filtroStatus === 'todos'
|
||||||
|
? 'bg-slate-900 text-white dark:bg-white dark:text-slate-900 shadow-sm'
|
||||||
|
: 'bg-slate-100 hover:bg-slate-200 text-slate-700 dark:bg-slate-800 dark:hover:bg-slate-700 dark:text-slate-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Todas ({listaKits.length})
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setFiltroStatus('liberados')}
|
||||||
|
className={`px-3 py-1.5 rounded-xl font-bold flex items-center gap-1.5 transition-colors ${
|
||||||
|
filtroStatus === 'liberados'
|
||||||
|
? 'bg-emerald-600 text-white shadow-sm'
|
||||||
|
: 'bg-emerald-50 text-emerald-800 hover:bg-emerald-100 dark:bg-emerald-950/50 dark:text-emerald-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="w-2 h-2 rounded-full bg-emerald-400"></span>
|
||||||
|
Liberadas para Solda ({listaKits.filter((k) => k.kits_completos_cortados > 0).length})
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setFiltroStatus('parciais')}
|
||||||
|
className={`px-3 py-1.5 rounded-xl font-bold flex items-center gap-1.5 transition-colors ${
|
||||||
|
filtroStatus === 'parciais'
|
||||||
|
? 'bg-amber-600 text-white shadow-sm'
|
||||||
|
: 'bg-amber-50 text-amber-800 hover:bg-amber-100 dark:bg-amber-950/50 dark:text-amber-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="w-2 h-2 rounded-full bg-amber-400"></span>
|
||||||
|
Em Andamento / Quase Prontas ({listaKits.filter((k) => k.status_termico === 'amarelo' || (k.kits_completos_cortados > 0 && k.kits_completos_cortados < k.quantidade_total)).length})
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setFiltroStatus('travados')}
|
||||||
|
className={`px-3 py-1.5 rounded-xl font-bold flex items-center gap-1.5 transition-colors ${
|
||||||
|
filtroStatus === 'travados'
|
||||||
|
? 'bg-rose-600 text-white shadow-sm'
|
||||||
|
: 'bg-rose-50 text-rose-800 hover:bg-rose-100 dark:bg-rose-950/50 dark:text-rose-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="w-2 h-2 rounded-full bg-rose-400"></span>
|
||||||
|
Travadas no Corte ({listaKits.filter((k) => k.status_termico === 'vermelho').length})
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button variant="ghost" size="sm" onClick={expandirTodas} className="h-7 text-[11px] font-semibold text-slate-600 dark:text-slate-400">
|
||||||
|
Expandir Todos
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="sm" onClick={recolherTodas} className="h-7 text-[11px] font-semibold text-slate-600 dark:text-slate-400">
|
||||||
|
Recolher Todos
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ──────────────── Lista de Peças com Mapa Térmico ──────────────── */}
|
||||||
|
{loading ? (
|
||||||
|
<div className="py-16 flex flex-col items-center justify-center text-slate-400 gap-3">
|
||||||
|
<div className="h-10 w-10 animate-spin rounded-full border-4 border-orange-500 border-t-transparent" />
|
||||||
|
<span className="text-sm font-semibold">Calculando matriz de kits e completude...</span>
|
||||||
|
</div>
|
||||||
|
) : pecasFiltradas.length === 0 ? (
|
||||||
|
<Card className="border border-dashed border-slate-300 dark:border-slate-800 p-8 text-center bg-slate-50/50 dark:bg-slate-900/50">
|
||||||
|
<Layers className="w-10 h-10 text-slate-400 mx-auto mb-2 opacity-60" />
|
||||||
|
<h4 className="font-bold text-slate-700 dark:text-slate-300 text-sm">Nenhuma peça encontrada</h4>
|
||||||
|
<p className="text-xs text-slate-500 mt-1">Tente ajustar a busca ou os filtros de status</p>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{pecasFiltradas.map((peca) => {
|
||||||
|
const isExpanded = pecasExpandidas.has(peca.peca_id);
|
||||||
|
const totalKits = peca.quantidade_total;
|
||||||
|
const kitsProntos = peca.kits_completos_cortados;
|
||||||
|
const pct = peca.percentual_termico;
|
||||||
|
const eh100 = kitsProntos >= totalKits;
|
||||||
|
const temParcial = kitsProntos > 0 && kitsProntos < totalKits;
|
||||||
|
const gargalo = peca.componente_gargalo;
|
||||||
|
|
||||||
|
// Cores térmicas
|
||||||
|
let barColor = 'bg-rose-500';
|
||||||
|
let badgeBg = 'bg-rose-100 text-rose-800 border-rose-300 dark:bg-rose-950/60 dark:text-rose-300 dark:border-rose-800';
|
||||||
|
if (eh100) {
|
||||||
|
barColor = 'bg-emerald-500';
|
||||||
|
badgeBg = 'bg-emerald-100 text-emerald-800 border-emerald-300 dark:bg-emerald-950/60 dark:text-emerald-300 dark:border-emerald-800';
|
||||||
|
} else if (temParcial) {
|
||||||
|
barColor = 'bg-emerald-500';
|
||||||
|
badgeBg = 'bg-blue-100 text-blue-800 border-blue-300 dark:bg-blue-950/60 dark:text-blue-300 dark:border-blue-800';
|
||||||
|
} else if (pct >= 40) {
|
||||||
|
barColor = 'bg-amber-500';
|
||||||
|
badgeBg = 'bg-amber-100 text-amber-800 border-amber-300 dark:bg-amber-950/60 dark:text-amber-300 dark:border-amber-800';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={peca.peca_id}
|
||||||
|
className="rounded-2xl border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900/90 shadow-sm overflow-hidden transition-all hover:border-slate-300 dark:hover:border-slate-700"
|
||||||
|
>
|
||||||
|
{/* Linha Principal do Card */}
|
||||||
|
<div
|
||||||
|
onClick={() => toggleExpand(peca.peca_id)}
|
||||||
|
className="p-4 cursor-pointer flex flex-col sm:flex-row sm:items-center justify-between gap-3 hover:bg-slate-50/50 dark:hover:bg-slate-800/40 transition-colors select-none"
|
||||||
|
>
|
||||||
|
{/* Lado Esquerdo: Identificação da Peça */}
|
||||||
|
<div className="flex items-start gap-3 min-w-0">
|
||||||
|
<div
|
||||||
|
className={`w-10 h-10 rounded-xl flex items-center justify-center shrink-0 font-black text-sm shadow-sm ${
|
||||||
|
eh100
|
||||||
|
? 'bg-emerald-500 text-white'
|
||||||
|
: temParcial
|
||||||
|
? 'bg-blue-500 text-white'
|
||||||
|
: pct >= 40
|
||||||
|
? 'bg-amber-500 text-slate-950'
|
||||||
|
: 'bg-slate-200 dark:bg-slate-800 text-slate-600 dark:text-slate-400'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{pct}%
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="min-w-0 space-y-0.5">
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
<span className="text-lg font-black text-slate-900 dark:text-slate-100 tracking-wide">
|
||||||
|
{peca.marca}
|
||||||
|
</span>
|
||||||
|
<Badge variant="outline" className="text-[11px] font-bold">
|
||||||
|
{peca.etapa_fase.toLowerCase().startsWith('fase') ? peca.etapa_fase : `Fase ${peca.etapa_fase}`}
|
||||||
|
</Badge>
|
||||||
|
{peca.perfil_principal && (
|
||||||
|
<span className="text-xs px-2 py-0.5 rounded bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 font-semibold font-mono">
|
||||||
|
{peca.perfil_principal}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{peca.tem_componentes === false && (
|
||||||
|
<Badge variant="secondary" className="bg-purple-100 text-purple-800 dark:bg-purple-950 dark:text-purple-300 text-[10px] font-black">
|
||||||
|
S/M (Sem Montagem)
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-xs text-slate-500 dark:text-slate-400 line-clamp-1">
|
||||||
|
{peca.descricao || 'Peça Estrutural'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Lado Direito: Status dos Kits e Métricas */}
|
||||||
|
<div className="flex items-center gap-3 shrink-0 justify-between sm:justify-end">
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="flex items-center justify-end gap-1.5 flex-wrap">
|
||||||
|
<span className={`text-xs font-black px-2.5 py-0.5 rounded-full border ${badgeBg}`}>
|
||||||
|
{eh100
|
||||||
|
? `✅ 100% Liberado (${kitsProntos}/${totalKits} un)`
|
||||||
|
: temParcial
|
||||||
|
? `🟢 ${kitsProntos}/${totalKits} kits liberados na Solda`
|
||||||
|
: pct >= 40
|
||||||
|
? `🟡 0/${totalKits} kits (Corte ${pct}%)`
|
||||||
|
: `🔴 0/${totalKits} kits (Não iniciado)`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Gargalo em Destaque */}
|
||||||
|
{gargalo && kitsProntos < totalKits && (
|
||||||
|
<div className="text-[11px] font-semibold text-amber-700 dark:text-amber-400 mt-1 flex items-center justify-end gap-1">
|
||||||
|
<AlertTriangle className="w-3 h-3 text-amber-500 shrink-0" />
|
||||||
|
<span>
|
||||||
|
Gargalo: <strong>{gargalo.marca_componente}</strong> (faltam {gargalo.falta_para_prox_kit} un para próx. kit)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-1 rounded-lg text-slate-400 hover:text-slate-600 dark:hover:text-slate-200">
|
||||||
|
{isExpanded ? <ChevronUp className="w-5 h-5" /> : <ChevronDown className="w-5 h-5" />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Barra Térmica de Progresso */}
|
||||||
|
<div className="px-4 pb-3">
|
||||||
|
<div className="w-full bg-slate-100 dark:bg-slate-800 h-2.5 rounded-full overflow-hidden flex">
|
||||||
|
<div
|
||||||
|
className={`h-full transition-all duration-300 ${barColor}`}
|
||||||
|
style={{ width: `${pct}%` }}
|
||||||
|
title={`Prontidão térmica do kit: ${pct}%`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ──────────────── Detalhamento dos Componentes do Kit (Expansível) ──────────────── */}
|
||||||
|
{isExpanded && (
|
||||||
|
<div className="px-4 pb-4 pt-2 border-t border-slate-100 dark:border-slate-800/80 bg-slate-50/60 dark:bg-slate-950/40 space-y-2">
|
||||||
|
<div className="flex items-center justify-between text-xs font-bold text-slate-700 dark:text-slate-300 mb-2">
|
||||||
|
<span className="flex items-center gap-1.5">
|
||||||
|
<Wrench className="w-3.5 h-3.5 text-blue-500" />
|
||||||
|
Componentes Requeridos para {totalKits}x {peca.marca} ({peca.componentes.length} itens no kit):
|
||||||
|
</span>
|
||||||
|
{onSelectPecaParaApontar && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onSelectPecaParaApontar(peca);
|
||||||
|
}}
|
||||||
|
className="h-7 text-xs bg-amber-500 hover:bg-amber-600 text-slate-950 font-black gap-1"
|
||||||
|
>
|
||||||
|
<Scissors className="w-3 h-3" />
|
||||||
|
Apontar no Corte
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{peca.componentes.length === 0 ? (
|
||||||
|
<p className="text-xs text-slate-500 italic py-1">
|
||||||
|
Esta peça não possui subcomponentes cadastrados (é fabricada como peça inteira).
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||||
|
{peca.componentes.map((comp) => {
|
||||||
|
const isGargalo = gargalo?.id === comp.id && kitsProntos < totalKits;
|
||||||
|
const comp100 = comp.concluido;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={comp.id}
|
||||||
|
className={`p-2.5 rounded-xl border text-xs transition-colors flex items-center justify-between gap-2 ${
|
||||||
|
isGargalo
|
||||||
|
? 'bg-amber-50 dark:bg-amber-950/40 border-amber-400 dark:border-amber-700 shadow-sm'
|
||||||
|
: comp100
|
||||||
|
? 'bg-emerald-50/50 dark:bg-emerald-950/20 border-emerald-200 dark:border-emerald-800/60'
|
||||||
|
: 'bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="min-w-0 space-y-0.5">
|
||||||
|
<div className="flex items-center gap-1.5 flex-wrap">
|
||||||
|
<span className="font-mono font-black text-slate-900 dark:text-slate-100">
|
||||||
|
{comp.marca_componente}
|
||||||
|
</span>
|
||||||
|
{comp.perfil && (
|
||||||
|
<span className="text-[10px] px-1.5 py-0.2 rounded bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 font-mono">
|
||||||
|
{comp.perfil}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{isGargalo && (
|
||||||
|
<span className="text-[9px] font-black px-1.5 py-0.5 rounded bg-amber-500 text-slate-950 uppercase tracking-tight">
|
||||||
|
Gargalo
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-[11px] text-slate-500 dark:text-slate-400">
|
||||||
|
{comp.quantidade_por_peca} un/peça • Total da OF: <strong>{comp.quantidade_total_necessaria} un</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-right shrink-0">
|
||||||
|
<div className="font-mono font-black text-xs text-slate-800 dark:text-slate-200">
|
||||||
|
{comp.quantidade_cortada} / {comp.quantidade_total_necessaria} un
|
||||||
|
</div>
|
||||||
|
<div className="text-[10px] font-bold text-slate-500 dark:text-slate-400">
|
||||||
|
Dá para {comp.kits_possiveis} {comp.kits_possiveis === 1 ? 'kit' : 'kits'} ({comp.percentual_cortado}%)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,461 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { supabase } from '@/integrations/supabase/client';
|
||||||
|
|
||||||
|
export interface ComponenteKitInfo {
|
||||||
|
id: string;
|
||||||
|
peca_id: string;
|
||||||
|
marca_componente: string;
|
||||||
|
descricao?: string | null;
|
||||||
|
perfil?: string | null;
|
||||||
|
peso_unitario: number;
|
||||||
|
quantidade_por_peca: number;
|
||||||
|
quantidade_total_necessaria: number;
|
||||||
|
quantidade_cortada: number;
|
||||||
|
kits_possiveis: number;
|
||||||
|
percentual_cortado: number;
|
||||||
|
falta_para_prox_kit: number;
|
||||||
|
concluido: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PecaKitInfo {
|
||||||
|
peca_id: string;
|
||||||
|
marca: string;
|
||||||
|
descricao?: string | null;
|
||||||
|
perfil_principal?: string | null;
|
||||||
|
quantidade_total: number;
|
||||||
|
etapa_fase: string;
|
||||||
|
tem_componentes: boolean;
|
||||||
|
componentes: ComponenteKitInfo[];
|
||||||
|
kits_completos_cortados: number; // Quantidade de kits 100% montáveis liberados
|
||||||
|
percentual_termico: number; // 0 a 100%
|
||||||
|
status_termico: 'verde' | 'verde_parcial' | 'amarelo' | 'vermelho';
|
||||||
|
componente_gargalo: ComponenteKitInfo | null;
|
||||||
|
peso_total_componentes: number;
|
||||||
|
peso_cortado_componentes: number;
|
||||||
|
ja_apontado_solda: number;
|
||||||
|
saldo_disponivel_solda: number;
|
||||||
|
bloqueado_para_solda: boolean;
|
||||||
|
motivo_bloqueio_solda: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResumoKitsOF {
|
||||||
|
totalPecas: number;
|
||||||
|
totalKits100Porcento: number;
|
||||||
|
totalKitsParciais: number;
|
||||||
|
totalKitsNaoIniciados: number;
|
||||||
|
pesoTotalOFKg: number;
|
||||||
|
pesoCortadoTotalKg: number;
|
||||||
|
percentualGeralOF: number;
|
||||||
|
totalUnidadesTotal: number;
|
||||||
|
totalUnidadesLiberadasSolda: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Função pura para cálculo determinístico de Kits de Produção.
|
||||||
|
* Recebe peças, componentes e apontamentos e retorna o mapa térmico de kits.
|
||||||
|
*/
|
||||||
|
export const calcularKitsOF = (
|
||||||
|
pecas: Array<{
|
||||||
|
id: string;
|
||||||
|
marca: string;
|
||||||
|
descricao?: string | null;
|
||||||
|
perfil_principal?: string | null;
|
||||||
|
quantidade: number;
|
||||||
|
etapa_fase?: string | null;
|
||||||
|
tem_componentes?: boolean | null;
|
||||||
|
}>,
|
||||||
|
componentes: Array<{
|
||||||
|
id: string;
|
||||||
|
peca_id: string;
|
||||||
|
marca_componente: string;
|
||||||
|
descricao?: string | null;
|
||||||
|
perfil?: string | null;
|
||||||
|
peso_unitario?: number | null;
|
||||||
|
quantidade_por_peca?: number | null;
|
||||||
|
}>,
|
||||||
|
apontamentos: Array<{
|
||||||
|
id?: string;
|
||||||
|
peca_id?: string | null;
|
||||||
|
componente_id?: string | null;
|
||||||
|
processo_id: string;
|
||||||
|
quantidade_produzida: number;
|
||||||
|
tipo_apontamento?: string | null;
|
||||||
|
}>,
|
||||||
|
processoCorteId?: string,
|
||||||
|
processoSoldaId?: string
|
||||||
|
): Map<string, PecaKitInfo> => {
|
||||||
|
const mapaKits = new Map<string, PecaKitInfo>();
|
||||||
|
|
||||||
|
// 1. Mapear componentes por peca_id
|
||||||
|
const componentesPorPeca = new Map<string, typeof componentes>();
|
||||||
|
componentes.forEach((c) => {
|
||||||
|
if (!componentesPorPeca.has(c.peca_id)) {
|
||||||
|
componentesPorPeca.set(c.peca_id, []);
|
||||||
|
}
|
||||||
|
componentesPorPeca.get(c.peca_id)!.push(c);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Mapear apontamentos de corte por componente_id
|
||||||
|
const cortadoPorComponente = new Map<string, number>();
|
||||||
|
// Apontamentos de corte por peca_id (caso seja peça sem componentes ou apontamento direto)
|
||||||
|
const cortadoPecaNoCorte = new Map<string, number>();
|
||||||
|
// Apontamentos de solda por peca_id
|
||||||
|
const apontadoNaSoldaPorPeca = new Map<string, number>();
|
||||||
|
|
||||||
|
apontamentos.forEach((ap) => {
|
||||||
|
const qtd = Number(ap.quantidade_produzida || 0);
|
||||||
|
|
||||||
|
// Se for apontamento de componente no corte (ou se não especificou processoCorteId, computa todos de componente)
|
||||||
|
if (ap.tipo_apontamento === 'componente' && ap.componente_id) {
|
||||||
|
if (!processoCorteId || ap.processo_id === processoCorteId) {
|
||||||
|
cortadoPorComponente.set(
|
||||||
|
ap.componente_id,
|
||||||
|
(cortadoPorComponente.get(ap.componente_id) || 0) + qtd
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Se for apontamento de peça mestre no corte
|
||||||
|
if (
|
||||||
|
(!ap.tipo_apontamento || ap.tipo_apontamento === 'peca') &&
|
||||||
|
!ap.componente_id &&
|
||||||
|
ap.peca_id
|
||||||
|
) {
|
||||||
|
if (!processoCorteId || ap.processo_id === processoCorteId) {
|
||||||
|
cortadoPecaNoCorte.set(
|
||||||
|
ap.peca_id,
|
||||||
|
(cortadoPecaNoCorte.get(ap.peca_id) || 0) + qtd
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Se for apontamento de solda
|
||||||
|
if (
|
||||||
|
(!ap.tipo_apontamento || ap.tipo_apontamento === 'peca') &&
|
||||||
|
!ap.componente_id &&
|
||||||
|
ap.peca_id
|
||||||
|
) {
|
||||||
|
if (processoSoldaId && ap.processo_id === processoSoldaId) {
|
||||||
|
apontadoNaSoldaPorPeca.set(
|
||||||
|
ap.peca_id,
|
||||||
|
(apontadoNaSoldaPorPeca.get(ap.peca_id) || 0) + qtd
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Processar cada peça
|
||||||
|
pecas.forEach((peca) => {
|
||||||
|
const compsRaw = componentesPorPeca.get(peca.id) || [];
|
||||||
|
const qtdTotalPeca = Number(peca.quantidade) || 0;
|
||||||
|
const fase = peca.etapa_fase || 'Geral';
|
||||||
|
const temComps = peca.tem_componentes !== false && compsRaw.length > 0;
|
||||||
|
const jaApontadoSolda = apontadoNaSoldaPorPeca.get(peca.id) || 0;
|
||||||
|
|
||||||
|
// Cenário A: Peça sem componentes (monobloco / S/M)
|
||||||
|
if (!temComps) {
|
||||||
|
const qtdCortada = cortadoPecaNoCorte.get(peca.id) || 0;
|
||||||
|
const kitsCompletos = Math.min(qtdTotalPeca, qtdCortada);
|
||||||
|
const pct = qtdTotalPeca > 0 ? Math.min(100, Math.round((kitsCompletos / qtdTotalPeca) * 100)) : 0;
|
||||||
|
const saldoSolda = Math.max(0, kitsCompletos - jaApontadoSolda);
|
||||||
|
|
||||||
|
mapaKits.set(peca.id, {
|
||||||
|
peca_id: peca.id,
|
||||||
|
marca: peca.marca,
|
||||||
|
descricao: peca.descricao,
|
||||||
|
perfil_principal: peca.perfil_principal,
|
||||||
|
quantidade_total: qtdTotalPeca,
|
||||||
|
etapa_fase: fase,
|
||||||
|
tem_componentes: false,
|
||||||
|
componentes: [],
|
||||||
|
kits_completos_cortados: kitsCompletos,
|
||||||
|
percentual_termico: pct,
|
||||||
|
status_termico:
|
||||||
|
kitsCompletos >= qtdTotalPeca
|
||||||
|
? 'verde'
|
||||||
|
: kitsCompletos > 0
|
||||||
|
? 'verde_parcial'
|
||||||
|
: pct >= 40
|
||||||
|
? 'amarelo'
|
||||||
|
: 'vermelho',
|
||||||
|
componente_gargalo: null,
|
||||||
|
peso_total_componentes: 0,
|
||||||
|
peso_cortado_componentes: 0,
|
||||||
|
ja_apontado_solda: jaApontadoSolda,
|
||||||
|
saldo_disponivel_solda: peca.tem_componentes === false ? 0 : saldoSolda,
|
||||||
|
bloqueado_para_solda: peca.tem_componentes === false || saldoSolda <= 0,
|
||||||
|
motivo_bloqueio_solda:
|
||||||
|
peca.tem_componentes === false
|
||||||
|
? 'Peça S/M (Pula Solda)'
|
||||||
|
: saldoSolda <= 0 && kitsCompletos === 0
|
||||||
|
? 'Aguardando corte da peça'
|
||||||
|
: saldoSolda <= 0
|
||||||
|
? `Todos os ${kitsCompletos} kits cortados já foram soldados`
|
||||||
|
: null,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cenário B: Peça com componentes
|
||||||
|
let pesoTotalComps = 0;
|
||||||
|
let pesoCortadoComps = 0;
|
||||||
|
let menorKits = qtdTotalPeca;
|
||||||
|
let gargaloComp: ComponenteKitInfo | null = null;
|
||||||
|
let menorRazao = 999999;
|
||||||
|
|
||||||
|
const listaCompsInfo: ComponenteKitInfo[] = [];
|
||||||
|
|
||||||
|
compsRaw.forEach((comp) => {
|
||||||
|
const porPeca = Math.max(1, Number(comp.quantidade_por_peca) || 1);
|
||||||
|
const totalNecessario = qtdTotalPeca * porPeca;
|
||||||
|
const cortado = cortadoPorComponente.get(comp.id) || 0;
|
||||||
|
const pesoUnit = Number(comp.peso_unitario) || 0;
|
||||||
|
|
||||||
|
const kitsDesteComp = Math.floor(cortado / porPeca);
|
||||||
|
if (kitsDesteComp < menorKits) {
|
||||||
|
menorKits = kitsDesteComp;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pctComp = totalNecessario > 0 ? Math.min(100, Math.round((cortado / totalNecessario) * 100)) : 0;
|
||||||
|
const pesoTotalComp = totalNecessario * pesoUnit;
|
||||||
|
const pesoCortadoComp = Math.min(cortado, totalNecessario) * pesoUnit;
|
||||||
|
|
||||||
|
pesoTotalComps += pesoTotalComp;
|
||||||
|
pesoCortadoComps += pesoCortadoComp;
|
||||||
|
|
||||||
|
const infoComp: ComponenteKitInfo = {
|
||||||
|
id: comp.id,
|
||||||
|
peca_id: comp.peca_id,
|
||||||
|
marca_componente: comp.marca_componente,
|
||||||
|
descricao: comp.descricao,
|
||||||
|
perfil: comp.perfil,
|
||||||
|
peso_unitario: pesoUnit,
|
||||||
|
quantidade_por_peca: porPeca,
|
||||||
|
quantidade_total_necessaria: totalNecessario,
|
||||||
|
quantidade_cortada: cortado,
|
||||||
|
kits_possiveis: kitsDesteComp,
|
||||||
|
percentual_cortado: pctComp,
|
||||||
|
falta_para_prox_kit: 0, // calculado logo abaixo
|
||||||
|
concluido: cortado >= totalNecessario,
|
||||||
|
};
|
||||||
|
|
||||||
|
listaCompsInfo.push(infoComp);
|
||||||
|
});
|
||||||
|
|
||||||
|
const kitsCompletos = Math.min(qtdTotalPeca, Math.max(0, menorKits));
|
||||||
|
|
||||||
|
// Determinar o componente gargalo para o próximo kit
|
||||||
|
listaCompsInfo.forEach((ci) => {
|
||||||
|
const necessarioParaProxKit = Math.min(qtdTotalPeca, kitsCompletos + 1) * ci.quantidade_por_peca;
|
||||||
|
ci.falta_para_prox_kit = Math.max(0, necessarioParaProxKit - ci.quantidade_cortada);
|
||||||
|
|
||||||
|
// Critério de gargalo: quem mais segura a formação de novos kits
|
||||||
|
const razao = ci.kits_possiveis;
|
||||||
|
if (razao < menorRazao && kitsCompletos < qtdTotalPeca) {
|
||||||
|
menorRazao = razao;
|
||||||
|
gargaloComp = ci;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Percentual térmico de prontidão ponderado por peso (ou por qtd se peso=0)
|
||||||
|
let percentualTermico = 0;
|
||||||
|
if (pesoTotalComps > 0) {
|
||||||
|
percentualTermico = Math.min(100, Math.round((pesoCortadoComps / pesoTotalComps) * 100));
|
||||||
|
} else {
|
||||||
|
const qtdTotalNecessaria = listaCompsInfo.reduce((s, c) => s + c.quantidade_total_necessaria, 0);
|
||||||
|
const qtdTotalCortada = listaCompsInfo.reduce((s, c) => s + Math.min(c.quantidade_cortada, c.quantidade_total_necessaria), 0);
|
||||||
|
percentualTermico = qtdTotalNecessaria > 0 ? Math.min(100, Math.round((qtdTotalCortada / qtdTotalNecessaria) * 100)) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status térmico
|
||||||
|
let statusTermico: 'verde' | 'verde_parcial' | 'amarelo' | 'vermelho' = 'vermelho';
|
||||||
|
if (kitsCompletos >= qtdTotalPeca) {
|
||||||
|
statusTermico = 'verde';
|
||||||
|
} else if (kitsCompletos > 0) {
|
||||||
|
statusTermico = 'verde_parcial';
|
||||||
|
} else if (percentualTermico >= 40) {
|
||||||
|
statusTermico = 'amarelo';
|
||||||
|
} else {
|
||||||
|
statusTermico = 'vermelho';
|
||||||
|
}
|
||||||
|
|
||||||
|
const saldoSolda = Math.max(0, kitsCompletos - jaApontadoSolda);
|
||||||
|
const bloqueadoSolda = saldoSolda <= 0;
|
||||||
|
|
||||||
|
let motivoBloqueioSolda: string | null = null;
|
||||||
|
if (bloqueadoSolda) {
|
||||||
|
if (kitsCompletos === 0) {
|
||||||
|
const detGargalo = gargaloComp ? ` • Gargalo: ${gargaloComp.marca_componente} (faltam ${gargaloComp.falta_para_prox_kit} un)` : '';
|
||||||
|
motivoBloqueioSolda = `Aguardando kits completos de corte (0 un liberadas)${detGargalo}`;
|
||||||
|
} else if (jaApontadoSolda >= kitsCompletos) {
|
||||||
|
motivoBloqueioSolda = `Todos os ${kitsCompletos} kits cortados já foram soldados (${jaApontadoSolda}/${qtdTotalPeca} un) • Aguardando novos kits cortados`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mapaKits.set(peca.id, {
|
||||||
|
peca_id: peca.id,
|
||||||
|
marca: peca.marca,
|
||||||
|
descricao: peca.descricao,
|
||||||
|
perfil_principal: peca.perfil_principal,
|
||||||
|
quantidade_total: qtdTotalPeca,
|
||||||
|
etapa_fase: fase,
|
||||||
|
tem_componentes: true,
|
||||||
|
componentes: listaCompsInfo,
|
||||||
|
kits_completos_cortados: kitsCompletos,
|
||||||
|
percentual_termico: percentualTermico,
|
||||||
|
status_termico: statusTermico,
|
||||||
|
componente_gargalo: gargaloComp,
|
||||||
|
peso_total_componentes: pesoTotalComps,
|
||||||
|
peso_cortado_componentes: pesoCortadoComps,
|
||||||
|
ja_apontado_solda: jaApontadoSolda,
|
||||||
|
saldo_disponivel_solda: saldoSolda,
|
||||||
|
bloqueado_para_solda: bloqueadoSolda,
|
||||||
|
motivo_bloqueio_solda: motivoBloqueioSolda,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapaKits;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook completo para carregar e gerenciar os Kits de uma OF e Fase.
|
||||||
|
*/
|
||||||
|
export const useKitsProducao = (ofNumber?: string, fase?: string) => {
|
||||||
|
const { data, isLoading, refetch, error } = useQuery({
|
||||||
|
queryKey: ['kits-producao', ofNumber, fase],
|
||||||
|
enabled: Boolean(ofNumber),
|
||||||
|
staleTime: 10000, // 10 segundos
|
||||||
|
queryFn: async () => {
|
||||||
|
if (!ofNumber) return { mapaKits: new Map<string, PecaKitInfo>(), resumo: null, lista: [] };
|
||||||
|
|
||||||
|
// 1. Buscar processos para identificar IDs de Corte e Solda
|
||||||
|
const { data: procs } = await supabase
|
||||||
|
.from('processos_fabricacao')
|
||||||
|
.select('id, nome, ordem')
|
||||||
|
.order('ordem');
|
||||||
|
|
||||||
|
const procCorte = (procs || []).find(
|
||||||
|
(p) => p.nome.toLowerCase().includes('corte') || p.nome.toLowerCase().includes('prepar') || p.ordem === 1
|
||||||
|
);
|
||||||
|
const procSolda = (procs || []).find(
|
||||||
|
(p) => p.nome.toLowerCase().includes('solda') || p.nome.toLowerCase().includes('montag') || p.ordem === 3
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Buscar peças da OF
|
||||||
|
let pecasQuery = supabase
|
||||||
|
.from('pecas')
|
||||||
|
.select('id, marca, descricao, perfil_principal, quantidade, etapa_fase, tem_componentes')
|
||||||
|
.eq('of_number', ofNumber);
|
||||||
|
|
||||||
|
if (fase && fase !== 'all' && fase !== 'Todas as fases' && fase !== 'todas') {
|
||||||
|
pecasQuery = pecasQuery.eq('etapa_fase', fase);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: pecasData, error: pecasErr } = await pecasQuery;
|
||||||
|
if (pecasErr) throw pecasErr;
|
||||||
|
|
||||||
|
const pecas = pecasData || [];
|
||||||
|
const pecaIds = pecas.map((p) => p.id);
|
||||||
|
|
||||||
|
// 3. Buscar componentes das peças
|
||||||
|
let componentes: Array<{
|
||||||
|
id: string;
|
||||||
|
peca_id: string;
|
||||||
|
marca_componente: string;
|
||||||
|
descricao?: string | null;
|
||||||
|
perfil?: string | null;
|
||||||
|
peso_unitario?: number | null;
|
||||||
|
quantidade_por_peca?: number | null;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
if (pecaIds.length > 0) {
|
||||||
|
for (let i = 0; i < pecaIds.length; i += 300) {
|
||||||
|
const chunk = pecaIds.slice(i, i + 300);
|
||||||
|
const { data: compsChunk, error: compsErr } = await supabase
|
||||||
|
.from('componentes_peca')
|
||||||
|
.select('id, peca_id, marca_componente, descricao, perfil, peso_unitario, quantidade_por_peca')
|
||||||
|
.in('peca_id', chunk);
|
||||||
|
|
||||||
|
if (compsErr) throw compsErr;
|
||||||
|
if (compsChunk) {
|
||||||
|
componentes = [...componentes, ...compsChunk];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Buscar apontamentos da OF
|
||||||
|
const { data: apontamentosData, error: aptsErr } = await supabase
|
||||||
|
.from('apontamentos_producao')
|
||||||
|
.select('id, peca_id, componente_id, processo_id, quantidade_produzida, tipo_apontamento')
|
||||||
|
.eq('of_number', ofNumber);
|
||||||
|
|
||||||
|
if (aptsErr) throw aptsErr;
|
||||||
|
|
||||||
|
// 5. Calcular kits
|
||||||
|
const mapaKits = calcularKitsOF(
|
||||||
|
pecas,
|
||||||
|
componentes,
|
||||||
|
apontamentosData || [],
|
||||||
|
procCorte?.id,
|
||||||
|
procSolda?.id
|
||||||
|
);
|
||||||
|
|
||||||
|
const lista = Array.from(mapaKits.values()).sort((a, b) =>
|
||||||
|
a.marca.localeCompare(b.marca, undefined, { numeric: true, sensitivity: 'base' })
|
||||||
|
);
|
||||||
|
|
||||||
|
// 6. Resumo geral
|
||||||
|
let total100 = 0;
|
||||||
|
let totalParciais = 0;
|
||||||
|
let totalTravados = 0;
|
||||||
|
let pesoTot = 0;
|
||||||
|
let pesoCort = 0;
|
||||||
|
let unTot = 0;
|
||||||
|
let unLib = 0;
|
||||||
|
|
||||||
|
lista.forEach((item) => {
|
||||||
|
unTot += item.quantidade_total;
|
||||||
|
unLib += item.kits_completos_cortados;
|
||||||
|
pesoTot += item.peso_total_componentes;
|
||||||
|
pesoCort += item.peso_cortado_componentes;
|
||||||
|
|
||||||
|
if (item.status_termico === 'verde') total100++;
|
||||||
|
else if (item.status_termico === 'verde_parcial' || item.status_termico === 'amarelo') totalParciais++;
|
||||||
|
else totalTravados++;
|
||||||
|
});
|
||||||
|
|
||||||
|
const resumo: ResumoKitsOF = {
|
||||||
|
totalPecas: lista.length,
|
||||||
|
totalKits100Porcento: total100,
|
||||||
|
totalKitsParciais: totalParciais,
|
||||||
|
totalKitsNaoIniciados: totalTravados,
|
||||||
|
pesoTotalOFKg: pesoTot,
|
||||||
|
pesoCortadoTotalKg: pesoCort,
|
||||||
|
percentualGeralOF: pesoTot > 0 ? Math.round((pesoCortadoTotalKg(pesoTot, pesoCort)) * 100) : 0,
|
||||||
|
totalUnidadesTotal: unTot,
|
||||||
|
totalUnidadesLiberadasSolda: unLib,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
mapaKits,
|
||||||
|
resumo,
|
||||||
|
lista,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
kitsPorPeca: data?.mapaKits || new Map<string, PecaKitInfo>(),
|
||||||
|
listaKits: data?.lista || [],
|
||||||
|
resumoKits: data?.resumo,
|
||||||
|
loading: isLoading,
|
||||||
|
refetch,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function pesoCortadoTotalKg(total: number, cortado: number): number {
|
||||||
|
if (total <= 0) return 0;
|
||||||
|
return Math.min(1, cortado / total);
|
||||||
|
}
|
||||||
@@ -3,10 +3,11 @@ import React, { useState } from 'react';
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import { ClipboardList, BarChart3, Settings } from 'lucide-react';
|
import { ClipboardList, BarChart3, Settings, Flame } from 'lucide-react';
|
||||||
import { ApontamentoForm } from '@/components/apontamento/ApontamentoForm';
|
import { ApontamentoForm } from '@/components/apontamento/ApontamentoForm';
|
||||||
import { ApontamentosListOtimizado } from '@/components/apontamento/ApontamentosListOtimizado';
|
import { ApontamentosListOtimizado } from '@/components/apontamento/ApontamentosListOtimizado';
|
||||||
import { ProcessosList } from '@/components/apontamento/ProcessosList';
|
import { ProcessosList } from '@/components/apontamento/ProcessosList';
|
||||||
|
import { MapaTermicoKitsView } from '@/components/apontamento/kits/MapaTermicoKitsView';
|
||||||
import { useApontamentosProducao } from '@/hooks/useApontamentosProducao';
|
import { useApontamentosProducao } from '@/hooks/useApontamentosProducao';
|
||||||
import { useIsMobile } from '@/hooks/use-mobile';
|
import { useIsMobile } from '@/hooks/use-mobile';
|
||||||
import { usePermissionControl } from '@/hooks/usePermissionControl';
|
import { usePermissionControl } from '@/hooks/usePermissionControl';
|
||||||
@@ -34,7 +35,7 @@ const ApontamentoProducao = () => {
|
|||||||
<TabsList className={`w-full bg-muted border-border ${
|
<TabsList className={`w-full bg-muted border-border ${
|
||||||
isMobile
|
isMobile
|
||||||
? 'grid grid-cols-1 gap-1 h-auto p-1'
|
? 'grid grid-cols-1 gap-1 h-auto p-1'
|
||||||
: 'grid grid-cols-3 h-10'
|
: 'grid grid-cols-4 h-10'
|
||||||
}`}>
|
}`}>
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="apontamento"
|
value="apontamento"
|
||||||
@@ -45,6 +46,15 @@ const ApontamentoProducao = () => {
|
|||||||
<ClipboardList className="h-4 w-4" />
|
<ClipboardList className="h-4 w-4" />
|
||||||
Novo Apontamento
|
Novo Apontamento
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
value="mapa-kits"
|
||||||
|
className={`flex items-center gap-2 text-muted-foreground data-[state=active]:bg-background data-[state=active]:text-foreground ${
|
||||||
|
isMobile ? 'w-full justify-start p-3 text-sm' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Flame className="h-4 w-4 text-orange-500 fill-orange-500 animate-pulse" />
|
||||||
|
Mapa de Kits
|
||||||
|
</TabsTrigger>
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="historico"
|
value="historico"
|
||||||
className={`flex items-center gap-2 text-muted-foreground data-[state=active]:bg-background data-[state=active]:text-foreground ${
|
className={`flex items-center gap-2 text-muted-foreground data-[state=active]:bg-background data-[state=active]:text-foreground ${
|
||||||
@@ -89,6 +99,23 @@ const ApontamentoProducao = () => {
|
|||||||
)}
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="mapa-kits" className="space-y-4 sm:space-y-6 mt-4">
|
||||||
|
<Card className="bg-card border-border shadow-sm">
|
||||||
|
<CardHeader className="pb-3 border-b border-border/60">
|
||||||
|
<CardTitle className="text-card-foreground flex items-center gap-2 text-lg sm:text-xl font-bold">
|
||||||
|
<Flame className="h-5 w-5 text-orange-500 fill-orange-500 animate-pulse" />
|
||||||
|
<span>Mapa Térmico de Kits de Montagem</span>
|
||||||
|
</CardTitle>
|
||||||
|
<p className="text-xs sm:text-sm text-muted-foreground">
|
||||||
|
Visão integrada do balanceamento do corte de componentes e liberação de kits completos para montagem/solda
|
||||||
|
</p>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="pt-4">
|
||||||
|
<MapaTermicoKitsView />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="historico" className="space-y-4 sm:space-y-6 mt-4">
|
<TabsContent value="historico" className="space-y-4 sm:space-y-6 mt-4">
|
||||||
<Card className="bg-card border-border">
|
<Card className="bg-card border-border">
|
||||||
<CardHeader className="pb-4">
|
<CardHeader className="pb-4">
|
||||||
|
|||||||
@@ -0,0 +1,411 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import {
|
||||||
|
Scissors,
|
||||||
|
CheckCircle2,
|
||||||
|
AlertTriangle,
|
||||||
|
Zap,
|
||||||
|
Plus,
|
||||||
|
RefreshCw,
|
||||||
|
Layers,
|
||||||
|
Wrench,
|
||||||
|
Flame
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { supabase } from '@/integrations/supabase/client';
|
||||||
|
import { useAuth } from '@/hooks/useAuth';
|
||||||
|
import { smartAudio } from '@/utils/smartAudio';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { PecaKitInfo, ComponenteKitInfo } from '@/hooks/useKitsProducao';
|
||||||
|
|
||||||
|
interface SmartComponentesCorteModalProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
pecaKit: PecaKitInfo | null;
|
||||||
|
ofNumber: string;
|
||||||
|
processoCorteId: string;
|
||||||
|
onApontamentoSalvo: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SmartComponentesCorteModal: React.FC<SmartComponentesCorteModalProps> = ({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
pecaKit,
|
||||||
|
ofNumber,
|
||||||
|
processoCorteId,
|
||||||
|
onApontamentoSalvo,
|
||||||
|
}) => {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const [salvando, setSalvando] = useState<boolean>(false);
|
||||||
|
const [quantidadesManual, setQuantidadesManual] = useState<Record<string, number>>({});
|
||||||
|
|
||||||
|
if (!pecaKit) return null;
|
||||||
|
|
||||||
|
const hoje = new Date().toISOString().split('T')[0];
|
||||||
|
const nomeUsuario = user?.user_metadata?.nome || user?.email?.split('@')[0] || 'Operador Corte';
|
||||||
|
|
||||||
|
// Atualizar valor digitado para um componente específico
|
||||||
|
const setQtdComp = (compId: string, valor: number) => {
|
||||||
|
setQuantidadesManual((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[compId]: Math.max(1, valor),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// AÇÃO 1: CORTAR 1 KIT COMPLETO (ATALHO RÁPIDO)
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
const handleCortarKitCompleto = async (multiplicadorKits: number = 1) => {
|
||||||
|
try {
|
||||||
|
setSalvando(true);
|
||||||
|
smartAudio.playClick();
|
||||||
|
|
||||||
|
if (!pecaKit.componentes || pecaKit.componentes.length === 0) {
|
||||||
|
toast.error('Esta peça não possui subcomponentes cadastrados.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preparar lote de inserções para cada componente da peça
|
||||||
|
const batchPayloads: Array<{
|
||||||
|
of_number: string;
|
||||||
|
peca_id: string;
|
||||||
|
componente_id: string;
|
||||||
|
tipo_apontamento: 'componente';
|
||||||
|
processo_id: string;
|
||||||
|
quantidade_produzida: number;
|
||||||
|
data_apontamento: string;
|
||||||
|
created_by?: string;
|
||||||
|
usuario_nome: string;
|
||||||
|
observacoes: string;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
let pesoTotalKg = 0;
|
||||||
|
|
||||||
|
for (const comp of pecaKit.componentes) {
|
||||||
|
const porKit = comp.quantidade_por_peca || 1;
|
||||||
|
const qtdParaApontar = porKit * multiplicadorKits;
|
||||||
|
const restante = Math.max(0, comp.quantidade_total_necessaria - comp.quantidade_cortada);
|
||||||
|
|
||||||
|
// Não apontar além do total se já estiver concluído, exceto se solicitado
|
||||||
|
const qtdFinal = Math.min(qtdParaApontar, Math.max(qtdParaApontar, restante));
|
||||||
|
|
||||||
|
if (qtdFinal > 0) {
|
||||||
|
batchPayloads.push({
|
||||||
|
of_number: ofNumber,
|
||||||
|
peca_id: pecaKit.peca_id,
|
||||||
|
componente_id: comp.id,
|
||||||
|
tipo_apontamento: 'componente',
|
||||||
|
processo_id: processoCorteId,
|
||||||
|
quantidade_produzida: qtdFinal,
|
||||||
|
data_apontamento: hoje,
|
||||||
|
created_by: user?.id,
|
||||||
|
usuario_nome: nomeUsuario,
|
||||||
|
observacoes: `Corte de Kit Completo (${multiplicadorKits}x ${pecaKit.marca}) via Modo Smart`,
|
||||||
|
});
|
||||||
|
|
||||||
|
pesoTotalKg += qtdFinal * (comp.peso_unitario || 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (batchPayloads.length === 0) {
|
||||||
|
toast.info('Todos os componentes desta peça já estão 100% cortados!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { error } = await supabase.from('apontamentos_producao').insert(batchPayloads);
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
smartAudio.playSuccess();
|
||||||
|
toast.success(
|
||||||
|
`✅ ${multiplicadorKits} Kit Completo da peça ${pecaKit.marca} cortado com sucesso! (+${pesoTotalKg.toFixed(1)} kg)`
|
||||||
|
);
|
||||||
|
|
||||||
|
onApontamentoSalvo();
|
||||||
|
onOpenChange(false);
|
||||||
|
} catch (err: any) {
|
||||||
|
smartAudio.playAlert();
|
||||||
|
console.error('Erro ao cortar kit completo:', err);
|
||||||
|
toast.error('Erro ao registrar kit completo: ' + (err?.message || 'Falha de rede'));
|
||||||
|
} finally {
|
||||||
|
setSalvando(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// AÇÃO 2: APONTAR COMPONENTE INDIVIDUAL (DESBALANCEADO / NESTING)
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
const handleApontarComponenteIndividual = async (comp: ComponenteKitInfo) => {
|
||||||
|
try {
|
||||||
|
const qtd = quantidadesManual[comp.id] || 1;
|
||||||
|
if (qtd <= 0) return;
|
||||||
|
|
||||||
|
setSalvando(true);
|
||||||
|
smartAudio.playClick();
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
of_number: ofNumber,
|
||||||
|
peca_id: pecaKit.peca_id,
|
||||||
|
componente_id: comp.id,
|
||||||
|
tipo_apontamento: 'componente' as const,
|
||||||
|
processo_id: processoCorteId,
|
||||||
|
quantidade_produzida: qtd,
|
||||||
|
data_apontamento: hoje,
|
||||||
|
created_by: user?.id,
|
||||||
|
usuario_nome: nomeUsuario,
|
||||||
|
observacoes: `Corte de subcomponente ${comp.marca_componente} (${qtd} un) via Modo Smart`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { error } = await supabase.from('apontamentos_producao').insert(payload);
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
const pesoKg = qtd * (comp.peso_unitario || 0);
|
||||||
|
smartAudio.playSuccess();
|
||||||
|
toast.success(
|
||||||
|
`✂️ Apontado no Corte: ${qtd}x ${comp.marca_componente} (+${pesoKg.toFixed(1)} kg)`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Limpar valor digitado
|
||||||
|
setQuantidadesManual((prev) => {
|
||||||
|
const next = { ...prev };
|
||||||
|
delete next[comp.id];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
|
||||||
|
onApontamentoSalvo();
|
||||||
|
} catch (err: any) {
|
||||||
|
smartAudio.playAlert();
|
||||||
|
console.error('Erro ao apontar componente:', err);
|
||||||
|
toast.error('Erro ao apontar componente: ' + (err?.message || 'Falha de rede'));
|
||||||
|
} finally {
|
||||||
|
setSalvando(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const kitsProntos = pecaKit.kits_completos_cortados;
|
||||||
|
const totalKits = pecaKit.quantidade_total;
|
||||||
|
const pct = pecaKit.percentual_termico;
|
||||||
|
const gargalo = pecaKit.componente_gargalo;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto p-4 sm:p-6">
|
||||||
|
<DialogHeader className="pb-2 border-b border-slate-200 dark:border-slate-800">
|
||||||
|
<DialogTitle className="flex items-center justify-between gap-2 flex-wrap">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="p-2 rounded-xl bg-orange-100 text-orange-600 dark:bg-orange-950/60 dark:text-orange-400">
|
||||||
|
<Scissors className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xl font-black text-amber-700 dark:text-amber-400 tracking-wider">
|
||||||
|
{pecaKit.marca}
|
||||||
|
</span>
|
||||||
|
<Badge variant="outline" className="text-xs font-bold">
|
||||||
|
{pecaKit.etapa_fase.toLowerCase().startsWith('fase')
|
||||||
|
? pecaKit.etapa_fase
|
||||||
|
: `Fase ${pecaKit.etapa_fase}`}
|
||||||
|
</Badge>
|
||||||
|
{pecaKit.perfil_principal && (
|
||||||
|
<span className="text-xs px-2 py-0.5 rounded bg-slate-100 dark:bg-slate-800 font-mono font-bold">
|
||||||
|
{pecaKit.perfil_principal}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-slate-500">
|
||||||
|
{pecaKit.descricao || 'Peça Estrutural'} • Total: <strong>{totalKits} un</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className={`text-xs font-black ${
|
||||||
|
kitsProntos >= totalKits
|
||||||
|
? 'bg-emerald-100 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-300'
|
||||||
|
: kitsProntos > 0
|
||||||
|
? 'bg-blue-100 text-blue-800 dark:bg-blue-950 dark:text-blue-300'
|
||||||
|
: 'bg-amber-100 text-amber-800 dark:bg-amber-950 dark:text-amber-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Flame className="w-3.5 h-3.5 mr-1 animate-pulse" />
|
||||||
|
{kitsProntos}/{totalKits} Kits Montáveis ({pct}%)
|
||||||
|
</Badge>
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4 py-2">
|
||||||
|
{/* ──────────────── Barra de Prontidão e Gargalo ──────────────── */}
|
||||||
|
<div className="p-3 rounded-2xl bg-slate-50 dark:bg-slate-900 border border-slate-200 dark:border-slate-800 space-y-2">
|
||||||
|
<div className="flex items-center justify-between text-xs font-bold text-slate-700 dark:text-slate-300">
|
||||||
|
<span>Prontidão Térmica do Kit de Montagem</span>
|
||||||
|
<span>{pct}% dos componentes cortados</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full bg-slate-200 dark:bg-slate-800 h-3 rounded-full overflow-hidden flex">
|
||||||
|
<div
|
||||||
|
className={`h-full transition-all duration-300 ${
|
||||||
|
kitsProntos >= totalKits
|
||||||
|
? 'bg-emerald-500'
|
||||||
|
: kitsProntos > 0
|
||||||
|
? 'bg-blue-500'
|
||||||
|
: 'bg-amber-500'
|
||||||
|
}`}
|
||||||
|
style={{ width: `${pct}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{gargalo && kitsProntos < totalKits && (
|
||||||
|
<div className="p-2 rounded-xl bg-amber-100/70 dark:bg-amber-950/40 border border-amber-300 dark:border-amber-800 flex items-center gap-2 text-xs text-amber-900 dark:text-amber-300 font-semibold">
|
||||||
|
<AlertTriangle className="w-4 h-4 text-amber-600 dark:text-amber-400 shrink-0 animate-bounce" />
|
||||||
|
<span>
|
||||||
|
<strong>Gargalo do Próximo Kit:</strong> Falta cortar{' '}
|
||||||
|
<span className="underline decoration-amber-500 font-black">
|
||||||
|
{gargalo.falta_para_prox_kit} un do componente {gargalo.marca_componente}
|
||||||
|
</span>{' '}
|
||||||
|
para liberar mais 1 kit completo na Solda!
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ──────────────── ATALHO MESTRE: CORTAR 1 KIT COMPLETO ──────────────── */}
|
||||||
|
<div className="p-3.5 rounded-2xl bg-gradient-to-r from-amber-500/10 via-amber-500/20 to-orange-500/10 border-2 border-amber-500/40 dark:border-amber-500/30 flex flex-col sm:flex-row items-center justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-1.5 text-xs font-black uppercase text-amber-800 dark:text-amber-400">
|
||||||
|
<Zap className="w-4 h-4 text-amber-600 dark:text-amber-400 fill-current" />
|
||||||
|
<span>Atalho Rápido de Corte por Conjunto</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-slate-600 dark:text-slate-300 font-medium">
|
||||||
|
Corta todos os componentes necessários para fechar mais 1 kit montável de uma só vez.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
disabled={salvando || kitsProntos >= totalKits}
|
||||||
|
onClick={() => handleCortarKitCompleto(1)}
|
||||||
|
className="w-full sm:w-auto h-11 px-4 bg-amber-500 hover:bg-amber-400 active:bg-amber-600 text-slate-950 font-black text-xs uppercase tracking-wider rounded-xl shadow-md active:scale-95 transition-all flex items-center justify-center gap-1.5 shrink-0"
|
||||||
|
>
|
||||||
|
<Zap className="w-4 h-4 fill-current" />
|
||||||
|
<span>CORTAR 1 KIT COMPLETO (+1 UN)</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ──────────────── LISTA DE SUBCOMPONENTES PARA CORTE AVULSO ──────────────── */}
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between text-xs font-bold text-slate-700 dark:text-slate-300 mb-2">
|
||||||
|
<span className="flex items-center gap-1.5">
|
||||||
|
<Wrench className="w-3.5 h-3.5 text-blue-500" />
|
||||||
|
Corte Individual de Subcomponentes (Nesting / Chapas avulsas):
|
||||||
|
</span>
|
||||||
|
<span className="text-slate-400">{pecaKit.componentes.length} subcomponentes</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2.5">
|
||||||
|
{pecaKit.componentes.map((comp) => {
|
||||||
|
const isGargalo = gargalo?.id === comp.id && kitsProntos < totalKits;
|
||||||
|
const comp100 = comp.concluido;
|
||||||
|
const valorAtual = quantidadesManual[comp.id] || comp.quantidade_por_peca || 1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={comp.id}
|
||||||
|
className={`p-3 rounded-2xl border transition-all ${
|
||||||
|
isGargalo
|
||||||
|
? 'bg-amber-50/70 dark:bg-amber-950/30 border-amber-400 dark:border-amber-700 shadow-sm'
|
||||||
|
: comp100
|
||||||
|
? 'bg-emerald-50/40 dark:bg-emerald-950/20 border-emerald-200 dark:border-emerald-800'
|
||||||
|
: 'bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2.5">
|
||||||
|
{/* Info do componente */}
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
<span className="font-mono font-black text-base text-slate-900 dark:text-slate-100">
|
||||||
|
{comp.marca_componente}
|
||||||
|
</span>
|
||||||
|
{comp.perfil && (
|
||||||
|
<span className="text-xs px-2 py-0.5 rounded bg-slate-100 dark:bg-slate-800 font-mono text-slate-700 dark:text-slate-300 font-bold">
|
||||||
|
{comp.perfil}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{isGargalo && (
|
||||||
|
<span className="text-[10px] font-black px-2 py-0.5 rounded bg-amber-500 text-slate-950 uppercase tracking-tight">
|
||||||
|
Gargalo do Kit
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{comp100 && (
|
||||||
|
<span className="text-[10px] font-black px-2 py-0.5 rounded bg-emerald-100 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-300">
|
||||||
|
✅ 100% Cortado
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">
|
||||||
|
{comp.quantidade_por_peca} un/peça • Total da OF:{' '}
|
||||||
|
<strong>{comp.quantidade_total_necessaria} un</strong> • Já cortado:{' '}
|
||||||
|
<strong className="text-slate-900 dark:text-slate-100 font-mono">
|
||||||
|
{comp.quantidade_cortada} un
|
||||||
|
</strong>{' '}
|
||||||
|
({comp.percentual_cortado}%) • {comp.peso_unitario > 0 ? `${comp.peso_unitario.toFixed(2)} kg/un` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Teclado de corte rápido deste componente */}
|
||||||
|
<div className="flex items-center gap-1.5 shrink-0 self-end sm:self-center">
|
||||||
|
<div className="flex items-center gap-1 bg-slate-100 dark:bg-slate-800 p-1 rounded-xl border border-slate-200 dark:border-slate-700">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setQtdComp(comp.id, comp.quantidade_por_peca)}
|
||||||
|
className="px-2 py-1 rounded-lg text-xs font-black bg-white dark:bg-slate-700 hover:bg-slate-200 dark:hover:bg-slate-600 shadow-xs"
|
||||||
|
title="1 kit da peça"
|
||||||
|
>
|
||||||
|
+{comp.quantidade_por_peca}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setQtdComp(comp.id, comp.quantidade_por_peca * 2)}
|
||||||
|
className="px-2 py-1 rounded-lg text-xs font-black bg-white dark:bg-slate-700 hover:bg-slate-200 dark:hover:bg-slate-600 shadow-xs"
|
||||||
|
title="2 kits da peça"
|
||||||
|
>
|
||||||
|
+{comp.quantidade_por_peca * 2}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
value={valorAtual}
|
||||||
|
onChange={(e) => setQtdComp(comp.id, parseInt(e.target.value) || 1)}
|
||||||
|
className="w-14 h-7 text-xs text-center font-bold px-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
disabled={salvando}
|
||||||
|
onClick={() => handleApontarComponenteIndividual(comp)}
|
||||||
|
className="h-8 px-3 bg-slate-900 hover:bg-slate-800 text-white dark:bg-amber-500 dark:hover:bg-amber-400 dark:text-slate-950 font-black text-xs uppercase rounded-xl active:scale-95"
|
||||||
|
>
|
||||||
|
Apontar ({valorAtual})
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -15,7 +15,8 @@ import {
|
|||||||
Filter,
|
Filter,
|
||||||
Delete,
|
Delete,
|
||||||
Pencil,
|
Pencil,
|
||||||
CheckCheck
|
CheckCheck,
|
||||||
|
Flame
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { OFAtiva } from '@/hooks/useOFsAtivas';
|
import { OFAtiva } from '@/hooks/useOFsAtivas';
|
||||||
import { useProcessosFabricacao, ProcessoFabricacao } from '@/hooks/useProcessosFabricacao';
|
import { useProcessosFabricacao, ProcessoFabricacao } from '@/hooks/useProcessosFabricacao';
|
||||||
@@ -25,6 +26,9 @@ import { smartAudio } from '@/utils/smartAudio';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { SmartForcarApontamentoModal } from './SmartForcarApontamentoModal';
|
import { SmartForcarApontamentoModal } from './SmartForcarApontamentoModal';
|
||||||
|
import { calcularKitsOF, PecaKitInfo } from '@/hooks/useKitsProducao';
|
||||||
|
import { MapaTermicoKitsModal } from '@/components/apontamento/kits/MapaTermicoKitsModal';
|
||||||
|
import { SmartComponentesCorteModal } from './SmartComponentesCorteModal';
|
||||||
|
|
||||||
interface SmartProducaoFlowProps {
|
interface SmartProducaoFlowProps {
|
||||||
obra: OFAtiva;
|
obra: OFAtiva;
|
||||||
@@ -58,7 +62,8 @@ export const calcularSaldoProcessoPeca = (
|
|||||||
peca: PecaData,
|
peca: PecaData,
|
||||||
processoAtual: ProcessoFabricacao,
|
processoAtual: ProcessoFabricacao,
|
||||||
todosProcessos: ProcessoFabricacao[],
|
todosProcessos: ProcessoFabricacao[],
|
||||||
mapaProd: Map<string, number>
|
mapaProd: Map<string, number>,
|
||||||
|
mapaKits?: Map<string, PecaKitInfo>
|
||||||
): CalculoSaldoPeca => {
|
): CalculoSaldoPeca => {
|
||||||
const ehSemMontagem = peca.tem_componentes === false;
|
const ehSemMontagem = peca.tem_componentes === false;
|
||||||
const keyAtual = `${peca.id}_${processoAtual.id}`;
|
const keyAtual = `${peca.id}_${processoAtual.id}`;
|
||||||
@@ -68,7 +73,7 @@ export const calcularSaldoProcessoPeca = (
|
|||||||
const indexAtual = procsOrdenados.findIndex((p) => p.id === processoAtual.id);
|
const indexAtual = procsOrdenados.findIndex((p) => p.id === processoAtual.id);
|
||||||
const nomeProcAtual = (processoAtual.nome || '').toLowerCase();
|
const nomeProcAtual = (processoAtual.nome || '').toLowerCase();
|
||||||
|
|
||||||
const ehSolda = nomeProcAtual.includes('solda');
|
const ehSolda = nomeProcAtual.includes('solda') || nomeProcAtual.includes('montag');
|
||||||
|
|
||||||
let qtdDisponivelParaEntrar = Number(peca.quantidade) || 0;
|
let qtdDisponivelParaEntrar = Number(peca.quantidade) || 0;
|
||||||
let motivoBloqueio: string | null = null;
|
let motivoBloqueio: string | null = null;
|
||||||
@@ -77,6 +82,18 @@ export const calcularSaldoProcessoPeca = (
|
|||||||
if (ehSemMontagem && ehSolda) {
|
if (ehSemMontagem && ehSolda) {
|
||||||
qtdDisponivelParaEntrar = 0;
|
qtdDisponivelParaEntrar = 0;
|
||||||
motivoBloqueio = 'Peça S/M (Pula Solda - vai direto para Pintura)';
|
motivoBloqueio = 'Peça S/M (Pula Solda - vai direto para Pintura)';
|
||||||
|
} else if (ehSolda && !ehSemMontagem && mapaKits?.has(peca.id)) {
|
||||||
|
// REGRA 1.1: REGRA DO KIT COMPLETO DE CORTE PARA A SOLDA!
|
||||||
|
// Apenas peças com componentes 100% cortados (em lotes de kits inteiros) são liberadas para Solda!
|
||||||
|
const kitInfo = mapaKits.get(peca.id)!;
|
||||||
|
if (kitInfo.tem_componentes) {
|
||||||
|
qtdDisponivelParaEntrar = kitInfo.kits_completos_cortados;
|
||||||
|
if (kitInfo.kits_completos_cortados === 0) {
|
||||||
|
motivoBloqueio = kitInfo.motivo_bloqueio_solda || 'Aguardando corte completo dos kits';
|
||||||
|
} else if (kitInfo.kits_completos_cortados <= jaProduzidoAtual) {
|
||||||
|
motivoBloqueio = `Todos os ${kitInfo.kits_completos_cortados} kits cortados já foram soldados (${jaProduzidoAtual}/${peca.quantidade})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (indexAtual > 0) {
|
} else if (indexAtual > 0) {
|
||||||
// REGRA 2: Encontrar processo anterior válido
|
// REGRA 2: Encontrar processo anterior válido
|
||||||
let procAnteriorValido: ProcessoFabricacao | null = null;
|
let procAnteriorValido: ProcessoFabricacao | null = null;
|
||||||
@@ -176,12 +193,17 @@ export const SmartProducaoFlow: React.FC<SmartProducaoFlowProps> = ({
|
|||||||
// Modal de Forçar Apontamento
|
// Modal de Forçar Apontamento
|
||||||
const [isForcarModalOpen, setIsForcarModalOpen] = useState<boolean>(false);
|
const [isForcarModalOpen, setIsForcarModalOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
// Estados do Mapa Térmico de Kits e Corte de Componentes
|
||||||
|
const [isMapaKitsOpen, setIsMapaKitsOpen] = useState<boolean>(false);
|
||||||
|
const [pecaParaCorteModal, setPecaParaCorteModal] = useState<PecaKitInfo | null>(null);
|
||||||
|
const [componentesOF, setComponentesOF] = useState<any[]>([]);
|
||||||
|
|
||||||
// Dados carregados da OF
|
// Dados carregados da OF
|
||||||
const [pecas, setPecas] = useState<PecaData[]>([]);
|
const [pecas, setPecas] = useState<PecaData[]>([]);
|
||||||
const [apontamentosExistentes, setApontamentosExistentes] = useState<ApontamentoItem[]>([]);
|
const [apontamentosExistentes, setApontamentosExistentes] = useState<ApontamentoItem[]>([]);
|
||||||
const [carregandoDados, setCarregandoDados] = useState<boolean>(true);
|
const [carregandoDados, setCarregandoDados] = useState<boolean>(true);
|
||||||
|
|
||||||
// Carregar peças e apontamentos da OF
|
// Carregar peças, componentes e apontamentos da OF
|
||||||
const carregarDadosOF = React.useCallback(async () => {
|
const carregarDadosOF = React.useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
setCarregandoDados(true);
|
setCarregandoDados(true);
|
||||||
@@ -199,6 +221,22 @@ export const SmartProducaoFlow: React.FC<SmartProducaoFlowProps> = ({
|
|||||||
|
|
||||||
if (resPecas.data) {
|
if (resPecas.data) {
|
||||||
setPecas(resPecas.data);
|
setPecas(resPecas.data);
|
||||||
|
|
||||||
|
// Buscar componentes das peças da OF em lotes
|
||||||
|
const pecaIds = resPecas.data.map((p) => p.id);
|
||||||
|
if (pecaIds.length > 0) {
|
||||||
|
let todosComps: any[] = [];
|
||||||
|
for (let i = 0; i < pecaIds.length; i += 300) {
|
||||||
|
const chunk = pecaIds.slice(i, i + 300);
|
||||||
|
const { data: compsData } = await supabase
|
||||||
|
.from('componentes_peca')
|
||||||
|
.select('id, peca_id, marca_componente, descricao, perfil, peso_unitario, quantidade_por_peca')
|
||||||
|
.in('peca_id', chunk);
|
||||||
|
|
||||||
|
if (compsData) todosComps = [...todosComps, ...compsData];
|
||||||
|
}
|
||||||
|
setComponentesOF(todosComps);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (resApontamentos.data) {
|
if (resApontamentos.data) {
|
||||||
setApontamentosExistentes(resApontamentos.data as any);
|
setApontamentosExistentes(resApontamentos.data as any);
|
||||||
@@ -292,6 +330,32 @@ export const SmartProducaoFlow: React.FC<SmartProducaoFlowProps> = ({
|
|||||||
return map;
|
return map;
|
||||||
}, [apontamentosPendentes, processoSelecionado]);
|
}, [apontamentosPendentes, processoSelecionado]);
|
||||||
|
|
||||||
|
// Identificar processos de Corte e Solda
|
||||||
|
const procCorteId = useMemo(() => {
|
||||||
|
return processos.find((p) => {
|
||||||
|
const n = (p.nome || '').toLowerCase();
|
||||||
|
return n.includes('corte') || n.includes('prepar') || p.ordem === 1;
|
||||||
|
})?.id;
|
||||||
|
}, [processos]);
|
||||||
|
|
||||||
|
const procSoldaId = useMemo(() => {
|
||||||
|
return processos.find((p) => {
|
||||||
|
const n = (p.nome || '').toLowerCase();
|
||||||
|
return n.includes('solda') || n.includes('montag') || p.ordem === 3;
|
||||||
|
})?.id;
|
||||||
|
}, [processos]);
|
||||||
|
|
||||||
|
// Mapa Térmico e de Kits completo para toda a OF
|
||||||
|
const mapaKits = useMemo(() => {
|
||||||
|
return calcularKitsOF(
|
||||||
|
pecas,
|
||||||
|
componentesOF,
|
||||||
|
apontamentosExistentes,
|
||||||
|
procCorteId,
|
||||||
|
procSoldaId
|
||||||
|
);
|
||||||
|
}, [pecas, componentesOF, apontamentosExistentes, procCorteId, procSoldaId]);
|
||||||
|
|
||||||
// Peças filtradas pela fase selecionada com cálculo estrito de precedência de processos e regra S/M
|
// Peças filtradas pela fase selecionada com cálculo estrito de precedência de processos e regra S/M
|
||||||
const pecasComSaldo = useMemo(() => {
|
const pecasComSaldo = useMemo(() => {
|
||||||
if (!processoSelecionado) return [];
|
if (!processoSelecionado) return [];
|
||||||
@@ -302,7 +366,7 @@ export const SmartProducaoFlow: React.FC<SmartProducaoFlowProps> = ({
|
|||||||
return (p.etapa_fase || 'Geral').trim() === faseSelecionada;
|
return (p.etapa_fase || 'Geral').trim() === faseSelecionada;
|
||||||
})
|
})
|
||||||
.map((p) => {
|
.map((p) => {
|
||||||
const info = calcularSaldoProcessoPeca(p, processoSelecionado, processos, mapaProducao);
|
const info = calcularSaldoProcessoPeca(p, processoSelecionado, processos, mapaProducao, mapaKits);
|
||||||
return {
|
return {
|
||||||
...p,
|
...p,
|
||||||
...info,
|
...info,
|
||||||
@@ -399,7 +463,7 @@ export const SmartProducaoFlow: React.FC<SmartProducaoFlowProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const info = calcularSaldoProcessoPeca(pecaSelecionada, processoSelecionado, processos, mapaProducao);
|
const info = calcularSaldoProcessoPeca(pecaSelecionada, processoSelecionado, processos, mapaProducao, mapaKits);
|
||||||
if (modoEdicao && qtdApontar < info.minPermitido) {
|
if (modoEdicao && qtdApontar < info.minPermitido) {
|
||||||
smartAudio.playAlert();
|
smartAudio.playAlert();
|
||||||
toast.error(`Não é possível reduzir para ${qtdApontar}. O processo seguinte (${info.procSeguinteNome}) já consumiu ${info.minPermitido} peças. Ajuste o processo seguinte primeiro.`);
|
toast.error(`Não é possível reduzir para ${qtdApontar}. O processo seguinte (${info.procSeguinteNome}) já consumiu ${info.minPermitido} peças. Ajuste o processo seguinte primeiro.`);
|
||||||
@@ -760,6 +824,19 @@ export const SmartProducaoFlow: React.FC<SmartProducaoFlowProps> = ({
|
|||||||
>
|
>
|
||||||
<span>🏢 {faseSelecionada}</span>
|
<span>🏢 {faseSelecionada}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
smartAudio.playClick();
|
||||||
|
setIsMapaKitsOpen(true);
|
||||||
|
}}
|
||||||
|
className="px-2.5 py-1 rounded-lg bg-orange-100 text-orange-800 dark:bg-orange-500/20 dark:text-orange-300 font-bold text-xs flex items-center gap-1 border border-orange-200 dark:border-orange-500/30 hover:bg-orange-200 dark:hover:bg-orange-500/30 shrink-0 transition-colors"
|
||||||
|
title="Abrir Mapa Térmico de Kits de Montagem"
|
||||||
|
>
|
||||||
|
<Flame className="w-3.5 h-3.5 text-orange-600 dark:text-orange-400 fill-current animate-pulse" />
|
||||||
|
<span>Mapa de Kits</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -805,6 +882,10 @@ export const SmartProducaoFlow: React.FC<SmartProducaoFlowProps> = ({
|
|||||||
const temPendencia = pendenciasItem.length > 0;
|
const temPendencia = pendenciasItem.length > 0;
|
||||||
const concluida = item.jaProduzido >= item.quantidade;
|
const concluida = item.jaProduzido >= item.quantidade;
|
||||||
const aguardandoAnterior = !concluida && item.saldo <= 0;
|
const aguardandoAnterior = !concluida && item.saldo <= 0;
|
||||||
|
const kitInfo = mapaKits.get(item.id);
|
||||||
|
const ehCorte = processoSelecionado?.id === procCorteId;
|
||||||
|
const ehSolda = processoSelecionado?.id === procSoldaId;
|
||||||
|
const temComponentes = item.tem_componentes !== false && (kitInfo?.componentes.length || 0) > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -886,7 +967,7 @@ export const SmartProducaoFlow: React.FC<SmartProducaoFlowProps> = ({
|
|||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="inline-flex items-center gap-1 text-[11px] font-bold text-slate-600 dark:text-slate-400 px-2 py-0.5 rounded-full bg-slate-100 dark:bg-slate-800 border border-slate-300 dark:border-slate-700 mt-1" title={item.motivoBloqueio || 'Aguardando processo anterior'}>
|
<span className="inline-flex items-center gap-1 text-[11px] font-bold text-slate-600 dark:text-slate-400 px-2 py-0.5 rounded-full bg-slate-100 dark:bg-slate-800 border border-slate-300 dark:border-slate-700 mt-1" title={item.motivoBloqueio || 'Aguardando processo anterior'}>
|
||||||
{item.motivoBloqueio ? item.motivoBloqueio.slice(0, 22) : 'Aguardando anterior'}
|
{item.motivoBloqueio ? item.motivoBloqueio.slice(0, 26) : 'Aguardando anterior'}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -904,6 +985,68 @@ export const SmartProducaoFlow: React.FC<SmartProducaoFlowProps> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* MAPA TÉRMICO DE KITS NO CARD (QUANDO PEÇA POSSUI COMPONENTES) */}
|
||||||
|
{kitInfo && temComponentes && (
|
||||||
|
<div className="mt-2.5 p-2.5 rounded-xl bg-orange-50/70 dark:bg-orange-950/20 border border-orange-200/80 dark:border-orange-800/40 flex flex-col gap-1.5">
|
||||||
|
<div className="flex items-center justify-between text-xs">
|
||||||
|
<div className="flex items-center gap-1.5 font-bold">
|
||||||
|
<Flame
|
||||||
|
className={`w-3.5 h-3.5 shrink-0 ${
|
||||||
|
kitInfo.kits_completos_cortados > 0
|
||||||
|
? 'text-orange-500 fill-orange-500 animate-pulse'
|
||||||
|
: 'text-slate-400'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
<span className="text-slate-800 dark:text-slate-200 font-extrabold text-[11px] uppercase tracking-wide">
|
||||||
|
Kits Montáveis no Corte:
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={`px-1.5 py-0.5 rounded text-[11px] font-black ${
|
||||||
|
kitInfo.kits_completos_cortados >= item.quantidade
|
||||||
|
? 'bg-emerald-100 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-300'
|
||||||
|
: kitInfo.kits_completos_cortados > 0
|
||||||
|
? 'bg-amber-100 text-amber-800 dark:bg-amber-950 dark:text-amber-300'
|
||||||
|
: 'bg-slate-200 text-slate-700 dark:bg-slate-800 dark:text-slate-400'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{kitInfo.kits_completos_cortados} de {item.quantidade} un
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-[11px] font-black text-orange-700 dark:text-orange-400">
|
||||||
|
{kitInfo.percentual_termico}% peso
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mini barra térmica do kit com gradiente */}
|
||||||
|
<div className="w-full bg-slate-200 dark:bg-slate-800 h-1.5 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className={`h-full transition-all duration-300 ${
|
||||||
|
kitInfo.percentual_termico >= 100
|
||||||
|
? 'bg-emerald-500'
|
||||||
|
: kitInfo.percentual_termico >= 50
|
||||||
|
? 'bg-amber-500'
|
||||||
|
: kitInfo.percentual_termico > 0
|
||||||
|
? 'bg-orange-500'
|
||||||
|
: 'bg-slate-300 dark:bg-slate-700'
|
||||||
|
}`}
|
||||||
|
style={{ width: `${kitInfo.percentual_termico}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Gargalo se houver */}
|
||||||
|
{kitInfo.componente_gargalo && kitInfo.kits_completos_cortados < item.quantidade && (
|
||||||
|
<div className="text-[10px] text-amber-800 dark:text-amber-300 font-medium flex items-center justify-between">
|
||||||
|
<span className="truncate">
|
||||||
|
⚠️ Gargalo: <strong>{kitInfo.componente_gargalo.marca_componente || kitInfo.componente_gargalo.descricao}</strong> (falta {kitInfo.componente_gargalo.falta_para_prox_kit} un p/ próx kit)
|
||||||
|
</span>
|
||||||
|
<span className="text-[10px] text-slate-400 shrink-0 ml-1">
|
||||||
|
{kitInfo.componentes.length} itens
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* ALERTA VISUAL: BAIXA FORÇADA PENDENTE DE CONFIRMAÇÃO */}
|
{/* ALERTA VISUAL: BAIXA FORÇADA PENDENTE DE CONFIRMAÇÃO */}
|
||||||
{temPendencia && (
|
{temPendencia && (
|
||||||
<div className="mt-3 p-2.5 rounded-xl bg-red-100 dark:bg-red-950/60 border border-red-300 dark:border-red-800 flex items-center justify-between gap-2 flex-wrap">
|
<div className="mt-3 p-2.5 rounded-xl bg-red-100 dark:bg-red-950/60 border border-red-300 dark:border-red-800 flex items-center justify-between gap-2 flex-wrap">
|
||||||
@@ -927,40 +1070,107 @@ export const SmartProducaoFlow: React.FC<SmartProducaoFlowProps> = ({
|
|||||||
|
|
||||||
{/* Botões de Ação no Card */}
|
{/* Botões de Ação no Card */}
|
||||||
<div className="mt-3 grid grid-cols-1 sm:grid-cols-3 gap-2">
|
<div className="mt-3 grid grid-cols-1 sm:grid-cols-3 gap-2">
|
||||||
<Button
|
{ehCorte && temComponentes && kitInfo ? (
|
||||||
type="button"
|
<>
|
||||||
disabled={concluida || item.saldo <= 0}
|
<Button
|
||||||
onClick={() => handleIniciarApontamentoPeca(item, item.saldo)}
|
type="button"
|
||||||
className={`sm:col-span-2 h-13 text-sm font-black rounded-xl uppercase tracking-wider transition-all active:scale-[0.98] ${
|
onClick={() => {
|
||||||
concluida
|
smartAudio.playClick();
|
||||||
? 'bg-slate-100 text-slate-400 border border-slate-200 dark:bg-slate-800 dark:text-slate-500 dark:border-slate-700 cursor-not-allowed'
|
setPecaParaCorteModal(kitInfo);
|
||||||
: item.saldo <= 0
|
}}
|
||||||
? 'bg-slate-100 text-slate-400 border border-slate-200 dark:bg-slate-800 dark:text-slate-400 dark:border-slate-700 cursor-not-allowed'
|
className="sm:col-span-2 h-13 text-xs sm:text-sm font-black rounded-xl uppercase tracking-wider bg-orange-600 hover:bg-orange-500 active:bg-orange-700 text-white shadow-md shadow-orange-950/20 active:scale-95 transition-all flex items-center justify-center gap-2"
|
||||||
: 'bg-amber-500 hover:bg-amber-400 active:bg-amber-600 text-slate-950 shadow-md shadow-amber-900/20 dark:shadow-amber-950/30'
|
title="Apontar componentes individuais cortados ou cortar 1 kit completo"
|
||||||
}`}
|
>
|
||||||
>
|
<Scissors className="w-4 h-4 shrink-0" />
|
||||||
{concluida
|
<span>Cortar Componentes ({kitInfo.componentes.length} itens)</span>
|
||||||
? 'Processo Concluído'
|
</Button>
|
||||||
: item.saldo <= 0
|
|
||||||
? item.motivoBloqueio || 'Aguardando processo anterior'
|
|
||||||
: `DIGITAR / ESCOLHER QUANTIDADE`}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{!concluida && item.saldo > 0 && (
|
<Button
|
||||||
<Button
|
type="button"
|
||||||
type="button"
|
disabled={concluida || item.saldo <= 0}
|
||||||
onClick={() => {
|
onClick={() => handleIniciarApontamentoPeca(item, item.saldo)}
|
||||||
smartAudio.playClick();
|
className="h-13 bg-amber-50 hover:bg-amber-100 active:bg-amber-200 text-amber-800 border border-amber-300 dark:bg-slate-700 dark:hover:bg-slate-650 dark:active:bg-slate-600 dark:text-amber-400 dark:border-slate-600 text-xs font-black rounded-xl uppercase active:scale-95 transition-colors disabled:opacity-50"
|
||||||
setPecaSelecionada(item);
|
title="Apontar peça mestre inteira"
|
||||||
setQtdApontar(item.saldo);
|
>
|
||||||
setModoEdicao(false);
|
Peça Cheia ({item.saldo})
|
||||||
setEtapaAtual('quantidade');
|
</Button>
|
||||||
}}
|
</>
|
||||||
className="h-13 bg-amber-50 hover:bg-amber-100 active:bg-amber-200 text-amber-800 border border-amber-300 dark:bg-slate-700 dark:hover:bg-slate-650 dark:active:bg-slate-600 dark:text-amber-400 dark:border-slate-600 text-xs font-black rounded-xl uppercase active:scale-95 transition-colors"
|
) : ehSolda && temComponentes && kitInfo ? (
|
||||||
title="Apontar todo o saldo disponível de uma vez"
|
<>
|
||||||
>
|
<Button
|
||||||
TODAS ({item.saldo})
|
type="button"
|
||||||
</Button>
|
disabled={concluida || item.saldo <= 0}
|
||||||
|
onClick={() => handleIniciarApontamentoPeca(item, item.saldo)}
|
||||||
|
className={`sm:col-span-2 h-13 text-xs sm:text-sm font-black rounded-xl uppercase tracking-wider transition-all active:scale-[0.98] ${
|
||||||
|
concluida
|
||||||
|
? 'bg-slate-100 text-slate-400 border border-slate-200 dark:bg-slate-800 dark:text-slate-500 dark:border-slate-700 cursor-not-allowed'
|
||||||
|
: item.saldo <= 0
|
||||||
|
? 'bg-slate-100 text-slate-400 border border-slate-200 dark:bg-slate-800 dark:text-slate-400 dark:border-slate-700 cursor-not-allowed'
|
||||||
|
: 'bg-amber-500 hover:bg-amber-400 active:bg-amber-600 text-slate-950 shadow-md shadow-amber-900/20 dark:shadow-amber-950/30'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{concluida
|
||||||
|
? 'Processo Concluído'
|
||||||
|
: item.saldo <= 0
|
||||||
|
? (item.motivoBloqueio || 'Aguardando kits completos no Corte')
|
||||||
|
: `⚡ SOLDAR (${item.saldo} KITS LIBERADOS)`}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{!concluida && item.saldo > 0 && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
smartAudio.playClick();
|
||||||
|
setPecaSelecionada(item);
|
||||||
|
setQtdApontar(item.saldo);
|
||||||
|
setModoEdicao(false);
|
||||||
|
setEtapaAtual('quantidade');
|
||||||
|
}}
|
||||||
|
className="h-13 bg-amber-50 hover:bg-amber-100 active:bg-amber-200 text-amber-800 border border-amber-300 dark:bg-slate-700 dark:hover:bg-slate-650 dark:active:bg-slate-600 dark:text-amber-400 dark:border-slate-600 text-xs font-black rounded-xl uppercase active:scale-95 transition-colors"
|
||||||
|
title="Apontar todos os kits disponíveis de uma vez"
|
||||||
|
>
|
||||||
|
TODAS ({item.saldo})
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
disabled={concluida || item.saldo <= 0}
|
||||||
|
onClick={() => handleIniciarApontamentoPeca(item, item.saldo)}
|
||||||
|
className={`sm:col-span-2 h-13 text-sm font-black rounded-xl uppercase tracking-wider transition-all active:scale-[0.98] ${
|
||||||
|
concluida
|
||||||
|
? 'bg-slate-100 text-slate-400 border border-slate-200 dark:bg-slate-800 dark:text-slate-500 dark:border-slate-700 cursor-not-allowed'
|
||||||
|
: item.saldo <= 0
|
||||||
|
? 'bg-slate-100 text-slate-400 border border-slate-200 dark:bg-slate-800 dark:text-slate-400 dark:border-slate-700 cursor-not-allowed'
|
||||||
|
: 'bg-amber-500 hover:bg-amber-400 active:bg-amber-600 text-slate-950 shadow-md shadow-amber-900/20 dark:shadow-amber-950/30'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{concluida
|
||||||
|
? 'Processo Concluído'
|
||||||
|
: item.saldo <= 0
|
||||||
|
? item.motivoBloqueio || 'Aguardando processo anterior'
|
||||||
|
: `DIGITAR / ESCOLHER QUANTIDADE`}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{!concluida && item.saldo > 0 && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
smartAudio.playClick();
|
||||||
|
setPecaSelecionada(item);
|
||||||
|
setQtdApontar(item.saldo);
|
||||||
|
setModoEdicao(false);
|
||||||
|
setEtapaAtual('quantidade');
|
||||||
|
}}
|
||||||
|
className="h-13 bg-amber-50 hover:bg-amber-100 active:bg-amber-200 text-amber-800 border border-amber-300 dark:bg-slate-700 dark:hover:bg-slate-650 dark:active:bg-slate-600 dark:text-amber-400 dark:border-slate-600 text-xs font-black rounded-xl uppercase active:scale-95 transition-colors"
|
||||||
|
title="Apontar todo o saldo disponível de uma vez"
|
||||||
|
>
|
||||||
|
TODAS ({item.saldo})
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -998,6 +1208,42 @@ export const SmartProducaoFlow: React.FC<SmartProducaoFlowProps> = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Modal de Mapa Térmico de Kits */}
|
||||||
|
<MapaTermicoKitsModal
|
||||||
|
open={isMapaKitsOpen}
|
||||||
|
onOpenChange={setIsMapaKitsOpen}
|
||||||
|
ofNumber={obra.of_number}
|
||||||
|
fase={faseSelecionada}
|
||||||
|
onSelectPecaParaApontar={(pecaKit) => {
|
||||||
|
const ehProcCorte = processoSelecionado?.id === procCorteId;
|
||||||
|
if (ehProcCorte && pecaKit.tem_componentes) {
|
||||||
|
setPecaParaCorteModal(pecaKit);
|
||||||
|
} else {
|
||||||
|
const pItem = pecasComSaldo.find((p) => p.id === pecaKit.peca_id);
|
||||||
|
if (pItem && pItem.saldo > 0) {
|
||||||
|
handleIniciarApontamentoPeca(pItem, pItem.saldo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Modal de Apontamento de Componentes de Corte */}
|
||||||
|
{pecaParaCorteModal && procCorteId && (
|
||||||
|
<SmartComponentesCorteModal
|
||||||
|
open={!!pecaParaCorteModal}
|
||||||
|
onOpenChange={(aberto) => {
|
||||||
|
if (!aberto) setPecaParaCorteModal(null);
|
||||||
|
}}
|
||||||
|
pecaKit={pecaParaCorteModal}
|
||||||
|
ofNumber={obra.of_number}
|
||||||
|
processoCorteId={procCorteId}
|
||||||
|
onApontamentoSalvo={() => {
|
||||||
|
carregarDadosOF();
|
||||||
|
onApontamentoRealizado(1, 0);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1010,7 +1256,8 @@ export const SmartProducaoFlow: React.FC<SmartProducaoFlowProps> = ({
|
|||||||
pecaSelecionada,
|
pecaSelecionada,
|
||||||
processoSelecionado,
|
processoSelecionado,
|
||||||
processos,
|
processos,
|
||||||
mapaProducao
|
mapaProducao,
|
||||||
|
mapaKits
|
||||||
);
|
);
|
||||||
const saldoPendente = infoSaldoAtual.saldo;
|
const saldoPendente = infoSaldoAtual.saldo;
|
||||||
const maxPermitido = modoEdicao ? infoSaldoAtual.jaProduzido + infoSaldoAtual.saldo : infoSaldoAtual.saldo;
|
const maxPermitido = modoEdicao ? infoSaldoAtual.jaProduzido + infoSaldoAtual.saldo : infoSaldoAtual.saldo;
|
||||||
|
|||||||
Reference in New Issue
Block a user