fastcheck/src/pages/admin/Ley21121Page.tsx
2026-04-08 13:58:46 -04:00

523 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect } from 'react';
import { Plus, Edit, Trash2, Loader, CheckCircle, XCircle } from 'lucide-react';
import { apiClient, validateChileanRut, formatChileanRut } from '../../services/api';
import logger from '../../utils/logger';
import { useTranslation } from 'react-i18next';
interface Ley21121Record {
_id: string;
rut: string;
razonSocial: string;
rolCausa: string;
fechaDesde: string;
fechaHasta: string;
duracion: string;
motivo: string;
createdAt: string;
updatedAt: string;
}
const Ley21121Page: React.FC = () => {
const { t } = useTranslation();
const token = localStorage.getItem('token');
const [records, setRecords] = useState<Ley21121Record[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
const [openDialog, setOpenDialog] = useState<boolean>(false);
const [dialogMode, setDialogMode] = useState<'create' | 'edit'>('create');
const [currentRecord, setCurrentRecord] = useState<Partial<Ley21121Record>>({});
const [isValidRut, setIsValidRut] = useState<boolean | null>(null);
const [formattedRut, setFormattedRut] = useState('');
const [snackbar, setSnackbar] = useState<{ open: boolean; message: string; severity: 'success' | 'error' }>({
open: false,
message: '',
severity: 'success'
});
const [selected, setSelected] = useState<string[]>([]);
const [deleting, setDeleting] = useState(false);
// Fetch all records
const fetchRecords = async () => {
setLoading(true);
try {
const response = await apiClient.get('/ley21121');
setRecords(response.data.data);
setError(null);
} catch (err) {
logger.error('Error fetching records:', err);
setError(t('ley21121.messages.loadError'));
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchRecords();
}, [token]);
// Handle form input changes
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
// If the field is RUT, validate and format it
if (name === 'rut') {
// Format RUT as user types
if (value) {
const formatted = formatChileanRut(value);
setFormattedRut(formatted);
// Validate RUT
const isValid = validateChileanRut(value);
setIsValidRut(isValid);
// Store the sanitized RUT (without dots, uppercase)
const sanitizedRut = formatted.replace(/\./g, '');
setCurrentRecord({ ...currentRecord, [name]: sanitizedRut });
} else {
setFormattedRut('');
setIsValidRut(null);
setCurrentRecord({ ...currentRecord, [name]: '' });
}
} else {
setCurrentRecord({ ...currentRecord, [name]: value });
}
};
// Handle date changes
const handleDateChange = (date: Date | null, field: 'fechaDesde' | 'fechaHasta') => {
if (date) {
setCurrentRecord({ ...currentRecord, [field]: date.toISOString() });
}
};
// Open dialog for creating a new record
const handleOpenCreateDialog = () => {
setDialogMode('create');
setCurrentRecord({});
setOpenDialog(true);
};
// Open dialog for editing an existing record
const handleOpenEditDialog = (record: Ley21121Record) => {
setDialogMode('edit');
setCurrentRecord({ ...record });
setOpenDialog(true);
};
// Close dialog
const handleCloseDialog = () => {
setOpenDialog(false);
};
// Save record (create or update)
const handleSaveRecord = async () => {
// Validate RUT before saving
if (currentRecord.rut && !validateChileanRut(currentRecord.rut)) {
setSnackbar({
open: true,
message: t('ley21121.form.rut.invalid'),
severity: 'error'
});
return;
}
try {
if (dialogMode === 'create') {
await apiClient.post('/ley21121', currentRecord);
setSnackbar({
open: true,
message: t('ley21121.messages.createSuccess'),
severity: 'success'
});
} else {
await apiClient.put(`/ley21121/${currentRecord._id}`, currentRecord);
setSnackbar({
open: true,
message: t('ley21121.messages.updateSuccess'),
severity: 'success'
});
}
handleCloseDialog();
fetchRecords();
} catch (err) {
logger.error('Error saving record:', err);
setSnackbar({
open: true,
message: t('ley21121.messages.saveError'),
severity: 'error'
});
}
};
// Delete record
const handleDeleteRecord = async (id: string) => {
if (window.confirm(t('ley21121.messages.deleteConfirm'))) {
try {
await apiClient.delete(`/ley21121/${id}`);
setSnackbar({
open: true,
message: t('ley21121.messages.deleteSuccess'),
severity: 'success'
});
fetchRecords();
} catch (err) {
logger.error('Error deleting record:', err);
setSnackbar({
open: true,
message: t('ley21121.messages.deleteError'),
severity: 'error'
});
}
}
};
// Handle select all checkbox
const handleSelectAll = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.checked) {
setSelected(records.map((r) => r._id));
} else {
setSelected([]);
}
};
// Handle select one checkbox
const handleSelectOne = (id: string) => {
if (selected.includes(id)) {
setSelected(selected.filter((s) => s !== id));
} else {
setSelected([...selected, id]);
}
};
// Handle delete selected records
const handleDeleteSelected = async () => {
if (selected.length === 0) return;
if (!window.confirm(t('ley21121.messages.deleteConfirm'))) return;
setDeleting(true);
let deletedCount = 0;
let errorCount = 0;
for (const id of selected) {
try {
await apiClient.delete(`/ley21121/${id}`);
deletedCount++;
} catch (err) {
errorCount++;
logger.error('Error deleting record:', err);
}
}
setDeleting(false);
setSelected([]);
if (errorCount > 0) {
setSnackbar({
open: true,
message: `Se eliminaron ${deletedCount} registros. ${errorCount} errores.`,
severity: 'error'
});
} else {
setSnackbar({
open: true,
message: `Se eliminaron ${deletedCount} registros exitosamente.`,
severity: 'success'
});
}
await fetchRecords();
};
// Close snackbar
const handleCloseSnackbar = () => {
setSnackbar({ ...snackbar, open: false });
};
// Format date for display
const formatDate = (dateString: string) => {
try {
const date = new Date(dateString);
if (isNaN(date.getTime())) {
return t('ley21121.messages.invalidDate');
}
// Format as dd/MM/yyyy
const day = date.getDate().toString().padStart(2, '0');
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const year = date.getFullYear();
return `${day}/${month}/${year}`;
} catch (error) {
return t('ley21121.messages.invalidDate');
}
};
if (loading) {
return (
<div className="flex justify-center items-center my-16">
<Loader className="animate-spin h-8 w-8 text-primary-500" />
</div>
);
} else if (error) {
return (
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4 dark:bg-red-900/50 dark:text-red-300 dark:border-red-800">
{error}
</div>
);
}
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-8 mb-8">
<div className="bg-white dark:bg-gray-800 shadow rounded-lg p-6 mb-6">
<div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
{t('ley21121.title')}
</h1>
<div className="flex gap-2">
<button
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 disabled:bg-gray-400 disabled:cursor-not-allowed"
onClick={handleDeleteSelected}
disabled={selected.length === 0 || deleting}
>
<Trash2 className="h-5 w-5 mr-2" /> {t('common.delete')} {selected.length > 0 && `(${selected.length})`}
</button>
<button
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
onClick={handleOpenCreateDialog}
>
<Plus className="h-5 w-5 mr-2" /> {t('ley21121.addNew')}
</button>
</div>
</div>
<div className="overflow-x-auto">
<table className="w-full table-fixed divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-700">
<tr>
<th scope="col" className="px-6 py-3 text-left w-[10%]">
<input
type="checkbox"
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
checked={selected.length === records.length && records.length > 0}
onChange={handleSelectAll}
/>
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider w-[10%]">{t('ley21121.table.headers.rut')}</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider w-[10%]">{t('ley21121.table.headers.company')}</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider w-[10%]">{t('ley21121.table.headers.caseRole')}</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider w-[10%]">{t('ley21121.table.headers.dates')}</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider w-[10%]">{t('ley21121.table.headers.duration')}</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider w-[30%]">{t('ley21121.table.headers.reason')}</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider w-[10%]">{t('ley21121.table.headers.actions')}</th>
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
{records.length === 0 ? (
<tr>
<td colSpan={8} className="px-6 py-4 text-center text-sm text-gray-500 dark:text-gray-400">
{t('ley21121.table.empty')}
</td>
</tr>
) : (
records.map((record) => (
<tr key={record._id}>
<td className="px-6 py-4">
<input
type="checkbox"
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
checked={selected.includes(record._id)}
onChange={() => handleSelectOne(record._id)}
/>
</td>
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-400 break-words">{record.rut}</td>
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-400 break-words">{record.razonSocial}</td>
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-400 break-words">{record.rolCausa}</td>
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-400 break-words">
{t('ley21121.from')} {formatDate(record.fechaDesde)}
<br />
{t('ley21121.to')} {formatDate(record.fechaHasta)}
</td>
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-400 break-words">{record.duracion}</td>
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-400 break-words">{record.motivo}</td>
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">
<button
className="text-primary-600 hover:text-primary-900 dark:text-primary-400 dark:hover:text-primary-300 mr-2"
onClick={() => handleOpenEditDialog(record)}
>
<Edit className="h-5 w-5" />
</button>
<button
className="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300"
onClick={() => handleDeleteRecord(record._id)}
>
<Trash2 className="h-5 w-5" />
</button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
{/* Dialog for creating/editing records */}
{openDialog && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-3xl w-full mx-4">
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-medium text-gray-900 dark:text-white">
{dialogMode === 'create' ? t('ley21121.dialog.createTitle') : t('ley21121.dialog.editTitle')}
</h3>
</div>
<div className="px-6 py-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{t('ley21121.form.labels.rut')}</label>
<div className="relative">
<input
type="text"
name="rut"
className={`w-full pl-3 pr-10 py-2 border rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white ${
isValidRut === false
? 'border-red-500 text-red-900 dark:text-red-200 focus:ring-red-500 focus:border-red-500'
: isValidRut === true
? 'border-green-500 text-green-900 dark:text-green-200 focus:ring-green-500 focus:border-green-500'
: 'border-gray-300 dark:border-gray-600'
}`}
value={formattedRut || currentRecord.rut || ''}
onChange={handleInputChange}
placeholder={t('ley21121.form.rut.placeholder')}
required
/>
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
{isValidRut === true && <CheckCircle className="h-5 w-5 text-green-500" />}
{isValidRut === false && <XCircle className="h-5 w-5 text-red-500" />}
</div>
</div>
{isValidRut === false && (
<p className="mt-1 text-sm text-red-600 dark:text-red-400">
{t('ley21121.form.rut.invalid')}
</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{t('ley21121.form.labels.company')}</label>
<input
type="text"
name="razonSocial"
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
value={currentRecord.razonSocial || ''}
onChange={handleInputChange}
required
/>
</div>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{t('ley21121.form.labels.caseRole')}</label>
<input
type="text"
name="rolCausa"
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
value={currentRecord.rolCausa || ''}
onChange={handleInputChange}
required
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{t('ley21121.form.labels.startDate')}</label>
<input
type="date"
name="fechaDesde"
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
value={currentRecord.fechaDesde ? new Date(currentRecord.fechaDesde).toISOString().split('T')[0] : ''}
onChange={(e) => {
const date = e.target.value ? new Date(e.target.value) : null;
handleDateChange(date, 'fechaDesde');
}}
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{t('ley21121.form.labels.endDate')}</label>
<input
type="date"
name="fechaHasta"
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
value={currentRecord.fechaHasta ? new Date(currentRecord.fechaHasta).toISOString().split('T')[0] : ''}
onChange={(e) => {
const date = e.target.value ? new Date(e.target.value) : null;
handleDateChange(date, 'fechaHasta');
}}
required
/>
</div>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{t('ley21121.form.labels.duration')}</label>
<input
type="text"
name="duracion"
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
value={currentRecord.duracion || ''}
onChange={handleInputChange}
placeholder={t('ley21121.form.duration.placeholder')}
required
/>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{t('ley21121.form.labels.reason')}</label>
<textarea
name="motivo"
rows={4}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
value={currentRecord.motivo || ''}
onChange={handleInputChange}
required
></textarea>
</div>
</div>
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex justify-end">
<button
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-md mr-2"
onClick={handleCloseDialog}
>
{t('common.cancel')}
</button>
<button
className="px-4 py-2 text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 rounded-md disabled:bg-gray-400 dark:disabled:bg-gray-600 disabled:cursor-not-allowed"
onClick={handleSaveRecord}
disabled={Boolean(currentRecord.rut && isValidRut === false)}
>
{t('common.save')}
</button>
</div>
</div>
</div>
)}
{/* Notification */}
{snackbar.open && (
<div className={`fixed bottom-4 left-1/2 transform -translate-x-1/2 px-4 py-3 rounded shadow-lg ${
snackbar.severity === 'success' ? 'bg-green-100 text-green-800 dark:bg-green-900/50 dark:text-green-300' : 'bg-red-100 text-red-800 dark:bg-red-900/50 dark:text-red-300'
}`}>
{snackbar.message}
<button
className="ml-4 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
onClick={handleCloseSnackbar}
>
×
</button>
</div>
)}
</div>
);
};
export default Ley21121Page;