592 lines
19 KiB
Plaintext
592 lines
19 KiB
Plaintext
import React, { useState } from 'react';
|
|
import { Card, Button, Badge, Input, Select } from '../../ui';
|
|
import type {
|
|
ExportOptionsProps,
|
|
ExportOptions,
|
|
ExportFormat,
|
|
ExportTemplate,
|
|
ReportSchedule
|
|
} from './types';
|
|
|
|
const FORMAT_LABELS: Record<ExportFormat, string> = {
|
|
pdf: 'PDF',
|
|
excel: 'Excel',
|
|
csv: 'CSV',
|
|
png: 'PNG',
|
|
svg: 'SVG',
|
|
json: 'JSON',
|
|
};
|
|
|
|
const FORMAT_DESCRIPTIONS: Record<ExportFormat, string> = {
|
|
pdf: 'Documento PDF con formato profesional',
|
|
excel: 'Hoja de cálculo de Microsoft Excel',
|
|
csv: 'Archivo de valores separados por comas',
|
|
png: 'Imagen PNG de alta calidad',
|
|
svg: 'Imagen vectorial escalable',
|
|
json: 'Datos estructurados en formato JSON',
|
|
};
|
|
|
|
const FORMAT_ICONS: Record<ExportFormat, string> = {
|
|
pdf: '📄',
|
|
excel: '📊',
|
|
csv: '📋',
|
|
png: '🖼️',
|
|
svg: '🎨',
|
|
json: '⚙️',
|
|
};
|
|
|
|
const FREQUENCY_OPTIONS = [
|
|
{ value: 'daily', label: 'Diario' },
|
|
{ value: 'weekly', label: 'Semanal' },
|
|
{ value: 'monthly', label: 'Mensual' },
|
|
{ value: 'quarterly', label: 'Trimestral' },
|
|
{ value: 'yearly', label: 'Anual' },
|
|
];
|
|
|
|
const TIME_OPTIONS = [
|
|
{ value: '06:00', label: '6:00 AM' },
|
|
{ value: '09:00', label: '9:00 AM' },
|
|
{ value: '12:00', label: '12:00 PM' },
|
|
{ value: '15:00', label: '3:00 PM' },
|
|
{ value: '18:00', label: '6:00 PM' },
|
|
{ value: '21:00', label: '9:00 PM' },
|
|
];
|
|
|
|
const DAYS_OF_WEEK = [
|
|
{ value: 1, label: 'Lunes' },
|
|
{ value: 2, label: 'Martes' },
|
|
{ value: 3, label: 'Miércoles' },
|
|
{ value: 4, label: 'Jueves' },
|
|
{ value: 5, label: 'Viernes' },
|
|
{ value: 6, label: 'Sábado' },
|
|
{ value: 0, label: 'Domingo' },
|
|
];
|
|
|
|
interface ExportOptionsComponentProps extends ExportOptionsProps {
|
|
onClose?: () => void;
|
|
}
|
|
|
|
export const ExportOptions: React.FC<ExportOptionsComponentProps> = ({
|
|
type,
|
|
title,
|
|
description,
|
|
availableFormats = ['pdf', 'excel', 'csv', 'png'],
|
|
templates = [],
|
|
onExport,
|
|
onSchedule,
|
|
loading = false,
|
|
disabled = false,
|
|
showScheduling = true,
|
|
showTemplates = true,
|
|
showAdvanced = true,
|
|
defaultOptions,
|
|
className = '',
|
|
onClose,
|
|
}) => {
|
|
const [selectedFormat, setSelectedFormat] = useState<ExportFormat>(
|
|
defaultOptions?.format || availableFormats[0]
|
|
);
|
|
const [selectedTemplate, setSelectedTemplate] = useState<string>(
|
|
templates.find(t => t.is_default)?.id || ''
|
|
);
|
|
const [exportOptions, setExportOptions] = useState<Partial<ExportOptions>>({
|
|
include_headers: true,
|
|
include_filters: true,
|
|
include_summary: true,
|
|
date_format: 'DD/MM/YYYY',
|
|
number_format: '#,##0.00',
|
|
currency_format: '€#,##0.00',
|
|
locale: 'es-ES',
|
|
timezone: 'Europe/Madrid',
|
|
page_size: 'A4',
|
|
orientation: 'portrait',
|
|
password_protected: false,
|
|
...defaultOptions,
|
|
});
|
|
|
|
const [scheduleOptions, setScheduleOptions] = useState<Partial<ReportSchedule>>({
|
|
enabled: false,
|
|
frequency: 'weekly',
|
|
time: '09:00',
|
|
days_of_week: [1], // Monday
|
|
recipients: [],
|
|
format: selectedFormat,
|
|
include_attachments: true,
|
|
});
|
|
|
|
const [recipientsInput, setRecipientsInput] = useState('');
|
|
const [activeTab, setActiveTab] = useState<'export' | 'schedule'>('export');
|
|
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
|
|
|
|
const handleExportOptionChange = (key: keyof ExportOptions, value: any) => {
|
|
setExportOptions(prev => ({ ...prev, [key]: value }));
|
|
};
|
|
|
|
const handleScheduleOptionChange = (key: keyof ReportSchedule, value: any) => {
|
|
setScheduleOptions(prev => ({ ...prev, [key]: value }));
|
|
};
|
|
|
|
const handleExport = () => {
|
|
if (onExport) {
|
|
const fullOptions: ExportOptions = {
|
|
format: selectedFormat,
|
|
template: selectedTemplate || undefined,
|
|
include_headers: true,
|
|
include_filters: true,
|
|
include_summary: true,
|
|
date_format: 'DD/MM/YYYY',
|
|
number_format: '#,##0.00',
|
|
currency_format: '€#,##0.00',
|
|
locale: 'es-ES',
|
|
timezone: 'Europe/Madrid',
|
|
page_size: 'A4',
|
|
orientation: 'portrait',
|
|
...exportOptions,
|
|
// format: selectedFormat, // Ensure format matches selection - handled by exportOptions
|
|
};
|
|
onExport(fullOptions);
|
|
}
|
|
};
|
|
|
|
const handleSchedule = () => {
|
|
if (onSchedule) {
|
|
const recipients = recipientsInput
|
|
.split(',')
|
|
.map(email => email.trim())
|
|
.filter(email => email.length > 0);
|
|
|
|
const fullSchedule: ReportSchedule = {
|
|
enabled: true,
|
|
frequency: 'weekly',
|
|
time: '09:00',
|
|
recipients: recipients,
|
|
format: selectedFormat,
|
|
include_attachments: true,
|
|
...scheduleOptions,
|
|
};
|
|
onSchedule(fullSchedule);
|
|
}
|
|
};
|
|
|
|
const renderFormatSelector = () => (
|
|
<div className="space-y-3">
|
|
<h4 className="font-medium text-gray-900">Formato de exportación</h4>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
{availableFormats.map(format => (
|
|
<button
|
|
key={format}
|
|
onClick={() => setSelectedFormat(format)}
|
|
className={`p-3 border rounded-lg text-left transition-colors ${
|
|
selectedFormat === format
|
|
? 'border-blue-500 bg-blue-50'
|
|
: 'border-gray-200 hover:border-gray-300'
|
|
}`}
|
|
disabled={disabled || loading}
|
|
>
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<span className="text-xl">{FORMAT_ICONS[format]}</span>
|
|
<span className="font-medium">{FORMAT_LABELS[format]}</span>
|
|
</div>
|
|
<p className="text-sm text-gray-600">{FORMAT_DESCRIPTIONS[format]}</p>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const renderTemplateSelector = () => {
|
|
if (!showTemplates || templates.length === 0) return null;
|
|
|
|
return (
|
|
<div className="space-y-3">
|
|
<h4 className="font-medium text-gray-900">Plantilla</h4>
|
|
<Select
|
|
value={selectedTemplate}
|
|
onChange={(e) => setSelectedTemplate(e.target.value)}
|
|
disabled={disabled || loading}
|
|
>
|
|
<option value="">Plantilla predeterminada</option>
|
|
{templates.map(template => (
|
|
<option key={template.id} value={template.id}>
|
|
{template.name}
|
|
{template.is_default && ' (Predeterminada)'}
|
|
</option>
|
|
))}
|
|
</Select>
|
|
|
|
{selectedTemplate && (
|
|
<div className="text-sm text-gray-600">
|
|
{templates.find(t => t.id === selectedTemplate)?.description}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const renderBasicOptions = () => (
|
|
<div className="space-y-4">
|
|
<h4 className="font-medium text-gray-900">Opciones básicas</h4>
|
|
|
|
<div className="space-y-3">
|
|
<label className="flex items-center">
|
|
<input
|
|
type="checkbox"
|
|
checked={exportOptions.include_headers || false}
|
|
onChange={(e) => handleExportOptionChange('include_headers', e.target.checked)}
|
|
className="mr-2 rounded"
|
|
disabled={disabled || loading}
|
|
/>
|
|
Incluir encabezados de columna
|
|
</label>
|
|
|
|
<label className="flex items-center">
|
|
<input
|
|
type="checkbox"
|
|
checked={exportOptions.include_filters || false}
|
|
onChange={(e) => handleExportOptionChange('include_filters', e.target.checked)}
|
|
className="mr-2 rounded"
|
|
disabled={disabled || loading}
|
|
/>
|
|
Incluir información de filtros aplicados
|
|
</label>
|
|
|
|
<label className="flex items-center">
|
|
<input
|
|
type="checkbox"
|
|
checked={exportOptions.include_summary || false}
|
|
onChange={(e) => handleExportOptionChange('include_summary', e.target.checked)}
|
|
className="mr-2 rounded"
|
|
disabled={disabled || loading}
|
|
/>
|
|
Incluir resumen y estadísticas
|
|
</label>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const renderAdvancedOptions = () => {
|
|
if (!showAdvanced || !showAdvancedOptions) return null;
|
|
|
|
return (
|
|
<div className="space-y-4 pt-4 border-t border-gray-200">
|
|
<h4 className="font-medium text-gray-900">Opciones avanzadas</h4>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Formato de fecha
|
|
</label>
|
|
<Select
|
|
value={exportOptions.date_format || 'DD/MM/YYYY'}
|
|
onChange={(e) => handleExportOptionChange('date_format', e.target.value)}
|
|
disabled={disabled || loading}
|
|
>
|
|
<option value="DD/MM/YYYY">DD/MM/YYYY</option>
|
|
<option value="MM/DD/YYYY">MM/DD/YYYY</option>
|
|
<option value="YYYY-MM-DD">YYYY-MM-DD</option>
|
|
<option value="DD-MMM-YYYY">DD-MMM-YYYY</option>
|
|
</Select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Formato de números
|
|
</label>
|
|
<Select
|
|
value={exportOptions.number_format || '#,##0.00'}
|
|
onChange={(e) => handleExportOptionChange('number_format', e.target.value)}
|
|
disabled={disabled || loading}
|
|
>
|
|
<option value="#,##0.00">#,##0.00</option>
|
|
<option value="#.##0,00">#.##0,00</option>
|
|
<option value="#,##0">#,##0</option>
|
|
<option value="0.00">0.00</option>
|
|
</Select>
|
|
</div>
|
|
|
|
{(['pdf'] as ExportFormat[]).includes(selectedFormat) && (
|
|
<>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Tamaño de página
|
|
</label>
|
|
<Select
|
|
value={exportOptions.page_size || 'A4'}
|
|
onChange={(e) => handleExportOptionChange('page_size', e.target.value)}
|
|
disabled={disabled || loading}
|
|
>
|
|
<option value="A4">A4</option>
|
|
<option value="A3">A3</option>
|
|
<option value="Letter">Carta</option>
|
|
<option value="Legal">Legal</option>
|
|
</Select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Orientación
|
|
</label>
|
|
<Select
|
|
value={exportOptions.orientation || 'portrait'}
|
|
onChange={(e) => handleExportOptionChange('orientation', e.target.value)}
|
|
disabled={disabled || loading}
|
|
>
|
|
<option value="portrait">Vertical</option>
|
|
<option value="landscape">Horizontal</option>
|
|
</Select>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<label className="flex items-center">
|
|
<input
|
|
type="checkbox"
|
|
checked={exportOptions.password_protected || false}
|
|
onChange={(e) => handleExportOptionChange('password_protected', e.target.checked)}
|
|
className="mr-2 rounded"
|
|
disabled={disabled || loading}
|
|
/>
|
|
Proteger con contraseña
|
|
</label>
|
|
|
|
{exportOptions.password_protected && (
|
|
<div className="mt-2">
|
|
<Input
|
|
type="password"
|
|
placeholder="Contraseña"
|
|
value={exportOptions.password || ''}
|
|
onChange={(e) => handleExportOptionChange('password', e.target.value)}
|
|
disabled={disabled || loading}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const renderScheduleTab = () => (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="flex items-center mb-4">
|
|
<input
|
|
type="checkbox"
|
|
checked={scheduleOptions.enabled || false}
|
|
onChange={(e) => handleScheduleOptionChange('enabled', e.target.checked)}
|
|
className="mr-2 rounded"
|
|
disabled={disabled || loading}
|
|
/>
|
|
<span className="font-medium text-gray-900">Habilitar programación automática</span>
|
|
</label>
|
|
</div>
|
|
|
|
{scheduleOptions.enabled && (
|
|
<div className="space-y-4 pl-6 border-l-2 border-blue-200">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Frecuencia
|
|
</label>
|
|
<Select
|
|
value={scheduleOptions.frequency || 'weekly'}
|
|
onChange={(e) => handleScheduleOptionChange('frequency', e.target.value)}
|
|
disabled={disabled || loading}
|
|
>
|
|
{FREQUENCY_OPTIONS.map(option => (
|
|
<option key={option.value} value={option.value}>
|
|
{option.label}
|
|
</option>
|
|
))}
|
|
</Select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Hora de envío
|
|
</label>
|
|
<Select
|
|
value={scheduleOptions.time || '09:00'}
|
|
onChange={(e) => handleScheduleOptionChange('time', e.target.value)}
|
|
disabled={disabled || loading}
|
|
>
|
|
{TIME_OPTIONS.map(option => (
|
|
<option key={option.value} value={option.value}>
|
|
{option.label}
|
|
</option>
|
|
))}
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
|
|
{scheduleOptions.frequency === 'weekly' && (
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Días de la semana
|
|
</label>
|
|
<div className="flex flex-wrap gap-2">
|
|
{DAYS_OF_WEEK.map(day => (
|
|
<label key={day.value} className="flex items-center">
|
|
<input
|
|
type="checkbox"
|
|
checked={(scheduleOptions.days_of_week || []).includes(day.value)}
|
|
onChange={(e) => {
|
|
const currentDays = scheduleOptions.days_of_week || [];
|
|
const newDays = e.target.checked
|
|
? [...currentDays, day.value]
|
|
: currentDays.filter(d => d !== day.value);
|
|
handleScheduleOptionChange('days_of_week', newDays);
|
|
}}
|
|
className="mr-1 rounded"
|
|
disabled={disabled || loading}
|
|
/>
|
|
<span className="text-sm">{day.label}</span>
|
|
</label>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Destinatarios (emails separados por comas)
|
|
</label>
|
|
<Input
|
|
type="text"
|
|
value={recipientsInput}
|
|
onChange={(e) => setRecipientsInput(e.target.value)}
|
|
placeholder="ejemplo@empresa.com, otro@empresa.com"
|
|
disabled={disabled || loading}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="flex items-center">
|
|
<input
|
|
type="checkbox"
|
|
checked={scheduleOptions.include_attachments || false}
|
|
onChange={(e) => handleScheduleOptionChange('include_attachments', e.target.checked)}
|
|
className="mr-2 rounded"
|
|
disabled={disabled || loading}
|
|
/>
|
|
Incluir archivo adjunto con los datos
|
|
</label>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<div className={`bg-white rounded-lg shadow-lg max-w-2xl mx-auto ${className}`}>
|
|
<Card className="p-0">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between p-6 border-b border-gray-200">
|
|
<div>
|
|
<h2 className="text-xl font-semibold text-gray-900">{title}</h2>
|
|
{description && (
|
|
<p className="text-sm text-gray-600 mt-1">{description}</p>
|
|
)}
|
|
</div>
|
|
|
|
{onClose && (
|
|
<Button variant="ghost" size="sm" onClick={onClose}>
|
|
✕
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Tabs */}
|
|
<div className="border-b border-gray-200">
|
|
<nav className="flex space-x-8 px-6">
|
|
<button
|
|
onClick={() => setActiveTab('export')}
|
|
className={`py-4 px-1 border-b-2 font-medium text-sm ${
|
|
activeTab === 'export'
|
|
? 'border-blue-500 text-blue-600'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700'
|
|
}`}
|
|
>
|
|
📥 Exportación inmediata
|
|
</button>
|
|
|
|
{showScheduling && (
|
|
<button
|
|
onClick={() => setActiveTab('schedule')}
|
|
className={`py-4 px-1 border-b-2 font-medium text-sm ${
|
|
activeTab === 'schedule'
|
|
? 'border-blue-500 text-blue-600'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700'
|
|
}`}
|
|
>
|
|
📅 Programación
|
|
</button>
|
|
)}
|
|
</nav>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="p-6 space-y-6" style={{ maxHeight: '70vh', overflowY: 'auto' }}>
|
|
{activeTab === 'export' ? (
|
|
<>
|
|
{renderFormatSelector()}
|
|
{renderTemplateSelector()}
|
|
{renderBasicOptions()}
|
|
|
|
{showAdvanced && (
|
|
<div>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setShowAdvancedOptions(!showAdvancedOptions)}
|
|
className="text-blue-600"
|
|
>
|
|
{showAdvancedOptions ? '▲' : '▼'} Opciones avanzadas
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
{renderAdvancedOptions()}
|
|
</>
|
|
) : (
|
|
renderScheduleTab()
|
|
)}
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<div className="flex justify-end gap-3 p-6 border-t border-gray-200">
|
|
{onClose && (
|
|
<Button
|
|
variant="outline"
|
|
onClick={onClose}
|
|
disabled={loading}
|
|
>
|
|
Cancelar
|
|
</Button>
|
|
)}
|
|
|
|
{activeTab === 'export' ? (
|
|
<Button
|
|
variant="primary"
|
|
onClick={handleExport}
|
|
disabled={disabled || loading}
|
|
>
|
|
{loading ? '🔄 Exportando...' : `${FORMAT_ICONS[selectedFormat]} Exportar ${FORMAT_LABELS[selectedFormat]}`}
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
variant="primary"
|
|
onClick={handleSchedule}
|
|
disabled={disabled || loading || !scheduleOptions.enabled || !recipientsInput.trim()}
|
|
>
|
|
{loading ? '🔄 Programando...' : '📅 Programar reporte'}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ExportOptions; |