Restauração do código oficial do GPI-JWT-V3

This commit is contained in:
2026-03-18 21:55:33 +00:00
commit 405d121b0e
208 changed files with 38123 additions and 0 deletions

View File

@@ -0,0 +1,186 @@
import React, { useRef, useLayoutEffect } from 'react';
import { format, isValid } from 'date-fns';
import type { Project } from '../../types';
import '../../styles/reports.css';
interface AnalyticalReportProps {
project: Project;
logoUrl?: string;
}
const ProgressFill: React.FC<{ progress: number }> = ({ progress }) => {
const fillRef = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
if (fillRef.current) {
fillRef.current.style.setProperty('--progress', `${progress}%`);
}
}, [progress]);
return <div ref={fillRef} className="evol-fill" />;
};
export const AnalyticalReport: React.FC<AnalyticalReportProps> = ({ project, logoUrl }) => {
const inspections = project.inspections || [];
const sumWeight = inspections.reduce((acc, curr) => acc + (curr.weightKg || 0), 0);
const totalWeight = project.weightKg || 0;
const progress = totalWeight > 0 ? Math.min(Math.round((sumWeight / totalWeight) * 100), 100) : 0;
// Período
const startDate = project.startDate ? new Date(project.startDate) : null;
const endDate = project.endDate ? new Date(project.endDate) : null;
const periodStr = (startDate && isValid(startDate) && endDate && isValid(endDate))
? `${format(startDate, 'MM/yyyy')} ${format(endDate, 'MM/yyyy')}`
: '--/----';
return (
<div className="report-container print:block hidden" id="analytical-report">
<header className="report-header">
<div className="brand">
{logoUrl ? (
<img src={logoUrl} alt="Logo" className="brand-logo" />
) : (
<div className="logo-placeholder"></div>
)}
</div>
<div className="text-center">
<div className="brand-title">RELATÓRIO ANALÍTICO DE OBRA</div>
<div className="brand-subtitle">
Detalhamento de Inspeções, Aplicativos e Esquemas de Pintura
</div>
</div>
<div className="meta">
<div><strong>Data:</strong> {format(new Date(), 'dd/MM/yyyy')}</div>
<div><strong>Obra:</strong> {project.name.toUpperCase()}</div>
</div>
</header>
<section className="summary">
<div className="summary-item">
<div className="summary-label">Evolução Real</div>
<div className="summary-value">{progress}%</div>
<div className="evol-bar">
<ProgressFill progress={progress} />
</div>
</div>
<div className="summary-item">
<div className="summary-label">Peso Medido (kgf)</div>
<div className="summary-value">{sumWeight.toLocaleString('pt-BR')}</div>
<div className="summary-sub">de {totalWeight.toLocaleString('pt-BR')} total</div>
</div>
<div className="summary-item">
<div className="summary-label">Responsável</div>
<div className="summary-value text-11pt">{project.technician || '________________'}</div>
<div className="summary-sub">Técnico Encarregado</div>
</div>
<div className="summary-item">
<div className="summary-label">Período de Obra</div>
<div className="summary-value text-11pt">{periodStr}</div>
<div className="summary-sub">Cronograma Previsto</div>
</div>
</section>
<div className="section-title">
<h2>ESQUEMA DE PINTURA REQUERIDO</h2>
<span>Especificação técnica por demão</span>
</div>
<table className="table">
<thead>
<tr>
<th className="w-20">Etapa</th>
<th className="w-40">Produto</th>
<th className="w-20">Cor</th>
<th className="w-20">EPS (μm)</th>
</tr>
</thead>
<tbody>
{project.paintingSchemes?.map((s, idx) => (
<tr key={idx}>
<td className="font-bold uppercase">{s.coat || s.type}</td>
<td>{s.name.toUpperCase()}</td>
<td>{s.color || '--'}</td>
<td className="font-bold">{s.epsMin}-{s.epsMax} μm</td>
</tr>
))}
{(!project.paintingSchemes || project.paintingSchemes.length === 0) && (
<tr><td colSpan={4} className="text-center p-10mm text-gray-muted">Nenhum esquema definido</td></tr>
)}
</tbody>
</table>
<div className="grid-2col">
<div>
<div className="section-title">
<h2>INSPEÇÕES REALIZADAS</h2>
</div>
<table className="table">
<thead>
<tr>
<th className="w-25">Data</th>
<th className="w-40">Peça / Área</th>
<th className="w-20">Peso</th>
<th className="w-15">Status</th>
</tr>
</thead>
<tbody>
{inspections.slice(0, 15).map((insp, idx) => (
<tr key={idx}>
<td>{insp.date ? format(new Date(insp.date), 'dd/MM/yy') : '--'}</td>
<td className="uppercase font-medium text-8pt">{insp.pieceDescription}</td>
<td className="font-bold">{insp.weightKg?.toLocaleString('pt-BR')}</td>
<td>
<span className={`badge ${insp.appearance === 'approved' ? 'badge-ok' : 'badge-err'}`}>
{insp.appearance === 'approved' ? 'OK' : 'REJ'}
</span>
</td>
</tr>
))}
{inspections.length === 0 && (
<tr><td colSpan={4} className="text-center p-5mm text-gray-muted">Sem registros</td></tr>
)}
</tbody>
</table>
</div>
<div>
<div className="section-title">
<h2>REGISTROS DE APLICAÇÃO</h2>
</div>
<table className="table">
<thead>
<tr>
<th className="w-25">Data</th>
<th className="w-35">Etapa</th>
<th className="w-20">EPS Seca</th>
<th className="w-20">Pintor</th>
</tr>
</thead>
<tbody>
{project.applicationRecords?.slice(0, 15).map((record, idx) => (
<tr key={idx}>
<td>{record.date ? format(new Date(record.date), 'dd/MM/yy') : '--'}</td>
<td className="uppercase font-medium text-8pt">{record.coatStage}</td>
<td className="font-bold">{record.dryThicknessCalc || '--'} μm</td>
<td className="uppercase text-7pt">{record.operator?.split(' ')[0]}</td>
</tr>
))}
{(!project.applicationRecords || project.applicationRecords.length === 0) && (
<tr><td colSpan={4} className="text-center p-5mm text-gray-muted">Sem registros</td></tr>
)}
</tbody>
</table>
</div>
</div>
<footer className="report-footer">
<div className="system-title">
SteelPaint - Gestão de Pintura Industrial
</div>
<div>
Gerado em {format(new Date(), 'dd/MM/yyyy')} às {format(new Date(), 'HH:mm')}h
</div>
<div className="sig-group">
<div className="sig-line">Responsável Qualidade<span></span></div>
</div>
</footer>
</div>
);
};

