651 lines
19 KiB
Plaintext
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;
|