chore: synchronize local fixes to gitea
This commit is contained in:
584
src/client/pages/ProjectList.tsx
Normal file
584
src/client/pages/ProjectList.tsx
Normal file
@@ -0,0 +1,584 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user