feat: adicionar somatorias de peso nos 4 cards de KPIs do mapa termico de kits

This commit is contained in:
2026-10-20 15:53:21 -03:00
parent 9ade5d0d5a
commit 8a707894e0
2 changed files with 126 additions and 38 deletions
@@ -120,6 +120,12 @@ export const MapaTermicoKitsView: React.FC<MapaTermicoKitsViewProps> = ({
});
}, [listaKits, selectedFase, filtroStatus, busca]);
// Formatação de pesos em kg
const formatarKg = (valorKg: number) => {
if (!valorKg || valorKg <= 0) return '0 kg';
return `${valorKg.toLocaleString('pt-BR', { minimumFractionDigits: 1, maximumFractionDigits: 1 })} kg`;
};
// KPIs calculados com base nas peças filtradas
const resumoExibido = useMemo(() => {
let total100 = 0;
@@ -127,18 +133,30 @@ export const MapaTermicoKitsView: React.FC<MapaTermicoKitsViewProps> = ({
let totalTravados = 0;
let pesoTot = 0;
let pesoCort = 0;
let peso100 = 0;
let pesoUnLiberadas = 0;
let pesoParciais = 0;
let pesoNaoIniciados = 0;
let unTot = 0;
let unLib = 0;
pecasFiltradas.forEach((item) => {
unTot += item.quantidade_total;
unLib += item.kits_completos_cortados;
pesoTot += item.peso_total_componentes;
pesoTot += item.peso_total_peca;
pesoCort += item.peso_cortado_componentes;
pesoUnLiberadas += item.peso_kits_liberados;
if (item.status_termico === 'verde') total100++;
else if (item.status_termico === 'verde_parcial' || item.status_termico === 'amarelo') totalParciais++;
else totalTravados++;
if (item.status_termico === 'verde') {
total100++;
peso100 += item.peso_total_peca;
} else if (item.status_termico === 'verde_parcial' || item.status_termico === 'amarelo') {
totalParciais++;
pesoParciais += item.peso_total_peca;
} else {
totalTravados++;
pesoNaoIniciados += item.peso_total_peca;
}
});
return {
@@ -148,6 +166,10 @@ export const MapaTermicoKitsView: React.FC<MapaTermicoKitsViewProps> = ({
totalKitsNaoIniciados: totalTravados,
pesoTotalOFKg: pesoTot,
pesoCortadoTotalKg: pesoCort,
peso100PorcentoKg: peso100,
pesoUnidadesLiberadasKg: pesoUnLiberadas,
pesoParciaisKg: pesoParciais,
pesoNaoIniciadosKg: pesoNaoIniciados,
percentualGeralOF: pesoTot > 0 ? Math.min(100, Math.round((pesoCort / pesoTot) * 100)) : 0,
totalUnidadesTotal: unTot,
totalUnidadesLiberadasSolda: unLib,
@@ -268,72 +290,92 @@ export const MapaTermicoKitsView: React.FC<MapaTermicoKitsViewProps> = ({
<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 className="min-w-0">
<div className="flex items-center gap-1.5 flex-wrap">
<span className="text-[11px] font-black uppercase text-emerald-700 dark:text-emerald-400">
100% Liberadas
</span>
<span className="text-[11px] font-black text-emerald-800 dark:text-emerald-300 bg-emerald-200/80 dark:bg-emerald-900/70 px-1.5 py-0.5 rounded-md border border-emerald-300/80 dark:border-emerald-700 shadow-2xs">
{formatarKg(resumoExibido.peso100PorcentoKg)}
</span>
</div>
<div className="text-2xl font-black text-emerald-900 dark:text-emerald-100 mt-0.5">
<div className="text-2xl font-black text-emerald-900 dark:text-emerald-100 mt-1">
{resumoExibido.totalKits100Porcento}
</div>
<div className="text-[10px] text-emerald-700/80 dark:text-emerald-400/80 font-medium">
<div className="text-[10px] text-emerald-700/80 dark:text-emerald-400/80 font-medium truncate">
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">
<div className="p-2.5 rounded-xl bg-emerald-100 text-emerald-700 dark:bg-emerald-900/60 dark:text-emerald-300 shrink-0 ml-2">
<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 className="min-w-0">
<div className="flex items-center gap-1.5 flex-wrap">
<span className="text-[11px] font-black uppercase text-blue-700 dark:text-blue-400">
Unidades Liberadas
</span>
<span className="text-[11px] font-black text-blue-800 dark:text-blue-300 bg-blue-200/80 dark:bg-blue-900/70 px-1.5 py-0.5 rounded-md border border-blue-300/80 dark:border-blue-700 shadow-2xs">
{formatarKg(resumoExibido.pesoUnidadesLiberadasKg)}
</span>
</div>
<div className="text-2xl font-black text-blue-900 dark:text-blue-100 mt-0.5">
<div className="text-2xl font-black text-blue-900 dark:text-blue-100 mt-1">
{resumoExibido.totalUnidadesLiberadasSolda} <span className="text-xs text-blue-600 font-bold">/ {resumoExibido.totalUnidadesTotal} un</span>
</div>
<div className="text-[10px] text-blue-700/80 dark:text-blue-400/80 font-medium">
<div className="text-[10px] text-blue-700/80 dark:text-blue-400/80 font-medium truncate">
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">
<div className="p-2.5 rounded-xl bg-blue-100 text-blue-700 dark:bg-blue-900/60 dark:text-blue-300 shrink-0 ml-2">
<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 className="min-w-0">
<div className="flex items-center gap-1.5 flex-wrap">
<span className="text-[11px] font-black uppercase text-amber-700 dark:text-amber-400">
Corte em Andamento
</span>
<span className="text-[11px] font-black text-amber-800 dark:text-amber-300 bg-amber-200/80 dark:bg-amber-900/70 px-1.5 py-0.5 rounded-md border border-amber-300/80 dark:border-amber-700 shadow-2xs">
{formatarKg(resumoExibido.pesoParciaisKg)}
</span>
</div>
<div className="text-2xl font-black text-amber-900 dark:text-amber-100 mt-0.5">
<div className="text-2xl font-black text-amber-900 dark:text-amber-100 mt-1">
{resumoExibido.totalKitsParciais}
</div>
<div className="text-[10px] text-amber-700/80 dark:text-amber-400/80 font-medium">
<div className="text-[10px] text-amber-700/80 dark:text-amber-400/80 font-medium truncate">
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">
<div className="p-2.5 rounded-xl bg-amber-100 text-amber-700 dark:bg-amber-900/60 dark:text-amber-300 shrink-0 ml-2">
<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 className="min-w-0">
<div className="flex items-center gap-1.5 flex-wrap">
<span className="text-[11px] font-black uppercase text-rose-700 dark:text-rose-400">
Não Iniciados
</span>
<span className="text-[11px] font-black text-rose-800 dark:text-rose-300 bg-rose-200/80 dark:bg-rose-900/70 px-1.5 py-0.5 rounded-md border border-rose-300/80 dark:border-rose-700 shadow-2xs">
{formatarKg(resumoExibido.pesoNaoIniciadosKg)}
</span>
</div>
<div className="text-2xl font-black text-rose-900 dark:text-rose-100 mt-0.5">
<div className="text-2xl font-black text-rose-900 dark:text-rose-100 mt-1">
{resumoExibido.totalKitsNaoIniciados}
</div>
<div className="text-[10px] text-rose-700/80 dark:text-rose-400/80 font-medium">
<div className="text-[10px] text-rose-700/80 dark:text-rose-400/80 font-medium truncate">
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">
<div className="p-2.5 rounded-xl bg-rose-100 text-rose-700 dark:bg-rose-900/60 dark:text-rose-300 shrink-0 ml-2">
<AlertTriangle className="w-6 h-6" />
</div>
</div>
@@ -485,8 +527,13 @@ export const MapaTermicoKitsView: React.FC<MapaTermicoKitsViewProps> = ({
)}
</div>
<div className="text-xs text-slate-500 dark:text-slate-400 line-clamp-1">
{peca.descricao || 'Peça Estrutural'}
<div className="text-xs text-slate-500 dark:text-slate-400 flex items-center gap-1.5 flex-wrap">
<span className="line-clamp-1">{peca.descricao || 'Peça Estrutural'}</span>
{peca.peso_unitario > 0 && (
<span className="font-semibold text-slate-600 dark:text-slate-400 whitespace-nowrap">
• {peca.peso_unitario.toFixed(1)} kg/un (Total: {peca.peso_total_peca.toFixed(1)} kg)
</span>
)}
</div>
</div>
</div>
+49 -8
View File
@@ -26,6 +26,9 @@ export interface PecaKitInfo {
quantidade_total: number;
etapa_fase: string;
tem_componentes: boolean;
peso_unitario: number;
peso_total_peca: number;
peso_kits_liberados: number;
componentes: ComponenteKitInfo[];
kits_completos_cortados: number; // Quantidade de kits 100% montáveis liberados
percentual_termico: number; // 0 a 100%
@@ -46,6 +49,10 @@ export interface ResumoKitsOF {
totalKitsNaoIniciados: number;
pesoTotalOFKg: number;
pesoCortadoTotalKg: number;
peso100PorcentoKg: number;
pesoUnidadesLiberadasKg: number;
pesoParciaisKg: number;
pesoNaoIniciadosKg: number;
percentualGeralOF: number;
totalUnidadesTotal: number;
totalUnidadesLiberadasSolda: number;
@@ -64,6 +71,8 @@ export const calcularKitsOF = (
quantidade: number;
etapa_fase?: string | null;
tem_componentes?: boolean | null;
peso_unitario?: number | null;
peso_total?: number | null;
}>,
componentes: Array<{
id: string;
@@ -153,12 +162,17 @@ export const calcularKitsOF = (
const temComps = peca.tem_componentes !== false && compsRaw.length > 0;
const jaApontadoSolda = apontadoNaSoldaPorPeca.get(peca.id) || 0;
const pesoUnitPeca = Number(peca.peso_unitario) || (qtdTotalPeca > 0 && peca.peso_total ? Number(peca.peso_total) / qtdTotalPeca : 0);
const pesoTotalPeca = Number(peca.peso_total) || (qtdTotalPeca * pesoUnitPeca);
// 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);
const pesoCortado = kitsCompletos * pesoUnitPeca;
const pesoLiberado = kitsCompletos * pesoUnitPeca;
mapaKits.set(peca.id, {
peca_id: peca.id,
@@ -168,6 +182,9 @@ export const calcularKitsOF = (
quantidade_total: qtdTotalPeca,
etapa_fase: fase,
tem_componentes: false,
peso_unitario: pesoUnitPeca,
peso_total_peca: pesoTotalPeca,
peso_kits_liberados: pesoLiberado,
componentes: [],
kits_completos_cortados: kitsCompletos,
percentual_termico: pct,
@@ -180,8 +197,8 @@ export const calcularKitsOF = (
? 'amarelo'
: 'vermelho',
componente_gargalo: null,
peso_total_componentes: 0,
peso_cortado_componentes: 0,
peso_total_componentes: pesoTotalPeca,
peso_cortado_componentes: pesoCortado,
ja_apontado_solda: jaApontadoSolda,
saldo_disponivel_solda: peca.tem_componentes === false ? 0 : saldoSolda,
bloqueado_para_solda: peca.tem_componentes === false || saldoSolda <= 0,
@@ -293,6 +310,11 @@ export const calcularKitsOF = (
}
}
const pesoUnitComps = pesoTotalComps > 0 && qtdTotalPeca > 0 ? pesoTotalComps / qtdTotalPeca : 0;
const pesoUnitPecaFinal = Number(peca.peso_unitario) || pesoUnitComps;
const pesoTotalPecaFinal = pesoTotalComps > 0 ? pesoTotalComps : (Number(peca.peso_total) || (qtdTotalPeca * pesoUnitPecaFinal));
const pesoLiberadoFinal = kitsCompletos * pesoUnitPecaFinal;
mapaKits.set(peca.id, {
peca_id: peca.id,
marca: peca.marca,
@@ -301,12 +323,15 @@ export const calcularKitsOF = (
quantidade_total: qtdTotalPeca,
etapa_fase: fase,
tem_componentes: true,
peso_unitario: pesoUnitPecaFinal,
peso_total_peca: pesoTotalPecaFinal,
peso_kits_liberados: pesoLiberadoFinal,
componentes: listaCompsInfo,
kits_completos_cortados: kitsCompletos,
percentual_termico: percentualTermico,
status_termico: statusTermico,
componente_gargalo: gargaloComp,
peso_total_componentes: pesoTotalComps,
peso_total_componentes: pesoTotalPecaFinal,
peso_cortado_componentes: pesoCortadoComps,
ja_apontado_solda: jaApontadoSolda,
saldo_disponivel_solda: saldoSolda,
@@ -345,7 +370,7 @@ export const useKitsProducao = (ofNumber?: string, fase?: string) => {
// 2. Buscar peças da OF
let pecasQuery = supabase
.from('pecas')
.select('id, marca, descricao, perfil_principal, quantidade, etapa_fase, tem_componentes')
.select('id, marca, descricao, perfil_principal, quantidade, etapa_fase, tem_componentes, peso_unitario, peso_total')
.eq('of_number', ofNumber);
if (fase && fase !== 'all' && fase !== 'Todas as fases' && fase !== 'todas') {
@@ -411,18 +436,30 @@ export const useKitsProducao = (ofNumber?: string, fase?: string) => {
let totalTravados = 0;
let pesoTot = 0;
let pesoCort = 0;
let peso100 = 0;
let pesoUnLiberadas = 0;
let pesoParciais = 0;
let pesoNaoIniciados = 0;
let unTot = 0;
let unLib = 0;
lista.forEach((item) => {
unTot += item.quantidade_total;
unLib += item.kits_completos_cortados;
pesoTot += item.peso_total_componentes;
pesoTot += item.peso_total_peca;
pesoCort += item.peso_cortado_componentes;
pesoUnLiberadas += item.peso_kits_liberados;
if (item.status_termico === 'verde') total100++;
else if (item.status_termico === 'verde_parcial' || item.status_termico === 'amarelo') totalParciais++;
else totalTravados++;
if (item.status_termico === 'verde') {
total100++;
peso100 += item.peso_total_peca;
} else if (item.status_termico === 'verde_parcial' || item.status_termico === 'amarelo') {
totalParciais++;
pesoParciais += item.peso_total_peca;
} else {
totalTravados++;
pesoNaoIniciados += item.peso_total_peca;
}
});
const resumo: ResumoKitsOF = {
@@ -432,6 +469,10 @@ export const useKitsProducao = (ofNumber?: string, fase?: string) => {
totalKitsNaoIniciados: totalTravados,
pesoTotalOFKg: pesoTot,
pesoCortadoTotalKg: pesoCort,
peso100PorcentoKg: peso100,
pesoUnidadesLiberadasKg: pesoUnLiberadas,
pesoParciaisKg: pesoParciais,
pesoNaoIniciadosKg: pesoNaoIniciados,
percentualGeralOF: pesoTot > 0 ? Math.round((pesoCortadoTotalKg(pesoTot, pesoCort)) * 100) : 0,
totalUnidadesTotal: unTot,
totalUnidadesLiberadasSolda: unLib,