EPIC-P2-001: Frontend Actions Implementation - Replace 31 console.log placeholders with navigate() calls - 16 pages updated with proper routing EPIC-P2-002: Settings Subpages Creation - Add CompanySettingsPage.tsx - Add ProfileSettingsPage.tsx - Add SecuritySettingsPage.tsx - Add SystemSettingsPage.tsx - Update router with new routes EPIC-P2-003: Bug Fix ValuationReportsPage - Fix recursive getToday() function EPIC-P2-006: CRM Pipeline Kanban - Add PipelineKanbanPage.tsx - Add KanbanColumn.tsx component - Add KanbanCard.tsx component - Add CRM routes to router Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
865 lines
28 KiB
TypeScript
865 lines
28 KiB
TypeScript
import { useState } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import {
|
|
FileText,
|
|
Plus,
|
|
MoreVertical,
|
|
Eye,
|
|
Play,
|
|
Clock,
|
|
RefreshCw,
|
|
Search,
|
|
Download,
|
|
Trash2,
|
|
Edit2,
|
|
XCircle,
|
|
CheckCircle,
|
|
AlertCircle,
|
|
BarChart3,
|
|
PlayCircle,
|
|
PauseCircle,
|
|
} from 'lucide-react';
|
|
import { Button } from '@components/atoms/Button';
|
|
import { Card, CardHeader, CardTitle, CardContent } from '@components/molecules/Card';
|
|
import { DataTable, type Column } from '@components/organisms/DataTable';
|
|
import { Dropdown, type DropdownItem } from '@components/organisms/Dropdown';
|
|
import { Breadcrumbs } from '@components/organisms/Breadcrumbs';
|
|
import { ConfirmModal } from '@components/organisms/Modal';
|
|
import { NoDataEmptyState, ErrorEmptyState } from '@components/templates/EmptyState';
|
|
import {
|
|
useReportDefinitions,
|
|
useReportExecutions,
|
|
useReportSchedules,
|
|
} from '@features/reports/hooks';
|
|
import type {
|
|
ReportDefinition,
|
|
ReportExecution,
|
|
ReportSchedule,
|
|
ExecutionStatus,
|
|
ReportType,
|
|
} from '@features/reports/types';
|
|
import {
|
|
REPORT_TYPE_LABELS,
|
|
EXECUTION_STATUS_LABELS,
|
|
} from '@features/reports/types';
|
|
import { formatDate } from '@utils/formatters';
|
|
|
|
type TabType = 'definitions' | 'executions' | 'schedules';
|
|
|
|
const statusColors: Record<ExecutionStatus, string> = {
|
|
pending: 'bg-yellow-100 text-yellow-700',
|
|
running: 'bg-blue-100 text-blue-700',
|
|
completed: 'bg-green-100 text-green-700',
|
|
failed: 'bg-red-100 text-red-700',
|
|
cancelled: 'bg-gray-100 text-gray-700',
|
|
};
|
|
|
|
const typeColors: Record<ReportType, string> = {
|
|
financial: 'bg-green-100 text-green-700',
|
|
accounting: 'bg-blue-100 text-blue-700',
|
|
tax: 'bg-purple-100 text-purple-700',
|
|
management: 'bg-orange-100 text-orange-700',
|
|
custom: 'bg-gray-100 text-gray-700',
|
|
};
|
|
|
|
export function ReportsPage() {
|
|
const navigate = useNavigate();
|
|
const [activeTab, setActiveTab] = useState<TabType>('definitions');
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [selectedType, setSelectedType] = useState<ReportType | ''>('');
|
|
const [selectedStatus, setSelectedStatus] = useState<ExecutionStatus | ''>('');
|
|
const [definitionToDelete, setDefinitionToDelete] = useState<ReportDefinition | null>(null);
|
|
const [executionToCancel, setExecutionToCancel] = useState<ReportExecution | null>(null);
|
|
const [scheduleToDelete, setScheduleToDelete] = useState<ReportSchedule | null>(null);
|
|
const [scheduleToToggle, setScheduleToToggle] = useState<ReportSchedule | null>(null);
|
|
|
|
// Definitions hook
|
|
const {
|
|
definitions,
|
|
total: definitionsTotal,
|
|
page: definitionsPage,
|
|
totalPages: definitionsTotalPages,
|
|
isLoading: definitionsLoading,
|
|
error: definitionsError,
|
|
setFilters: setDefinitionsFilters,
|
|
refresh: refreshDefinitions,
|
|
remove: removeDefinition,
|
|
toggleActive: toggleDefinitionActive,
|
|
} = useReportDefinitions({
|
|
initialFilters: { search: searchTerm, reportType: selectedType || undefined },
|
|
});
|
|
|
|
// Executions hook
|
|
const {
|
|
executions,
|
|
total: executionsTotal,
|
|
page: executionsPage,
|
|
totalPages: executionsTotalPages,
|
|
isLoading: executionsLoading,
|
|
error: executionsError,
|
|
refresh: refreshExecutions,
|
|
cancel: cancelExecution,
|
|
download: downloadExecution,
|
|
} = useReportExecutions({
|
|
initialFilters: { status: selectedStatus || undefined },
|
|
pollInterval: 5000,
|
|
});
|
|
|
|
// Schedules hook
|
|
const {
|
|
schedules,
|
|
total: schedulesTotal,
|
|
page: schedulesPage,
|
|
totalPages: schedulesTotalPages,
|
|
isLoading: schedulesLoading,
|
|
error: schedulesError,
|
|
refresh: refreshSchedules,
|
|
remove: removeSchedule,
|
|
toggle: toggleSchedule,
|
|
runNow: runScheduleNow,
|
|
} = useReportSchedules();
|
|
|
|
// Definition columns
|
|
const definitionColumns: Column<ReportDefinition>[] = [
|
|
{
|
|
key: 'name',
|
|
header: 'Reporte',
|
|
render: (def) => (
|
|
<div className="flex items-center gap-3">
|
|
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-blue-50">
|
|
<FileText className="h-5 w-5 text-blue-600" />
|
|
</div>
|
|
<div>
|
|
<div className="font-medium text-gray-900">{def.name}</div>
|
|
<div className="text-sm text-gray-500">{def.code}</div>
|
|
</div>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
key: 'reportType',
|
|
header: 'Tipo',
|
|
render: (def) => (
|
|
<span className={`inline-flex rounded-full px-2 py-1 text-xs font-medium ${typeColors[def.reportType]}`}>
|
|
{REPORT_TYPE_LABELS[def.reportType]}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
key: 'category',
|
|
header: 'Categoria',
|
|
render: (def) => (
|
|
<span className="text-sm text-gray-600">{def.category || '-'}</span>
|
|
),
|
|
},
|
|
{
|
|
key: 'isActive',
|
|
header: 'Estado',
|
|
render: (def) => (
|
|
<span className={`inline-flex items-center gap-1 rounded-full px-2 py-1 text-xs font-medium ${
|
|
def.isActive ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-600'
|
|
}`}>
|
|
{def.isActive ? (
|
|
<>
|
|
<CheckCircle className="h-3 w-3" />
|
|
Activo
|
|
</>
|
|
) : (
|
|
<>
|
|
<XCircle className="h-3 w-3" />
|
|
Inactivo
|
|
</>
|
|
)}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
key: 'isSystem',
|
|
header: 'Sistema',
|
|
render: (def) => (
|
|
<span className={`text-sm ${def.isSystem ? 'text-blue-600' : 'text-gray-500'}`}>
|
|
{def.isSystem ? 'Si' : 'No'}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
key: 'actions',
|
|
header: '',
|
|
render: (def) => {
|
|
const items: DropdownItem[] = [
|
|
{
|
|
key: 'view',
|
|
label: 'Ver detalle',
|
|
icon: <Eye className="h-4 w-4" />,
|
|
onClick: () => navigate(`/reports/definitions/${def.id}`),
|
|
},
|
|
{
|
|
key: 'execute',
|
|
label: 'Ejecutar',
|
|
icon: <Play className="h-4 w-4" />,
|
|
// TODO: Keep for now - opens report execution modal/flow
|
|
onClick: () => navigate(`/reports/definitions/${def.id}/execute`),
|
|
},
|
|
];
|
|
|
|
if (!def.isSystem) {
|
|
items.push(
|
|
{
|
|
key: 'edit',
|
|
label: 'Editar',
|
|
icon: <Edit2 className="h-4 w-4" />,
|
|
onClick: () => navigate(`/reports/definitions/${def.id}/edit`),
|
|
},
|
|
{
|
|
key: 'toggle',
|
|
label: def.isActive ? 'Desactivar' : 'Activar',
|
|
icon: def.isActive ? <PauseCircle className="h-4 w-4" /> : <PlayCircle className="h-4 w-4" />,
|
|
onClick: () => toggleDefinitionActive(def.id),
|
|
},
|
|
{
|
|
key: 'delete',
|
|
label: 'Eliminar',
|
|
icon: <Trash2 className="h-4 w-4" />,
|
|
danger: true,
|
|
onClick: () => setDefinitionToDelete(def),
|
|
}
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Dropdown
|
|
trigger={
|
|
<button className="rounded p-1 hover:bg-gray-100">
|
|
<MoreVertical className="h-4 w-4 text-gray-500" />
|
|
</button>
|
|
}
|
|
items={items}
|
|
align="right"
|
|
/>
|
|
);
|
|
},
|
|
},
|
|
];
|
|
|
|
// Execution columns
|
|
const executionColumns: Column<ReportExecution>[] = [
|
|
{
|
|
key: 'report',
|
|
header: 'Reporte',
|
|
render: (exec) => (
|
|
<div className="flex items-center gap-3">
|
|
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-purple-50">
|
|
<BarChart3 className="h-5 w-5 text-purple-600" />
|
|
</div>
|
|
<div>
|
|
<div className="font-medium text-gray-900">{exec.definitionName || 'Reporte'}</div>
|
|
<div className="text-sm text-gray-500">{exec.definitionCode || exec.definitionId}</div>
|
|
</div>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
key: 'status',
|
|
header: 'Estado',
|
|
render: (exec) => (
|
|
<span className={`inline-flex items-center gap-1 rounded-full px-2 py-1 text-xs font-medium ${statusColors[exec.status]}`}>
|
|
{exec.status === 'running' && <RefreshCw className="h-3 w-3 animate-spin" />}
|
|
{exec.status === 'completed' && <CheckCircle className="h-3 w-3" />}
|
|
{exec.status === 'failed' && <AlertCircle className="h-3 w-3" />}
|
|
{EXECUTION_STATUS_LABELS[exec.status]}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
key: 'startedAt',
|
|
header: 'Iniciado',
|
|
render: (exec) => (
|
|
<span className="text-sm text-gray-600">
|
|
{exec.startedAt ? formatDate(exec.startedAt, 'short') : '-'}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
key: 'completedAt',
|
|
header: 'Completado',
|
|
render: (exec) => (
|
|
<span className="text-sm text-gray-600">
|
|
{exec.completedAt ? formatDate(exec.completedAt, 'short') : '-'}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
key: 'duration',
|
|
header: 'Duracion',
|
|
render: (exec) => (
|
|
<span className="text-sm text-gray-600">
|
|
{exec.executionTimeMs ? `${(exec.executionTimeMs / 1000).toFixed(2)}s` : '-'}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
key: 'rows',
|
|
header: 'Filas',
|
|
render: (exec) => (
|
|
<span className="text-sm text-gray-600">
|
|
{exec.rowCount?.toLocaleString() || '-'}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
key: 'requestedBy',
|
|
header: 'Solicitado por',
|
|
render: (exec) => (
|
|
<span className="text-sm text-gray-600">
|
|
{exec.requestedByName || exec.requestedBy}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
key: 'actions',
|
|
header: '',
|
|
render: (exec) => {
|
|
const items: DropdownItem[] = [
|
|
{
|
|
key: 'view',
|
|
label: 'Ver detalle',
|
|
icon: <Eye className="h-4 w-4" />,
|
|
onClick: () => navigate(`/reports/executions/${exec.id}`),
|
|
},
|
|
];
|
|
|
|
if (exec.status === 'completed' && exec.outputFiles.length > 0) {
|
|
exec.outputFiles.forEach((file) => {
|
|
items.push({
|
|
key: `download-${file.format}`,
|
|
label: `Descargar ${file.format.toUpperCase()}`,
|
|
icon: <Download className="h-4 w-4" />,
|
|
onClick: () => downloadExecution(exec.id, file.format),
|
|
});
|
|
});
|
|
}
|
|
|
|
if (exec.status === 'pending' || exec.status === 'running') {
|
|
items.push({
|
|
key: 'cancel',
|
|
label: 'Cancelar',
|
|
icon: <XCircle className="h-4 w-4" />,
|
|
danger: true,
|
|
onClick: () => setExecutionToCancel(exec),
|
|
});
|
|
}
|
|
|
|
return (
|
|
<Dropdown
|
|
trigger={
|
|
<button className="rounded p-1 hover:bg-gray-100">
|
|
<MoreVertical className="h-4 w-4 text-gray-500" />
|
|
</button>
|
|
}
|
|
items={items}
|
|
align="right"
|
|
/>
|
|
);
|
|
},
|
|
},
|
|
];
|
|
|
|
// Schedule columns
|
|
const scheduleColumns: Column<ReportSchedule>[] = [
|
|
{
|
|
key: 'name',
|
|
header: 'Programacion',
|
|
render: (sched) => (
|
|
<div className="flex items-center gap-3">
|
|
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-orange-50">
|
|
<Clock className="h-5 w-5 text-orange-600" />
|
|
</div>
|
|
<div>
|
|
<div className="font-medium text-gray-900">{sched.name}</div>
|
|
<div className="text-sm text-gray-500">{sched.definitionName}</div>
|
|
</div>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
key: 'cronExpression',
|
|
header: 'Frecuencia',
|
|
render: (sched) => (
|
|
<code className="text-sm text-gray-600 bg-gray-100 px-2 py-1 rounded">
|
|
{sched.cronExpression}
|
|
</code>
|
|
),
|
|
},
|
|
{
|
|
key: 'isActive',
|
|
header: 'Estado',
|
|
render: (sched) => (
|
|
<span className={`inline-flex items-center gap-1 rounded-full px-2 py-1 text-xs font-medium ${
|
|
sched.isActive ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-600'
|
|
}`}>
|
|
{sched.isActive ? 'Activo' : 'Inactivo'}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
key: 'lastRunAt',
|
|
header: 'Ultima ejecucion',
|
|
render: (sched) => (
|
|
<span className="text-sm text-gray-600">
|
|
{sched.lastRunAt ? formatDate(sched.lastRunAt, 'short') : 'Nunca'}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
key: 'nextRunAt',
|
|
header: 'Proxima ejecucion',
|
|
render: (sched) => (
|
|
<span className="text-sm text-gray-600">
|
|
{sched.nextRunAt ? formatDate(sched.nextRunAt, 'short') : '-'}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
key: 'actions',
|
|
header: '',
|
|
render: (sched) => {
|
|
const items: DropdownItem[] = [
|
|
{
|
|
key: 'view',
|
|
label: 'Ver detalle',
|
|
icon: <Eye className="h-4 w-4" />,
|
|
onClick: () => navigate(`/reports/schedules/${sched.id}`),
|
|
},
|
|
{
|
|
key: 'edit',
|
|
label: 'Editar',
|
|
icon: <Edit2 className="h-4 w-4" />,
|
|
onClick: () => navigate(`/reports/schedules/${sched.id}/edit`),
|
|
},
|
|
{
|
|
key: 'runNow',
|
|
label: 'Ejecutar ahora',
|
|
icon: <Play className="h-4 w-4" />,
|
|
onClick: () => runScheduleNow(sched.id),
|
|
},
|
|
{
|
|
key: 'toggle',
|
|
label: sched.isActive ? 'Pausar' : 'Activar',
|
|
icon: sched.isActive ? <PauseCircle className="h-4 w-4" /> : <PlayCircle className="h-4 w-4" />,
|
|
onClick: () => setScheduleToToggle(sched),
|
|
},
|
|
{
|
|
key: 'delete',
|
|
label: 'Eliminar',
|
|
icon: <Trash2 className="h-4 w-4" />,
|
|
danger: true,
|
|
onClick: () => setScheduleToDelete(sched),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<Dropdown
|
|
trigger={
|
|
<button className="rounded p-1 hover:bg-gray-100">
|
|
<MoreVertical className="h-4 w-4 text-gray-500" />
|
|
</button>
|
|
}
|
|
items={items}
|
|
align="right"
|
|
/>
|
|
);
|
|
},
|
|
},
|
|
];
|
|
|
|
const handleDeleteDefinition = async () => {
|
|
if (definitionToDelete) {
|
|
await removeDefinition(definitionToDelete.id);
|
|
setDefinitionToDelete(null);
|
|
}
|
|
};
|
|
|
|
const handleCancelExecution = async () => {
|
|
if (executionToCancel) {
|
|
await cancelExecution(executionToCancel.id);
|
|
setExecutionToCancel(null);
|
|
}
|
|
};
|
|
|
|
const handleDeleteSchedule = async () => {
|
|
if (scheduleToDelete) {
|
|
await removeSchedule(scheduleToDelete.id);
|
|
setScheduleToDelete(null);
|
|
}
|
|
};
|
|
|
|
const handleToggleSchedule = async () => {
|
|
if (scheduleToToggle) {
|
|
await toggleSchedule(scheduleToToggle.id);
|
|
setScheduleToToggle(null);
|
|
}
|
|
};
|
|
|
|
const getCurrentRefresh = () => {
|
|
switch (activeTab) {
|
|
case 'definitions': return refreshDefinitions;
|
|
case 'executions': return refreshExecutions;
|
|
case 'schedules': return refreshSchedules;
|
|
}
|
|
};
|
|
|
|
const isCurrentLoading = () => {
|
|
switch (activeTab) {
|
|
case 'definitions': return definitionsLoading;
|
|
case 'executions': return executionsLoading;
|
|
case 'schedules': return schedulesLoading;
|
|
}
|
|
};
|
|
|
|
const getCurrentError = () => {
|
|
switch (activeTab) {
|
|
case 'definitions': return definitionsError;
|
|
case 'executions': return executionsError;
|
|
case 'schedules': return schedulesError;
|
|
}
|
|
};
|
|
|
|
// Stats
|
|
const activeDefinitions = definitions.filter(d => d.isActive).length;
|
|
const runningExecutions = executions.filter(e => e.status === 'running').length;
|
|
const completedToday = executions.filter(e => {
|
|
if (!e.completedAt) return false;
|
|
const today = new Date().toDateString();
|
|
return new Date(e.completedAt).toDateString() === today && e.status === 'completed';
|
|
}).length;
|
|
const activeSchedules = schedules.filter(s => s.isActive).length;
|
|
|
|
const error = getCurrentError();
|
|
if (error) {
|
|
return (
|
|
<div className="p-6">
|
|
<ErrorEmptyState onRetry={getCurrentRefresh()} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6 p-6">
|
|
<Breadcrumbs items={[
|
|
{ label: 'Reportes' },
|
|
]} />
|
|
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-gray-900">Reportes</h1>
|
|
<p className="text-sm text-gray-500">
|
|
Gestiona definiciones, ejecuciones y programaciones de reportes
|
|
</p>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<Button variant="outline" onClick={getCurrentRefresh()} disabled={isCurrentLoading()}>
|
|
<RefreshCw className={`mr-2 h-4 w-4 ${isCurrentLoading() ? 'animate-spin' : ''}`} />
|
|
Actualizar
|
|
</Button>
|
|
{activeTab === 'definitions' && (
|
|
<Button>
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
Nuevo reporte
|
|
</Button>
|
|
)}
|
|
{activeTab === 'schedules' && (
|
|
<Button>
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
Nueva programacion
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Summary Stats */}
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-4">
|
|
<Card className="cursor-pointer hover:shadow-md transition-shadow" onClick={() => setActiveTab('definitions')}>
|
|
<CardContent className="p-4">
|
|
<div className="flex items-center gap-3">
|
|
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-blue-100">
|
|
<FileText className="h-5 w-5 text-blue-600" />
|
|
</div>
|
|
<div>
|
|
<div className="text-sm text-gray-500">Definiciones activas</div>
|
|
<div className="text-xl font-bold text-blue-600">{activeDefinitions}</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="cursor-pointer hover:shadow-md transition-shadow" onClick={() => setActiveTab('executions')}>
|
|
<CardContent className="p-4">
|
|
<div className="flex items-center gap-3">
|
|
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-yellow-100">
|
|
<RefreshCw className={`h-5 w-5 text-yellow-600 ${runningExecutions > 0 ? 'animate-spin' : ''}`} />
|
|
</div>
|
|
<div>
|
|
<div className="text-sm text-gray-500">En ejecucion</div>
|
|
<div className="text-xl font-bold text-yellow-600">{runningExecutions}</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardContent className="p-4">
|
|
<div className="flex items-center gap-3">
|
|
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-green-100">
|
|
<CheckCircle className="h-5 w-5 text-green-600" />
|
|
</div>
|
|
<div>
|
|
<div className="text-sm text-gray-500">Completados hoy</div>
|
|
<div className="text-xl font-bold text-green-600">{completedToday}</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="cursor-pointer hover:shadow-md transition-shadow" onClick={() => setActiveTab('schedules')}>
|
|
<CardContent className="p-4">
|
|
<div className="flex items-center gap-3">
|
|
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-orange-100">
|
|
<Clock className="h-5 w-5 text-orange-600" />
|
|
</div>
|
|
<div>
|
|
<div className="text-sm text-gray-500">Programaciones</div>
|
|
<div className="text-xl font-bold text-orange-600">{activeSchedules}</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Tabs */}
|
|
<div className="border-b border-gray-200">
|
|
<nav className="-mb-px flex space-x-8">
|
|
<button
|
|
onClick={() => setActiveTab('definitions')}
|
|
className={`py-4 px-1 border-b-2 font-medium text-sm ${
|
|
activeTab === 'definitions'
|
|
? 'border-blue-500 text-blue-600'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
|
}`}
|
|
>
|
|
<FileText className="inline-block h-4 w-4 mr-2" />
|
|
Definiciones ({definitionsTotal})
|
|
</button>
|
|
<button
|
|
onClick={() => setActiveTab('executions')}
|
|
className={`py-4 px-1 border-b-2 font-medium text-sm ${
|
|
activeTab === 'executions'
|
|
? 'border-blue-500 text-blue-600'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
|
}`}
|
|
>
|
|
<BarChart3 className="inline-block h-4 w-4 mr-2" />
|
|
Ejecuciones ({executionsTotal})
|
|
</button>
|
|
<button
|
|
onClick={() => setActiveTab('schedules')}
|
|
className={`py-4 px-1 border-b-2 font-medium text-sm ${
|
|
activeTab === 'schedules'
|
|
? 'border-blue-500 text-blue-600'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
|
}`}
|
|
>
|
|
<Clock className="inline-block h-4 w-4 mr-2" />
|
|
Programaciones ({schedulesTotal})
|
|
</button>
|
|
</nav>
|
|
</div>
|
|
|
|
{/* Content based on active tab */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>
|
|
{activeTab === 'definitions' && 'Definiciones de Reportes'}
|
|
{activeTab === 'executions' && 'Historial de Ejecuciones'}
|
|
{activeTab === 'schedules' && 'Programaciones'}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-4">
|
|
{/* Filters */}
|
|
<div className="flex flex-wrap items-center gap-4">
|
|
<div className="relative flex-1 min-w-[200px]">
|
|
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" />
|
|
<input
|
|
type="text"
|
|
placeholder="Buscar..."
|
|
value={searchTerm}
|
|
onChange={(e) => {
|
|
setSearchTerm(e.target.value);
|
|
if (activeTab === 'definitions') {
|
|
setDefinitionsFilters({ search: e.target.value });
|
|
}
|
|
}}
|
|
className="w-full rounded-md border border-gray-300 py-2 pl-10 pr-4 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
|
/>
|
|
</div>
|
|
|
|
{activeTab === 'definitions' && (
|
|
<select
|
|
value={selectedType}
|
|
onChange={(e) => {
|
|
setSelectedType(e.target.value as ReportType | '');
|
|
setDefinitionsFilters({ reportType: e.target.value as ReportType || undefined });
|
|
}}
|
|
className="rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
|
>
|
|
<option value="">Todos los tipos</option>
|
|
{Object.entries(REPORT_TYPE_LABELS).map(([value, label]) => (
|
|
<option key={value} value={value}>{label}</option>
|
|
))}
|
|
</select>
|
|
)}
|
|
|
|
{activeTab === 'executions' && (
|
|
<select
|
|
value={selectedStatus}
|
|
onChange={(e) => setSelectedStatus(e.target.value as ExecutionStatus | '')}
|
|
className="rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
|
>
|
|
<option value="">Todos los estados</option>
|
|
{Object.entries(EXECUTION_STATUS_LABELS).map(([value, label]) => (
|
|
<option key={value} value={value}>{label}</option>
|
|
))}
|
|
</select>
|
|
)}
|
|
|
|
{(searchTerm || selectedType || selectedStatus) && (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => {
|
|
setSearchTerm('');
|
|
setSelectedType('');
|
|
setSelectedStatus('');
|
|
if (activeTab === 'definitions') {
|
|
setDefinitionsFilters({});
|
|
}
|
|
}}
|
|
>
|
|
Limpiar filtros
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Tables */}
|
|
{activeTab === 'definitions' && (
|
|
definitions.length === 0 && !definitionsLoading ? (
|
|
<NoDataEmptyState entityName="definiciones de reportes" />
|
|
) : (
|
|
<DataTable
|
|
data={definitions}
|
|
columns={definitionColumns}
|
|
isLoading={definitionsLoading}
|
|
pagination={{
|
|
page: definitionsPage,
|
|
totalPages: definitionsTotalPages,
|
|
total: definitionsTotal,
|
|
limit: 20,
|
|
onPageChange: (p) => setDefinitionsFilters({ page: p }),
|
|
}}
|
|
/>
|
|
)
|
|
)}
|
|
|
|
{activeTab === 'executions' && (
|
|
executions.length === 0 && !executionsLoading ? (
|
|
<NoDataEmptyState entityName="ejecuciones" />
|
|
) : (
|
|
<DataTable
|
|
data={executions}
|
|
columns={executionColumns}
|
|
isLoading={executionsLoading}
|
|
pagination={{
|
|
page: executionsPage,
|
|
totalPages: executionsTotalPages,
|
|
total: executionsTotal,
|
|
limit: 20,
|
|
onPageChange: () => {},
|
|
}}
|
|
/>
|
|
)
|
|
)}
|
|
|
|
{activeTab === 'schedules' && (
|
|
schedules.length === 0 && !schedulesLoading ? (
|
|
<NoDataEmptyState entityName="programaciones" />
|
|
) : (
|
|
<DataTable
|
|
data={schedules}
|
|
columns={scheduleColumns}
|
|
isLoading={schedulesLoading}
|
|
pagination={{
|
|
page: schedulesPage,
|
|
totalPages: schedulesTotalPages,
|
|
total: schedulesTotal,
|
|
limit: 20,
|
|
onPageChange: () => {},
|
|
}}
|
|
/>
|
|
)
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Delete Definition Modal */}
|
|
<ConfirmModal
|
|
isOpen={!!definitionToDelete}
|
|
onClose={() => setDefinitionToDelete(null)}
|
|
onConfirm={handleDeleteDefinition}
|
|
title="Eliminar definicion"
|
|
message={`¿Eliminar la definicion "${definitionToDelete?.name}"? Esta accion no se puede deshacer.`}
|
|
variant="danger"
|
|
confirmText="Eliminar"
|
|
/>
|
|
|
|
{/* Cancel Execution Modal */}
|
|
<ConfirmModal
|
|
isOpen={!!executionToCancel}
|
|
onClose={() => setExecutionToCancel(null)}
|
|
onConfirm={handleCancelExecution}
|
|
title="Cancelar ejecucion"
|
|
message="¿Cancelar la ejecucion de este reporte? El reporte no se generara."
|
|
variant="danger"
|
|
confirmText="Cancelar ejecucion"
|
|
/>
|
|
|
|
{/* Delete Schedule Modal */}
|
|
<ConfirmModal
|
|
isOpen={!!scheduleToDelete}
|
|
onClose={() => setScheduleToDelete(null)}
|
|
onConfirm={handleDeleteSchedule}
|
|
title="Eliminar programacion"
|
|
message={`¿Eliminar la programacion "${scheduleToDelete?.name}"? Esta accion no se puede deshacer.`}
|
|
variant="danger"
|
|
confirmText="Eliminar"
|
|
/>
|
|
|
|
{/* Toggle Schedule Modal */}
|
|
<ConfirmModal
|
|
isOpen={!!scheduleToToggle}
|
|
onClose={() => setScheduleToToggle(null)}
|
|
onConfirm={handleToggleSchedule}
|
|
title={scheduleToToggle?.isActive ? 'Pausar programacion' : 'Activar programacion'}
|
|
message={scheduleToToggle?.isActive
|
|
? `¿Pausar la programacion "${scheduleToToggle?.name}"? No se ejecutara automaticamente hasta que se reactive.`
|
|
: `¿Activar la programacion "${scheduleToToggle?.name}"? Se ejecutara segun la frecuencia configurada.`
|
|
}
|
|
variant={scheduleToToggle?.isActive ? 'warning' : 'success'}
|
|
confirmText={scheduleToToggle?.isActive ? 'Pausar' : 'Activar'}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default ReportsPage;
|