erp-construccion-frontend-v2/web/src/pages/admin/contratos/ContratoDetailPage.tsx
Adrian Flores Cortes 55261598a2 [FIX] fix: Resolve TypeScript errors for successful build
Fixes:
- Add teal, cyan, slate colors to StatusColor type and StatusBadge
- Create StatsCard component with color prop for backward compatibility
- Add label/required props to FormGroup component
- Fix Pagination to accept both currentPage and page props
- Fix unused imports in quality and contracts pages
- Add missing Plus, Trash2, User icon imports in contracts pages
- Remove duplicate formatDate function in ContratoDetailPage

New components:
- StatsCard, StatsCardGrid for statistics display

Build: Success (npm run build passes)
Dev: Success (npm run dev starts on port 3020)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 11:36:21 -06:00

853 lines
31 KiB
TypeScript

/**
* ContratoDetailPage - Detalle del contrato con tabs
*/
import { useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import {
ArrowLeft,
FileText,
Building2,
Calendar,
DollarSign,
Pencil,
Send,
CheckCircle,
PlayCircle,
XCircle,
ListTodo,
FilePlus,
Clock,
Plus,
Trash2,
} from 'lucide-react';
import {
useContract,
useContractPartidas,
useContractAddendums,
useSubmitContract,
useApproveContract,
useActivateContract,
useCompleteContract,
useTerminateContract,
useDeleteContractPartida,
useDeleteContractAddendum,
} from '../../../hooks/useContracts';
import type { Contract, ContractPartida, ContractAddendum } from '../../../types/contracts.types';
import {
CONTRACT_TYPE_OPTIONS,
CONTRACT_STATUS_OPTIONS,
ADDENDUM_TYPE_OPTIONS,
ADDENDUM_STATUS_OPTIONS,
} from '../../../types/contracts.types';
import {
StatusBadgeFromOptions,
LoadingOverlay,
EmptyState,
ConfirmDialog,
Modal,
ModalFooter,
TextareaField,
} from '../../../components/common';
import { ContractForm } from '../../../components/contracts/ContractForm';
import { AddendaModal } from '../../../components/contracts/AddendaModal';
import { PartidaModal } from '../../../components/contracts/PartidaModal';
type TabType = 'info' | 'partidas' | 'addendas';
export function ContratoDetailPage() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const [activeTab, setActiveTab] = useState<TabType>('info');
const [showEditModal, setShowEditModal] = useState(false);
const [showAddendaModal, setShowAddendaModal] = useState(false);
const [showPartidaModal, setShowPartidaModal] = useState(false);
const [editingAddenda, setEditingAddenda] = useState<ContractAddendum | null>(null);
const [editingPartida, setEditingPartida] = useState<ContractPartida | null>(null);
const [deletePartidaId, setDeletePartidaId] = useState<string | null>(null);
const [deleteAddendaId, setDeleteAddendaId] = useState<string | null>(null);
const [showTerminateModal, setShowTerminateModal] = useState(false);
const [terminateReason, setTerminateReason] = useState('');
const { data: contract, isLoading, error } = useContract(id || '');
const { data: partidas, isLoading: loadingPartidas } = useContractPartidas(id || '');
const { data: addendums, isLoading: loadingAddendums } = useContractAddendums(id || '');
const submitMutation = useSubmitContract();
const approveMutation = useApproveContract();
const activateMutation = useActivateContract();
const completeMutation = useCompleteContract();
const terminateMutation = useTerminateContract();
const deletePartidaMutation = useDeleteContractPartida();
const deleteAddendaMutation = useDeleteContractAddendum();
const formatCurrency = (value: number) => {
return new Intl.NumberFormat('es-MX', {
style: 'currency',
currency: 'MXN',
}).format(value);
};
const handleSubmit = async () => {
if (id) await submitMutation.mutateAsync(id);
};
const handleApprove = async () => {
if (id) await approveMutation.mutateAsync(id);
};
const handleActivate = async () => {
if (id) await activateMutation.mutateAsync(id);
};
const handleComplete = async () => {
if (id) await completeMutation.mutateAsync(id);
};
const handleTerminate = async () => {
if (id && terminateReason) {
await terminateMutation.mutateAsync({ id, reason: terminateReason });
setShowTerminateModal(false);
setTerminateReason('');
}
};
const handleDeletePartida = async () => {
if (id && deletePartidaId) {
await deletePartidaMutation.mutateAsync({ contractId: id, partidaId: deletePartidaId });
setDeletePartidaId(null);
}
};
const handleDeleteAddenda = async () => {
if (id && deleteAddendaId) {
await deleteAddendaMutation.mutateAsync({ contractId: id, addendumId: deleteAddendaId });
setDeleteAddendaId(null);
}
};
if (isLoading) {
return <LoadingOverlay message="Cargando contrato..." />;
}
if (error || !contract) {
return (
<EmptyState
title="Contrato no encontrado"
description="El contrato solicitado no existe o fue eliminado."
/>
);
}
const tabs = [
{ id: 'info', label: 'Informacion General', icon: FileText },
{ id: 'partidas', label: 'Partidas', icon: ListTodo, count: partidas?.length },
{ id: 'addendas', label: 'Addendas', icon: FilePlus, count: addendums?.length },
];
const canSubmit = contract.status === 'draft';
const canApprove = contract.status === 'review';
const canActivate = contract.status === 'approved';
const canComplete = contract.status === 'active';
const canTerminate = ['active', 'approved'].includes(contract.status);
return (
<div>
{/* Header */}
<div className="mb-6">
<button
onClick={() => navigate('/admin/contratos')}
className="flex items-center text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 mb-4"
>
<ArrowLeft className="w-4 h-4 mr-2" />
Volver a contratos
</button>
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
<div className="flex items-start gap-4">
<div className="p-3 bg-blue-100 dark:bg-blue-900/30 rounded-lg">
<FileText className="w-8 h-8 text-blue-600 dark:text-blue-400" />
</div>
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100">
{contract.contractNumber}
</h1>
<p className="text-gray-600 dark:text-gray-400">{contract.name}</p>
<div className="flex items-center gap-3 mt-2">
<StatusBadgeFromOptions
value={contract.contractType}
options={[...CONTRACT_TYPE_OPTIONS]}
/>
<StatusBadgeFromOptions
value={contract.status}
options={[...CONTRACT_STATUS_OPTIONS]}
/>
</div>
</div>
</div>
<div className="flex flex-wrap gap-2">
<button
onClick={() => setShowEditModal(true)}
className="flex items-center px-4 py-2 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700"
>
<Pencil className="w-4 h-4 mr-2" />
Editar
</button>
{canSubmit && (
<button
onClick={handleSubmit}
disabled={submitMutation.isPending}
className="flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50"
>
<Send className="w-4 h-4 mr-2" />
Enviar a Revision
</button>
)}
{canApprove && (
<button
onClick={handleApprove}
disabled={approveMutation.isPending}
className="flex items-center px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50"
>
<CheckCircle className="w-4 h-4 mr-2" />
Aprobar
</button>
)}
{canActivate && (
<button
onClick={handleActivate}
disabled={activateMutation.isPending}
className="flex items-center px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50"
>
<PlayCircle className="w-4 h-4 mr-2" />
Activar
</button>
)}
{canComplete && (
<button
onClick={handleComplete}
disabled={completeMutation.isPending}
className="flex items-center px-4 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700 disabled:opacity-50"
>
<CheckCircle className="w-4 h-4 mr-2" />
Completar
</button>
)}
{canTerminate && (
<button
onClick={() => setShowTerminateModal(true)}
className="flex items-center px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700"
>
<XCircle className="w-4 h-4 mr-2" />
Terminar
</button>
)}
</div>
</div>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-500 dark:text-gray-400">Monto Contrato</p>
<p className="text-xl font-bold text-gray-900 dark:text-gray-100">
{formatCurrency(contract.contractAmount)}
</p>
</div>
<DollarSign className="w-8 h-8 text-blue-500" />
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-500 dark:text-gray-400">Facturado</p>
<p className="text-xl font-bold text-green-600">
{formatCurrency(contract.invoicedAmount)}
</p>
</div>
<FileText className="w-8 h-8 text-green-500" />
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-500 dark:text-gray-400">Pagado</p>
<p className="text-xl font-bold text-blue-600">
{formatCurrency(contract.paidAmount)}
</p>
</div>
<DollarSign className="w-8 h-8 text-blue-500" />
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-500 dark:text-gray-400">Avance</p>
<p className="text-xl font-bold text-gray-900 dark:text-gray-100">
{contract.progressPercentage}%
</p>
</div>
<div className="w-12 h-12 relative">
<svg className="w-full h-full" viewBox="0 0 36 36">
<path
className="text-gray-200 dark:text-gray-700"
strokeWidth="3"
stroke="currentColor"
fill="none"
d="M18 2.0845a 15.9155 15.9155 0 0 1 0 31.831a 15.9155 15.9155 0 0 1 0 -31.831"
/>
<path
className="text-blue-600"
strokeWidth="3"
strokeLinecap="round"
stroke="currentColor"
fill="none"
strokeDasharray={`${contract.progressPercentage}, 100`}
d="M18 2.0845a 15.9155 15.9155 0 0 1 0 31.831a 15.9155 15.9155 0 0 1 0 -31.831"
/>
</svg>
</div>
</div>
</div>
</div>
{/* Tabs */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm">
<div className="border-b border-gray-200 dark:border-gray-700">
<nav className="flex -mb-px">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as TabType)}
className={`flex items-center px-6 py-4 border-b-2 font-medium text-sm transition-colors ${
activeTab === tab.id
? 'border-blue-500 text-blue-600 dark:text-blue-400'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
}`}
>
<tab.icon className="w-4 h-4 mr-2" />
{tab.label}
{tab.count !== undefined && (
<span className="ml-2 px-2 py-0.5 bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 text-xs rounded-full">
{tab.count}
</span>
)}
</button>
))}
</nav>
</div>
<div className="p-6">
{activeTab === 'info' && <ContractInfoTab contract={contract} />}
{activeTab === 'partidas' && (
<PartidasTab
partidas={partidas || []}
isLoading={loadingPartidas}
onAdd={() => {
setEditingPartida(null);
setShowPartidaModal(true);
}}
onEdit={(p) => {
setEditingPartida(p);
setShowPartidaModal(true);
}}
onDelete={setDeletePartidaId}
/>
)}
{activeTab === 'addendas' && (
<AddendasTab
addendums={addendums || []}
isLoading={loadingAddendums}
onAdd={() => {
setEditingAddenda(null);
setShowAddendaModal(true);
}}
onEdit={(a) => {
setEditingAddenda(a);
setShowAddendaModal(true);
}}
onDelete={setDeleteAddendaId}
/>
)}
</div>
</div>
{/* Modals */}
{showEditModal && (
<ContractForm
contract={contract}
onClose={() => setShowEditModal(false)}
/>
)}
{showAddendaModal && (
<AddendaModal
contractId={id || ''}
addendum={editingAddenda}
onClose={() => {
setShowAddendaModal(false);
setEditingAddenda(null);
}}
/>
)}
{showPartidaModal && (
<PartidaModal
contractId={id || ''}
partida={editingPartida}
onClose={() => {
setShowPartidaModal(false);
setEditingPartida(null);
}}
/>
)}
{/* Terminate Modal */}
{showTerminateModal && (
<Modal
isOpen={true}
onClose={() => setShowTerminateModal(false)}
title="Terminar Contrato"
size="md"
footer={
<ModalFooter>
<button
type="button"
className="px-4 py-2 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700"
onClick={() => setShowTerminateModal(false)}
>
Cancelar
</button>
<button
type="button"
onClick={handleTerminate}
disabled={!terminateReason || terminateMutation.isPending}
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 disabled:opacity-50"
>
{terminateMutation.isPending ? 'Terminando...' : 'Terminar Contrato'}
</button>
</ModalFooter>
}
>
<TextareaField
label="Razon de terminacion"
required
value={terminateReason}
onChange={(e) => setTerminateReason(e.target.value)}
placeholder="Describa la razon por la cual se termina el contrato..."
rows={4}
/>
</Modal>
)}
{/* Delete Confirmations */}
<ConfirmDialog
isOpen={!!deletePartidaId}
onClose={() => setDeletePartidaId(null)}
onConfirm={handleDeletePartida}
title="Eliminar Partida"
message="Esta seguro de eliminar esta partida del contrato?"
confirmLabel="Eliminar"
variant="danger"
isLoading={deletePartidaMutation.isPending}
/>
<ConfirmDialog
isOpen={!!deleteAddendaId}
onClose={() => setDeleteAddendaId(null)}
onConfirm={handleDeleteAddenda}
title="Eliminar Addenda"
message="Esta seguro de eliminar esta addenda?"
confirmLabel="Eliminar"
variant="danger"
isLoading={deleteAddendaMutation.isPending}
/>
</div>
);
}
// ============================================================================
// CONTRACT INFO TAB
// ============================================================================
function ContractInfoTab({ contract }: { contract: Contract }) {
const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleDateString('es-MX', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
};
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* General Info */}
<div className="space-y-4">
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 flex items-center">
<FileText className="w-5 h-5 mr-2" />
Datos Generales
</h3>
<div className="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4 space-y-3">
<InfoRow label="Numero de Contrato" value={contract.contractNumber} />
<InfoRow label="Nombre" value={contract.name} />
{contract.description && (
<InfoRow label="Descripcion" value={contract.description} />
)}
<InfoRow label="Moneda" value={contract.currency} />
<InfoRow label="Retencion" value={`${contract.retentionPercentage}%`} />
<InfoRow label="Anticipo" value={`${contract.advancePercentage}%`} />
</div>
</div>
{/* Client/Subcontractor Info */}
<div className="space-y-4">
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 flex items-center">
<Building2 className="w-5 h-5 mr-2" />
{contract.contractType === 'client' ? 'Datos del Cliente' : 'Datos del Subcontratista'}
</h3>
<div className="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4 space-y-3">
{contract.contractType === 'client' ? (
<>
<InfoRow label="Cliente" value={contract.clientName || '-'} />
<InfoRow label="RFC" value={contract.clientRfc || '-'} />
<InfoRow label="Direccion" value={contract.clientAddress || '-'} />
</>
) : (
<>
<InfoRow label="Subcontratista" value={contract.subcontractor?.businessName || '-'} />
<InfoRow label="RFC" value={contract.subcontractor?.rfc || '-'} />
<InfoRow label="Especialidad" value={contract.specialty || '-'} />
</>
)}
</div>
</div>
{/* Dates */}
<div className="space-y-4">
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 flex items-center">
<Calendar className="w-5 h-5 mr-2" />
Vigencia
</h3>
<div className="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4 space-y-3">
<InfoRow label="Fecha Inicio" value={formatDate(contract.startDate)} />
<InfoRow label="Fecha Fin" value={formatDate(contract.endDate)} />
{contract.signedAt && (
<InfoRow label="Fecha Firma" value={formatDate(contract.signedAt)} />
)}
</div>
</div>
{/* Payment Terms */}
<div className="space-y-4">
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 flex items-center">
<DollarSign className="w-5 h-5 mr-2" />
Condiciones de Pago
</h3>
<div className="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4">
<p className="text-sm text-gray-700 dark:text-gray-300">
{contract.paymentTerms || 'Sin condiciones especificas'}
</p>
</div>
</div>
{/* Audit Info */}
<div className="space-y-4 lg:col-span-2">
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 flex items-center">
<Clock className="w-5 h-5 mr-2" />
Historial
</h3>
<div className="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4 space-y-3">
<InfoRow label="Creado" value={formatDate(contract.createdAt)} />
<InfoRow label="Actualizado" value={formatDate(contract.updatedAt)} />
{contract.submittedAt && (
<InfoRow label="Enviado a revision" value={formatDate(contract.submittedAt)} />
)}
{contract.approvedAt && (
<InfoRow label="Aprobado" value={formatDate(contract.approvedAt)} />
)}
{contract.terminatedAt && (
<>
<InfoRow label="Terminado" value={formatDate(contract.terminatedAt)} />
<InfoRow label="Razon" value={contract.terminationReason || '-'} />
</>
)}
</div>
</div>
{/* Notes */}
{contract.notes && (
<div className="space-y-4 lg:col-span-2">
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
Notas
</h3>
<div className="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4">
<p className="text-sm text-gray-700 dark:text-gray-300 whitespace-pre-wrap">
{contract.notes}
</p>
</div>
</div>
)}
</div>
);
}
function InfoRow({ label, value }: { label: string; value: string }) {
return (
<div className="flex justify-between">
<span className="text-sm text-gray-500 dark:text-gray-400">{label}</span>
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">{value}</span>
</div>
);
}
// ============================================================================
// PARTIDAS TAB
// ============================================================================
interface PartidasTabProps {
partidas: ContractPartida[];
isLoading: boolean;
onAdd: () => void;
onEdit: (partida: ContractPartida) => void;
onDelete: (id: string) => void;
}
function PartidasTab({ partidas, isLoading, onAdd, onEdit, onDelete }: PartidasTabProps) {
const formatCurrency = (value: number) => {
return new Intl.NumberFormat('es-MX', {
style: 'currency',
currency: 'MXN',
}).format(value);
};
if (isLoading) {
return <div className="text-center py-8 text-gray-500">Cargando partidas...</div>;
}
const total = partidas.reduce((sum, p) => sum + (p.totalAmount || p.quantity * p.unitPrice), 0);
return (
<div>
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
Partidas del Contrato
</h3>
<button
onClick={onAdd}
className="flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
<Plus className="w-4 h-4 mr-2" />
Agregar Partida
</button>
</div>
{partidas.length === 0 ? (
<EmptyState
icon={<ListTodo className="w-12 h-12 text-gray-400" />}
title="Sin partidas"
description="Agrega las partidas del contrato."
/>
) : (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-700">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase">
Concepto
</th>
<th className="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase">
Cantidad
</th>
<th className="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase">
P.U.
</th>
<th className="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase">
Total
</th>
<th className="px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase">
Acciones
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
{partidas.map((partida) => (
<tr key={partida.id} className="hover:bg-gray-50 dark:hover:bg-gray-700">
<td className="px-4 py-3">
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
{partida.conceptoCode || 'N/A'}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
{partida.conceptoDescription || '-'}
</div>
</td>
<td className="px-4 py-3 text-right text-sm text-gray-900 dark:text-gray-100">
{partida.quantity.toLocaleString()} {partida.unit || ''}
</td>
<td className="px-4 py-3 text-right text-sm text-gray-900 dark:text-gray-100">
{formatCurrency(partida.unitPrice)}
</td>
<td className="px-4 py-3 text-right text-sm font-medium text-gray-900 dark:text-gray-100">
{formatCurrency(partida.totalAmount || partida.quantity * partida.unitPrice)}
</td>
<td className="px-4 py-3 text-right">
<div className="flex items-center justify-end gap-1">
<button
onClick={() => onEdit(partida)}
className="p-1 text-gray-500 hover:text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded"
>
<Pencil className="w-4 h-4" />
</button>
<button
onClick={() => onDelete(partida.id)}
className="p-1 text-gray-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 rounded"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</td>
</tr>
))}
</tbody>
<tfoot className="bg-gray-50 dark:bg-gray-700">
<tr>
<td colSpan={3} className="px-4 py-3 text-right text-sm font-medium text-gray-900 dark:text-gray-100">
Total:
</td>
<td className="px-4 py-3 text-right text-sm font-bold text-gray-900 dark:text-gray-100">
{formatCurrency(total)}
</td>
<td></td>
</tr>
</tfoot>
</table>
</div>
)}
</div>
);
}
// ============================================================================
// ADDENDAS TAB
// ============================================================================
interface AddendasTabProps {
addendums: ContractAddendum[];
isLoading: boolean;
onAdd: () => void;
onEdit: (addendum: ContractAddendum) => void;
onDelete: (id: string) => void;
}
function AddendasTab({ addendums, isLoading, onAdd, onEdit, onDelete }: AddendasTabProps) {
const formatCurrency = (value: number) => {
return new Intl.NumberFormat('es-MX', {
style: 'currency',
currency: 'MXN',
}).format(value);
};
const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleDateString('es-MX', {
year: 'numeric',
month: 'short',
day: 'numeric',
});
};
if (isLoading) {
return <div className="text-center py-8 text-gray-500">Cargando addendas...</div>;
}
return (
<div>
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
Addendas del Contrato
</h3>
<button
onClick={onAdd}
className="flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
<Plus className="w-4 h-4 mr-2" />
Nueva Addenda
</button>
</div>
{addendums.length === 0 ? (
<EmptyState
icon={<FilePlus className="w-12 h-12 text-gray-400" />}
title="Sin addendas"
description="No hay addendas registradas para este contrato."
/>
) : (
<div className="space-y-4">
{addendums.map((addendum) => (
<div
key={addendum.id}
className="border border-gray-200 dark:border-gray-700 rounded-lg p-4 hover:bg-gray-50 dark:hover:bg-gray-700/50"
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<span className="font-medium text-gray-900 dark:text-gray-100">
{addendum.addendumNumber}
</span>
<StatusBadgeFromOptions
value={addendum.addendumType}
options={[...ADDENDUM_TYPE_OPTIONS]}
/>
<StatusBadgeFromOptions
value={addendum.status}
options={[...ADDENDUM_STATUS_OPTIONS]}
/>
</div>
<h4 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-1">
{addendum.title}
</h4>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-2">
{addendum.description}
</p>
<div className="flex items-center gap-4 text-sm text-gray-500 dark:text-gray-400">
<span>Vigencia: {formatDate(addendum.effectiveDate)}</span>
{addendum.amountChange !== 0 && (
<span className={addendum.amountChange > 0 ? 'text-green-600' : 'text-red-600'}>
{addendum.amountChange > 0 ? '+' : ''}{formatCurrency(addendum.amountChange)}
</span>
)}
{addendum.newEndDate && (
<span>Nueva fecha fin: {formatDate(addendum.newEndDate)}</span>
)}
</div>
</div>
<div className="flex items-center gap-1 ml-4">
<button
onClick={() => onEdit(addendum)}
className="p-2 text-gray-500 hover:text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded-lg"
>
<Pencil className="w-4 h-4" />
</button>
<button
onClick={() => onDelete(addendum.id)}
className="p-2 text-gray-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
</div>
))}
</div>
)}
</div>
);
}