feat: mapa termico de kits de corte e liberacao balanceada para solda
This commit is contained in:
@@ -3,10 +3,11 @@ import React, { useState } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
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 { ApontamentosListOtimizado } from '@/components/apontamento/ApontamentosListOtimizado';
|
||||
import { ProcessosList } from '@/components/apontamento/ProcessosList';
|
||||
import { MapaTermicoKitsView } from '@/components/apontamento/kits/MapaTermicoKitsView';
|
||||
import { useApontamentosProducao } from '@/hooks/useApontamentosProducao';
|
||||
import { useIsMobile } from '@/hooks/use-mobile';
|
||||
import { usePermissionControl } from '@/hooks/usePermissionControl';
|
||||
@@ -34,7 +35,7 @@ const ApontamentoProducao = () => {
|
||||
<TabsList className={`w-full bg-muted border-border ${
|
||||
isMobile
|
||||
? 'grid grid-cols-1 gap-1 h-auto p-1'
|
||||
: 'grid grid-cols-3 h-10'
|
||||
: 'grid grid-cols-4 h-10'
|
||||
}`}>
|
||||
<TabsTrigger
|
||||
value="apontamento"
|
||||
@@ -45,6 +46,15 @@ const ApontamentoProducao = () => {
|
||||
<ClipboardList className="h-4 w-4" />
|
||||
Novo Apontamento
|
||||
</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
|
||||
value="historico"
|
||||
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 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">
|
||||
<Card className="bg-card border-border">
|
||||
<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,
|
||||
Delete,
|
||||
Pencil,
|
||||
CheckCheck
|
||||
CheckCheck,
|
||||
Flame
|
||||
} from 'lucide-react';
|
||||
import { OFAtiva } from '@/hooks/useOFsAtivas';
|
||||
import { useProcessosFabricacao, ProcessoFabricacao } from '@/hooks/useProcessosFabricacao';
|
||||
@@ -25,6 +26,9 @@ import { smartAudio } from '@/utils/smartAudio';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { toast } from 'sonner';
|
||||
import { SmartForcarApontamentoModal } from './SmartForcarApontamentoModal';
|
||||
import { calcularKitsOF, PecaKitInfo } from '@/hooks/useKitsProducao';
|
||||
import { MapaTermicoKitsModal } from '@/components/apontamento/kits/MapaTermicoKitsModal';
|
||||
import { SmartComponentesCorteModal } from './SmartComponentesCorteModal';
|
||||
|
||||
interface SmartProducaoFlowProps {
|
||||
obra: OFAtiva;
|
||||
@@ -58,7 +62,8 @@ export const calcularSaldoProcessoPeca = (
|
||||
peca: PecaData,
|
||||
processoAtual: ProcessoFabricacao,
|
||||
todosProcessos: ProcessoFabricacao[],
|
||||
mapaProd: Map<string, number>
|
||||
mapaProd: Map<string, number>,
|
||||
mapaKits?: Map<string, PecaKitInfo>
|
||||
): CalculoSaldoPeca => {
|
||||
const ehSemMontagem = peca.tem_componentes === false;
|
||||
const keyAtual = `${peca.id}_${processoAtual.id}`;
|
||||
@@ -68,7 +73,7 @@ export const calcularSaldoProcessoPeca = (
|
||||
const indexAtual = procsOrdenados.findIndex((p) => p.id === processoAtual.id);
|
||||
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 motivoBloqueio: string | null = null;
|
||||
@@ -77,6 +82,18 @@ export const calcularSaldoProcessoPeca = (
|
||||
if (ehSemMontagem && ehSolda) {
|
||||
qtdDisponivelParaEntrar = 0;
|
||||
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) {
|
||||
// REGRA 2: Encontrar processo anterior válido
|
||||
let procAnteriorValido: ProcessoFabricacao | null = null;
|
||||
@@ -176,12 +193,17 @@ export const SmartProducaoFlow: React.FC<SmartProducaoFlowProps> = ({
|
||||
// Modal de Forçar Apontamento
|
||||
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
|
||||
const [pecas, setPecas] = useState<PecaData[]>([]);
|
||||
const [apontamentosExistentes, setApontamentosExistentes] = useState<ApontamentoItem[]>([]);
|
||||
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 () => {
|
||||
try {
|
||||
setCarregandoDados(true);
|
||||
@@ -199,6 +221,22 @@ export const SmartProducaoFlow: React.FC<SmartProducaoFlowProps> = ({
|
||||
|
||||
if (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) {
|
||||
setApontamentosExistentes(resApontamentos.data as any);
|
||||
@@ -292,6 +330,32 @@ export const SmartProducaoFlow: React.FC<SmartProducaoFlowProps> = ({
|
||||
return map;
|
||||
}, [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
|
||||
const pecasComSaldo = useMemo(() => {
|
||||
if (!processoSelecionado) return [];
|
||||
@@ -302,7 +366,7 @@ export const SmartProducaoFlow: React.FC<SmartProducaoFlowProps> = ({
|
||||
return (p.etapa_fase || 'Geral').trim() === faseSelecionada;
|
||||
})
|
||||
.map((p) => {
|
||||
const info = calcularSaldoProcessoPeca(p, processoSelecionado, processos, mapaProducao);
|
||||
const info = calcularSaldoProcessoPeca(p, processoSelecionado, processos, mapaProducao, mapaKits);
|
||||
return {
|
||||
...p,
|
||||
...info,
|
||||
@@ -399,7 +463,7 @@ export const SmartProducaoFlow: React.FC<SmartProducaoFlowProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const info = calcularSaldoProcessoPeca(pecaSelecionada, processoSelecionado, processos, mapaProducao);
|
||||
const info = calcularSaldoProcessoPeca(pecaSelecionada, processoSelecionado, processos, mapaProducao, mapaKits);
|
||||
if (modoEdicao && qtdApontar < info.minPermitido) {
|
||||
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.`);
|
||||
@@ -760,6 +824,19 @@ export const SmartProducaoFlow: React.FC<SmartProducaoFlowProps> = ({
|
||||
>
|
||||
<span>🏢 {faseSelecionada}</span>
|
||||
</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 className="flex items-center gap-2">
|
||||
@@ -805,6 +882,10 @@ export const SmartProducaoFlow: React.FC<SmartProducaoFlowProps> = ({
|
||||
const temPendencia = pendenciasItem.length > 0;
|
||||
const concluida = item.jaProduzido >= item.quantidade;
|
||||
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 (
|
||||
<div
|
||||
@@ -886,7 +967,7 @@ export const SmartProducaoFlow: React.FC<SmartProducaoFlowProps> = ({
|
||||
</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'}>
|
||||
{item.motivoBloqueio ? item.motivoBloqueio.slice(0, 22) : 'Aguardando anterior'}
|
||||
{item.motivoBloqueio ? item.motivoBloqueio.slice(0, 26) : 'Aguardando anterior'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -904,6 +985,68 @@ export const SmartProducaoFlow: React.FC<SmartProducaoFlowProps> = ({
|
||||
/>
|
||||
</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 */}
|
||||
{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">
|
||||
@@ -927,40 +1070,107 @@ export const SmartProducaoFlow: React.FC<SmartProducaoFlowProps> = ({
|
||||
|
||||
{/* Botões de Ação no Card */}
|
||||
<div className="mt-3 grid grid-cols-1 sm:grid-cols-3 gap-2">
|
||||
<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>
|
||||
{ehCorte && temComponentes && kitInfo ? (
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
smartAudio.playClick();
|
||||
setPecaParaCorteModal(kitInfo);
|
||||
}}
|
||||
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"
|
||||
title="Apontar componentes individuais cortados ou cortar 1 kit completo"
|
||||
>
|
||||
<Scissors className="w-4 h-4 shrink-0" />
|
||||
<span>Cortar Componentes ({kitInfo.componentes.length} itens)</span>
|
||||
</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>
|
||||
<Button
|
||||
type="button"
|
||||
disabled={concluida || item.saldo <= 0}
|
||||
onClick={() => handleIniciarApontamentoPeca(item, item.saldo)}
|
||||
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"
|
||||
title="Apontar peça mestre inteira"
|
||||
>
|
||||
Peça Cheia ({item.saldo})
|
||||
</Button>
|
||||
</>
|
||||
) : ehSolda && temComponentes && kitInfo ? (
|
||||
<>
|
||||
<Button
|
||||
type="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>
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -1010,7 +1256,8 @@ export const SmartProducaoFlow: React.FC<SmartProducaoFlowProps> = ({
|
||||
pecaSelecionada,
|
||||
processoSelecionado,
|
||||
processos,
|
||||
mapaProducao
|
||||
mapaProducao,
|
||||
mapaKits
|
||||
);
|
||||
const saldoPendente = infoSaldoAtual.saldo;
|
||||
const maxPermitido = modoEdicao ? infoSaldoAtual.jaProduzido + infoSaldoAtual.saldo : infoSaldoAtual.saldo;
|
||||
|
||||
Reference in New Issue
Block a user