feat: Implement asynchronous invoice report generation with email delivery and frontend status polling.
This commit is contained in:
@@ -62,6 +62,15 @@ export interface AllMovementsFilter {
|
||||
exchange_rate_type: ExchangeRateType;
|
||||
is_shelter: boolean;
|
||||
operation_type?: 'imp' | 'exp' | null;
|
||||
send_email?: boolean;
|
||||
// Granular flags
|
||||
import_temp?: boolean;
|
||||
import_def?: boolean;
|
||||
import_rep?: boolean;
|
||||
export_def?: boolean;
|
||||
export_rep?: boolean;
|
||||
export_types?: string[];
|
||||
discharge_filter?: DischargeFilter;
|
||||
}
|
||||
|
||||
export interface MovementItem {
|
||||
@@ -181,5 +190,14 @@ export const invoiceMovementsApi = {
|
||||
|
||||
// All Movements
|
||||
getAllMovements: (filters: AllMovementsFilter) =>
|
||||
api.post<MovementItem[]>('/v1/a76/reports/movements/invoices/all', filters)
|
||||
api.post<MovementItem[]>('/v1/a76/reports/movements/invoices/all', filters),
|
||||
|
||||
// Async Generation
|
||||
generateReportAsync: (filters: AllMovementsFilter) =>
|
||||
api.post<{ task_id: string }>('/v1/a76/reports/movements/invoices/generate', filters),
|
||||
|
||||
getTaskStatus: (taskId: string) =>
|
||||
api.get<{ task_id: string; status: string; result?: any; meta?: any }>(
|
||||
`/v1/a76/reports/movements/invoices/task/${taskId}`
|
||||
)
|
||||
};
|
||||
|
||||
@@ -28,7 +28,8 @@
|
||||
ShieldCheck,
|
||||
Calculator,
|
||||
Download,
|
||||
Folder
|
||||
Folder,
|
||||
Eye
|
||||
} from 'lucide-svelte';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
@@ -346,395 +347,229 @@
|
||||
|
||||
// --- LÓGICA ---
|
||||
|
||||
async function handleGenerateReport() {
|
||||
// Validar fechas
|
||||
const dateValidation = validateDates();
|
||||
if (!dateValidation.valid) {
|
||||
toast.error(`Error: ${dateValidation.message}`);
|
||||
return;
|
||||
}
|
||||
// Estado del Context Menu
|
||||
let contextMenu = $state({
|
||||
open: false,
|
||||
x: 0,
|
||||
y: 0
|
||||
});
|
||||
|
||||
// Validar tipos de movimiento
|
||||
const movementValidation = validateMovementTypes();
|
||||
if (!movementValidation.valid) {
|
||||
toast.error(`Error: ${movementValidation.message}`);
|
||||
return;
|
||||
}
|
||||
function handleContextMenu(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
contextMenu = {
|
||||
open: true,
|
||||
x: e.clientX,
|
||||
y: e.clientY
|
||||
};
|
||||
}
|
||||
|
||||
// Validar reglas de negocio del Clarion
|
||||
const businessValidation = validateBusinessRules();
|
||||
if (!businessValidation.valid) {
|
||||
toast.error(`Error: ${businessValidation.message}`);
|
||||
return;
|
||||
function closeContextMenu() {
|
||||
contextMenu.open = false;
|
||||
}
|
||||
|
||||
// Acción: Generar Reporte (Email / Background) - Click Izquierdo
|
||||
async function generateReport() {
|
||||
const filter = buildAllMovementsFilter();
|
||||
if (!filter) return;
|
||||
|
||||
// Forzar envío de correo para esta acción
|
||||
filter.send_email = true;
|
||||
|
||||
loading = true;
|
||||
// Initial toast
|
||||
const toastId = toast.loading('Iniciando generación de reporte...');
|
||||
|
||||
try {
|
||||
// 1. Trigger Async Generation
|
||||
const response = await invoiceMovementsApi.generateReportAsync(filter);
|
||||
|
||||
if (!response.data || !response.data.task_id) {
|
||||
toast.error('Error al iniciar la generación del reporte', { id: toastId });
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const taskId = response.data.task_id;
|
||||
|
||||
// 2. Poll for status
|
||||
const pollInterval = setInterval(async () => {
|
||||
try {
|
||||
const statusResponse = await invoiceMovementsApi.getTaskStatus(taskId);
|
||||
const statusData = statusResponse.data;
|
||||
|
||||
if (!statusData) return;
|
||||
|
||||
if (statusData.status === 'SUCCESS') {
|
||||
clearInterval(pollInterval);
|
||||
loading = false;
|
||||
toast.success(
|
||||
'Reporte generado correctamente. Se ha enviado un correo con los resultados.',
|
||||
{ id: toastId }
|
||||
);
|
||||
} else if (statusData.status === 'FAILURE') {
|
||||
clearInterval(pollInterval);
|
||||
loading = false;
|
||||
// Try to extract specific error from meta or result
|
||||
const errorMsg =
|
||||
statusData.meta?.exc_message ||
|
||||
statusData.result?.exc_message ||
|
||||
statusData.result?.detail ||
|
||||
'Error en la generación del reporte';
|
||||
|
||||
console.error('Task Failure Details:', statusData);
|
||||
toast.error(errorMsg, { id: toastId });
|
||||
} else if (statusData.status === 'PROCESSING') {
|
||||
// Update progress
|
||||
if (statusData.meta) {
|
||||
const { current, total, status } = statusData.meta;
|
||||
const percentage = Math.round((current / total) * 100);
|
||||
toast.loading(`${status} (${percentage}%)`, { id: toastId });
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error polling status:', err);
|
||||
// Don't stop polling on transient network errors, but maybe log it
|
||||
}
|
||||
}, 1000); // Poll every 1 second
|
||||
} catch (error: any) {
|
||||
console.error('Error generando reporte:', error);
|
||||
toast.error(error.message || 'Error al generar el reporte', { id: toastId });
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Acción: Vista Previa (Tabla) - Click Derecho -> Opción
|
||||
async function previewReport() {
|
||||
closeContextMenu();
|
||||
const filter = buildAllMovementsFilter();
|
||||
if (!filter) return;
|
||||
|
||||
// Para preview, no enviamos correo (o respetamos config, pero generalmente preview es solo ver)
|
||||
filter.send_email = false;
|
||||
|
||||
loading = true;
|
||||
results = [];
|
||||
showResults = false;
|
||||
|
||||
toast.info('Cargando vista previa...');
|
||||
|
||||
try {
|
||||
// Si "TODAS" en otras opciones o exportaciones está marcado, usar el endpoint especial
|
||||
if (types.other.TODAS || types.export.additional.TODAS) {
|
||||
toast.info('Obteniendo todos los movimientos...');
|
||||
const response = await invoiceMovementsApi.getAllMovements(filter);
|
||||
|
||||
// Determinar el tipo de operación basado en cuál "TODAS" está marcado
|
||||
let operation_type: 'imp' | 'exp' | null = null;
|
||||
|
||||
if (types.other.TODAS) {
|
||||
// "TODAS" de la sección Otras = traer TODO (importaciones + exportaciones)
|
||||
operation_type = null;
|
||||
} else if (types.export.additional.TODAS) {
|
||||
// "TODAS" de exportaciones = solo exportaciones
|
||||
operation_type = 'exp';
|
||||
}
|
||||
|
||||
const allMovementsFilter: AllMovementsFilter = {
|
||||
range_type: dates.type === 'invoice' ? 'FF' : 'FP',
|
||||
start_date: formatDateToYYYYMMDD(dates.from),
|
||||
end_date: formatDateToYYYYMMDD(dates.to),
|
||||
include_cancelled: filters.includeNA,
|
||||
provider: selectors.provider || null,
|
||||
buyer: selectors.soldTo || null,
|
||||
pedimento_code: selectors.pedimentoKey || null,
|
||||
report_type: config.reportType === 'normal' ? 'Normal' : 'Detallado',
|
||||
currency_type: config.currency === 'foreign' ? 'ME' : 'MN',
|
||||
exchange_rate_type: config.exchangeRate === 'invoice_date' ? 'FF' : 'FP',
|
||||
is_shelter: config.shelter,
|
||||
operation_type
|
||||
};
|
||||
|
||||
const response = await invoiceMovementsApi.getAllMovements(allMovementsFilter);
|
||||
|
||||
if (response.error) {
|
||||
toast.error(response.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.data) {
|
||||
results = response.data;
|
||||
showResults = true;
|
||||
}
|
||||
|
||||
toast.success(`Se encontraron ${results.length} movimientos`);
|
||||
return; // Salir temprano, no ejecutar la lógica individual
|
||||
if (response.error) {
|
||||
toast.error(response.error);
|
||||
return;
|
||||
}
|
||||
|
||||
const baseFilter: Omit<BaseFilter, 'database_name'> = {
|
||||
range_type: dates.type === 'invoice' ? 'FF' : 'FP',
|
||||
start_date: formatDateToYYYYMMDD(dates.from),
|
||||
end_date: formatDateToYYYYMMDD(dates.to),
|
||||
include_cancelled: filters.includeNA,
|
||||
provider: selectors.provider || null,
|
||||
buyer: selectors.soldTo || null,
|
||||
pedimento_code: selectors.pedimentoKey || null,
|
||||
report_type: config.reportType === 'normal' ? 'Normal' : 'Detallado',
|
||||
currency_type: config.currency === 'foreign' ? 'ME' : 'MN',
|
||||
exchange_rate_type: config.exchangeRate === 'invoice_date' ? 'FF' : 'FP',
|
||||
is_shelter: config.shelter
|
||||
};
|
||||
if (response.data) {
|
||||
results = response.data;
|
||||
showResults = true;
|
||||
|
||||
const allResults: (MovementItem | MovementItemDetailed)[] = [];
|
||||
|
||||
// Importaciones Temporales (IMTEM)
|
||||
if (types.import.TEM) {
|
||||
toast.info('Obteniendo importaciones temporales...');
|
||||
const response =
|
||||
config.reportType === 'normal'
|
||||
? await invoiceMovementsApi.getTemporaryImports({
|
||||
...baseFilter,
|
||||
database_name: 'default'
|
||||
})
|
||||
: await invoiceMovementsApi.getTemporaryImportsDetailed({
|
||||
...baseFilter,
|
||||
database_name: 'default'
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
toast.error(response.error);
|
||||
return;
|
||||
if (results.length === 0) {
|
||||
toast.warning('No se encontraron resultados con los filtros seleccionados');
|
||||
} else {
|
||||
toast.success(`${results.length} registros cargados en vista previa`);
|
||||
}
|
||||
|
||||
if (response.data) allResults.push(...response.data);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Error generando vista previa:', error);
|
||||
toast.error(error.message || 'Error al generar la vista previa');
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Importaciones Definitivas (IMPDF) o COMEX
|
||||
if (types.import.DEF) {
|
||||
toast.info('Obteniendo importaciones definitivas...');
|
||||
const movementType = types.other.COMEX ? 'COMEX' : 'IMPDF';
|
||||
const response =
|
||||
config.reportType === 'normal'
|
||||
? await invoiceMovementsApi.getDefinitiveImports({
|
||||
...baseFilter,
|
||||
database_name: 'default',
|
||||
movement_type: movementType
|
||||
})
|
||||
: await invoiceMovementsApi.getDefinitiveImportsDetailed({
|
||||
...baseFilter,
|
||||
database_name: 'default',
|
||||
movement_type: movementType
|
||||
});
|
||||
// Helper para construir el filtro (extraído de la lógica anterior)
|
||||
function buildAllMovementsFilter(): AllMovementsFilter | null {
|
||||
// Validaciones
|
||||
const dateValidation = validateDates();
|
||||
if (!dateValidation.valid) {
|
||||
toast.error(`Error: ${dateValidation.message}`);
|
||||
return null;
|
||||
}
|
||||
const movementValidation = validateMovementTypes();
|
||||
if (!movementValidation.valid) {
|
||||
toast.error(`Error: ${movementValidation.message}`);
|
||||
return null;
|
||||
}
|
||||
const businessValidation = validateBusinessRules();
|
||||
if (!businessValidation.valid) {
|
||||
toast.error(`Error: ${businessValidation.message}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (response.error) {
|
||||
toast.error(response.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.data) allResults.push(...response.data);
|
||||
}
|
||||
|
||||
// Importaciones de Reparación (IMPRE)
|
||||
if (types.import.REP) {
|
||||
toast.info('Obteniendo importaciones de reparación...');
|
||||
const dischargeFilter =
|
||||
filters.downloaded === 'downloaded'
|
||||
? 'SiDes'
|
||||
: filters.downloaded === 'not_downloaded'
|
||||
? 'NoDes'
|
||||
: 'ALL';
|
||||
const response =
|
||||
config.reportType === 'normal'
|
||||
? await invoiceMovementsApi.getRepairImports({
|
||||
...baseFilter,
|
||||
database_name: 'default',
|
||||
discharge_filter: dischargeFilter
|
||||
})
|
||||
: await invoiceMovementsApi.getRepairImportsDetailed({
|
||||
...baseFilter,
|
||||
database_name: 'default',
|
||||
discharge_filter: dischargeFilter
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
toast.error(response.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.data) allResults.push(...response.data);
|
||||
}
|
||||
|
||||
// Exportaciones Definitivas (incluye VEMEX)
|
||||
if (
|
||||
const allMovementsFilter: AllMovementsFilter = {
|
||||
range_type: dates.type === 'invoice' ? 'FF' : 'FP',
|
||||
start_date: formatDateToYYYYMMDD(dates.from),
|
||||
end_date: formatDateToYYYYMMDD(dates.to),
|
||||
include_cancelled: filters.includeNA,
|
||||
provider: selectors.provider || null,
|
||||
buyer: selectors.soldTo || null,
|
||||
pedimento_code: selectors.pedimentoKey || null,
|
||||
report_type: config.reportType === 'normal' ? 'Normal' : 'Detallado',
|
||||
currency_type: config.currency === 'foreign' ? 'ME' : 'MN',
|
||||
exchange_rate_type: config.exchangeRate === 'invoice_date' ? 'FF' : 'FP',
|
||||
is_shelter: config.shelter,
|
||||
// Granular flags
|
||||
import_temp: !!types.import.TEM,
|
||||
import_def: !!types.import.DEF,
|
||||
import_rep: !!types.import.REP,
|
||||
export_def: !!(
|
||||
types.export.main.DEF ||
|
||||
types.other.VEMEX ||
|
||||
types.export.additional.TODAS ||
|
||||
Object.values(types.export.additional).some((v) => v)
|
||||
) {
|
||||
toast.info('Obteniendo exportaciones...');
|
||||
),
|
||||
export_rep: !!types.export.main.REP,
|
||||
send_email: config.sendEmail
|
||||
};
|
||||
|
||||
// Determinar tipo de movimiento basado en checkboxes adicionales
|
||||
let movementType: any = 'ALL';
|
||||
if (types.export.additional.AFIJO) movementType = 'AFIJO';
|
||||
else if (types.export.additional.NODES) movementType = 'NODES';
|
||||
else if (types.export.additional.SCRAP) movementType = 'SCRAP';
|
||||
else if (types.export.additional.REEXP) movementType = 'REEXP';
|
||||
else if (types.export.additional.DONAC) movementType = 'DONAC';
|
||||
else if (types.other.VEMEX) movementType = 'VEMEX';
|
||||
// Override flags if "TODAS" is selected
|
||||
if (types.other.TODAS) {
|
||||
allMovementsFilter.import_temp = true;
|
||||
allMovementsFilter.import_def = true;
|
||||
allMovementsFilter.import_rep = true;
|
||||
allMovementsFilter.export_def = true;
|
||||
allMovementsFilter.export_rep = true;
|
||||
allMovementsFilter.operation_type = null;
|
||||
} else if (types.export.additional.TODAS) {
|
||||
allMovementsFilter.import_temp = false;
|
||||
allMovementsFilter.import_def = false;
|
||||
allMovementsFilter.import_rep = false;
|
||||
allMovementsFilter.export_def = true;
|
||||
allMovementsFilter.export_rep = true;
|
||||
allMovementsFilter.operation_type = 'exp';
|
||||
} else {
|
||||
const hasImports =
|
||||
allMovementsFilter.import_temp ||
|
||||
allMovementsFilter.import_def ||
|
||||
allMovementsFilter.import_rep;
|
||||
const hasExports = allMovementsFilter.export_def || allMovementsFilter.export_rep;
|
||||
|
||||
const dischargeFilter =
|
||||
filters.downloaded === 'downloaded'
|
||||
? 'SiDes'
|
||||
: filters.downloaded === 'not_downloaded'
|
||||
? 'NoDes'
|
||||
: 'ALL';
|
||||
|
||||
const response =
|
||||
config.reportType === 'normal'
|
||||
? await invoiceMovementsApi.getExports({
|
||||
...baseFilter,
|
||||
database_name: 'default',
|
||||
movement_type: movementType,
|
||||
discharge_filter: dischargeFilter,
|
||||
use_transport_method: false
|
||||
})
|
||||
: await invoiceMovementsApi.getExportsDetailed({
|
||||
...baseFilter,
|
||||
database_name: 'default',
|
||||
movement_type: movementType,
|
||||
discharge_filter: dischargeFilter,
|
||||
use_transport_method: false
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
toast.error(response.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.data) allResults.push(...response.data);
|
||||
if (hasImports && hasExports) {
|
||||
allMovementsFilter.operation_type = null;
|
||||
} else if (hasImports) {
|
||||
allMovementsFilter.operation_type = 'imp';
|
||||
} else if (hasExports) {
|
||||
allMovementsFilter.operation_type = 'exp';
|
||||
}
|
||||
|
||||
// Exportaciones de Reparación
|
||||
if (types.export.main.REP || types.export.additional.TODAS) {
|
||||
toast.info('Obteniendo exportaciones de reparación...');
|
||||
|
||||
// Para reparaciones solo aplican AFIJO y NODES
|
||||
let movementType: any = 'ALL';
|
||||
if (types.export.additional.AFIJO) movementType = 'AFIJO';
|
||||
else if (types.export.additional.NODES) movementType = 'NODES';
|
||||
|
||||
const dischargeFilter =
|
||||
filters.downloaded === 'downloaded'
|
||||
? 'SiDes'
|
||||
: filters.downloaded === 'not_downloaded'
|
||||
? 'NoDes'
|
||||
: 'ALL';
|
||||
|
||||
const response =
|
||||
config.reportType === 'normal'
|
||||
? await invoiceMovementsApi.getExportRepairs({
|
||||
...baseFilter,
|
||||
database_name: 'default',
|
||||
movement_type: movementType,
|
||||
discharge_filter: dischargeFilter
|
||||
})
|
||||
: await invoiceMovementsApi.getExportRepairsDetailed({
|
||||
...baseFilter,
|
||||
database_name: 'default',
|
||||
movement_type: movementType,
|
||||
discharge_filter: dischargeFilter
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
toast.error(response.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.data) allResults.push(...response.data);
|
||||
}
|
||||
|
||||
// Cambio de Régimen (CREG) - Exportaciones con cambio de régimen
|
||||
if (types.export.main.CREG) {
|
||||
toast.info('Obteniendo cambios de régimen...');
|
||||
|
||||
// Para cambio de régimen solo aplican AFIJO y SCRAP
|
||||
let movementType: any = 'ALL';
|
||||
if (types.export.additional.AFIJO) movementType = 'AFIJO';
|
||||
else if (types.export.additional.SCRAP) movementType = 'SCRAP';
|
||||
|
||||
const dischargeFilter =
|
||||
filters.downloaded === 'downloaded'
|
||||
? 'SiDes'
|
||||
: filters.downloaded === 'not_downloaded'
|
||||
? 'NoDes'
|
||||
: 'ALL';
|
||||
|
||||
const response =
|
||||
config.reportType === 'normal'
|
||||
? await invoiceMovementsApi.getExports({
|
||||
...baseFilter,
|
||||
database_name: 'default',
|
||||
movement_type: movementType,
|
||||
discharge_filter: dischargeFilter,
|
||||
use_transport_method: false
|
||||
})
|
||||
: await invoiceMovementsApi.getExportsDetailed({
|
||||
...baseFilter,
|
||||
database_name: 'default',
|
||||
movement_type: movementType,
|
||||
discharge_filter: dischargeFilter,
|
||||
use_transport_method: false
|
||||
});
|
||||
if (response.data) allResults.push(...response.data);
|
||||
}
|
||||
|
||||
// CREGEXP (Cambio Régimen Export) - Caso especial
|
||||
if (types.other.CREGEXP) {
|
||||
toast.info('Obteniendo cambios de régimen export...');
|
||||
|
||||
const dischargeFilter =
|
||||
filters.downloaded === 'downloaded'
|
||||
? 'SiDes'
|
||||
: filters.downloaded === 'not_downloaded'
|
||||
? 'NoDes'
|
||||
: 'ALL';
|
||||
|
||||
const response =
|
||||
config.reportType === 'normal'
|
||||
? await invoiceMovementsApi.getExports({
|
||||
...baseFilter,
|
||||
database_name: 'default',
|
||||
movement_type: 'ALL',
|
||||
discharge_filter: dischargeFilter,
|
||||
use_transport_method: false
|
||||
})
|
||||
: await invoiceMovementsApi.getExportsDetailed({
|
||||
...baseFilter,
|
||||
database_name: 'default',
|
||||
movement_type: 'ALL',
|
||||
discharge_filter: dischargeFilter,
|
||||
use_transport_method: false
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
toast.error(response.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.data) allResults.push(...response.data);
|
||||
}
|
||||
|
||||
results = allResults;
|
||||
showResults = true;
|
||||
|
||||
// Ordenar resultados según configuración y modo de reporte
|
||||
if (config.reportType === 'normal') {
|
||||
// LLENADOCSVNORMAL - SORT con 3 campos
|
||||
if (config.shelter) {
|
||||
results.sort((a, b) => {
|
||||
if (a.BaseDeDatos !== b.BaseDeDatos) return a.BaseDeDatos.localeCompare(b.BaseDeDatos);
|
||||
if (a.TipoMovTemDef !== b.TipoMovTemDef)
|
||||
return a.TipoMovTemDef.localeCompare(b.TipoMovTemDef);
|
||||
return (a.FechaFactura || '').localeCompare(b.FechaFactura || '');
|
||||
});
|
||||
} else {
|
||||
results.sort((a, b) => {
|
||||
if (a.TipoMovTemDef !== b.TipoMovTemDef)
|
||||
return a.TipoMovTemDef.localeCompare(b.TipoMovTemDef);
|
||||
if ((a.FechaFactura || '') !== (b.FechaFactura || ''))
|
||||
return (a.FechaFactura || '').localeCompare(b.FechaFactura || '');
|
||||
return a.BaseDeDatos.localeCompare(b.BaseDeDatos);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// LLENADOCSVDETALLADO - SORT más simple
|
||||
if (config.shelter) {
|
||||
results.sort((a, b) => {
|
||||
if (a.BaseDeDatos !== b.BaseDeDatos) return a.BaseDeDatos.localeCompare(b.BaseDeDatos);
|
||||
if (a.TipoMovTemDef !== b.TipoMovTemDef)
|
||||
return a.TipoMovTemDef.localeCompare(b.TipoMovTemDef);
|
||||
return (a.FechaFactura || '').localeCompare(b.FechaFactura || '');
|
||||
});
|
||||
} else {
|
||||
results.sort((a, b) => {
|
||||
if (a.TipoMovTemDef !== b.TipoMovTemDef)
|
||||
return a.TipoMovTemDef.localeCompare(b.TipoMovTemDef);
|
||||
return (a.FechaFactura || '').localeCompare(b.FechaFactura || '');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Configurar título del reporte
|
||||
reportTitle = 'REPORTE DE FACTURAS';
|
||||
|
||||
// Configurar etiqueta de moneda
|
||||
if (config.currency === 'foreign') {
|
||||
currencyLabel = 'Moneda: Dólares';
|
||||
} else if (config.currency === 'national') {
|
||||
currencyLabel = 'Moneda: Pesos';
|
||||
} else {
|
||||
currencyLabel = 'Moneda: Captura';
|
||||
}
|
||||
|
||||
if (allResults.length === 0) {
|
||||
toast.warning('No se encontraron resultados con los filtros seleccionados');
|
||||
} else {
|
||||
toast.success(`Reporte generado exitosamente: ${allResults.length} registros encontrados`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Error generando reporte:', error);
|
||||
toast.error(error.message || 'Error al generar el reporte');
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
// Set discharge filter (Global)
|
||||
const dischargeFilter =
|
||||
filters.downloaded === 'downloaded'
|
||||
? 'SiDes'
|
||||
: filters.downloaded === 'not_downloaded'
|
||||
? 'NoDes'
|
||||
: 'ALL';
|
||||
allMovementsFilter.discharge_filter = dischargeFilter;
|
||||
|
||||
return allMovementsFilter;
|
||||
}
|
||||
|
||||
async function handleGenerateReport() {
|
||||
// Wrapper for compatibility if button still calls this
|
||||
await generateReport();
|
||||
}
|
||||
|
||||
function downloadCSV() {
|
||||
@@ -1171,11 +1006,31 @@
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex-1 space-y-1">
|
||||
<Label class="text-xs font-bold text-muted-foreground uppercase">Fecha Inicio</Label>
|
||||
<Input type="date" class="h-8" bind:value={dates.from} />
|
||||
<Input
|
||||
type="date"
|
||||
class="h-8 cursor-pointer"
|
||||
bind:value={dates.from}
|
||||
onclick={(e) => {
|
||||
const input = e.currentTarget;
|
||||
if (input && typeof input.showPicker === 'function') {
|
||||
input.showPicker();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-1 space-y-1">
|
||||
<Label class="text-xs font-bold text-muted-foreground uppercase">Fecha Fin</Label>
|
||||
<Input type="date" class="h-8" bind:value={dates.to} />
|
||||
<Input
|
||||
type="date"
|
||||
class="h-8 cursor-pointer"
|
||||
bind:value={dates.to}
|
||||
onclick={(e) => {
|
||||
const input = e.currentTarget;
|
||||
if (input && typeof input.showPicker === 'function') {
|
||||
input.showPicker();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1517,7 +1372,8 @@
|
||||
<Button
|
||||
class="h-9 flex-1 text-sm shadow-sm"
|
||||
size="default"
|
||||
onclick={handleGenerateReport}
|
||||
onclick={generateReport}
|
||||
oncontextmenu={handleContextMenu}
|
||||
disabled={loading}
|
||||
>
|
||||
{#if loading}
|
||||
@@ -1860,3 +1716,25 @@
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
<svelte:window onclick={closeContextMenu} />
|
||||
|
||||
<!-- Manual Context Menu -->
|
||||
{#if contextMenu.open}
|
||||
<div
|
||||
class="animate-in fade-in-80 fixed z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md"
|
||||
style="top: {contextMenu.y}px; left: {contextMenu.x}px;"
|
||||
>
|
||||
<button
|
||||
class="relative flex w-full cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-none select-none hover:bg-accent hover:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50"
|
||||
onclick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
previewReport();
|
||||
}}
|
||||
>
|
||||
<Eye class="mr-2 h-4 w-4" />
|
||||
Ver vista previa
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user