import React, { useEffect, useState } from 'react'; import { useParams, Link, useLocation } from 'react-router-dom'; import api from '../services/api'; import { Button } from '../components/Button'; import { Card } from '../components/Card'; import { ArrowLeft, Layers, PenTool, ClipboardCheck, Activity, Trash2, Pencil, Copy, RefreshCw, Thermometer, Droplets, Sun } from 'lucide-react'; import { clsx } from 'clsx'; import { format } from 'date-fns'; import { useAuth } from '../context/useAuth'; import { useToast } from '../hooks/useToast'; import { CreatePartModal } from '../components/modals/CreatePartModal'; import { CreatePaintingSchemeModal } from '../components/modals/CreatePaintingSchemeModal'; import { CreateControlRecordModal } from '../components/modals/CreateControlRecordModal'; import { CreateInspectionModal } from '../components/modals/CreateInspectionModal'; import { CreateProjectModal } from '../components/modals/CreateProjectModal'; import { CloneSchemeModal } from '../components/modals/CloneSchemeModal'; import { ImportSchemeModal } from '../components/modals/ImportSchemeModal'; import { MobileList } from '../components/MobileList'; import { useNavigate } from 'react-router-dom'; import type { Project, Part, PaintingScheme, Inspection, ApplicationRecord, GeometryType, TechnicalDataSheet } from '../types'; import { AnalysisDashboard } from './AnalysisDashboard'; import * as geometryTypeService from '../services/geometryTypeService'; import { ColorBubble } from '../components/ColorBubble'; export const ProjectDetails: React.FC = () => { const { id } = useParams<{ id: string }>(); const { isAdmin, isUser, appUser, isGuest } = useAuth(); const { showGuestWarning } = useToast(); const canEditItem = (item: { createdBy?: string }) => { if (isAdmin()) return true; if (!isUser() || !appUser) return false; return item.createdBy === appUser.id; }; const navigate = useNavigate(); const location = useLocation(); const [project, setProject] = useState(null); const [loading, setLoading] = useState(true); const [isPartModalOpen, setIsPartModalOpen] = useState(false); const [isSchemeModalOpen, setIsSchemeModalOpen] = useState(false); const [isControlModalOpen, setIsControlModalOpen] = useState(false); const [isInspectionModalOpen, setIsInspectionModalOpen] = useState(false); const [isEditProjectModalOpen, setIsEditProjectModalOpen] = useState(false); const [isCloneModalOpen, setIsCloneModalOpen] = useState(false); const [isImportModalOpen, setIsImportModalOpen] = useState(false); const [isExchangeMode, setIsExchangeMode] = useState(false); interface LocationState { activeTab?: 'parts' | 'scheme' | 'control' | 'inspection' | 'analysis'; } const initialTab = (location.state as LocationState)?.activeTab || 'parts'; const [activeTab, setActiveTab] = useState<'parts' | 'scheme' | 'control' | 'inspection' | 'analysis'>(initialTab); const [editingScheme, setEditingScheme] = useState(undefined); const [cloningScheme, setCloningScheme] = useState(undefined); const [editingPart, setEditingPart] = useState(undefined); const [editingInspection, setEditingInspection] = useState(undefined); const [editingRecord, setEditingRecord] = useState(undefined); const [geometryTypes, setGeometryTypes] = useState([]); const getLossForType = (type?: string) => { if (!type) return '--'; const geo = geometryTypes.find(g => g.name === type); return geo ? `${geo.efficiencyLoss}%` : '--'; }; const handleDeleteScheme = async (schemeId: string) => { if (isGuest()) { showGuestWarning(); return; } if (!confirm('Tem certeza que deseja excluir este esquema?')) return; try { await api.delete(`/painting-schemes/${schemeId}`); fetchProject(); } catch (error) { console.error('Error deleting scheme', error); alert('Erro ao excluir esquema'); } }; const handleDeletePart = async (partId: string) => { if (isGuest()) { showGuestWarning(); return; } if (!confirm('Tem certeza que deseja excluir esta peça?')) return; try { await api.delete(`/parts/${partId}`); fetchProject(); } catch (error) { console.error('Error deleting part', error); alert('Erro ao excluir peça'); } }; const handleDeleteInspection = async (id: string) => { if (isGuest()) { showGuestWarning(); return; } if (!confirm('Tem certeza que deseja excluir esta inspeção?')) return; try { await api.delete(`/inspections/${id}`); fetchProject(); } catch (error) { console.error('Error deleting inspection', error); alert('Erro ao excluir inspeção'); } }; const handleDeleteRecord = async (id: string) => { if (isGuest()) { showGuestWarning(); return; } if (!confirm('Tem certeza que deseja excluir este registro?')) return; try { await api.delete(`/application-records/${id}`); fetchProject(); } catch (error) { console.error('Error deleting record', error); alert('Erro ao excluir registro'); } }; const handleDeleteProject = async () => { if (!id) return; if (isGuest()) { showGuestWarning(); return; } if (!confirm('Tem certeza que deseja excluir este projeto COMPLETO? Todos os dados vinculados serão perdidos permanentemente.')) return; try { await api.delete(`/projects/${id}`); navigate('/'); } catch (error) { console.error('Error deleting project', error); alert('Erro ao excluir projeto'); } }; const openEditScheme = (scheme: PaintingScheme) => { setEditingScheme(scheme); setIsSchemeModalOpen(true); }; const openEditPart = (part: Part) => { setEditingPart(part); setIsPartModalOpen(true); }; const openEditInspection = (insp: Inspection) => { setEditingInspection(insp); setIsInspectionModalOpen(true); }; const openEditRecord = (record: ApplicationRecord) => { setEditingRecord(record); setIsControlModalOpen(true); }; const fetchProject = React.useCallback(async () => { try { const response = await api.get(`/projects/${id}`); setProject(response.data); } catch (error) { console.error('Error fetching project:', error); } finally { setLoading(false); } }, [id]); useEffect(() => { fetchProject(); // Buscar tipos de geometria para as perdas geometryTypeService.getAllTypes().then(res => setGeometryTypes(res.data)).catch(console.error); }, [fetchProject]); if (loading) return
Carregando detalhes...
; if (!project) return
Projeto não encontrado.
; const tabs: { id: typeof activeTab, label: string, icon: React.ElementType }[] = [ { id: 'parts', label: 'Geometria & Peças', icon: Layers }, { id: 'scheme', label: 'Esquema de Pintura', icon: PenTool }, { id: 'inspection', label: 'Inspeção', icon: ClipboardCheck }, { id: 'control', label: 'Controle de Aplicação', icon: Activity }, { id: 'analysis', label: 'Análise de Conformidade', icon: ClipboardCheck }, ]; return (
{/* Page Header */}

{project.name}

Em Andamento
M
G
{project.client} • {project.environment}
{isAdmin() && (
)}
{/* Premium Summary Grid */}
Data de Início

{project.startDate ? format(new Date(project.startDate), 'dd MMM, yyyy') : '-'}

Previsão Término

{project.endDate ? format(new Date(project.endDate), 'dd MMM, yyyy') : '-'}

Peso da Obra (KGF)

{project.weightKg ? Number(project.weightKg).toLocaleString('pt-BR') : '0'} kg

Evolução%

{project.weightKg && project.paintedWeight !== undefined ? Math.min(Math.round((project.paintedWeight / project.weightKg) * 100), 100) : 0} %

Localização/Amb.

{project.environment || '-'}

Eng. Responsável

{project.technician || '-'}

{/* Navigation Tabs */}
{activeTab === 'parts' && (

Listagem de Geometrias

Gerencie as peças e áreas cadastradas para esta obra

{isAdmin() && ( )}
{project.parts?.length === 0 ? (

Nenhuma peça cadastrada neste projeto.

) : (
{project.parts?.map((part: Part) => (
{part.type || 'GEOMETRIA'}

{part.description}

{isAdmin() && (
)}
Peso Total Estimado

{(part.weight || 0)} kg

Perdas Estim.(%)

{getLossForType(part.type)}

Área Total Superfície {(part.area || 0).toFixed(2)} m²
))}
)}
)} {activeTab === 'scheme' && (

Esquema de Pintura

Defina as tintas, etapas e espessuras requeridas

{isAdmin() && (
)}
{project.paintingSchemes?.length === 0 ? (

Nenhum esquema cadastrado neste projeto.

) : (
{project.paintingSchemes?.map((scheme: PaintingScheme) => (
{scheme.type}

{scheme.name}

Redutor: {scheme.thinnerSymbol || (typeof scheme.thinnerId === 'object' ? (scheme.thinnerId as TechnicalDataSheet)?.name : '---')}
{isAdmin() && (
)}
EPS Requerida
{scheme.epsMin}-{scheme.epsMax} μm
Sólidos Vol.

{scheme.solidsVolume}%

Cor / Cod.

{scheme.color || '-'}

Ativo no Sistema
Item verificado NBR 12103
))}
)}
)} {activeTab === 'control' && (

Controle de Aplicação

Registro diário de demãos e consumos

{isUser() && ( )}
{project.applicationRecords?.length === 0 ? (

Nenhum registro de aplicação encontrado.

) : (
data={project.applicationRecords || []} keyExtractor={(item) => item.id} titleAccessor={(item) => `${item.coatStage} - ${item.pieceDescription}`} subtitleAccessor={(item) => item.date ? format(new Date(item.date), 'dd/MM/yyyy') : '-'} columns={[ { header: 'Data', accessor: (item) => item.date ? format(new Date(item.date), 'dd MMM') : '-' }, { header: 'Etapa/Peça', accessor: (item) => (
{item.coatStage} {item.pieceDescription}
) }, { header: 'Pintor', accessor: 'operator' }, { header: 'EPS Seca (μm)', accessor: (item) => ( {item.dryThicknessCalc || '-'} ) }, ]} actionRender={(item) => canEditItem(item) ? (
) : null} />
)}
)} {activeTab === 'inspection' && (

Relatórios de Inspeção

Verificações de qualidade e medições de espessura final

{isUser() && ( )}
{project.inspections?.length === 0 ? (

Nenhuma inspeção registrada.

) : (
{project.inspections?.map((insp: Inspection) => (
{insp.appearance === 'approved' ? 'Aprovado' : insp.appearance === 'rejected' ? 'Reprovado' : 'Ressalvas'}
{insp.date ? format(new Date(insp.date), 'dd/MM/yyyy') : '-'}

{insp.pieceDescription}

Inspector: {insp.inspector}

{canEditItem(insp) && (
)}
{(insp.stockItemId || insp.treatmentType || (insp.roughnessReadings && insp.roughnessReadings.filter(p => p !== null).length > 0) || (project.weightKg && insp.weightKg)) && (
{/* Evolution Pie */} {project.weightKg && insp.weightKg && (
Peso / Evolução
{Math.min(Math.round((insp.weightKg / project.weightKg) * 100), 100)}%
{insp.weightKg} kg
)}
{typeof insp.stockItemId === 'object' && insp.stockItemId && (
Tinta Utilizada
{(insp.stockItemId as any).dataSheetId?.name || 'Item sem nome'}
Lote: {(insp.stockItemId as any).batchNumber}
)} {(insp.treatmentType || (insp.roughnessReadings && insp.roughnessReadings.filter(p => p !== null).length > 0)) && (
{insp.treatmentType && (
Tipo Jato {insp.treatmentType === 'dry_abrasive_blasting' ? 'Seco' : insp.treatmentType === 'water_jetting' ? 'Hidrojato' : insp.treatmentType === 'mechanical_cleaning' ? 'Mecânica' : insp.treatmentType === 'manual_cleaning' ? 'Manual' : 'Outro'}
)} {insp.roughnessReadings && insp.roughnessReadings.filter(p => p !== null).length > 0 && (
Rug. Média {(insp.roughnessReadings.filter((p): p is number => p !== null).reduce((a, b) => a + b, 0) / insp.roughnessReadings.filter(p => p !== null).length).toFixed(0)} μm
)}
)}
)} {(insp.temperature || insp.relativeHumidity) && (
{insp.temperature && (
{insp.temperature}°C
)} {insp.relativeHumidity && (
{insp.relativeHumidity}% UR
)} {insp.partTemperature && (
{insp.partTemperature}°C (Peça)
)} {insp.period && (
{insp.period === 'morning' ? 'Manhã' : insp.period === 'afternoon' ? 'Tarde' : 'Noite'}
)}
)}
{insp.type === 'painting' ? ( <>
EPS Média {insp.epsPoints && insp.epsPoints.filter(p => p !== null).length > 0 ? ( {(insp.epsPoints.filter((p): p is number => p !== null).reduce((a, b) => a + b, 0) / insp.epsPoints.filter(p => p !== null).length).toFixed(0)} μm ) : ( Sem dados )}
Min {insp.epsPoints && insp.epsPoints.filter(p => p !== null).length > 0 ? Math.min(...insp.epsPoints.filter((p): p is number => p !== null)).toFixed(0) : '-'}
Max {insp.epsPoints && insp.epsPoints.filter(p => p !== null).length > 0 ? Math.max(...insp.epsPoints.filter((p): p is number => p !== null)).toFixed(0) : '-'}
) : ( <>
Rugosidade Média {insp.roughnessReadings && insp.roughnessReadings.filter(p => p !== null).length > 0 ? ( {(insp.roughnessReadings.filter((p): p is number => p !== null).reduce((a, b) => a + b, 0) / insp.roughnessReadings.filter(p => p !== null).length).toFixed(0)} μm ) : ( Sem dados )}
Min {insp.roughnessReadings && insp.roughnessReadings.filter(p => p !== null).length > 0 ? Math.min(...insp.roughnessReadings.filter((p): p is number => p !== null)).toFixed(0) : '-'}
Max {insp.roughnessReadings && insp.roughnessReadings.filter(p => p !== null).length > 0 ? Math.max(...insp.roughnessReadings.filter((p): p is number => p !== null)).toFixed(0) : '-'}
)}
))}
)}
)} {activeTab === 'analysis' && id && ( )} {id && ( <> { setIsPartModalOpen(false); setEditingPart(undefined); }} onSuccess={fetchProject} projectId={id} initialData={editingPart} /> { setIsSchemeModalOpen(false); setEditingScheme(undefined); }} onSuccess={fetchProject} projectId={id} initialData={editingScheme} /> { setIsControlModalOpen(false); setEditingRecord(undefined); }} onSuccess={fetchProject} projectId={id} initialData={editingRecord} availableParts={project.parts || []} existingRecords={project.applicationRecords || []} availableBatches={project.inspections || []} /> { setIsInspectionModalOpen(false); setEditingInspection(undefined); }} onSuccess={fetchProject} projectId={id} initialData={editingInspection} existingInspections={project.inspections || []} /> setIsEditProjectModalOpen(false)} onSuccess={fetchProject} initialData={project} /> { setIsCloneModalOpen(false); setCloningScheme(undefined); }} onSuccess={fetchProject} // Actually we don't need to fetchProject here as the cloned scheme is in ANOTHER project. But maybe good for hygiene. schemeToClone={cloningScheme} /> setIsImportModalOpen(false)} onSuccess={fetchProject} targetProjectId={project.id} isExchangeMode={isExchangeMode} hasInspections={(project.inspections && project.inspections.length > 0) || (project.applicationRecords && project.applicationRecords.length > 0)} /> )}
); };