ADD new frontend
This commit is contained in:
@@ -0,0 +1,592 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user