Initialize fresh project without Clerk

This commit is contained in:
2026-03-14 22:57:39 -03:00
commit 6898297935
401 changed files with 71631 additions and 0 deletions

View File

@@ -0,0 +1,896 @@
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<Project | null>(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<PaintingScheme | undefined>(undefined);
const [cloningScheme, setCloningScheme] = useState<PaintingScheme | undefined>(undefined);
const [editingPart, setEditingPart] = useState<Part | undefined>(undefined);
const [editingInspection, setEditingInspection] = useState<Inspection | undefined>(undefined);
const [editingRecord, setEditingRecord] = useState<ApplicationRecord | undefined>(undefined);
const [geometryTypes, setGeometryTypes] = useState<GeometryType[]>([]);
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 <div className="p-8 text-center">Carregando detalhes...</div>;
if (!project) return <div className="p-8 text-center text-error">Projeto não encontrado.</div>;
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 (
<div className="space-y-8 animate-in fade-in duration-700">
{/* Page Header */}
<div className="flex flex-col md:flex-row md:items-end justify-between gap-6 pb-2">
<div className="space-y-4">
<Link to="/projects">
<button className="flex items-center text-[10px] font-bold text-text-muted uppercase tracking-[0.2em] hover:text-primary transition-colors group">
<ArrowLeft className="w-3 h-3 mr-2 group-hover:-translate-x-1 transition-transform" />
Voltar aos Projetos
</button>
</Link>
<div className="flex flex-col sm:flex-row sm:items-center gap-4">
<h1 className="text-3xl md:text-4xl font-black text-text-main tracking-tight mb-0">{project.name}</h1>
<span className="inline-flex items-center px-3 py-1 rounded-full bg-primary/10 text-primary text-[10px] font-black uppercase tracking-widest border border-primary/20 shadow-sm shadow-primary/5">
<Activity className="w-3 h-3 mr-2" />
Em Andamento
</span>
</div>
<div className="flex items-center gap-3">
<div className="flex -space-x-2">
<div className="w-6 h-6 rounded-full bg-surface-highlight border-2 border-surface flex items-center justify-center text-[10px] font-bold">M</div>
<div className="w-6 h-6 rounded-full bg-primary/20 border-2 border-surface flex items-center justify-center text-[10px] font-bold text-primary">G</div>
</div>
<span className="text-sm text-text-secondary font-medium">{project.client} {project.environment}</span>
</div>
</div>
{isAdmin() && (
<div className="flex gap-3">
<Button variant="secondary" size="sm" onClick={() => setIsEditProjectModalOpen(true)}>
<Pencil className="w-4 h-4 mr-2" /> Editar Obra
</Button>
<Button variant="danger" size="sm" className="bg-error/10 text-error border border-error/20 hover:bg-error transition-all" onClick={handleDeleteProject}>
<Trash2 className="w-4 h-4 mr-2" /> Excluir
</Button>
</div>
)}
</div>
{/* Premium Summary Grid */}
<div className="relative overflow-hidden rounded-3xl bg-surface border border-border/40 shadow-soft p-8">
<div className="absolute top-0 right-0 p-8 opacity-5">
<Activity size={120} strokeWidth={1} />
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 relative z-10">
<div className="space-y-1">
<span className="text-[10px] font-bold text-text-muted uppercase tracking-widest block">Data de Início</span>
<p className="text-lg font-bold text-text-main">{project.startDate ? format(new Date(project.startDate), 'dd MMM, yyyy') : '-'}</p>
</div>
<div className="space-y-1">
<span className="text-[10px] font-bold text-text-muted uppercase tracking-widest block">Previsão Término</span>
<p className="text-lg font-bold text-text-main">{project.endDate ? format(new Date(project.endDate), 'dd MMM, yyyy') : '-'}</p>
</div>
<div className="space-y-1">
<span className="text-[10px] font-bold text-text-muted uppercase tracking-widest block">Peso da Obra (KGF)</span>
<p className="text-lg font-bold text-text-main">
{project.weightKg ? Number(project.weightKg).toLocaleString('pt-BR') : '0'} <span className="text-sm text-text-muted">kg</span>
</p>
</div>
<div className="space-y-1">
<span className="text-[10px] font-bold text-text-muted uppercase tracking-widest block">Evolução%</span>
<p className="text-lg font-bold text-primary">
{project.weightKg && project.paintedWeight !== undefined
? Math.min(Math.round((project.paintedWeight / project.weightKg) * 100), 100)
: 0}
<span className="text-sm">%</span>
</p>
</div>
</div>
<div className="mt-6 pt-6 border-t border-border/20 grid grid-cols-1 md:grid-cols-2 gap-6 relative z-10">
<div className="space-y-1">
<span className="text-[10px] font-bold text-text-muted uppercase tracking-widest block">Localização/Amb.</span>
<p className="text-sm font-medium text-text-secondary">{project.environment || '-'}</p>
</div>
<div className="space-y-1">
<span className="text-[10px] font-bold text-text-muted uppercase tracking-widest block">Eng. Responsável</span>
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-blue-500"></div>
<p className="text-sm font-bold text-text-main">{project.technician || '-'}</p>
</div>
</div>
</div>
</div>
{/* Navigation Tabs */}
<div className="flex items-center justify-between border-b border-border/40 scrollbar-hide overflow-x-auto">
<nav className="flex space-x-10" aria-label="Tabs">
{tabs.map((tab) => {
const Icon = tab.icon;
const active = activeTab === tab.id;
return (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={clsx(
'whitespace-nowrap py-5 px-1 border-b-2 font-bold text-xs uppercase tracking-[0.15em] flex items-center gap-3 transition-all relative',
active
? 'border-primary text-primary'
: 'border-transparent text-text-muted hover:text-text-main hover:border-border'
)}
>
<Icon className={clsx("w-4 h-4", active ? "text-primary" : "text-text-muted")} />
{tab.label}
{active && <span className="absolute bottom-[-1px] left-0 right-0 h-[2px] bg-primary shadow-[0_0_12px_rgba(13,127,242,0.6)]"></span>}
</button>
);
})}
</nav>
</div>
<div className="min-h-[300px]">
{activeTab === 'parts' && (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-2 duration-500">
<div className="flex justify-between items-center bg-surface-soft/30 p-4 rounded-2xl border border-border/20">
<div className="flex items-center gap-3">
<div className="p-2 bg-primary/10 rounded-lg text-primary">
<Layers size={20} />
</div>
<div>
<h2 className="text-lg font-bold text-text-main tracking-tight mb-0">Listagem de Geometrias</h2>
<p className="text-xs text-text-muted font-medium">Gerencie as peças e áreas cadastradas para esta obra</p>
</div>
</div>
{isAdmin() && (
<Button size="sm" onClick={() => setIsPartModalOpen(true)}>
<Layers className="w-4 h-4 mr-2" /> Cadastrar Peça
</Button>
)}
</div>
{project.parts?.length === 0 ? (
<div className="py-20 text-center border-2 border-dashed border-border/40 rounded-3xl">
<Layers className="w-12 h-12 text-text-muted/20 mx-auto mb-4" />
<p className="text-text-muted font-medium">Nenhuma peça cadastrada neste projeto.</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{project.parts?.map((part: Part) => (
<Card key={part.id} className="relative group overflow-hidden border-border/20 shadow-sm hover:shadow-xl hover:-translate-y-1 transition-all duration-500">
<div className="absolute top-0 left-0 w-1.5 h-full bg-primary/20 group-hover:bg-primary transition-colors"></div>
<div className="flex justify-between items-start pl-2">
<div className="space-y-1">
<span className="text-[10px] font-bold text-primary uppercase tracking-widest">{part.type || 'GEOMETRIA'}</span>
<h4 className="text-lg font-bold text-text-main tracking-tight leading-tight">{part.description}</h4>
</div>
{isAdmin() && (
<div className="flex gap-1 opacity-100 md:opacity-0 md:group-hover:opacity-100 transition-all">
<button
onClick={() => openEditPart(part)}
className="p-2 text-text-muted hover:text-primary hover:bg-primary/5 rounded-xl transition-all"
title="Editar Peça"
aria-label="Editar Peça"
>
<Pencil size={14} />
</button>
<button
onClick={() => handleDeletePart(part.id)}
className="p-2 text-text-muted hover:text-error hover:bg-error/5 rounded-xl transition-all"
title="Excluir Peça"
aria-label="Excluir Peça"
>
<Trash2 size={14} />
</button>
</div>
)}
</div>
<div className="mt-6 pt-4 border-t border-border/40 grid grid-cols-2 gap-4">
<div className="space-y-0.5">
<span className="text-[9px] font-bold text-text-muted uppercase tracking-wider">Peso Total Estimado</span>
<p className="text-sm font-bold text-text-main">{(part.weight || 0)} kg</p>
</div>
<div className="space-y-0.5 text-right">
<span className="text-[9px] font-bold text-text-muted uppercase tracking-wider">Perdas Estim.(%)</span>
<p className="text-sm font-black text-error">{getLossForType(part.type)}</p>
</div>
</div>
<div className="mt-4 p-3 bg-surface-soft/50 rounded-xl flex items-center justify-between">
<span className="text-[10px] font-bold text-text-secondary uppercase">Área Total Superfície</span>
<span className="text-sm font-black text-primary">
{(part.area || 0).toFixed(2)} m²
</span>
</div>
</Card>
))}
</div>
)}
</div>
)}
{activeTab === 'scheme' && (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-2 duration-500">
<div className="flex justify-between items-center bg-surface-soft/30 p-4 rounded-2xl border border-border/20">
<div className="flex items-center gap-3">
<div className="p-2 bg-primary/10 rounded-lg text-primary">
<PenTool size={20} />
</div>
<div>
<h2 className="text-lg font-bold text-text-main tracking-tight mb-0">Esquema de Pintura</h2>
<p className="text-xs text-text-muted font-medium">Defina as tintas, etapas e espessuras requeridas</p>
</div>
</div>
{isAdmin() && (
<div className="flex gap-2">
<Button
onClick={() => { setIsExchangeMode(true); setIsImportModalOpen(true); }}
variant="ghost"
className="text-text-muted hover:text-primary"
title="Trocar ou Importar Esquema"
>
<RefreshCw className="w-5 h-5 mr-1" />
Trocar
</Button>
<Button onClick={() => { setEditingScheme(undefined); setIsSchemeModalOpen(true); }} className="shadow-primary/30">
<PenTool className="w-5 h-5 mr-2" />
Adicionar Demão
</Button>
</div>
)}
</div>
{project.paintingSchemes?.length === 0 ? (
<div className="py-20 text-center border-2 border-dashed border-border/40 rounded-3xl">
<PenTool className="w-12 h-12 text-text-muted/20 mx-auto mb-4" />
<p className="text-text-muted font-medium">Nenhum esquema cadastrado neste projeto.</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{project.paintingSchemes?.map((scheme: PaintingScheme) => (
<Card key={scheme.id} className="relative group overflow-hidden border-border/20 shadow-sm hover:shadow-xl transition-all duration-500">
<div className="flex justify-between items-start">
<div className="space-y-1">
<span className="px-2 py-0.5 rounded bg-primary text-white text-[9px] font-black uppercase tracking-widest">{scheme.type}</span>
<div className="flex items-center gap-6 mt-2">
<h4 className="text-xl font-bold text-text-main tracking-tight">{scheme.name}</h4>
<div className="flex items-center gap-2">
<span className="text-[10px] font-bold text-text-muted uppercase">Redutor:</span>
<span className="text-sm font-bold text-text-main">
{scheme.thinnerSymbol || (typeof scheme.thinnerId === 'object' ? (scheme.thinnerId as TechnicalDataSheet)?.name : '---')}
</span>
</div>
</div>
</div>
{isAdmin() && (
<div className="flex gap-1">
<button
onClick={() => { setCloningScheme(scheme); setIsCloneModalOpen(true); }}
className="p-2 text-text-muted hover:text-green-500 hover:bg-green-500/10 rounded-xl transition-all"
title="Clonar para outra obra"
aria-label="Clonar para outra obra"
>
<Copy size={16} />
</button>
<button
onClick={() => openEditScheme(scheme)}
className="p-2 text-text-muted hover:text-primary hover:bg-primary/5 rounded-xl transition-all"
title="Editar Esquema"
aria-label="Editar Esquema"
>
<Pencil size={16} />
</button>
<button
onClick={() => handleDeleteScheme(scheme.id)}
className="p-2 text-text-muted hover:text-error hover:bg-error/5 rounded-xl transition-all"
title="Excluir Esquema"
aria-label="Excluir Esquema"
>
<Trash2 size={16} />
</button>
</div>
)}
</div>
<div className="mt-8 grid grid-cols-3 gap-6">
<div className="space-y-1">
<span className="text-[10px] font-bold text-text-muted uppercase tracking-widest block">EPS Requerida</span>
<div className="flex items-baseline gap-1">
<span className="text-lg font-black text-text-main">{scheme.epsMin}-{scheme.epsMax}</span>
<span className="text-[10px] font-bold text-text-muted">μm</span>
</div>
</div>
<div className="space-y-1">
<span className="text-[10px] font-bold text-text-muted uppercase tracking-widest block">Sólidos Vol.</span>
<p className="text-lg font-black text-text-main">{scheme.solidsVolume}%</p>
</div>
<div className="space-y-1">
<span className="text-[10px] font-bold text-text-muted uppercase tracking-widest block">Cor / Cod.</span>
<div className="flex items-center gap-3">
<p className="text-lg font-black text-text-main truncate" title={scheme.color || '-'}>{scheme.color || '-'}</p>
<ColorBubble colorHex={scheme.colorHex} className="w-10 h-10" />
</div>
</div>
</div>
<div className="mt-6 pt-6 border-t border-border/40 flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-green-500 shadow-[0_0_8px_rgba(34,197,94,0.6)] animate-pulse"></div>
<span className="text-[10px] font-black text-text-muted uppercase tracking-widest">Ativo no Sistema</span>
</div>
<span className="text-[10px] font-bold text-text-muted italic">Item verificado NBR 12103</span>
</div>
</Card>
))}
</div>
)}
</div>
)}
{activeTab === 'control' && (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-2 duration-500">
<div className="flex justify-between items-center bg-surface-soft/30 p-4 rounded-2xl border border-border/20">
<div className="flex items-center gap-3">
<div className="p-2 bg-primary/10 rounded-lg text-primary">
<Activity size={20} />
</div>
<div>
<h2 className="text-lg font-bold text-text-main tracking-tight mb-0">Controle de Aplicação</h2>
<p className="text-xs text-text-muted font-medium">Registro diário de demãos e consumos</p>
</div>
</div>
{isUser() && (
<Button size="sm" onClick={() => setIsControlModalOpen(true)}>
<Activity className="w-4 h-4 mr-2" /> Novo Registro
</Button>
)}
</div>
{project.applicationRecords?.length === 0 ? (
<div className="py-20 text-center border-2 border-dashed border-border/40 rounded-3xl">
<Activity className="w-12 h-12 text-text-muted/20 mx-auto mb-4" />
<p className="text-text-muted font-medium">Nenhum registro de aplicação encontrado.</p>
</div>
) : (
<div className="rounded-3xl border border-border/40 overflow-hidden bg-surface shadow-soft">
<MobileList<ApplicationRecord>
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) => (
<div className="flex flex-col">
<span className="font-bold text-text-main">{item.coatStage}</span>
<span className="text-[10px] text-text-muted font-bold uppercase">{item.pieceDescription}</span>
</div>
)
},
{ header: 'Pintor', accessor: 'operator' },
{
header: 'EPS Seca (μm)', accessor: (item) => (
<span className="px-2 py-1 rounded-lg bg-primary/5 text-primary font-black">
{item.dryThicknessCalc || '-'}
</span>
)
},
]}
actionRender={(item) => canEditItem(item) ? (
<div className="flex gap-1 justify-end">
<button
onClick={(e) => { e.stopPropagation(); openEditRecord(item); }}
className="p-2 text-text-muted hover:text-primary hover:bg-primary/5 rounded-xl transition-all"
title="Editar Registro"
>
<Pencil size={14} />
</button>
<button
onClick={(e) => { e.stopPropagation(); handleDeleteRecord(item.id); }}
className="p-2 text-text-muted hover:text-error hover:bg-error/5 rounded-xl transition-all"
title="Excluir Registro"
>
<Trash2 size={14} />
</button>
</div>
) : null}
/>
</div>
)}
</div>
)}
{activeTab === 'inspection' && (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-2 duration-500">
<div className="flex justify-between items-center bg-surface-soft/30 p-4 rounded-2xl border border-border/20">
<div className="flex items-center gap-3">
<div className="p-2 bg-primary/10 rounded-lg text-primary">
<ClipboardCheck size={20} />
</div>
<div>
<h2 className="text-lg font-bold text-text-main tracking-tight mb-0">Relatórios de Inspeção</h2>
<p className="text-xs text-text-muted font-medium">Verificações de qualidade e medições de espessura final</p>
</div>
</div>
{isUser() && (
<Button size="sm" onClick={() => setIsInspectionModalOpen(true)}>
<ClipboardCheck className="w-4 h-4 mr-2" /> Nova Inspeção
</Button>
)}
</div>
{project.inspections?.length === 0 ? (
<div className="py-20 text-center border-2 border-dashed border-border/40 rounded-3xl">
<ClipboardCheck className="w-12 h-12 text-text-muted/20 mx-auto mb-4" />
<p className="text-text-muted font-medium">Nenhuma inspeção registrada.</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{project.inspections?.map((insp: Inspection) => (
<Card key={insp.id} className="relative group overflow-hidden border-border/20 shadow-sm hover:shadow-xl transition-all duration-500">
<div className="absolute top-0 right-0 p-4">
<div className={clsx(
"px-3 py-1 rounded-full text-[9px] font-black uppercase tracking-widest border shadow-sm",
insp.appearance === 'approved'
? 'bg-success/10 text-success border-success/20'
: insp.appearance === 'rejected'
? 'bg-error/10 text-error border-error/20'
: 'bg-warning/10 text-warning border-warning/20'
)}>
{insp.appearance === 'approved' ? 'Aprovado' : insp.appearance === 'rejected' ? 'Reprovado' : 'Ressalvas'}
</div>
</div>
<div className="flex justify-between items-start">
<div className="space-y-1">
<span className="text-[10px] font-bold text-text-muted uppercase tracking-[0.2em]">
{insp.date ? format(new Date(insp.date), 'dd/MM/yyyy') : '-'}
</span>
<h4 className="text-lg font-bold text-text-main tracking-tight leading-tight pt-1">
{insp.pieceDescription}
</h4>
<p className="text-xs text-text-muted font-medium">Inspector: {insp.inspector}</p>
</div>
{canEditItem(insp) && (
<div className="flex gap-1 mt-8">
<button
onClick={() => openEditInspection(insp)}
className="p-2 text-text-muted hover:text-primary hover:bg-primary/5 rounded-xl transition-all"
title="Editar Inspeção"
>
<Pencil size={16} />
</button>
<button
onClick={() => handleDeleteInspection(insp.id)}
className="p-2 text-text-muted hover:text-error hover:bg-error/5 rounded-xl transition-all"
title="Excluir Inspeção"
>
<Trash2 size={16} />
</button>
</div>
)}
</div>
{(insp.stockItemId || insp.treatmentType || (insp.roughnessReadings && insp.roughnessReadings.filter(p => p !== null).length > 0) || (project.weightKg && insp.weightKg)) && (
<div className="mt-6 px-1 flex flex-wrap md:flex-nowrap items-center gap-6 md:gap-10 border-t border-border/10 pt-6">
{/* Evolution Pie */}
{project.weightKg && insp.weightKg && (
<div className="flex flex-col items-center shrink-0">
<span className="text-[9px] font-bold text-text-muted uppercase tracking-[0.2em] block mb-2 whitespace-nowrap">Peso / Evolução</span>
<div className="relative w-16 h-16 flex items-center justify-center bg-surface-soft/40 dark:bg-black/10 rounded-full border border-border/10 shadow-sm">
<svg viewBox="0 0 36 36" className="w-[85%] h-[85%] -rotate-90">
<circle cx="18" cy="18" r="15.9155" fill="transparent" stroke="currentColor" strokeWidth="3" className="text-border/20" />
<circle
cx="18" cy="18" r="15.9155" fill="transparent" stroke="currentColor" strokeWidth="3"
strokeDasharray={`${Math.min(Math.round((insp.weightKg / project.weightKg) * 100), 100)} 100`}
className="text-primary transition-all duration-1000"
/>
</svg>
<span className="absolute inset-0 flex items-center justify-center text-base font-black text-text-main">
{Math.min(Math.round((insp.weightKg / project.weightKg) * 100), 100)}%
</span>
</div>
<span className="text-[10px] font-black text-primary mt-1">{insp.weightKg} kg</span>
</div>
)}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6 flex-1 h-full items-center md:border-l border-border/20 md:pl-8">
{typeof insp.stockItemId === 'object' && insp.stockItemId && (
<div className="col-span-1">
<span className="text-[9px] font-bold text-text-muted uppercase tracking-widest block mb-1">Tinta Utilizada</span>
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<div className="w-1.5 h-1.5 rounded-full bg-primary/40"></div>
<span className="text-xs font-bold text-text-main truncate max-w-[180px]" title={(insp.stockItemId as any).dataSheetId?.name}>
{(insp.stockItemId as any).dataSheetId?.name || 'Item sem nome'}
</span>
</div>
<div className="pl-3.5">
<span className="text-[10px] text-text-muted font-black uppercase tracking-wider bg-surface-soft/50 py-0.5 px-1 rounded border border-border/5">
Lote: {(insp.stockItemId as any).batchNumber}
</span>
</div>
</div>
</div>
)}
{(insp.treatmentType || (insp.roughnessReadings && insp.roughnessReadings.filter(p => p !== null).length > 0)) && (
<div className="col-span-1 flex gap-8">
{insp.treatmentType && (
<div>
<span className="text-[9px] font-bold text-text-muted uppercase tracking-widest block mb-0.5">Tipo Jato</span>
<span className="text-[11px] font-bold text-text-main bg-stone-100 px-1.5 py-0.5 rounded border border-stone-200 shadow-sm">
{insp.treatmentType === 'dry_abrasive_blasting' ? 'Seco' :
insp.treatmentType === 'water_jetting' ? 'Hidrojato' :
insp.treatmentType === 'mechanical_cleaning' ? 'Mecânica' :
insp.treatmentType === 'manual_cleaning' ? 'Manual' : 'Outro'}
</span>
</div>
)}
{insp.roughnessReadings && insp.roughnessReadings.filter(p => p !== null).length > 0 && (
<div>
<span className="text-[9px] font-bold text-text-muted uppercase tracking-widest block mb-0.5">Rug. Média</span>
<span className="text-xs font-black text-amber-600">
{(insp.roughnessReadings.filter((p): p is number => p !== null).reduce((a, b) => a + b, 0) / insp.roughnessReadings.filter(p => p !== null).length).toFixed(0)} <small className="text-[9px] font-medium opacity-70">μm</small>
</span>
</div>
)}
</div>
)}
</div>
</div>
)}
{(insp.temperature || insp.relativeHumidity) && (
<div className="mt-4 px-1 flex items-center gap-4 text-text-muted border-t border-border/10 pt-3">
{insp.temperature && (
<div className="flex items-center gap-1.5">
<Thermometer className="w-3 h-3 text-orange-500" />
<span className="text-[11px] font-bold text-text-secondary">{insp.temperature}°C</span>
</div>
)}
{insp.relativeHumidity && (
<div className="flex items-center gap-1.5">
<Droplets className="w-3 h-3 text-blue-500" />
<span className="text-[11px] font-bold text-text-secondary">{insp.relativeHumidity}% UR</span>
</div>
)}
{insp.partTemperature && (
<div className="flex items-center gap-1.5">
<Thermometer className="w-3 h-3 text-red-500" />
<span className="text-[11px] font-bold text-text-secondary">{insp.partTemperature}°C (Peça)</span>
</div>
)}
{insp.period && (
<div className="flex items-center gap-1.5 ml-auto">
<Sun className="w-3 h-3 text-amber-500" />
<span className="text-[9px] font-black uppercase tracking-tighter text-text-muted">{insp.period === 'morning' ? 'Manhã' : insp.period === 'afternoon' ? 'Tarde' : 'Noite'}</span>
</div>
)}
</div>
)}
<div className="mt-6 p-4 bg-surface-soft/50 rounded-2xl flex items-center justify-between border border-border/20">
{insp.type === 'painting' ? (
<>
<div className="flex flex-col">
<span className="text-[9px] font-bold text-text-muted uppercase tracking-widest">EPS Média</span>
{insp.epsPoints && insp.epsPoints.filter(p => p !== null).length > 0 ? (
<span className="text-xl font-black text-primary">
{(insp.epsPoints.filter((p): p is number => p !== null).reduce((a, b) => a + b, 0) / insp.epsPoints.filter(p => p !== null).length).toFixed(0)}
<span className="text-xs ml-1">μm</span>
</span>
) : (
<span className="text-sm font-bold text-text-muted">Sem dados</span>
)}
</div>
<div className="flex gap-4 border-l border-border/40 pl-4">
<div className="flex flex-col">
<span className="text-[9px] font-bold text-text-muted uppercase">Min</span>
<span className="text-sm font-bold text-text-main">
{insp.epsPoints && insp.epsPoints.filter(p => p !== null).length > 0
? Math.min(...insp.epsPoints.filter((p): p is number => p !== null)).toFixed(0)
: '-'}
</span>
</div>
<div className="flex flex-col">
<span className="text-[9px] font-bold text-text-muted uppercase">Max</span>
<span className="text-sm font-bold text-text-main">
{insp.epsPoints && insp.epsPoints.filter(p => p !== null).length > 0
? Math.max(...insp.epsPoints.filter((p): p is number => p !== null)).toFixed(0)
: '-'}
</span>
</div>
</div>
</>
) : (
<>
<div className="flex flex-col">
<span className="text-[9px] font-bold text-text-muted uppercase tracking-widest">Rugosidade Média</span>
{insp.roughnessReadings && insp.roughnessReadings.filter(p => p !== null).length > 0 ? (
<span className="text-xl font-black text-amber-600">
{(insp.roughnessReadings.filter((p): p is number => p !== null).reduce((a, b) => a + b, 0) / insp.roughnessReadings.filter(p => p !== null).length).toFixed(0)}
<span className="text-xs ml-1">μm</span>
</span>
) : (
<span className="text-sm font-bold text-text-muted">Sem dados</span>
)}
</div>
<div className="flex gap-4 border-l border-border/40 pl-4">
<div className="flex flex-col">
<span className="text-[9px] font-bold text-text-muted uppercase">Min</span>
<span className="text-sm font-bold text-text-main">
{insp.roughnessReadings && insp.roughnessReadings.filter(p => p !== null).length > 0
? Math.min(...insp.roughnessReadings.filter((p): p is number => p !== null)).toFixed(0)
: '-'}
</span>
</div>
<div className="flex flex-col">
<span className="text-[9px] font-bold text-text-muted uppercase">Max</span>
<span className="text-sm font-bold text-text-main">
{insp.roughnessReadings && insp.roughnessReadings.filter(p => p !== null).length > 0
? Math.max(...insp.roughnessReadings.filter((p): p is number => p !== null)).toFixed(0)
: '-'}
</span>
</div>
</div>
</>
)}
</div>
</Card>
))}
</div>
)}
</div>
)}
{activeTab === 'analysis' && id && (
<AnalysisDashboard projectId={id} />
)}
{id && (
<>
<CreatePartModal
isOpen={isPartModalOpen}
onClose={() => {
setIsPartModalOpen(false);
setEditingPart(undefined);
}}
onSuccess={fetchProject}
projectId={id}
initialData={editingPart}
/>
<CreatePaintingSchemeModal
isOpen={isSchemeModalOpen}
onClose={() => {
setIsSchemeModalOpen(false);
setEditingScheme(undefined);
}}
onSuccess={fetchProject}
projectId={id}
initialData={editingScheme}
/>
<CreateControlRecordModal
isOpen={isControlModalOpen}
onClose={() => {
setIsControlModalOpen(false);
setEditingRecord(undefined);
}}
onSuccess={fetchProject}
projectId={id}
initialData={editingRecord}
availableParts={project.parts || []}
existingRecords={project.applicationRecords || []}
availableBatches={project.inspections || []}
/>
<CreateInspectionModal
isOpen={isInspectionModalOpen}
onClose={() => {
setIsInspectionModalOpen(false);
setEditingInspection(undefined);
}}
onSuccess={fetchProject}
projectId={id}
initialData={editingInspection}
existingInspections={project.inspections || []}
/>
<CreateProjectModal
isOpen={isEditProjectModalOpen}
onClose={() => setIsEditProjectModalOpen(false)}
onSuccess={fetchProject}
initialData={project}
/>
<CloneSchemeModal
isOpen={isCloneModalOpen}
onClose={() => { 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}
/>
<ImportSchemeModal
isOpen={isImportModalOpen}
onClose={() => setIsImportModalOpen(false)}
onSuccess={fetchProject}
targetProjectId={project.id}
isExchangeMode={isExchangeMode}
hasInspections={(project.inspections && project.inspections.length > 0) || (project.applicationRecords && project.applicationRecords.length > 0)}
/>
</>
)}
</div>
</div >
);
};