Initialize fresh project without Clerk
This commit is contained in:
896
src/client/pages/ProjectDetails.tsx
Normal file
896
src/client/pages/ProjectDetails.tsx
Normal 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 >
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user