585 lines
28 KiB
TypeScript
585 lines
28 KiB
TypeScript
import React, { useEffect, useState, useCallback } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import api from '../services/api';
|
|
import { Button } from '../components/Button';
|
|
import { Plus, Search, Pencil, Trash2, Activity, Box, CheckCircle2, History, Printer } from 'lucide-react';
|
|
import { format } from 'date-fns';
|
|
import { useAuth } from '../context/useAuth';
|
|
import { useToast } from '../hooks/useToast';
|
|
import { MobileList } from '../components/MobileList';
|
|
import { CreateProjectModal } from '../components/modals/CreateProjectModal';
|
|
import { useOrganization } from '@clerk/clerk-react';
|
|
import { useSystemSettings } from '../context/SystemSettingsContext';
|
|
import { Modal } from '../components/Modal';
|
|
import { ConfirmModal } from '../components/ConfirmModal';
|
|
import { ColorBubble } from '../components/ColorBubble';
|
|
|
|
import type { Project, Inspection } from '../types';
|
|
import { AnalyticalReport } from '../components/reports/AnalyticalReport';
|
|
import { GeneralProjectReport } from '../components/reports/GeneralProjectReport';
|
|
|
|
export const ProjectList: React.FC = () => {
|
|
const [projects, setProjects] = useState<Project[]>([]);
|
|
const [inspections, setInspections] = useState<Inspection[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
const [selectedProject, setSelectedProject] = useState<Project | undefined>(undefined);
|
|
const [confirmModal, setConfirmModal] = useState<{
|
|
isOpen: boolean;
|
|
title: string;
|
|
description: string;
|
|
type: 'danger' | 'warning' | 'info';
|
|
onConfirm: () => void;
|
|
}>({
|
|
isOpen: false,
|
|
title: '',
|
|
description: '',
|
|
type: 'info',
|
|
onConfirm: () => { }
|
|
});
|
|
|
|
const [statsModal, setStatsModal] = useState<{ isOpen: boolean; title: string; type: 'alerts' | 'inspections' | null }>({
|
|
isOpen: false,
|
|
title: '',
|
|
type: null
|
|
});
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [viewStatus, setViewStatus] = useState<'active' | 'archived'>('active');
|
|
const [printingProject, setPrintingProject] = useState<Project | null>(null);
|
|
const [isPrinting, setIsPrinting] = useState(false);
|
|
const [isPrintingGeneral, setIsPrintingGeneral] = useState(false);
|
|
|
|
const navigate = useNavigate();
|
|
const { appUser } = useAuth();
|
|
const { showToast } = useToast();
|
|
const { organization } = useOrganization();
|
|
const { settings } = useSystemSettings();
|
|
|
|
const logoUrl = settings?.appLogoUrl || organization?.imageUrl;
|
|
|
|
const isAdmin = appUser?.email === 'admtracksteel@gmail.com' || appUser?.role === 'admin';
|
|
|
|
const fetchProjects = useCallback(async () => {
|
|
try {
|
|
setLoading(true);
|
|
const [projRes, inspRes] = await Promise.all([
|
|
api.get(`/projects?status=${viewStatus}`),
|
|
api.get('/inspections')
|
|
]);
|
|
setProjects(projRes.data);
|
|
setInspections(inspRes.data);
|
|
} catch (error) {
|
|
console.error('Error fetching data:', error);
|
|
showToast('Não foi possível carregar os dados.', 'error');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [viewStatus, showToast]);
|
|
|
|
useEffect(() => {
|
|
fetchProjects();
|
|
}, [fetchProjects]);
|
|
|
|
const handleEdit = (project: Project) => {
|
|
if (viewStatus === 'archived') return;
|
|
setSelectedProject(project);
|
|
setIsModalOpen(true);
|
|
};
|
|
|
|
const handleDelete = async (id: string) => {
|
|
const project = projects.find(p => p.id === id);
|
|
setConfirmModal({
|
|
isOpen: true,
|
|
title: 'Excluir Projeto',
|
|
description: `Tem certeza que deseja excluir o projeto "${project?.name}"? Todos os dados vinculados serão perdidos permanentemente.`,
|
|
type: 'danger',
|
|
onConfirm: async () => {
|
|
try {
|
|
await api.delete(`/projects/${id}`);
|
|
fetchProjects();
|
|
showToast('O projeto foi removido com sucesso.', 'success');
|
|
} catch (error) {
|
|
console.error('Error deleting project:', error);
|
|
showToast('Não foi possível excluir o projeto.', 'error');
|
|
}
|
|
setConfirmModal(prev => ({ ...prev, isOpen: false }));
|
|
}
|
|
});
|
|
};
|
|
|
|
const handlePrint = async (projectId: string) => {
|
|
try {
|
|
setIsPrinting(true);
|
|
const response = await api.get(`/projects/${projectId}`);
|
|
setPrintingProject(response.data);
|
|
|
|
// Wait for state to update and layout to render
|
|
setTimeout(() => {
|
|
window.print();
|
|
setIsPrinting(false);
|
|
setPrintingProject(null);
|
|
}, 500);
|
|
} catch (error) {
|
|
console.error('Error fetching project for print:', error);
|
|
showToast('Erro ao gerar relatório.', 'error');
|
|
setIsPrinting(false);
|
|
}
|
|
};
|
|
|
|
const handlePrintGeneral = () => {
|
|
setIsPrintingGeneral(true);
|
|
setTimeout(() => {
|
|
window.print();
|
|
setIsPrintingGeneral(false);
|
|
}, 500);
|
|
};
|
|
|
|
const handleArchive = async (project: Project) => {
|
|
const isArchiving = viewStatus === 'active';
|
|
setConfirmModal({
|
|
isOpen: true,
|
|
title: isArchiving ? 'Concluir Obras' : 'Reativar Projeto',
|
|
description: `Deseja ${isArchiving ? 'arquivar' : 'reativar'} o projeto "${project.name}"?`,
|
|
type: 'info',
|
|
onConfirm: async () => {
|
|
try {
|
|
await api.patch(`/projects/${project.id}/archive`);
|
|
showToast(`O projeto ${project.name} foi movido com sucesso.`, 'success');
|
|
fetchProjects();
|
|
} catch (error) {
|
|
console.error('Error archiving project:', error);
|
|
showToast('Não foi possível alterar o status do projeto.', 'error');
|
|
}
|
|
setConfirmModal(prev => ({ ...prev, isOpen: false }));
|
|
}
|
|
});
|
|
};
|
|
|
|
const filteredProjects = projects.filter(p =>
|
|
p.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
p.client.toLowerCase().includes(searchTerm.toLowerCase())
|
|
);
|
|
|
|
const columns = [
|
|
{
|
|
header: 'Obra / Projeto',
|
|
accessor: (item: Project) => (
|
|
<div
|
|
className="flex flex-col cursor-pointer hover:opacity-80 transition-opacity"
|
|
onClick={() => navigate(`/project/${item.id}`)}
|
|
>
|
|
<span className="text-text-main font-black text-sm uppercase leading-tight line-clamp-1">{item.name}</span>
|
|
<span className="text-[10px] text-text-muted font-bold uppercase tracking-wider">{item.client}</span>
|
|
<span className="text-[9px] text-text-secondary font-bold tracking-tight">
|
|
Gestor: <span className="text-text-main">{item.technician || 'Não informado'}</span>
|
|
</span>
|
|
</div>
|
|
),
|
|
className: 'min-w-[150px]'
|
|
},
|
|
{
|
|
header: 'Evolução',
|
|
accessor: (item: Project) => {
|
|
const projectInspections = inspections.filter(i => i.projectId === item.id);
|
|
const sumWeight = projectInspections.reduce((acc, curr) => acc + (curr.weightKg || 0), 0);
|
|
const totalWeight = item.weightKg || 0;
|
|
const percentage = totalWeight > 0 ? Math.min(Math.round((sumWeight / totalWeight) * 100), 100) : 0;
|
|
|
|
return (
|
|
<div className="flex items-center justify-center">
|
|
<div className="relative w-10 h-10 flex items-center justify-center">
|
|
<svg viewBox="0 0 36 36" className="w-full h-full -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={`${percentage} 100`}
|
|
className="text-primary transition-all duration-1000"
|
|
/>
|
|
</svg>
|
|
<span className="absolute inset-0 flex items-center justify-center text-[10px] font-black text-text-main">
|
|
{percentage}%
|
|
</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
},
|
|
className: 'hidden sm:table-cell w-[80px]'
|
|
},
|
|
{
|
|
header: 'Cronograma',
|
|
accessor: (item: Project) => (
|
|
<div className="flex flex-col gap-1">
|
|
<div className="flex items-center gap-1.5">
|
|
<span className="text-[8px] text-text-muted font-black uppercase w-10">Início:</span>
|
|
<span className="bg-orange-500/10 text-orange-500 px-1.5 py-0.5 rounded text-[10px] font-black">
|
|
{item.startDate ? format(new Date(item.startDate), 'dd/MM/yy') : '--/--/--'}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center gap-1.5">
|
|
<span className="text-[8px] text-text-muted font-black uppercase w-10">Término:</span>
|
|
<span className="bg-orange-950/20 text-orange-400 px-1.5 py-0.5 rounded text-[10px] font-black border border-orange-500/10">
|
|
{item.endDate ? format(new Date(item.endDate), 'dd/MM/yy') : '--/--/--'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
),
|
|
className: 'hidden md:table-cell w-[110px]'
|
|
},
|
|
{
|
|
header: 'Peso(kgf)',
|
|
accessor: (item: Project) => (
|
|
<div className="flex flex-col items-center">
|
|
<span className="text-text-main font-black text-sm">{item.weightKg ? item.weightKg.toLocaleString('pt-BR') : '0'}</span>
|
|
<span className="text-[8px] text-text-muted font-bold uppercase tracking-tighter">Est. Total</span>
|
|
</div>
|
|
),
|
|
className: 'w-[90px]'
|
|
},
|
|
{
|
|
header: 'Tinta',
|
|
accessor: (item: Project) => {
|
|
const schemes = item.paintingSchemes || [];
|
|
if (schemes.length === 0) return <span className="text-text-muted font-bold text-xs">---</span>;
|
|
|
|
return (
|
|
<div className="flex flex-col gap-1.5 py-1">
|
|
{schemes.slice(0, 2).map((scheme, idx) => (
|
|
<div key={idx} className="flex flex-col max-w-[140px]">
|
|
<span className="text-text-main font-bold text-[10px] truncate uppercase leading-none" title={scheme.name}>
|
|
{scheme.name}
|
|
</span>
|
|
<span className="text-[7px] text-text-muted font-black uppercase tracking-tighter opacity-70">
|
|
{scheme.coat || 'Demão'}
|
|
</span>
|
|
</div>
|
|
))}
|
|
{schemes.length > 2 && (
|
|
<span className="text-[7px] text-primary font-black uppercase">+ {schemes.length - 2} demãos</span>
|
|
)}
|
|
</div>
|
|
);
|
|
},
|
|
className: 'hidden xl:table-cell w-[130px]'
|
|
},
|
|
{
|
|
header: 'Cor',
|
|
accessor: (item: Project) => {
|
|
const schemes = item.paintingSchemes || [];
|
|
if (schemes.length === 0) return <div className="w-4 h-4 rounded-full border border-border/20 opacity-20 border-dashed" />;
|
|
|
|
return (
|
|
<div className="flex flex-col gap-1.5 py-1">
|
|
{schemes.slice(0, 2).map((scheme, idx) => (
|
|
<div key={idx} className="flex items-center gap-2 h-[18px]">
|
|
<span className="text-[9px] text-text-secondary font-bold uppercase truncate max-w-[60px]" title={scheme.color}>
|
|
{scheme.color || '-'}
|
|
</span>
|
|
<ColorBubble colorHex={scheme.colorHex} className="w-3.5 h-3.5" />
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
},
|
|
className: 'hidden xl:table-cell w-[80px]'
|
|
},
|
|
{
|
|
header: 'Ações',
|
|
accessor: (item: Project) => (
|
|
<div className="flex items-center justify-end gap-1.5 min-w-[120px]">
|
|
<button
|
|
onClick={(e) => { e.stopPropagation(); handlePrint(item.id); }}
|
|
className="p-2 text-text-muted hover:text-primary hover:bg-primary/10 rounded-lg transition-colors flex items-center justify-center"
|
|
title="Gerar Relatório Analítico"
|
|
disabled={isPrinting}
|
|
>
|
|
<Printer size={18} />
|
|
</button>
|
|
{isAdmin && (
|
|
<button
|
|
onClick={(e) => { e.stopPropagation(); handleArchive(item); }}
|
|
className={`p-2 rounded-lg transition-colors ${viewStatus === 'active'
|
|
? 'text-green-500 hover:bg-green-500/10'
|
|
: 'text-indigo-500 hover:bg-indigo-500/10'
|
|
}`}
|
|
title={viewStatus === 'active' ? 'Concluir e Arquivar' : 'Reativar Projeto'}
|
|
>
|
|
{viewStatus === 'active' ? <CheckCircle2 size={16} /> : <History size={16} />}
|
|
</button>
|
|
)}
|
|
|
|
{isAdmin && viewStatus === 'active' && (
|
|
<>
|
|
<button
|
|
onClick={(e) => { e.stopPropagation(); handleEdit(item); }}
|
|
className="p-2 text-text-muted hover:text-primary hover:bg-primary/10 rounded-lg transition-colors"
|
|
title="Editar"
|
|
>
|
|
<Pencil size={18} />
|
|
</button>
|
|
<button
|
|
onClick={(e) => { e.stopPropagation(); handleDelete(item.id); }}
|
|
className="p-2 text-text-muted hover:text-red-500 hover:bg-red-500/10 rounded-lg transition-colors"
|
|
title="Excluir"
|
|
>
|
|
<Trash2 size={18} />
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
];
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex justify-center items-center h-64">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-10 animate-in fade-in duration-700">
|
|
{/* Page Header */}
|
|
<div className="flex flex-col md:flex-row md:items-end justify-between gap-6">
|
|
<div className="space-y-2">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-12 h-12 rounded-2xl bg-primary/10 flex items-center justify-center text-primary shadow-sm">
|
|
<Activity className="w-6 h-6" />
|
|
</div>
|
|
<div>
|
|
<h1 className="text-3xl md:text-4xl font-black text-text-main tracking-tight mb-0">
|
|
{viewStatus === 'active' ? 'Gestão de Obras' : 'Obras Arquivadas'}
|
|
</h1>
|
|
<p className="text-sm text-text-muted font-medium tracking-wide">
|
|
{viewStatus === 'active'
|
|
? 'Monitoramento e controle de esquemas industriais'
|
|
: 'Histórico de projetos concluídos e arquivados'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-wrap items-center gap-3">
|
|
<Button
|
|
variant="secondary"
|
|
onClick={() => setViewStatus(viewStatus === 'active' ? 'archived' : 'active')}
|
|
className={`shadow-sm ${viewStatus === 'archived' ? 'bg-indigo-500/10 text-indigo-500 border-indigo-500/20' : ''}`}
|
|
>
|
|
{viewStatus === 'active' ? <Box className="w-5 h-5 mr-2" /> : <Activity className="w-5 h-5 mr-2" />}
|
|
{viewStatus === 'active' ? 'Ver Arquivados' : 'Ver Ativos'}
|
|
</Button>
|
|
|
|
<div className="relative hidden lg:block group">
|
|
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-text-muted group-focus-within:text-primary transition-colors" />
|
|
<input
|
|
type="text"
|
|
placeholder="Buscar obras..."
|
|
className="h-12 w-48 bg-surface-soft border border-border/40 rounded-xl pl-12 pr-4 text-sm focus:ring-4 focus:ring-primary/10 focus:border-primary transition-all font-medium"
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
<Button
|
|
variant="secondary"
|
|
onClick={handlePrintGeneral}
|
|
className="shadow-sm"
|
|
disabled={isPrintingGeneral || projects.length === 0}
|
|
>
|
|
<Printer className="w-5 h-5 mr-2" />
|
|
Relatório Geral
|
|
</Button>
|
|
|
|
{isAdmin && viewStatus === 'active' && (
|
|
<Button onClick={() => { setSelectedProject(undefined); setIsModalOpen(true); }} size="md" className="shadow-primary/30">
|
|
<Plus className="w-5 h-5 mr-2" />
|
|
Novo Projeto
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
|
|
{/* Quick Stats */}
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
{[
|
|
{
|
|
label: viewStatus === 'active' ? 'Projetos Ativos' : 'Projetos Arquivados',
|
|
value: projects.length,
|
|
color: viewStatus === 'active' ? 'text-primary' : 'text-indigo-500',
|
|
bg: 'bg-primary/5',
|
|
onClick: () => { },
|
|
interactive: false
|
|
},
|
|
{
|
|
label: 'Inspeções Realizadas',
|
|
value: inspections.length,
|
|
color: 'text-success',
|
|
bg: 'bg-success/5',
|
|
onClick: () => setStatsModal({ isOpen: true, title: 'Resumo de Inspeções', type: 'inspections' }),
|
|
interactive: true
|
|
},
|
|
{
|
|
label: 'Conformidade',
|
|
value: inspections.length > 0
|
|
? `${((inspections.filter(i => i.appearance !== 'rejected').length / inspections.length) * 100).toFixed(1)}%`
|
|
: '100%',
|
|
color: 'text-blue-500',
|
|
bg: 'bg-blue-500/5',
|
|
onClick: () => { },
|
|
interactive: false
|
|
},
|
|
{
|
|
label: 'Alertas Pendentes',
|
|
value: inspections.filter(i => i.appearance === 'rejected' || i.defects).length,
|
|
color: 'text-error',
|
|
bg: 'bg-error/5',
|
|
onClick: () => setStatsModal({ isOpen: true, title: 'Resumo de Alertas', type: 'alerts' }),
|
|
interactive: true
|
|
}
|
|
].map((stat, i) => (
|
|
<div
|
|
key={i}
|
|
onClick={stat.onClick}
|
|
className={`p-6 bg-surface border border-border/40 rounded-2xl shadow-sm group transition-all ${stat.interactive ? 'cursor-pointer hover:border-primary/40 hover:shadow-md active:scale-95' : ''}`}
|
|
>
|
|
<span className="text-[10px] font-black text-text-muted uppercase tracking-[0.2em]">{stat.label}</span>
|
|
<div className="mt-2 flex items-baseline gap-2">
|
|
<span className={`text-3xl font-black ${stat.color}`}>{stat.value}</span>
|
|
<span className="text-[10px] text-text-muted font-bold">TOTAL</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Project Table/List */}
|
|
<div className="bg-surface rounded-[32px] border border-border/40 shadow-soft p-2">
|
|
<MobileList
|
|
data={filteredProjects}
|
|
columns={columns}
|
|
keyExtractor={(item) => item.id}
|
|
onItemClick={(item) => navigate(`/project/${item.id}`)}
|
|
titleAccessor="name"
|
|
subtitleAccessor="client"
|
|
/>
|
|
|
|
{projects.length === 0 && (
|
|
<div className="text-center py-24">
|
|
<div className="mx-auto h-20 w-20 bg-surface-soft rounded-full flex items-center justify-center text-text-muted/20 mb-6">
|
|
{viewStatus === 'active' ? <Search className="w-10 h-10" /> : <Box className="w-10 h-10" />}
|
|
</div>
|
|
<h3 className="text-xl font-bold text-text-main">
|
|
{viewStatus === 'active' ? 'Nenhuma obra encontrada' : 'Nenhuma obra arquivada'}
|
|
</h3>
|
|
<p className="mt-2 text-text-muted font-medium">
|
|
{viewStatus === 'active'
|
|
? 'Inicie o monitoramento criando seu primeiro projeto.'
|
|
: 'Ainda não existem projetos concluídos no histórico.'}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<CreateProjectModal
|
|
isOpen={isModalOpen}
|
|
onClose={() => {
|
|
setIsModalOpen(false);
|
|
setSelectedProject(undefined);
|
|
}}
|
|
onSuccess={fetchProjects}
|
|
initialData={selectedProject}
|
|
/>
|
|
|
|
<Modal
|
|
isOpen={statsModal.isOpen}
|
|
onClose={() => setStatsModal({ ...statsModal, isOpen: false })}
|
|
title={statsModal.title}
|
|
>
|
|
<div className="space-y-4">
|
|
{statsModal.type === 'alerts' && (
|
|
inspections.filter(i => i.appearance === 'rejected' || i.defects).map(insp => {
|
|
const project = projects.find(p => p.id === insp.projectId);
|
|
return (
|
|
<div
|
|
key={insp.id}
|
|
className="p-4 rounded-xl bg-error/5 border border-error/20 cursor-pointer hover:bg-error/10 transition-all active:scale-[0.98]"
|
|
onClick={() => navigate(`/project/${insp.projectId}`, { state: { activeTab: 'inspection' } })}
|
|
>
|
|
<div className="flex justify-between items-start mb-2">
|
|
<span className="font-bold text-text-main text-sm uppercase">{project?.name || 'Projeto Desconhecido'}</span>
|
|
<span className="text-[10px] font-black text-error border border-error/30 px-1 rounded uppercase">CRÍTICO</span>
|
|
</div>
|
|
<p className="text-sm text-text-secondary font-medium italic">"{insp.defects || 'Defeito não especificado'}"</p>
|
|
<div className="mt-3 flex justify-between items-center text-[10px] text-text-muted font-bold uppercase">
|
|
<span>Inspetor: {insp.inspector || '--'}</span>
|
|
<span>Data: {insp.date ? format(new Date(insp.date), 'dd/MM/yy') : '--'}</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
})
|
|
)}
|
|
|
|
{statsModal.type === 'inspections' && (
|
|
inspections.slice(0, 10).map(insp => {
|
|
const project = projects.find(p => p.id === insp.projectId);
|
|
return (
|
|
<div
|
|
key={insp.id}
|
|
className="p-4 rounded-xl bg-surface-soft border border-border/40 flex justify-between items-center cursor-pointer hover:bg-surface-hover transition-all active:scale-[0.98]"
|
|
onClick={() => navigate(`/project/${insp.projectId}`, { state: { activeTab: 'inspection' } })}
|
|
>
|
|
<div>
|
|
<span className="block font-bold text-text-main text-xs uppercase">{project?.name || '---'}</span>
|
|
<span className="text-[10px] text-text-muted font-medium uppercase">{insp.pieceDescription || 'Geral'}</span>
|
|
</div>
|
|
<div className="text-right">
|
|
<span className={`text-[10px] font-black px-2 py-0.5 rounded uppercase ${insp.appearance === 'rejected' ? 'bg-error/10 text-error' : 'bg-success/10 text-success'}`}>
|
|
{insp.appearance === 'rejected' ? 'REJEITADO' : 'APROVADO'}
|
|
</span>
|
|
<span className="block text-[10px] text-text-muted mt-1">{insp.date ? format(new Date(insp.date), 'dd/MM/yy') : '--'}</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
})
|
|
)}
|
|
</div>
|
|
</Modal>
|
|
<ConfirmModal
|
|
isOpen={confirmModal.isOpen}
|
|
onClose={() => setConfirmModal(prev => ({ ...prev, isOpen: false }))}
|
|
onConfirm={confirmModal.onConfirm}
|
|
title={confirmModal.title}
|
|
description={confirmModal.description}
|
|
type={confirmModal.type}
|
|
/>
|
|
|
|
{printingProject && (
|
|
<AnalyticalReport project={printingProject} logoUrl={logoUrl} />
|
|
)}
|
|
|
|
{isPrintingGeneral && (
|
|
<GeneralProjectReport
|
|
projects={filteredProjects}
|
|
inspections={inspections}
|
|
logoUrl={logoUrl}
|
|
title={viewStatus === 'active' ? 'Relatório de Obras Ativas' : 'Relatório de Obras Arquivadas'}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|