Se movio la parte de descargas del sidbar al botoncito en reportes

This commit is contained in:
2026-05-08 11:10:29 -05:00
parent fd26b8e1dd
commit 2c550eed04
4 changed files with 36 additions and 23 deletions

View File

@@ -0,0 +1,865 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import {
FileSearch,
Filter,
Boxes,
Search,
Printer,
X,
Package,
Scale,
BadgeDollarSign,
Clock3
} from 'lucide-svelte';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import { Separator } from '$lib/components/ui/separator';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { Checkbox } from '$lib/components/ui/checkbox';
import * as RadioGroup from '$lib/components/ui/radio-group';
import * as Select from '$lib/components/ui/select';
import { companyStore } from '$lib/stores/company.svelte';
import { clientsProvidersApi, type ClientProvider } from '$lib/api/dashboard/a76/clients-providers';
import { partsApi, type Part } from '$lib/api/dashboard/a76/parts';
import { classesApi, type A76Class } from '$lib/api/dashboard/a76/classes';
import { invoiceTypesApi, type InvoiceType } from '$lib/api/dashboard/reference_data/invoice_types';
import { pedimentoCodesApi, type PedimentoCode } from '$lib/api/dashboard/reference_data/pedimento_codes';
import { materialTypesApi, type MaterialType } from '$lib/api/dashboard/reference_data/material_types';
import { customsSectionsApi, type CustomsSection } from '$lib/api/dashboard/reference_data/customs_sections';
import {
downloadedPartsReportsApi,
type DownloadedPartsReportBootstrap,
type DownloadedPartsReportRequest
} from '$lib/api/dashboard/a76/reports/reports-partes-descargadas';
type FilterFieldKey =
| 'materialType'
| 'invoiceType'
| 'parts'
| 'pedimentoKey'
| 'provider'
| 'destinationCustoms'
| 'soldTo'
| 'shippedTo';
type OptionKey =
| 'sendEmailSelection'
| 'respectImportInvoiceValueInPesos'
| 'showAllTemporaryBalances'
| 'shelterOption'
| 'showExportFraction'
| 'includeRuleOctava'
| 'includeAmericanFractionAndCountry'
| 'includeSeries'
| 'printClassTotal'
| 'includeTotalsByFraction'
| 'julianDate'
| 'showItemDescription'
| 'includeExemptFraction';
let bootstrap = $state<DownloadedPartsReportBootstrap | null>(null);
let isLoading = $state(false);
let isGenerating = $state(false);
let isCatalogLoading = $state(false);
let lastCompanyId = $state<number | null>(null);
let clientsProviders = $state<ClientProvider[]>([]);
let invoiceTypes = $state<InvoiceType[]>([]);
let pedimentoCodes = $state<PedimentoCode[]>([]);
let materialTypes = $state<MaterialType[]>([]);
let customsSections = $state<CustomsSection[]>([]);
let partsCatalog = $state<Part[]>([]);
let classesCatalog = $state<A76Class[]>([]);
let dates = $state({ from: '', to: '' });
let classRange = $state({ from: '', to: '' });
let filters = $state<Record<FilterFieldKey, string>>({
materialType: '',
invoiceType: '',
parts: '',
pedimentoKey: '',
provider: '',
destinationCustoms: '',
soldTo: '',
shippedTo: ''
});
let printClassMode = $state<'exported' | 'downloaded'>('downloaded');
let currencyMode = $state<'dollars' | 'pesos' | 'both'>('both');
let exchangeRateMode = $state<'invoice' | 'pedimento_payment'>('invoice');
let temporalityMode = $state<'temporales' | 'definitivos' | 'ambos'>('temporales');
let weightTypeMode = $state<'kilos' | 'libras' | 'ambos'>('kilos');
let operationMode = $state<'importacion' | 'exportacion'>('importacion');
let options = $state<Record<OptionKey, boolean>>({
sendEmailSelection: false,
respectImportInvoiceValueInPesos: false,
showAllTemporaryBalances: false,
shelterOption: false,
showExportFraction: false,
includeRuleOctava: false,
includeAmericanFractionAndCountry: false,
includeSeries: false,
printClassTotal: false,
includeTotalsByFraction: false,
julianDate: false,
showItemDescription: true,
includeExemptFraction: false
});
const leftOptions: Array<{ id: string; label: string; key: OptionKey }> = [
{
id: 'email-selection',
label: 'Seleccione para enviar correo electrónico',
key: 'sendEmailSelection'
},
{
id: 'respect-pesos',
label: 'Respetar Valor en Pesos Facturas de Importación',
key: 'respectImportInvoiceValueInPesos'
},
{
id: 'show-temp-balances',
label: 'Mostrar todos los saldos temporales',
key: 'showAllTemporaryBalances'
},
{ id: 'shelter-option', label: 'Opción Shelter', key: 'shelterOption' },
{
id: 'show-export-fraction',
label: 'Mostrar Fraccion de Exportación',
key: 'showExportFraction'
},
{ id: 'include-rule-8', label: 'Incluir Regla Octava', key: 'includeRuleOctava' },
{
id: 'include-american-fraction',
label: 'Incluir Fracción Americana y País de Origen',
key: 'includeAmericanFractionAndCountry'
}
];
const rightOptions: Array<{ id: string; label: string; key: OptionKey }> = [
{ id: 'include-series', label: 'Incluir Series', key: 'includeSeries' },
{ id: 'print-total-class', label: 'Imprimir Total Clase', key: 'printClassTotal' },
{
id: 'include-totals-fraction',
label: 'Incluir Totales x Fracción',
key: 'includeTotalsByFraction'
},
{ id: 'julian-date', label: 'Fecha Juliana', key: 'julianDate' },
{
id: 'show-item-description',
label: 'Mostrar Descripcion de Partida',
key: 'showItemDescription'
},
{
id: 'include-exempt-fraction',
label: 'Incluir Fraccion Exenta',
key: 'includeExemptFraction'
}
];
const allOptions = [...leftOptions, ...rightOptions];
const CLEAR_SELECT_VALUE = '__clear__';
async function loadBootstrap(companyId?: number) {
const activeCompanyId = companyId ?? companyStore.activeCompany?.id;
if (!activeCompanyId) {
bootstrap = null;
return;
}
isLoading = true;
const response = await downloadedPartsReportsApi.getBootstrap(activeCompanyId);
if (response.data) {
bootstrap = response.data;
} else {
toast.error(response.error || 'No se pudo cargar la base del reporte');
}
isLoading = false;
}
async function loadCatalogs(companyId: number) {
isCatalogLoading = true;
try {
const [
clientsProvidersResponse,
invoiceTypesResponse,
pedimentoCodesResponse,
materialTypesResponse,
customsSectionsResponse,
partsResponse,
classesResponse
] = await Promise.all([
clientsProvidersApi.list(companyId, 1, 1000),
invoiceTypesApi.list(1, 1000),
pedimentoCodesApi.list(1, 1000),
materialTypesApi.list(1, 1000),
customsSectionsApi.list(1, 1000),
partsApi.list({ company_id: companyId, page: 1, page_size: 1000 }),
classesApi.getWithFAData({ company_id: companyId, page: 1, page_size: 1000 })
]);
clientsProviders = clientsProvidersResponse.data?.items || [];
invoiceTypes = invoiceTypesResponse.data?.items || [];
pedimentoCodes = pedimentoCodesResponse.data?.items || [];
materialTypes = materialTypesResponse.data?.items || [];
customsSections = customsSectionsResponse.data?.items || [];
partsCatalog = partsResponse.data?.items || [];
classesCatalog = classesResponse.data?.items ?? [];
} catch (error) {
console.error('Error cargando catálogos para Partes descargadas:', error);
toast.error('No se pudieron cargar algunos catálogos');
} finally {
isCatalogLoading = false;
}
}
let providerOptions = $derived.by(() =>
clientsProviders.filter(
(item) => item.client_or_provider === 'provider' || item.client_or_provider === 'both'
)
);
let soldToOptions = $derived.by(() =>
clientsProviders.filter(
(item) => item.client_or_provider === 'client' || item.client_or_provider === 'both'
)
);
let shippedToOptions = $derived.by(() => {
const unique = new Map<number, ClientProvider>();
clientsProviders.forEach((item) => unique.set(item.id, item));
return Array.from(unique.values());
});
let filteredInvoiceTypes = $derived.by(() => {
const op = operationMode === 'importacion' ? 'imp' : 'exp';
return invoiceTypes.filter((item) => !item.operation || item.operation === 'both' || item.operation === op);
});
$effect(() => {
const companyId = companyStore.activeCompany?.id ?? null;
if (!companyId) {
lastCompanyId = null;
bootstrap = null;
clientsProviders = [];
invoiceTypes = [];
pedimentoCodes = [];
materialTypes = [];
customsSections = [];
partsCatalog = [];
classesCatalog = [];
return;
}
if (companyId === lastCompanyId) {
return;
}
lastCompanyId = companyId;
void loadBootstrap(companyId);
void loadCatalogs(companyId);
});
$effect(() => {
if (!filters.invoiceType) {
return;
}
const stillExists = filteredInvoiceTypes.some((item) => item.key === filters.invoiceType);
if (!stillExists) {
filters.invoiceType = '';
}
});
function selectPlaceholder() {
return isCatalogLoading ? 'Cargando...' : 'Selecciona...';
}
function normalizeSelectValue(value?: string) {
return value === CLEAR_SELECT_VALUE ? '' : value ?? '';
}
async function runReport() {
if (!dates.from) {
toast.error('Es necesario asignar la fecha inicial para generar el reporte');
return;
}
if (!dates.to) {
toast.error('Es necesario asignar la fecha final para generar el reporte');
return;
}
if (dates.to < dates.from) {
toast.error('La fecha inicial no puede ser superior a la final');
return;
}
if (classRange.from && !classRange.to) {
toast.error('Es necesario asignar la clase final para generar el reporte');
return;
}
if (!classRange.from && classRange.to) {
toast.error('Es necesario asignar la clase inicial para generar el reporte');
return;
}
const companyId = companyStore.activeCompany?.id;
if (!companyId) {
toast.error('No hay empresa activa seleccionada');
return;
}
const params: DownloadedPartsReportRequest = {
date_from: dates.from,
date_to: dates.to,
class_from: classRange.from || undefined,
class_to: classRange.to || undefined,
print_class_mode: printClassMode,
exchange_rate_mode: exchangeRateMode,
currency_mode: currencyMode,
temporality_mode: temporalityMode,
weight_type_mode: weightTypeMode,
operation_mode: operationMode,
material_type: filters.materialType || undefined,
invoice_type: filters.invoiceType || undefined,
parts: filters.parts ? [filters.parts] : undefined,
pedimento_key: filters.pedimentoKey || undefined,
provider_id: filters.provider ? Number(filters.provider) : undefined,
sold_to_id: filters.soldTo ? Number(filters.soldTo) : undefined,
shipped_to_id: filters.shippedTo ? Number(filters.shippedTo) : undefined,
destination_customs: filters.destinationCustoms || undefined,
include_series: options.includeSeries,
print_class_total: options.printClassTotal,
include_totals_by_fraction: options.includeTotalsByFraction,
julian_date: options.julianDate,
show_item_description: options.showItemDescription,
include_exempt_fraction: options.includeExemptFraction,
show_export_fraction: options.showExportFraction,
include_rule_octava: options.includeRuleOctava,
include_american_fraction_and_country: options.includeAmericanFractionAndCountry,
respect_import_invoice_value_in_pesos: options.respectImportInvoiceValueInPesos,
show_all_temporary_balances: options.showAllTemporaryBalances
};
isGenerating = true;
try {
await downloadedPartsReportsApi.generate(companyId, params);
} catch (err: any) {
const msg: string = err?.message || '';
try {
const parsed = JSON.parse(msg);
if (parsed?.detail?.missing_dates?.length) {
toast.error(
'Faltan tipos de cambio para: ' + parsed.detail.missing_dates.join(', ')
);
return;
}
} catch {
// not JSON
}
toast.error(msg || 'Error al generar el reporte');
} finally {
isGenerating = false;
}
}
function closeView() {
toast.info('Acción cancelar pendiente');
}
</script>
<div
class="animate-in fade-in slide-in-from-bottom-4 flex min-h-full flex-col gap-2 pb-4 duration-500"
>
<div class="flex shrink-0 items-center justify-between px-1">
<div class="flex items-center gap-3">
<h1 class="flex items-center gap-2 text-xl font-bold tracking-tight text-foreground">
<Boxes class="h-6 w-6 text-primary" />
Reporte de clases exportadas / descargadas
</h1>
<span
class="rounded-full border bg-muted px-2 py-0.5 text-[10px] font-medium text-muted-foreground"
>
{bootstrap ? 'CONECTADO' : 'BASE'}
</span>
</div>
<div class="text-xs text-muted-foreground">Reportes de Control Fiscal</div>
</div>
<Separator />
<div class="grid min-h-0 flex-1 grid-cols-1 content-start items-start gap-3 text-foreground xl:grid-cols-3">
<Card.Root class="flex h-full flex-col gap-0 py-0">
<Card.Header class="shrink-0 border-b bg-muted/20 px-3 py-2">
<Card.Title class="flex items-center gap-2 text-sm font-semibold text-primary">
<FileSearch class="h-4 w-4" /> Rango de Fechas y Clases
</Card.Title>
</Card.Header>
<Card.Content class="flex-1 space-y-3 p-3">
<div class="space-y-2 rounded-md border p-3">
<p class="text-xs font-bold text-muted-foreground uppercase">Rango de fechas</p>
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
<div class="space-y-1">
<Label class="text-xs font-bold text-muted-foreground uppercase">Del</Label>
<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="space-y-1">
<Label class="text-xs font-bold text-muted-foreground uppercase">Al</Label>
<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>
</div>
<div class="space-y-2 rounded-md border p-3">
<p class="text-xs font-bold text-muted-foreground uppercase">Rango de clases</p>
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
<div class="space-y-1">
<Label class="text-xs font-bold text-muted-foreground uppercase">De la</Label>
<Select.Root type="single" value={classRange.from} onValueChange={(v) => (classRange.from = normalizeSelectValue(v))}>
<Select.Trigger class="h-8 w-full text-xs">
<span class="truncate">
{#if classRange.from}
{@const selectedClass = classesCatalog.find((item) => item.class_code === classRange.from)}
{selectedClass ? `${selectedClass.class_code} - ${selectedClass.description_es || ''}` : classRange.from}
{:else}
{selectPlaceholder()}
{/if}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
<Select.Item value={CLEAR_SELECT_VALUE}>Sin límite</Select.Item>
{#if classesCatalog.length}
{#each classesCatalog as item}
<Select.Item value={item.class_code}>
{item.class_code} - {item.description_es || item.description_en || ''}
</Select.Item>
{/each}
{:else}
<div class="px-3 py-2 text-xs text-muted-foreground">Sin opciones</div>
{/if}
</Select.Content>
</Select.Root>
</div>
<div class="space-y-1">
<Label class="text-xs font-bold text-muted-foreground uppercase">A la</Label>
<Select.Root type="single" value={classRange.to} onValueChange={(v) => (classRange.to = normalizeSelectValue(v))}>
<Select.Trigger class="h-8 w-full text-xs">
<span class="truncate">
{#if classRange.to}
{@const selectedClass = classesCatalog.find((item) => item.class_code === classRange.to)}
{selectedClass ? `${selectedClass.class_code} - ${selectedClass.description_es || ''}` : classRange.to}
{:else}
{selectPlaceholder()}
{/if}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
<Select.Item value={CLEAR_SELECT_VALUE}>Sin límite</Select.Item>
{#if classesCatalog.length}
{#each classesCatalog as item}
<Select.Item value={item.class_code}>
{item.class_code} - {item.description_es || item.description_en || ''}
</Select.Item>
{/each}
{:else}
<div class="px-3 py-2 text-xs text-muted-foreground">Sin opciones</div>
{/if}
</Select.Content>
</Select.Root>
</div>
</div>
</div>
</Card.Content>
</Card.Root>
<Card.Root class="flex h-full flex-col gap-0 py-0">
<Card.Header class="shrink-0 border-b bg-muted/20 px-3 py-2">
<Card.Title class="flex items-center gap-2 text-sm font-semibold text-primary">
<Filter class="h-4 w-4" /> Filtrar por
</Card.Title>
</Card.Header>
<Card.Content class="flex-1 space-y-3 p-3">
<div class="grid gap-2 md:grid-cols-3">
<div class="space-y-1">
<Label class="text-xs font-bold text-muted-foreground uppercase">Tipo de material</Label>
<Select.Root type="single" value={filters.materialType} onValueChange={(v) => (filters.materialType = normalizeSelectValue(v))}>
<Select.Trigger class="h-8 w-full text-xs">
<span class="truncate">
{#if filters.materialType}
{@const selectedMaterial = materialTypes.find((item) => item.key === filters.materialType)}
{selectedMaterial ? `${selectedMaterial.key} - ${selectedMaterial.description}` : filters.materialType}
{:else}
{selectPlaceholder()}
{/if}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
<Select.Item value={CLEAR_SELECT_VALUE}>Todos</Select.Item>
{#if materialTypes.length}
{#each materialTypes as item}
<Select.Item value={item.key}>{item.key} - {item.description}</Select.Item>
{/each}
{:else}
<div class="px-3 py-2 text-xs text-muted-foreground">Sin opciones</div>
{/if}
</Select.Content>
</Select.Root>
</div>
<div class="space-y-1">
<Label class="text-xs font-bold text-muted-foreground uppercase">Clave pedimento</Label>
<Select.Root type="single" value={filters.pedimentoKey} onValueChange={(v) => (filters.pedimentoKey = normalizeSelectValue(v))}>
<Select.Trigger class="h-8 w-full text-xs">
<span class="truncate">
{#if filters.pedimentoKey}
{@const selectedCode = pedimentoCodes.find((item) => item.code === filters.pedimentoKey)}
{selectedCode ? `${selectedCode.code} - ${selectedCode.description}` : filters.pedimentoKey}
{:else}
{selectPlaceholder()}
{/if}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
<Select.Item value={CLEAR_SELECT_VALUE}>Todos</Select.Item>
{#if pedimentoCodes.length}
{#each pedimentoCodes as item}
<Select.Item value={item.code}>{item.code} - {item.description}</Select.Item>
{/each}
{:else}
<div class="px-3 py-2 text-xs text-muted-foreground">Sin opciones</div>
{/if}
</Select.Content>
</Select.Root>
</div>
<div class="space-y-1">
<Label class="text-xs font-bold text-muted-foreground uppercase">Vendido a</Label>
<Select.Root type="single" value={filters.soldTo} onValueChange={(v) => (filters.soldTo = normalizeSelectValue(v))}>
<Select.Trigger class="h-8 w-full text-xs">
<span class="truncate">
{#if filters.soldTo}
{@const selectedClient = soldToOptions.find((item) => String(item.id) === filters.soldTo)}
{selectedClient?.name || selectPlaceholder()}
{:else}
{selectPlaceholder()}
{/if}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
<Select.Item value={CLEAR_SELECT_VALUE}>Todos</Select.Item>
{#if soldToOptions.length}
{#each soldToOptions as item}
<Select.Item value={String(item.id)}>{item.name}</Select.Item>
{/each}
{:else}
<div class="px-3 py-2 text-xs text-muted-foreground">Sin opciones</div>
{/if}
</Select.Content>
</Select.Root>
</div>
</div>
<div class="grid gap-2 md:grid-cols-3">
<div class="space-y-1">
<Label class="text-xs font-bold text-muted-foreground uppercase">Tipo de factura</Label>
<Select.Root type="single" value={filters.invoiceType} onValueChange={(v) => (filters.invoiceType = normalizeSelectValue(v))}>
<Select.Trigger class="h-8 w-full text-xs">
<span class="truncate">
{#if filters.invoiceType}
{@const selectedType = filteredInvoiceTypes.find((item) => item.key === filters.invoiceType)}
{selectedType ? `${selectedType.key} - ${selectedType.description}` : filters.invoiceType}
{:else}
{selectPlaceholder()}
{/if}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
<Select.Item value={CLEAR_SELECT_VALUE}>Todos</Select.Item>
{#if filteredInvoiceTypes.length}
{#each filteredInvoiceTypes as item}
<Select.Item value={item.key}>{item.key} - {item.description}</Select.Item>
{/each}
{:else}
<div class="px-3 py-2 text-xs text-muted-foreground">Sin opciones</div>
{/if}
</Select.Content>
</Select.Root>
</div>
<div class="space-y-1">
<Label class="text-xs font-bold text-muted-foreground uppercase">Proveedor</Label>
<Select.Root type="single" value={filters.provider} onValueChange={(v) => (filters.provider = normalizeSelectValue(v))}>
<Select.Trigger class="h-8 w-full text-xs">
<span class="truncate">
{#if filters.provider}
{@const selectedProvider = providerOptions.find((item) => String(item.id) === filters.provider)}
{selectedProvider?.name || selectPlaceholder()}
{:else}
{selectPlaceholder()}
{/if}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
<Select.Item value={CLEAR_SELECT_VALUE}>Todos</Select.Item>
{#if providerOptions.length}
{#each providerOptions as item}
<Select.Item value={String(item.id)}>{item.name}</Select.Item>
{/each}
{:else}
<div class="px-3 py-2 text-xs text-muted-foreground">Sin opciones</div>
{/if}
</Select.Content>
</Select.Root>
</div>
<div class="space-y-1">
<Label class="text-xs font-bold text-muted-foreground uppercase">Enviado a</Label>
<Select.Root type="single" value={filters.shippedTo} onValueChange={(v) => (filters.shippedTo = normalizeSelectValue(v))}>
<Select.Trigger class="h-8 w-full text-xs">
<span class="truncate">
{#if filters.shippedTo}
{@const selectedShippedTo = shippedToOptions.find((item) => String(item.id) === filters.shippedTo)}
{selectedShippedTo?.name || selectPlaceholder()}
{:else}
{selectPlaceholder()}
{/if}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
<Select.Item value={CLEAR_SELECT_VALUE}>Todos</Select.Item>
{#if shippedToOptions.length}
{#each shippedToOptions as item}
<Select.Item value={String(item.id)}>{item.name}</Select.Item>
{/each}
{:else}
<div class="px-3 py-2 text-xs text-muted-foreground">Sin opciones</div>
{/if}
</Select.Content>
</Select.Root>
</div>
</div>
<div class="grid gap-2 md:grid-cols-2">
<div class="space-y-1">
<Label class="text-xs font-bold text-muted-foreground uppercase">Partes</Label>
<Select.Root type="single" value={filters.parts} onValueChange={(v) => (filters.parts = normalizeSelectValue(v))}>
<Select.Trigger class="h-8 w-full text-xs">
<span class="truncate">
{#if filters.parts}
{@const selectedPart = partsCatalog.find((item) => item.part_number === filters.parts)}
{selectedPart ? `${selectedPart.part_number} - ${selectedPart.description_spanish || ''}` : filters.parts}
{:else}
{selectPlaceholder()}
{/if}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
<Select.Item value={CLEAR_SELECT_VALUE}>Todos</Select.Item>
{#if partsCatalog.length}
{#each partsCatalog as item}
<Select.Item value={item.part_number}>{item.part_number} - {item.description_spanish || item.description_english || ''}</Select.Item>
{/each}
{:else}
<div class="px-3 py-2 text-xs text-muted-foreground">Sin opciones</div>
{/if}
</Select.Content>
</Select.Root>
</div>
<div class="space-y-1">
<Label class="text-xs font-bold text-muted-foreground uppercase">Destino aduanero</Label>
<Select.Root type="single" value={filters.destinationCustoms} onValueChange={(v) => (filters.destinationCustoms = normalizeSelectValue(v))}>
<Select.Trigger class="h-8 w-full text-xs">
<span class="truncate">
{#if filters.destinationCustoms}
{@const selectedSection = customsSections.find((item) => item.customs_code === filters.destinationCustoms)}
{selectedSection ? `${selectedSection.customs_code} - ${selectedSection.section_name}` : filters.destinationCustoms}
{:else}
{selectPlaceholder()}
{/if}
</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
<Select.Item value={CLEAR_SELECT_VALUE}>Todos</Select.Item>
{#if customsSections.length}
{#each customsSections as item}
<Select.Item value={item.customs_code}>{item.customs_code} - {item.section_name}</Select.Item>
{/each}
{:else}
<div class="px-3 py-2 text-xs text-muted-foreground">Sin opciones</div>
{/if}
</Select.Content>
</Select.Root>
</div>
</div>
<Separator />
<div class="space-y-3">
<div>
<Label class="mb-2 block text-xs font-bold text-muted-foreground uppercase">Imprimir clases</Label>
<RadioGroup.Root bind:value={printClassMode} class="grid grid-cols-2 gap-2">
<div class="flex items-center space-x-2 rounded-md border px-3 py-1.5">
<RadioGroup.Item value="exported" id="print-exported" class="h-4 w-4" />
<Label for="print-exported" class="cursor-pointer text-sm">Exportadas</Label>
</div>
<div class="flex items-center space-x-2 rounded-md border px-3 py-1.5">
<RadioGroup.Item value="downloaded" id="print-downloaded" class="h-4 w-4" />
<Label for="print-downloaded" class="cursor-pointer text-sm">Descargadas</Label>
</div>
</RadioGroup.Root>
</div>
<div>
<Label class="mb-2 block text-xs font-bold text-muted-foreground uppercase">Temporalidad</Label>
<RadioGroup.Root bind:value={temporalityMode} class="grid grid-cols-3 gap-2">
{#each [
{ value: 'temporales', label: 'Temporales' },
{ value: 'definitivos', label: 'Definitivos' },
{ value: 'ambos', label: 'Ambos' }
] as item}
<div class="flex items-center space-x-2 rounded-md border px-3 py-1.5">
<RadioGroup.Item value={item.value} id={`temporality-${item.value}`} class="h-4 w-4" />
<Label for={`temporality-${item.value}`} class="cursor-pointer text-sm">{item.label}</Label>
</div>
{/each}
</RadioGroup.Root>
</div>
</div>
</Card.Content>
</Card.Root>
<Card.Root class="flex h-full flex-col gap-0 py-0">
<Card.Header class="shrink-0 border-b bg-muted/20 px-3 py-2">
<Card.Title class="flex items-center gap-2 text-sm font-semibold text-primary">
<Search class="h-4 w-4" /> Configuración y Salida
</Card.Title>
</Card.Header>
<Card.Content class="space-y-3 p-3">
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
<div class="space-y-2">
<Label class="flex items-center gap-2 text-xs font-bold text-muted-foreground uppercase">
<BadgeDollarSign class="h-3.5 w-3.5" /> Tipo Moneda
</Label>
<RadioGroup.Root bind:value={currencyMode} class="flex flex-col gap-2">
{#each [
{ value: 'dollars', label: 'Dólares' },
{ value: 'pesos', label: 'Pesos' },
{ value: 'both', label: 'Ambos' }
] as item}
<div class="flex items-center space-x-2 rounded-md border px-3 py-1.5">
<RadioGroup.Item value={item.value} id={`currency-${item.value}`} class="h-4 w-4" />
<Label for={`currency-${item.value}`} class="cursor-pointer text-sm">{item.label}</Label>
</div>
{/each}
</RadioGroup.Root>
</div>
<div class="space-y-2">
<Label class="flex items-center gap-2 text-xs font-bold text-muted-foreground uppercase">
<Scale class="h-3.5 w-3.5" /> Tipo Cambio
</Label>
<RadioGroup.Root bind:value={exchangeRateMode} class="flex flex-col gap-2">
<div class="flex items-center space-x-2 rounded-md border px-3 py-1.5">
<RadioGroup.Item value="invoice" id="exchange-invoice" class="h-4 w-4" />
<Label for="exchange-invoice" class="cursor-pointer text-sm">Factura</Label>
</div>
<div class="flex items-center space-x-2 rounded-md border px-3 py-1.5">
<RadioGroup.Item value="pedimento_payment" id="exchange-payment" class="h-4 w-4" />
<Label for="exchange-payment" class="cursor-pointer text-sm">Pago de Pedimento</Label>
</div>
</RadioGroup.Root>
</div>
<div class="space-y-2">
<Label class="flex items-center gap-2 text-xs font-bold text-muted-foreground uppercase">
<Package class="h-3.5 w-3.5" /> Tipo Peso
</Label>
<RadioGroup.Root bind:value={weightTypeMode} class="flex flex-col gap-2">
{#each [
{ value: 'kilos', label: 'Kilos' },
{ value: 'libras', label: 'Libras' },
{ value: 'ambos', label: 'Ambos' }
] as item}
<div class="flex items-center space-x-2 rounded-md border px-3 py-1.5">
<RadioGroup.Item value={item.value} id={`weight-${item.value}`} class="h-4 w-4" />
<Label for={`weight-${item.value}`} class="cursor-pointer text-sm">{item.label}</Label>
</div>
{/each}
</RadioGroup.Root>
</div>
<div class="space-y-2">
<Label class="flex items-center gap-2 text-xs font-bold text-muted-foreground uppercase">
<Clock3 class="h-3.5 w-3.5" /> Operación
</Label>
<RadioGroup.Root bind:value={operationMode} class="flex flex-col gap-2">
<div class="flex items-center space-x-2 rounded-md border px-3 py-1.5">
<RadioGroup.Item value="importacion" id="operation-import" class="h-4 w-4" />
<Label for="operation-import" class="cursor-pointer text-sm">Importación</Label>
</div>
<div class="flex items-center space-x-2 rounded-md border px-3 py-1.5">
<RadioGroup.Item value="exportacion" id="operation-export" class="h-4 w-4" />
<Label for="operation-export" class="cursor-pointer text-sm">Exportación</Label>
</div>
</RadioGroup.Root>
</div>
</div>
</Card.Content>
</Card.Root>
<Card.Root class="gap-0 py-0 xl:col-span-3">
<Card.Header class="shrink-0 border-b bg-muted/20 px-3 py-2">
<Card.Title class="text-sm font-semibold text-primary">Opciones</Card.Title>
</Card.Header>
<Card.Content class="p-3">
<div class="grid grid-cols-1 gap-2 lg:grid-cols-3">
{#each allOptions as option}
<div class="flex items-center space-x-2 rounded-md border px-3 py-1.5">
<Checkbox id={option.id} bind:checked={options[option.key]} class="h-4 w-4" />
<Label for={option.id} class="cursor-pointer text-sm">{option.label}</Label>
</div>
{/each}
</div>
</Card.Content>
<Card.Footer class="gap-2 border-t bg-muted/10 p-2.5">
<Button class="h-9 flex-1 text-sm shadow-sm" size="default" onclick={runReport} disabled={isGenerating}>
<Printer class="mr-2 h-3.5 w-3.5" />
{isGenerating ? 'Generando...' : 'Imprimir'}
</Button>
<Button
variant="ghost"
size="icon"
class="h-9 w-9 shrink-0 text-muted-foreground hover:text-destructive"
onclick={closeView}
>
<X class="h-4 w-4" />
</Button>
</Card.Footer>
</Card.Root>
</div>
</div>

View File

@@ -608,10 +608,6 @@ export function getSidebarData(): SidebarData {
title: m["sidebar.reports.invoices"](),
url: "/dashboard/reports/invoices",
},
{
title: m["sidebar.reports.downloaded_parts"](),
url: "/dashboard/reports/partes-descargadas",
},
{
title: m["sidebar.reports.expiration"](),
url: "/dashboard/reports/vencimiento",