Files
bakery-ia/frontend/src/components/domain/analytics/ReportsTable.tsx.backup
2025-08-28 10:41:04 +02:00

651 lines
19 KiB
Plaintext

import React, { useState, useMemo } from 'react';
import { Card, Button, Badge, Input, Select, Modal } from '../../ui';
import { Table } from '../../ui';
import type {
AnalyticsReport,
ReportsTableProps,
ExportFormat,
ReportType,
CustomAction
} from './types';
const REPORT_TYPE_LABELS: Record<ReportType, string> = {
sales: 'Ventas',
production: 'Producción',
inventory: 'Inventario',
financial: 'Financiero',
customer: 'Clientes',
performance: 'Rendimiento',
};
const STATUS_COLORS = {
active: 'bg-green-100 text-green-800 border-green-200',
inactive: 'bg-yellow-100 text-yellow-800 border-yellow-200',
archived: 'bg-gray-100 text-gray-800 border-gray-200',
};
const STATUS_LABELS = {
active: 'Activo',
inactive: 'Inactivo',
archived: 'Archivado',
};
export const ReportsTable: React.FC<ReportsTableProps> = ({
reports = [],
loading = false,
error,
selectedReports = [],
onSelectionChange,
onReportClick,
onEditReport,
onDeleteReport,
onScheduleReport,
onShareReport,
onExportReport,
filters = [],
onFiltersChange,
sortable = true,
pagination,
bulkActions = false,
customActions = [],
tableConfig,
}) => {
const [searchTerm, setSearchTerm] = useState('');
const [typeFilter, setTypeFilter] = useState<ReportType | 'all'>('all');
const [statusFilter, setStatusFilter] = useState<'all' | 'active' | 'inactive' | 'archived'>('all');
const [sortField, setSortField] = useState<keyof AnalyticsReport>('updated_at');
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [reportToDelete, setReportToDelete] = useState<string | null>(null);
const [isScheduleModalOpen, setIsScheduleModalOpen] = useState(false);
const [reportToSchedule, setReportToSchedule] = useState<AnalyticsReport | null>(null);
const [isShareModalOpen, setIsShareModalOpen] = useState(false);
const [reportToShare, setReportToShare] = useState<AnalyticsReport | null>(null);
const filteredAndSortedReports = useMemo(() => {
let filtered = reports.filter(report => {
const matchesSearch = !searchTerm ||
report.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
report.description?.toLowerCase().includes(searchTerm.toLowerCase()) ||
report.category.toLowerCase().includes(searchTerm.toLowerCase());
const matchesType = typeFilter === 'all' || report.type === typeFilter;
const matchesStatus = statusFilter === 'all' || report.status === statusFilter;
return matchesSearch && matchesType && matchesStatus;
});
if (sortable && sortField) {
filtered.sort((a, b) => {
const aVal = a[sortField];
const bVal = b[sortField];
if (aVal === null || aVal === undefined) return 1;
if (bVal === null || bVal === undefined) return -1;
let comparison = 0;
if (typeof aVal === 'string' && typeof bVal === 'string') {
comparison = aVal.localeCompare(bVal);
} else if (typeof aVal === 'number' && typeof bVal === 'number') {
comparison = aVal - bVal;
} else {
comparison = String(aVal).localeCompare(String(bVal));
}
return sortDirection === 'asc' ? comparison : -comparison;
});
}
return filtered;
}, [reports, searchTerm, typeFilter, statusFilter, sortField, sortDirection, sortable]);
const handleSort = (field: keyof AnalyticsReport) => {
if (sortField === field) {
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
} else {
setSortField(field);
setSortDirection('asc');
}
};
const handleSelectAll = (checked: boolean) => {
if (onSelectionChange) {
onSelectionChange(checked ? filteredAndSortedReports.map(r => r.id) : []);
}
};
const handleSelectReport = (reportId: string, checked: boolean) => {
if (onSelectionChange) {
if (checked) {
onSelectionChange([...selectedReports, reportId]);
} else {
onSelectionChange(selectedReports.filter(id => id !== reportId));
}
}
};
const handleDeleteConfirm = () => {
if (reportToDelete && onDeleteReport) {
onDeleteReport(reportToDelete);
}
setIsDeleteModalOpen(false);
setReportToDelete(null);
};
const handleBulkDelete = () => {
if (selectedReports.length > 0 && onDeleteReport) {
selectedReports.forEach(id => onDeleteReport(id));
if (onSelectionChange) {
onSelectionChange([]);
}
}
};
const renderActionButton = (report: AnalyticsReport, action: string) => {
switch (action) {
case 'view':
return (
<Button
variant="ghost"
size="sm"
onClick={() => onReportClick?.(report)}
title="Ver reporte"
>
👁️
</Button>
);
case 'edit':
return (
<Button
variant="ghost"
size="sm"
onClick={() => onEditReport?.(report)}
title="Editar reporte"
>
✏️
</Button>
);
case 'schedule':
return (
<Button
variant="ghost"
size="sm"
onClick={() => {
setReportToSchedule(report);
setIsScheduleModalOpen(true);
}}
title="Programar reporte"
>
📅
</Button>
);
case 'share':
return (
<Button
variant="ghost"
size="sm"
onClick={() => {
setReportToShare(report);
setIsShareModalOpen(true);
}}
title="Compartir reporte"
>
📤
</Button>
);
case 'export':
return (
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="sm"
onClick={() => onExportReport?.(report, 'pdf')}
title="Exportar como PDF"
>
📄
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => onExportReport?.(report, 'excel')}
title="Exportar como Excel"
>
📊
</Button>
</div>
);
case 'delete':
return (
<Button
variant="ghost"
size="sm"
onClick={() => {
setReportToDelete(report.id);
setIsDeleteModalOpen(true);
}}
title="Eliminar reporte"
className="text-red-600 hover:text-red-800"
>
🗑️
</Button>
);
default:
return null;
}
};
const tableColumns = [
{
key: 'select',
title: (
<input
type="checkbox"
checked={selectedReports.length === filteredAndSortedReports.length && filteredAndSortedReports.length > 0}
onChange={(e) => handleSelectAll(e.target.checked)}
className="rounded"
/>
),
render: (report: AnalyticsReport) => (
<input
type="checkbox"
checked={selectedReports.includes(report.id)}
onChange={(e) => handleSelectReport(report.id, e.target.checked)}
className="rounded"
/>
),
width: 50,
visible: bulkActions,
},
{
key: 'name',
title: 'Nombre',
sortable: true,
render: (report: AnalyticsReport) => (
<div>
<button
onClick={() => onReportClick?.(report)}
className="font-medium text-blue-600 hover:text-blue-800 text-left"
>
{report.name}
</button>
{report.description && (
<p className="text-sm text-gray-500 mt-1">{report.description}</p>
)}
</div>
),
minWidth: 200,
},
{
key: 'type',
title: 'Tipo',
sortable: true,
render: (report: AnalyticsReport) => (
<Badge className="bg-blue-100 text-blue-800">
{REPORT_TYPE_LABELS[report.type]}
</Badge>
),
width: 120,
},
{
key: 'category',
title: 'Categoría',
sortable: true,
width: 120,
},
{
key: 'status',
title: 'Estado',
sortable: true,
render: (report: AnalyticsReport) => (
<Badge className={STATUS_COLORS[report.status]}>
{STATUS_LABELS[report.status]}
</Badge>
),
width: 100,
},
{
key: 'last_run',
title: 'Última ejecución',
sortable: true,
render: (report: AnalyticsReport) => (
report.last_run
? new Date(report.last_run).toLocaleDateString('es-ES')
: '-'
),
width: 140,
},
{
key: 'next_run',
title: 'Próxima ejecución',
sortable: true,
render: (report: AnalyticsReport) => (
report.next_run
? new Date(report.next_run).toLocaleDateString('es-ES')
: '-'
),
width: 140,
},
{
key: 'updated_at',
title: 'Actualizado',
sortable: true,
render: (report: AnalyticsReport) => (
new Date(report.updated_at).toLocaleDateString('es-ES')
),
width: 120,
},
{
key: 'actions',
title: 'Acciones',
render: (report: AnalyticsReport) => (
<div className="flex items-center gap-1">
{renderActionButton(report, 'view')}
{renderActionButton(report, 'edit')}
{renderActionButton(report, 'schedule')}
{renderActionButton(report, 'export')}
{renderActionButton(report, 'delete')}
</div>
),
width: 200,
},
].filter(col => col.visible !== false);
if (error) {
return (
<Card className="p-6">
<div className="text-center">
<p className="text-red-600 font-medium">Error al cargar los reportes</p>
<p className="text-sm text-gray-500 mt-1">{error}</p>
</div>
</Card>
);
}
return (
<>
<Card className="overflow-hidden">
{/* Filters and Actions */}
<div className="p-4 border-b border-gray-200 bg-gray-50">
<div className="flex flex-col lg:flex-row gap-4">
{/* Search */}
{(tableConfig?.showSearch !== false) && (
<div className="flex-1">
<Input
type="text"
placeholder="Buscar reportes..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full"
/>
</div>
)}
{/* Filters */}
<div className="flex gap-2">
<Select
value={typeFilter}
onChange={(e) => setTypeFilter(e.target.value as ReportType | 'all')}
className="w-40"
>
<option value="all">Todos los tipos</option>
{Object.entries(REPORT_TYPE_LABELS).map(([value, label]) => (
<option key={value} value={value}>{label}</option>
))}
</Select>
<Select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value as any)}
className="w-32"
>
<option value="all">Todos</option>
<option value="active">Activos</option>
<option value="inactive">Inactivos</option>
<option value="archived">Archivados</option>
</Select>
</div>
{/* Bulk Actions */}
{bulkActions && selectedReports.length > 0 && (
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={handleBulkDelete}
className="text-red-600 border-red-300 hover:bg-red-50"
>
Eliminar seleccionados ({selectedReports.length})
</Button>
</div>
)}
</div>
</div>
{/* Table */}
<div className="overflow-x-auto">
<Table
columns={tableColumns}
data={filteredAndSortedReports}
loading={loading}
sortable={sortable}
onSort={handleSort}
currentSort={{ field: sortField as string, direction: sortDirection }}
emptyMessage="No se encontraron reportes"
className="min-w-full"
/>
</div>
{/* Pagination */}
{pagination && (
<div className="p-4 border-t border-gray-200 bg-gray-50">
<div className="flex items-center justify-between">
<p className="text-sm text-gray-700">
Mostrando {Math.min(pagination.pageSize * (pagination.current - 1) + 1, pagination.total)} - {Math.min(pagination.pageSize * pagination.current, pagination.total)} de {pagination.total} reportes
</p>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => pagination.onChange(pagination.current - 1, pagination.pageSize)}
disabled={pagination.current <= 1}
>
Anterior
</Button>
<span className="text-sm font-medium">
Página {pagination.current} de {Math.ceil(pagination.total / pagination.pageSize)}
</span>
<Button
variant="outline"
size="sm"
onClick={() => pagination.onChange(pagination.current + 1, pagination.pageSize)}
disabled={pagination.current >= Math.ceil(pagination.total / pagination.pageSize)}
>
Siguiente
</Button>
</div>
</div>
</div>
)}
</Card>
{/* Delete Confirmation Modal */}
<Modal
isOpen={isDeleteModalOpen}
onClose={() => {
setIsDeleteModalOpen(false);
setReportToDelete(null);
}}
title="Confirmar eliminación"
>
<div className="space-y-4">
<p>¿Estás seguro de que deseas eliminar este reporte? Esta acción no se puede deshacer.</p>
<div className="flex justify-end gap-2">
<Button
variant="outline"
onClick={() => {
setIsDeleteModalOpen(false);
setReportToDelete(null);
}}
>
Cancelar
</Button>
<Button
variant="danger"
onClick={handleDeleteConfirm}
>
Eliminar
</Button>
</div>
</div>
</Modal>
{/* Schedule Modal */}
<Modal
isOpen={isScheduleModalOpen}
onClose={() => {
setIsScheduleModalOpen(false);
setReportToSchedule(null);
}}
title="Programar reporte"
>
<div className="space-y-4">
<p>Configurar programación para: <strong>{reportToSchedule?.name}</strong></p>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Frecuencia
</label>
<Select>
<option value="daily">Diario</option>
<option value="weekly">Semanal</option>
<option value="monthly">Mensual</option>
<option value="quarterly">Trimestral</option>
</Select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Hora de ejecución
</label>
<Input type="time" defaultValue="09:00" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Destinatarios (emails separados por coma)
</label>
<Input
type="text"
placeholder="ejemplo@empresa.com, otro@empresa.com"
/>
</div>
<div className="flex justify-end gap-2">
<Button
variant="outline"
onClick={() => {
setIsScheduleModalOpen(false);
setReportToSchedule(null);
}}
>
Cancelar
</Button>
<Button
variant="primary"
onClick={() => {
if (reportToSchedule && onScheduleReport) {
onScheduleReport(reportToSchedule);
}
setIsScheduleModalOpen(false);
setReportToSchedule(null);
}}
>
Programar
</Button>
</div>
</div>
</Modal>
{/* Share Modal */}
<Modal
isOpen={isShareModalOpen}
onClose={() => {
setIsShareModalOpen(false);
setReportToShare(null);
}}
title="Compartir reporte"
>
<div className="space-y-4">
<p>Compartir: <strong>{reportToShare?.name}</strong></p>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Generar enlace de acceso
</label>
<div className="flex gap-2">
<Input
type="text"
value={`${window.location.origin}/reports/${reportToShare?.id}/shared`}
readOnly
className="flex-1"
/>
<Button
variant="outline"
size="sm"
onClick={() => {
navigator.clipboard.writeText(`${window.location.origin}/reports/${reportToShare?.id}/shared`);
}}
>
Copiar
</Button>
</div>
</div>
<div className="flex items-center gap-2">
<input type="checkbox" id="expire-link" />
<label htmlFor="expire-link" className="text-sm">
El enlace expira en 7 días
</label>
</div>
<div className="flex items-center gap-2">
<input type="checkbox" id="password-protect" />
<label htmlFor="password-protect" className="text-sm">
Proteger con contraseña
</label>
</div>
<div className="flex justify-end gap-2">
<Button
variant="outline"
onClick={() => {
setIsShareModalOpen(false);
setReportToShare(null);
}}
>
Cancelar
</Button>
<Button
variant="primary"
onClick={() => {
if (reportToShare && onShareReport) {
onShareReport(reportToShare);
}
setIsShareModalOpen(false);
setReportToShare(null);
}}
>
Generar enlace
</Button>
</div>
</div>
</Modal>
</>
);
};
export default ReportsTable;