View File

@@ -0,0 +1,174 @@
import React, { useRef, useLayoutEffect } from 'react';
import { format, min, max, isValid } from 'date-fns';
import type { Project, Inspection } from '../../types';
import '../../styles/reports.css';
interface GeneralProjectReportProps {
projects: Project[];
inspections?: Inspection[];
title: string;
logoUrl?: string;
}
const ProgressFill: React.FC<{ progress: number }> = ({ progress }) => {
const fillRef = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
if (fillRef.current) {
fillRef.current.style.setProperty('--progress', `${progress}%`);
}
}, [progress]);
return <div ref={fillRef} className="evol-fill" />;
};
export const GeneralProjectReport: React.FC<GeneralProjectReportProps> = ({ projects, inspections = [], title, logoUrl }) => {
// Cálculos globais
const totalWeight = projects.reduce((acc, p) => acc + (p.weightKg || 0), 0);
const calculateProjectProgress = (project: Project) => {
const projectInspections = inspections.filter(i => i.projectId === project.id);
const sumWeight = projectInspections.reduce((acc, curr) => acc + (curr.weightKg || 0), 0);
const totalW = project.weightKg || 0;
return totalW > 0 ? Math.min((sumWeight / totalW) * 100, 100) : 0;
};
const avgProgress = projects.length > 0
? projects.reduce((acc, p) => acc + calculateProjectProgress(p), 0) / projects.length
: 0;
// Período (Min/Max das datas)
const allDates = projects.flatMap(p => [
p.startDate ? new Date(p.startDate) : null,
p.endDate ? new Date(p.endDate) : null
]).filter((d): d is Date => d !== null && isValid(d));
const periodStart = allDates.length > 0 ? format(min(allDates), 'MM/yyyy') : '--/----';
const periodEnd = allDates.length > 0 ? format(max(allDates), 'MM/yyyy') : '--/----';
return (
<div className="report-container print:block hidden" id="general-project-report">
<header className="report-header">
<div className="brand">
{logoUrl ? (
<img src={logoUrl} alt="Logo" className="brand-logo" />
) : (
<div className="logo-placeholder"></div>
)}
</div>
<div className="text-center">
<div className="brand-title">{title.toUpperCase()}</div>
<div className="brand-subtitle">
Obras / Projetos Situação de produção e pintura
</div>
</div>
<div className="meta">
<div><strong>Data:</strong> {format(new Date(), 'dd/MM/yyyy')}</div>
<div><strong>Responsável:</strong> ________________</div>
</div>
</header>
<section className="summary">
<div className="summary-item">
<div className="summary-label">Total de obras</div>
<div className="summary-value">{projects.length}</div>
<div className="summary-sub">Listadas neste relatório</div>
</div>
<div className="summary-item">
<div className="summary-label">Peso total (kgf)</div>
<div className="summary-value">{totalWeight.toLocaleString('pt-BR')}</div>
<div className="summary-sub">Soma dos projetos</div>
</div>
<div className="summary-item">
<div className="summary-label">Evolução média</div>
<div className="summary-value">{avgProgress.toFixed(1).replace('.', ',')}%</div>
<div className="summary-sub">Estimativa geral</div>
</div>
<div className="summary-item">
<div className="summary-label">Período</div>
<div className="summary-value">{periodStart} {periodEnd}</div>
<div className="summary-sub">Previsão de execução</div>
</div>
</section>
<div className="section-title">
<h2>OBRAS / PROJETOS</h2>
<span>Visão geral por cronograma, peso e sistema de pintura</span>
</div>
<table className="table">
<thead>
<tr>
<th className="col-obra">Obra / Projeto</th>
<th className="col-evol">Evol.</th>
<th className="col-cron">Cronograma</th>
<th className="col-peso text-center">Peso (kgf)</th>
<th className="col-tinta">Tinta</th>
<th className="col-cor">Cor</th>
</tr>
</thead>
<tbody>
{projects.map((project) => {
const progress = calculateProjectProgress(project);
const schemes = project.paintingSchemes || [];
return (
<tr key={project.id}>
<td className="col-obra">
<div className="obra-nome">{project.name.toUpperCase()}</div>
<div className="obra-cliente">Cliente: {project.client}</div>
<div className="obra-cliente">Gestor: {project.technician || '--'}</div>
</td>
<td className="col-evol">
<div className="font-bold">{Math.round(progress)}%</div>
<div className="evol-bar">
<ProgressFill progress={progress} />
</div>
</td>
<td className="col-cron">
<div className="cron">
<strong>Início:</strong> {project.startDate ? format(new Date(project.startDate), 'dd/MM/yyyy') : '--/--/----'}<br />
<strong>Término:</strong> {project.endDate ? format(new Date(project.endDate), 'dd/MM/yyyy') : '--/--/----'}
</div>
</td>
<td className="col-peso text-center">
<div className="font-bold">
{(project.weightKg || 0).toLocaleString('pt-BR')}
<span className="block text-[7pt] text-gray-400 font-normal">Est. total</span>
</div>
</td>
<td className="col-tinta">
<div className="tinta">
{schemes.length > 0 ? schemes.slice(0, 2).map((s, idx) => (
<React.Fragment key={idx}>
<strong>{s.name.toUpperCase()}</strong>
{s.coat || s.type || 'Esquema'}{idx < schemes.length - 1 && <br />}
</React.Fragment>
)) : <span className="text-gray-400 italic">Sem esquema</span>}
</div>
</td>
<td className="col-cor">
<div className="text-[8pt] leading-tight">
{schemes.length > 0 ? schemes.slice(0, 2).map((s, idx) => (
<div key={idx}>{s.color || '-'}</div>
)) : '-'}
</div>
</td>
</tr>
);
})}
</tbody>
</table>
<footer className="report-footer">
<div className="system-title">
SteelPaint - Gestão de Pintura Industrial
</div>
<div>
Gerado em {format(new Date(), 'dd/MM/yyyy')} às {format(new Date(), 'HH:mm')}h
</div>
<div className="sig-group">
<div className="sig-line">Responsável Qualidade<span></span></div>
</div>
</footer>
</div>
);
};

