✅ Restauração do código oficial do GPI-JWT-V3
This commit is contained in:
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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user