✅ Restauração do código oficial do GPI-JWT-V3
This commit is contained in:
186
src/client/components/reports/AnalyticalReport.tsx
Normal file
186
src/client/components/reports/AnalyticalReport.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
174
src/client/components/reports/GeneralProjectReport.tsx
Normal file
174
src/client/components/reports/GeneralProjectReport.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
270
src/client/components/reports/StockInventoryReport.tsx
Normal file
270
src/client/components/reports/StockInventoryReport.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user