View File

@@ -0,0 +1,270 @@
import React from 'react';
import { format } from 'date-fns';
import type { StockItem, StockMovement } from '../../services/stockService';
import '../../styles/reports.css';
interface StockInventoryReportProps {
items: StockItem[];
movements: Map<string, StockMovement[]>; // Key: stockItemId
logoUrl?: string;
reportType?: 'PAINT' | 'THINNER';
}
export const StockInventoryReport: React.FC<StockInventoryReportProps> = ({ items, movements, logoUrl, reportType = 'PAINT' }) => {
// Agrupamento por Produto + Cor
const groups = React.useMemo(() => {
const map = new Map<string, {
key: string;
productName: string;
manufacturer: string;
color: string;
minStock: number;
totalQty: number;
unit: string;
items: StockItem[];
isLowStock: boolean;
}>();
items.forEach(item => {
const productName = typeof item.dataSheetId === 'object' ? item.dataSheetId.name : 'Desconhecido';
const manufacturer = typeof item.dataSheetId === 'object' ? item.dataSheetId.manufacturer : '';
// Se for diluente, agrupar apenas pelo ID do produto, ignorando cor.
// Se for tinta, agrupar por produto + cor.
const key = reportType === 'THINNER'
? `${item.dataSheetId._id || item.dataSheetId}`
: `${item.dataSheetId._id || item.dataSheetId}-${item.color}`;
if (!map.has(key)) {
map.set(key, {
key,
productName,
manufacturer,
color: reportType === 'THINNER' ? '-' : (item.color || '-'),
minStock: item.minStock || 0,
totalQty: 0,
unit: item.unit,
items: [],
isLowStock: false
});
}
const group = map.get(key)!;
group.items.push(item);
group.totalQty += item.quantity;
if (item.minStock && item.minStock > group.minStock) {
group.minStock = item.minStock;
}
});
// Avaliar status de estoque baixo por grupo
for (const group of map.values()) {
if (group.minStock > 0 && group.totalQty < group.minStock) {
group.isLowStock = true;
}
}
return Array.from(map.values());
}, [items, reportType]);
// Cálculos globais
const totalItems = items.length;
const totalQuantity = items.reduce((acc, item) => acc + item.quantity, 0);
const expiredItems = items.filter(item =>
item.expirationDate && new Date(item.expirationDate) < new Date()
).length;
const lowStockGroupsCount = groups.filter(g => g.isLowStock).length;
const formatMovementType = (type: string) => {
switch (type) {
case 'ENTRY': return 'Entrada';
case 'ADJUSTMENT': return 'Ajuste';
case 'CONSUMPTION': return 'Consumo';
default: return type;
}
};
return (
<div className="report-container print:block hidden" id="stock-inventory-report">
<header className="report-header">
<div className="brand">
{logoUrl ? (
<img src={logoUrl} alt="Logo" className="brand-logo" />
) : (
<div className="logo-placeholder"></div>
)}
</div>
<div className="text-center">
<div className="brand-title">INVENTÁRIO DE ESTOQUE - {reportType === 'THINNER' ? 'DILUENTES' : 'TINTAS'}</div>
<div className="brand-subtitle">
Controle de {reportType === 'THINNER' ? 'Diluentes' : 'Tintas'} Agrupado por Produto
</div>
</div>
<div className="meta">
<div><strong>Data:</strong> {format(new Date(), 'dd/MM/yyyy')}</div>
<div><strong>Grupos Críticos:</strong> <span style={{ color: lowStockGroupsCount > 0 ? 'red' : 'inherit' }}>{lowStockGroupsCount}</span></div>
</div>
</header>
<section className="summary">
<div className="summary-item">
<div className="summary-label">Total de Lotes</div>
<div className="summary-value">{totalItems}</div>
<div className="summary-sub">Entradas ativas</div>
</div>
<div className="summary-item">
<div className="summary-label">Volume Total</div>
<div className="summary-value">{totalQuantity.toFixed(1)}</div>
<div className="summary-sub">Litros em estoque</div>
</div>
<div className="summary-item">
<div className="summary-label">Alertas de Estoque</div>
<div className="summary-value" style={{ color: lowStockGroupsCount > 0 ? '#ef4444' : '#10b981' }}>{lowStockGroupsCount}</div>
<div className="summary-sub">Produtos abaixo do mínimo</div>
</div>
<div className="summary-item">
<div className="summary-label">Vencidos</div>
<div className="summary-value" style={{ color: expiredItems > 0 ? '#ef4444' : 'inherit' }}>{expiredItems}</div>
<div className="summary-sub">Lotes expirados</div>
</div>
</section>
<div className="section-title">
<h2>DETALHAMENTO DO ESTOQUE</h2>
</div>
<table className="table">
<thead>
<tr>
<th className="col-obra" style={{ width: '35%' }}>Produto / RR</th>
<th className="col-cron" style={{ width: '20%' }}>Lote / Validade</th>
<th className="col-peso text-center" style={{ width: '15%' }}>Quantidade</th>
{reportType === 'PAINT' && <th className="col-tinta" style={{ width: '15%' }}>Cor</th>}
<th className="col-cor" style={{ width: '15%' }}>Nota Fiscal</th>
</tr>
</thead>
<tbody>
{groups.map((group) => (
<React.Fragment key={group.key}>
{/* Group Header Row */}
<tr style={{ backgroundColor: '#f3f4f6', borderTop: '2px solid #e5e7eb', borderBottom: '1px solid #e5e7eb' }}>
<td colSpan={2} style={{ padding: '8px 12px' }}>
<div style={{ fontWeight: 'bold', fontSize: '10pt', color: '#111827' }}>
{group.productName.toUpperCase()}
</div>
<div style={{ fontSize: '7pt', color: '#6b7280' }}>
{group.manufacturer}
</div>
</td>
<td className="text-center" style={{ padding: '8px 12px' }}>
<div style={{ fontWeight: 'bold', fontSize: '10pt', color: group.isLowStock ? '#ef4444' : '#111827' }}>
{group.isLowStock && <span style={{ marginRight: '4px' }}></span>}
{group.totalQty.toFixed(1)} {group.unit}
</div>
{group.minStock > 0 && (
<div style={{ fontSize: '7pt', color: '#6b7280' }}>
Mín: {group.minStock}
</div>
)}
</td>
{reportType === 'PAINT' && (
<td style={{ padding: '8px 12px', verticalAlign: 'middle', fontWeight: 'bold', fontSize: '9pt', color: '#374151' }}>
{group.color}
</td>
)}
<td style={{ padding: '8px 12px', textAlign: 'right', fontSize: '8pt', color: '#6b7280' }}>
{group.items.length} lote(s)
</td>
</tr>
{/* Individual Items */}
{group.items.map((item) => {
const isExpired = item.expirationDate && new Date(item.expirationDate) < new Date();
const itemMovements = movements.get(item._id!) || [];
return (
<React.Fragment key={item._id}>
<tr>
<td className="col-obra" style={{ paddingLeft: '24px' }}>
<div className="obra-nome" style={{ fontSize: '9pt' }}>RR: <strong>{item.rrNumber}</strong></div>
</td>
<td className="col-cron">
<div className="cron">
<strong>Lote:</strong> {item.batchNumber}<br />
{reportType === 'PAINT' && (
<>
<strong>Val:</strong>{' '}
<span style={{ color: isExpired ? '#ef4444' : 'inherit', fontWeight: isExpired ? 'bold' : 'normal' }}>
{item.expirationDate ? format(new Date(item.expirationDate), 'dd/MM/yyyy') : '-'}
</span>
</>
)}
</div>
</td>
<td className="col-peso text-center">
<div style={{ fontSize: '9pt' }}>
{item.quantity.toFixed(1)} {item.unit}
</div>
</td>
{reportType === 'PAINT' && (
<td className="col-tinta">
{/* Cor is already in group header, repeated here only if needed or keep empty/dash */}
<div style={{ opacity: 0.5 }}>-</div>
</td>
)}
<td className="col-cor">
<div style={{ fontSize: '8pt' }}>
{item.invoiceNumber || '-'}
</div>
{item.receivedBy && (
<div style={{ fontSize: '7pt', color: '#9ca3af' }}> Rec: {item.receivedBy}</div>
)}
</td>
</tr>
{/* Movimentações inline */}
{itemMovements.length > 0 && (
<tr>
<td colSpan={reportType === 'PAINT' ? 5 : 4} style={{ padding: '2px 24px 8px 24px', border: 'none' }}>
<div style={{ fontSize: '6.5pt', color: '#9ca3af', borderLeft: '2px solid #e5e7eb', paddingLeft: '8px' }}>
{itemMovements.slice(0, 5).map((mov, idx) => (
<span key={mov._id} style={{ marginRight: '8px', whiteSpace: 'nowrap' }}>
{format(new Date(mov.date), 'dd/MM/yy')} -{' '}
<strong>{formatMovementType(mov.type)}</strong>:{' '}
{mov.type === 'CONSUMPTION' ? '-' : '+'}{Math.abs(mov.quantity)}L
{mov.responsible && ` (${mov.responsible})`}
{idx < Math.min(itemMovements.length, 5) - 1 && ' | '}
</span>
))}
{itemMovements.length > 5 && (
<span style={{ fontStyle: 'italic', color: '#9ca3af' }}>
... +{itemMovements.length - 5} movimentações
</span>
)}
</div>
</td>
</tr>
)}
</React.Fragment>
);
})}
</React.Fragment>
))}
</tbody>
</table>
<footer className="report-footer">
<div className="system-title">
SteelPaint - Gestão de Pintura Industrial
</div>
<div>
Gerado em {format(new Date(), 'dd/MM/yyyy')} às {format(new Date(), 'HH:mm')}h
</div>
<div className="sig-group">
<div className="sig-line">Responsável Estoque<span></span></div>
<div className="sig-line">Responsável Qualidade<span></span></div>
</div>
</footer>
</div>
);
};