Se movio la parte de descargas del sidbar al botoncito en reportes
This commit is contained in:
@@ -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>
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user