Merge branch 'development' of https://git.aduanasoft.com/ADUANASOFT/anexo76 into feature/partePais_BOM

This commit is contained in:
2026-03-13 11:13:44 -05:00
84 changed files with 33338 additions and 1104 deletions

View File

@@ -1,64 +1,84 @@
import type { PaginatedResponse } from '$lib/types';
import { api } from '$lib/api';
/** Contexto de uso: Fixed Asset (activo fijo) o inventario */
export type LocationSystem = 'fixed_asset' | 'inventory';
export interface Location {
id: number;
location_code: string;
location_description: string | null;
company_id: number;
tenant_id: number;
clave_localizacion: string;
localizacion: string | null;
system: LocationSystem;
}
export interface LocationCreate {
location_code: string;
location_description?: string | null;
clave_localizacion: string;
localizacion?: string | null;
system: LocationSystem;
/** Used when system === 'fixed_asset' */
department?: string | null;
responsible?: string | null;
observations?: string | null;
}
export interface LocationUpdate {
location_description?: string | null;
clave_localizacion?: string;
localizacion?: string | null;
system?: LocationSystem;
}
export interface LocationListResponse extends PaginatedResponse {
export interface LocationListResponse {
items: Location[];
total: number;
page: number;
page_size: number;
}
export interface LocationFilters {
location_code?: string;
location_description?: string;
clave_localizacion?: string;
localizacion?: string;
system?: LocationSystem;
page?: number;
page_size?: number;
}
import { portsApi, PortType } from './ports';
const BASE_URL = '/v1/a76/locations';
function buildQuery(companyId: number, params?: LocationFilters): string {
const search = new URLSearchParams();
search.set('company_id', String(companyId));
if (params?.page != null) search.set('page', String(params.page));
if (params?.page_size != null) search.set('page_size', String(params.page_size));
if (params?.clave_localizacion) search.set('clave_localizacion', params.clave_localizacion);
if (params?.localizacion) search.set('localizacion', params.localizacion);
if (params?.system) search.set('system', params.system);
return search.toString();
}
export async function getLocations(
companyId: number,
filters?: LocationFilters
): Promise<LocationListResponse> {
const res = await portsApi.list(companyId, filters || {});
return (res.data || res) as unknown as LocationListResponse;
const q = buildQuery(companyId, filters);
const res = await api.get<LocationListResponse>(`${BASE_URL}/?${q}`);
return (res.data ?? res) as LocationListResponse;
}
export async function getLocation(
locationId: number,
companyId: number
): Promise<Location> {
const res = await portsApi.get(locationId, companyId);
return (res.data || res) as unknown as Location;
const q = new URLSearchParams({ company_id: String(companyId) });
const res = await api.get<Location>(`${BASE_URL}/${locationId}?${q}`);
return (res.data ?? res) as Location;
}
export async function createLocation(
data: LocationCreate,
companyId: number
): Promise<Location> {
const res = await portsApi.create({
port_code: data.location_code,
location_code: data.location_code,
description: null,
location_description: data.location_description || null,
port_type: PortType.ENTRY
}, companyId);
return (res.data || res) as unknown as Location;
const q = new URLSearchParams({ company_id: String(companyId) });
const res = await api.post<Location>(`${BASE_URL}/?${q}`, data);
return (res.data ?? res) as Location;
}
export async function updateLocation(
@@ -66,15 +86,15 @@ export async function updateLocation(
data: LocationUpdate,
companyId: number
): Promise<Location> {
const res = await portsApi.update(locationId, {
location_description: data.location_description
}, companyId);
return (res.data || res) as unknown as Location;
const q = new URLSearchParams({ company_id: String(companyId) });
const res = await api.put<Location>(`${BASE_URL}/${locationId}/?${q}`, data);
return (res.data ?? res) as Location;
}
export async function deleteLocation(
locationId: number,
companyId: number
): Promise<void> {
await portsApi.delete(locationId, companyId);
}
const q = new URLSearchParams({ company_id: String(companyId) });
await api.delete(`${BASE_URL}/${locationId}?${q}`);
}

View File

@@ -491,5 +491,29 @@ export const invoicesApi = {
});
return api.delete(`/v1/a76/invoices/${invoiceId}/collections/${collectionId}/?${params.toString()}`);
}
},
processInvoice: (invoiceId: number, companyId: number) => {
const params = new URLSearchParams({
company_id: companyId.toString()
});
return api.post<{ task_id: string }>(
`/v1/a76/invoices/${invoiceId}/process?${params.toString()}`,
{}
);
},
getProcessStatus: (taskId: string) => {
return api.get<{
state: 'PROCESSING' | 'SUCCESS' | 'FAILURE';
info?: { current: number; status: string };
result?: {
status: 'success' | 'validation_error' | 'error';
invoice_id?: number;
message?: string;
errors?: Array<{ field: string; message: string; code?: string; solution?: string[] }>;
sql_errors?: Array<{ consecutive: number; error: string }>;
};
}>(`/v1/a76/invoices/process/${taskId}/status`);
}
};

View File

@@ -156,8 +156,12 @@ export interface Item {
// Permits
permit_number?: string;
page_line?: string;
has_fda_code?: boolean;
fda_key?: string;
fcc_key?: string;
has_certificate?: boolean;
certificate_number?: string;
certificate_end_date?: string;
octave_permit?: string;
// Flags
@@ -168,8 +172,14 @@ export interface Item {
// Payment
payment_method?: string;
igi_payment_method?: string;
igi_amount?: number;
// Export valuation (Met Valor, Valor Det., Motivo De Uso)
valuation_method?: string;
valuation_determined_value?: number;
valuation_reason?: string;
// Additional notes
wildcard_field?: string;

View File

@@ -6,7 +6,8 @@
import {
createLocation,
updateLocation,
type Location
type Location,
type LocationSystem
} from '$lib/api/dashboard/a76/general_catalogs/locations';
import { companyStore } from '$lib/stores/company.svelte';
import { obtenerAtajosFormularioLocalidades } from '$lib/config/shortcuts/dashboard/general_catalogs/locations/edit';
@@ -21,14 +22,18 @@
onSuccess?: () => void;
} = $props();
// Atajos
const isEdit = $derived(!!item);
const title = $derived(isEdit ? 'Editar Ubicación' : 'Nueva Ubicación');
const systemOptions: { value: LocationSystem; label: string }[] = [
{ value: 'fixed_asset', label: 'Activo fijo (FA)' },
{ value: 'inventory', label: 'Inventario' }
];
let formData = $state({
location_code: '',
location_description: ''
clave_localizacion: '',
localizacion: '',
system: 'fixed_asset' as LocationSystem
});
let loading = $state(false);
@@ -38,11 +43,16 @@
if (open) {
if (item) {
formData = {
location_code: item.location_code || '',
location_description: item.location_description || ''
clave_localizacion: item.clave_localizacion ?? '',
localizacion: item.localizacion ?? '',
system: item.system ?? 'fixed_asset'
};
} else {
formData = { location_code: '', location_description: '' };
formData = {
clave_localizacion: '',
localizacion: '',
system: 'fixed_asset'
};
}
error = null;
}
@@ -55,20 +65,31 @@
const companyId = companyStore.activeCompany?.id;
if (!companyId) throw new Error('No hay una compañía seleccionada');
if (!formData.location_code.trim()) throw new Error('El código es requerido');
if (!formData.clave_localizacion.trim()) throw new Error('La clave es requerida');
const basePayload = {
location_description: formData.location_description?.trim() || null
localizacion: formData.localizacion?.trim() || null
};
if (isEdit && item) {
await updateLocation(item.id, basePayload, companyId);
await updateLocation(
item.id,
{
...basePayload,
clave_localizacion: formData.clave_localizacion.trim(),
system: formData.system
},
companyId
);
} else {
const createPayload = {
location_code: formData.location_code.trim(),
...basePayload
};
await createLocation(createPayload, companyId);
await createLocation(
{
clave_localizacion: formData.clave_localizacion.trim(),
...basePayload,
system: formData.system
},
companyId
);
}
open = false;
@@ -107,12 +128,12 @@
<div class="grid gap-4">
<div class="grid grid-cols-4 items-center gap-4">
<Label for="location_code" class="text-right">Código *</Label>
<Label for="clave_localizacion" class="text-right">Clave *</Label>
<div class="col-span-3">
<Input
id="location_code"
bind:value={formData.location_code}
maxlength={4}
id="clave_localizacion"
bind:value={formData.clave_localizacion}
maxlength={20}
disabled={loading || isEdit}
required
/>
@@ -120,16 +141,32 @@
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="location_description" class="text-right">Descripción</Label>
<Label for="localizacion" class="text-right">Localización</Label>
<div class="col-span-3">
<Input
id="location_description"
bind:value={formData.location_description}
maxlength={20}
id="localizacion"
bind:value={formData.localizacion}
maxlength={200}
disabled={loading}
/>
</div>
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="system" class="text-right">Sistema</Label>
<div class="col-span-3">
<select
id="system"
bind:value={formData.system}
disabled={loading}
class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
>
{#each systemOptions as opt}
<option value={opt.value}>{opt.label}</option>
{/each}
</select>
</div>
</div>
</div>
<Dialog.Footer>

View File

@@ -10,10 +10,12 @@
let {
open = $bindable(false),
regimen = 'Temporal',
operationType = 'imp' as 'imp' | 'exp',
onSelect
}: {
open: boolean;
regimen: string;
regimen?: string;
operationType?: 'imp' | 'exp';
onSelect: (invoice: Invoice) => void;
} = $props();
@@ -25,15 +27,16 @@
if (!companyStore.activeCompany) return;
loading = true;
try {
let filters: any = {
operation_type: 'imp',
invoice_number: searchTerm
const filters: any = {
operation_type: operationType,
invoice_number: searchTerm || undefined
};
if (regimen === 'Temporal' || regimen === 'TEMPORAL SCAF') {
filters.invoice_type = 'TEM';
} else if (regimen === 'Definitiva' || regimen === 'DEFINITIVO SCAF') {
filters.invoice_type = 'DEF';
if (operationType === 'imp' && regimen) {
if (regimen === 'Temporal' || regimen === 'TEMPORAL SCAF') {
filters.invoice_type = 'TEM';
} else if (regimen === 'Definitiva' || regimen === 'DEFINITIVO SCAF') {
filters.invoice_type = 'DEF';
}
}
const res = await invoicesApi.list(companyStore.activeCompany.id, 1, 50, filters);
@@ -60,12 +63,18 @@
});
</script>
<Dialog.Root bind:open>
<Dialog.Root bind:open>
<Dialog.Content class="max-w-2xl">
<Dialog.Header>
<Dialog.Title>Seleccionar Factura ({regimen})</Dialog.Title>
<Dialog.Title>
{operationType === 'exp' ? 'Seleccionar Factura de Exportación' : `Seleccionar Factura (${regimen})`}
</Dialog.Title>
<Dialog.Description>
Busca y selecciona una factura del catálogo de importación para el régimen {regimen}.
{#if operationType === 'exp'}
Busca y selecciona una factura del catálogo de exportación.
{:else}
Busca y selecciona una factura del catálogo de importación para el régimen {regimen}.
{/if}
</Dialog.Description>
</Dialog.Header>

View File

@@ -48,6 +48,12 @@
(lineItem as any).part_number_display = part.part_number;
(lineItem as any).part_description_es = part.description_spanish;
(lineItem as any).part_description_en = part.description_english;
if (!lineItem.fda_key && part.fda_key) {
lineItem.fda_key = part.fda_key;
}
if (!lineItem.fcc_key && part.fcc_key) {
lineItem.fcc_key = part.fcc_key;
}
}
</script>

View File

@@ -1,13 +1,21 @@
<script lang="ts">
import * as Sheet from '$lib/components/ui/sheet';
import * as Tabs from '$lib/components/ui/tabs';
import * as Select from '$lib/components/ui/select';
import * as Dialog from '$lib/components/ui/dialog';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { RadioGroup, RadioGroupItem } from '$lib/components/ui/radio-group';
import { Separator } from '$lib/components/ui/separator';
import { Loader2, Package, Save, X, FileText } from 'lucide-svelte';
import { Loader2, Package, Save, X, FileText, Folder } from 'lucide-svelte';
import type { Invoice } from '$lib/api/dashboard/a76/invoices';
import type { Item } from '$lib/api/dashboard/a76/items';
import { invoicesApi } from '$lib/api/dashboard/a76/invoices';
import { itemsApi, type Item } from '$lib/api/dashboard/a76/items';
import { companyStore } from '$lib/stores/company.svelte';
// Child components
import InvoiceSelectorModal from '../../InvoiceSelectorModal.svelte';
import MainData from './main-data.svelte';
import ItemConfiguration from './item-configuration.svelte';
import PackagesSection from './packages-section.svelte';
@@ -16,12 +24,15 @@
import TabSeries from './tab-series.svelte';
import TabLabeling from './tab-labeling.svelte';
import TabIdentifiers from './tab-identifiers.svelte';
import { getVisibility } from '$lib/config/invoice-item-visibility';
let {
open = $bindable(),
isEditMode = false,
editingItem = $bindable(),
invoice,
invoiceType = undefined,
operationType = undefined,
onSave,
onCancel,
isTargetingPreset = false,
@@ -31,6 +42,8 @@
isEditMode?: boolean;
editingItem: Partial<Item>;
invoice: Invoice | null;
invoiceType?: string | null;
operationType?: string | number | null;
onSave: () => void;
onCancel?: () => void;
isTargetingPreset?: boolean;
@@ -46,6 +59,172 @@
if (!Array.isArray(editingItem.series)) {
editingItem.series = editingItem.series != null ? [editingItem.series] : [];
}
if (!editingItem.fa_data) {
editingItem.fa_data = {};
}
if (editingItem.fa_data.own_equipment === undefined) {
editingItem.fa_data.own_equipment = false;
}
if (editingItem.fa_data.omit_annex31 === undefined) {
editingItem.fa_data.omit_annex31 = false;
}
}
});
// Selectores de factura (FK): estado y líneas
let showImportInvoiceModal = $state(false);
let showExportInvoiceModal = $state(false);
let showExportLinePicker = $state(false);
let showImportLinePicker = $state(false);
let selectedImportInvoiceId = $state<number | null>(null);
let selectedExportInvoiceId = $state<number | null>(null);
let importInvoiceLines = $state<Item[]>([]);
let exportInvoiceLines = $state<Item[]>([]);
let loadingImportLines = $state(false);
let loadingExportLines = $state(false);
async function loadImportLines(invoiceId: number) {
const companyId = companyStore?.activeCompany?.id;
if (!companyId) return;
loadingImportLines = true;
try {
const res = await itemsApi.listByInvoice(invoiceId, companyId);
importInvoiceLines = res.data?.items ?? [];
} catch {
importInvoiceLines = [];
} finally {
loadingImportLines = false;
}
}
async function loadExportLines(invoiceId: number) {
const companyId = companyStore?.activeCompany?.id;
if (!companyId) return;
loadingExportLines = true;
try {
const res = await itemsApi.listByInvoice(invoiceId, companyId);
exportInvoiceLines = res.data?.items ?? [];
} catch {
exportInvoiceLines = [];
} finally {
loadingExportLines = false;
}
}
function handleSelectImportInvoice(inv: Invoice) {
editingItem.fa_data = editingItem.fa_data || {};
editingItem.fa_data.search_invoice = inv.invoice_number ?? '';
editingItem.fa_data.movement_type_import = (inv.invoice_type as string) || 'TEM';
editingItem.fa_data.search_line = undefined;
selectedImportInvoiceId = inv.id ?? null;
if (selectedImportInvoiceId) loadImportLines(selectedImportInvoiceId);
}
function handleSelectExportInvoice(inv: Invoice) {
editingItem.fa_data = editingItem.fa_data || {};
editingItem.fa_data.search_invoice = inv.invoice_number ?? '';
editingItem.fa_data.search_line = undefined;
selectedExportInvoiceId = inv.id ?? null;
if (selectedExportInvoiceId) loadExportLines(selectedExportInvoiceId);
}
const visibility = $derived.by(() => getVisibility(invoiceType ?? invoice?.invoice_type, operationType ?? invoice?.operation_type));
/** Show link-to-import block for import (CR tracking) or for export when showExportLinkToImportBlock. */
const showLinkToImportBlock = $derived.by(() => {
const normalizedOperationType = operationType ?? invoice?.operation_type;
if (normalizedOperationType === 1 || normalizedOperationType === 'exp') {
return visibility.showExportLinkToImportBlock;
}
return visibility.showCrTrackingHeader;
});
/** Show repair-import block (Factura de Expo / Línea de Expo) for REP/REPAR. */
const showRepairBlock = $derived.by(() => {
const op = operationType ?? invoice?.operation_type;
if (op === 1 || op === 'exp') return false;
return visibility.showRepairLinkToExportBlock;
});
// Resolver invoice id desde search_invoice al abrir el sheet (para cargar líneas)
$effect(() => {
if (!open || !companyStore?.activeCompany?.id || !editingItem?.fa_data) return;
const num = editingItem.fa_data.search_invoice;
const movementType = editingItem.fa_data.movement_type_import;
if (showLinkToImportBlock && num && !selectedImportInvoiceId && !loadingImportLines) {
(async () => {
try {
const res = await invoicesApi.list(companyStore.activeCompany!.id, 1, 5, {
operation_type: 'imp',
invoice_number: num,
invoice_type: movementType === 'DEF' ? 'DEF' : 'TEM'
});
const items = res.data?.items ?? [];
const inv = items.find((i: Invoice) => i.invoice_number === num);
if (inv?.id) {
selectedImportInvoiceId = inv.id;
await loadImportLines(inv.id);
}
} catch {
// ignore
}
})();
}
if (showRepairBlock && num && !selectedExportInvoiceId && !loadingExportLines) {
(async () => {
try {
const res = await invoicesApi.list(companyStore.activeCompany!.id, 1, 5, {
operation_type: 'exp',
invoice_number: num
});
const items = res.data?.items ?? [];
const inv = items.find((i: Invoice) => i.invoice_number === num);
if (inv?.id) {
selectedExportInvoiceId = inv.id;
await loadExportLines(inv.id);
}
} catch {
// ignore
}
})();
}
});
$effect(() => {
if (!open) {
selectedImportInvoiceId = null;
selectedExportInvoiceId = null;
importInvoiceLines = [];
exportInvoiceLines = [];
}
});
const importRegimenLabel = $derived(
(editingItem.fa_data?.movement_type_import?.toUpperCase() === 'DEF' ? 'Definitiva' : 'Temporal')
);
const showCrTrackingBlock = $derived.by(() => {
const normalizedOperationType = operationType ?? invoice?.operation_type;
if (normalizedOperationType === 1 || normalizedOperationType === 'exp') {
return false;
}
return visibility.showCrTrackingHeader;
});
const isExport = $derived.by(() => {
const op = operationType ?? invoice?.operation_type;
return op === 1 || op === 'exp';
});
const visibleTabs = $derived.by(() => [
{ value: 'generales', label: 'General', visible: true },
{ value: 'continuacion', label: 'Continuación', visible: true },
{ value: 'series', label: 'Series', visible: true },
{ value: 'etiquetado', label: 'Etiquetado', visible: true },
{ value: 'identificadores', label: 'IDs', visible: visibility.showIdentifiersTab }
].filter((tab) => tab.visible));
const tabListStyle = $derived(`grid-template-columns: repeat(${visibleTabs.length || 1}, minmax(0, 1fr));`);
let activeTab = $state('generales');
$effect(() => {
if (!visibleTabs.some((tab) => tab.value === activeTab)) {
activeTab = visibleTabs[0]?.value || 'generales';
}
});
</script>
@@ -78,6 +257,214 @@
<div class="space-y-2">
{#if line}
{#if showRepairBlock}
<!-- Importación de Reparación: Genera Descarga? + Factura de Expo + Línea de Expo -->
<div class="space-y-3 rounded-md border border-zinc-200 bg-white p-3 dark:border-zinc-800 dark:bg-zinc-900">
<div class="space-y-1.5">
<Label class="text-xs font-medium text-muted-foreground">Genera Descarga?</Label>
<RadioGroup
value={editingItem.fa_data?.download === true ? 'si' : 'no'}
onValueChange={(v) => {
editingItem.fa_data = editingItem.fa_data || {};
editingItem.fa_data.download = v === 'si';
}}
class="flex gap-4"
>
<div class="flex items-center gap-2">
<RadioGroupItem value="si" id="fa_rep_download_si" />
<Label for="fa_rep_download_si" class="text-xs cursor-pointer"></Label>
</div>
<div class="flex items-center gap-2">
<RadioGroupItem value="no" id="fa_rep_download_no" />
<Label for="fa_rep_download_no" class="text-xs cursor-pointer">No</Label>
</div>
</RadioGroup>
</div>
<!-- Fila ligada: selector de factura (FK) + línea (FK), estilo Input + Folder -->
<div class="flex flex-wrap items-end gap-3">
<div class="min-w-[120px] flex-1 space-y-1">
<Label class="text-xs">Factura de Expo</Label>
<div class="flex gap-1">
<Input
readonly
value={editingItem.fa_data?.search_invoice || ''}
class="h-8 flex-1 bg-muted cursor-pointer text-sm"
placeholder="Seleccionar factura..."
onclick={() => (showExportInvoiceModal = true)}
/>
<Button
type="button"
variant="outline"
size="icon"
class="h-8 w-8 shrink-0"
onclick={() => (showExportInvoiceModal = true)}
>
<Folder class="h-4 w-4" />
</Button>
</div>
</div>
<div class="min-w-[90px] flex-1 space-y-1">
<Label for="fa_rep_search_line" class="text-xs">Línea de Expo</Label>
<div class="flex gap-1">
<Input
id="fa_rep_search_line"
readonly
value={editingItem.fa_data?.search_line != null ? String(editingItem.fa_data.search_line) : ''}
class="h-8 flex-1 bg-muted cursor-pointer text-sm"
placeholder={loadingExportLines ? 'Cargando...' : 'Línea'}
disabled={!selectedExportInvoiceId || loadingExportLines}
onclick={() => selectedExportInvoiceId && !loadingExportLines && (showExportLinePicker = true)}
/>
<Button
type="button"
variant="outline"
size="icon"
class="h-8 w-8 shrink-0"
disabled={!selectedExportInvoiceId || loadingExportLines}
onclick={() => (showExportLinePicker = true)}
>
<Folder class="h-4 w-4" />
</Button>
</div>
</div>
<div class="min-w-[100px] flex-1 space-y-1">
<Label for="fa_rep_search_type" class="text-xs">Tipo Búsqueda</Label>
<Select.Root
type="single"
value={editingItem.fa_data?.search_type || ''}
onValueChange={(v) => {
editingItem.fa_data = editingItem.fa_data || {};
editingItem.fa_data.search_type = v ?? undefined;
}}
>
<Select.Trigger id="fa_rep_search_type" class="h-8 text-sm">
<span class="truncate">
{editingItem.fa_data?.search_type || 'Seleccionar...'}
</span>
</Select.Trigger>
<Select.Content>
<Select.Item value="Factura">Factura</Select.Item>
<Select.Item value="NumParte">NumParte</Select.Item>
</Select.Content>
</Select.Root>
</div>
</div>
</div>
{:else if showLinkToImportBlock}
<div class="space-y-3 rounded-md border border-zinc-200 bg-white p-3 dark:border-zinc-800 dark:bg-zinc-900">
<div class="space-y-1.5">
<Label class="text-xs font-medium text-muted-foreground">Genera Descarga?</Label>
<RadioGroup
value={editingItem.fa_data?.download === true ? 'si' : 'no'}
onValueChange={(v) => {
editingItem.fa_data = editingItem.fa_data || {};
editingItem.fa_data.download = v === 'si';
}}
class="flex gap-4"
>
<div class="flex items-center gap-2">
<RadioGroupItem value="si" id="fa_download_si" />
<Label for="fa_download_si" class="text-xs cursor-pointer"></Label>
</div>
<div class="flex items-center gap-2">
<RadioGroupItem value="no" id="fa_download_no" />
<Label for="fa_download_no" class="text-xs cursor-pointer">No</Label>
</div>
</RadioGroup>
</div>
<!-- Fila ligada: selector de factura (FK) + línea (FK) -->
<div class="flex flex-wrap items-end gap-3">
<div class="min-w-[100px] flex-1 space-y-1">
<Label class="text-xs">Tipo Importación</Label>
<Select.Root
type="single"
value={editingItem.fa_data?.movement_type_import || 'TEM'}
onValueChange={(v) => {
editingItem.fa_data = editingItem.fa_data || {};
editingItem.fa_data.movement_type_import = v || 'TEM';
selectedImportInvoiceId = null;
importInvoiceLines = [];
}}
>
<Select.Trigger class="h-8 text-sm">
<span>{editingItem.fa_data?.movement_type_import === 'DEF' ? 'DEF' : 'TEM'}</span>
</Select.Trigger>
<Select.Content>
<Select.Item value="TEM">TEM (Temporal)</Select.Item>
<Select.Item value="DEF">DEF (Definitiva)</Select.Item>
</Select.Content>
</Select.Root>
</div>
<div class="min-w-[120px] flex-1 space-y-1">
<Label class="text-xs">Factura Impo</Label>
<div class="flex gap-1">
<Input
readonly
value={editingItem.fa_data?.search_invoice || ''}
class="h-8 flex-1 bg-muted cursor-pointer text-sm"
placeholder="Seleccionar factura..."
onclick={() => (showImportInvoiceModal = true)}
/>
<Button
type="button"
variant="outline"
size="icon"
class="h-8 w-8 shrink-0"
onclick={() => (showImportInvoiceModal = true)}
>
<Folder class="h-4 w-4" />
</Button>
</div>
</div>
<div class="min-w-[90px] flex-1 space-y-1">
<Label for="fa_search_line" class="text-xs">Línea</Label>
<div class="flex gap-1">
<Input
id="fa_search_line"
readonly
value={editingItem.fa_data?.search_line != null ? String(editingItem.fa_data.search_line) : ''}
class="h-8 flex-1 bg-muted cursor-pointer text-sm"
placeholder={loadingImportLines ? 'Cargando...' : 'Línea'}
disabled={!selectedImportInvoiceId || loadingImportLines}
onclick={() => selectedImportInvoiceId && !loadingImportLines && (showImportLinePicker = true)}
/>
<Button
type="button"
variant="outline"
size="icon"
class="h-8 w-8 shrink-0"
disabled={!selectedImportInvoiceId || loadingImportLines}
onclick={() => (showImportLinePicker = true)}
>
<Folder class="h-4 w-4" />
</Button>
</div>
</div>
<div class="min-w-[100px] flex-1 space-y-1">
<Label for="fa_search_type" class="text-xs">Tipo Búsqueda</Label>
<Select.Root
type="single"
value={editingItem.fa_data?.search_type || ''}
onValueChange={(v) => {
editingItem.fa_data = editingItem.fa_data || {};
editingItem.fa_data.search_type = v ?? undefined;
}}
>
<Select.Trigger id="fa_search_type" class="h-8 text-sm">
<span class="truncate">
{editingItem.fa_data?.search_type || 'Seleccionar...'}
</span>
</Select.Trigger>
<Select.Content>
<Select.Item value="Factura">Factura</Select.Item>
<Select.Item value="NumParte">NumParte</Select.Item>
</Select.Content>
</Select.Root>
</div>
</div>
</div>
{/if}
<div class="grid grid-cols-1 lg:grid-cols-12 gap-2">
<div class="lg:col-span-8">
<div class="bg-white dark:bg-zinc-900 rounded border border-zinc-200 dark:border-zinc-800 overflow-hidden">
@@ -111,23 +498,13 @@
</div>
</div>
<Tabs.Root value="generales" class="w-full">
<Tabs.List class="grid w-full grid-cols-5 bg-zinc-100 dark:bg-zinc-800/50 rounded p-0.5 gap-0.5">
<Tabs.Trigger value="generales" class="text-xs font-medium py-1.5 px-2 rounded transition-all data-[state=active]:bg-white dark:data-[state=active]:bg-zinc-700 data-[state=active]:shadow-sm data-[state=active]:text-black dark:data-[state=active]:text-white">
General
</Tabs.Trigger>
<Tabs.Trigger value="continuacion" class="text-xs font-medium py-1.5 px-2 rounded transition-all data-[state=active]:bg-white dark:data-[state=active]:bg-zinc-700 data-[state=active]:shadow-sm data-[state=active]:text-black dark:data-[state=active]:text-white">
Continuación
</Tabs.Trigger>
<Tabs.Trigger value="series" class="text-xs font-medium py-1.5 px-2 rounded transition-all data-[state=active]:bg-white dark:data-[state=active]:bg-zinc-700 data-[state=active]:shadow-sm data-[state=active]:text-black dark:data-[state=active]:text-white">
Series
</Tabs.Trigger>
<Tabs.Trigger value="etiquetado" class="text-xs font-medium py-1.5 px-2 rounded transition-all data-[state=active]:bg-white dark:data-[state=active]:bg-zinc-700 data-[state=active]:shadow-sm data-[state=active]:text-black dark:data-[state=active]:text-white">
Etiquetado
</Tabs.Trigger>
<Tabs.Trigger value="identificadores" class="text-xs font-medium py-1.5 px-2 rounded transition-all data-[state=active]:bg-white dark:data-[state=active]:bg-zinc-700 data-[state=active]:shadow-sm data-[state=active]:text-black dark:data-[state=active]:text-white">
IDs
</Tabs.Trigger>
<Tabs.Root bind:value={activeTab} class="w-full">
<Tabs.List class="grid w-full gap-0.5 rounded bg-zinc-100 p-0.5 dark:bg-zinc-800/50" style={tabListStyle}>
{#each visibleTabs as tab}
<Tabs.Trigger value={tab.value} class="text-xs font-medium py-1.5 px-2 rounded transition-all data-[state=active]:bg-white dark:data-[state=active]:bg-zinc-700 data-[state=active]:shadow-sm data-[state=active]:text-black dark:data-[state=active]:text-white">
{tab.label}
</Tabs.Trigger>
{/each}
</Tabs.List>
<div class="mt-1.5">
@@ -154,6 +531,7 @@
<TabContinuation
bind:lineItem={editingItem}
bind:descriptions={editingItem.description!}
visibility={visibility}
/>
</Tabs.Content>
@@ -170,9 +548,11 @@
<TabLabeling bind:descriptions={editingItem.description!} />
</Tabs.Content>
<Tabs.Content value="identificadores" class="m-0 focus-visible:outline-none">
<TabIdentifiers bind:lineItem={editingItem} />
</Tabs.Content>
{#if visibility.showIdentifiersTab}
<Tabs.Content value="identificadores" class="m-0 focus-visible:outline-none">
<TabIdentifiers bind:lineItem={editingItem} />
</Tabs.Content>
{/if}
</div>
</Tabs.Root>
{:else}
@@ -202,4 +582,66 @@
</footer>
</Sheet.Content>
</Sheet.Root>
</Sheet.Root>
<InvoiceSelectorModal
bind:open={showImportInvoiceModal}
regimen={importRegimenLabel}
operationType="imp"
onSelect={handleSelectImportInvoice}
/>
<InvoiceSelectorModal
bind:open={showExportInvoiceModal}
operationType="exp"
onSelect={handleSelectExportInvoice}
/>
<!-- Diálogo para elegir línea (Expo) -->
<Dialog.Root bind:open={showExportLinePicker}>
<Dialog.Content class="max-w-sm">
<Dialog.Header>
<Dialog.Title class="text-sm">Seleccionar línea</Dialog.Title>
</Dialog.Header>
<div class="max-h-[280px] overflow-y-auto py-2">
{#each exportInvoiceLines as lineItem}
{@const num = lineItem.line_number ?? lineItem.id}
<button
type="button"
class="w-full px-3 py-2 text-left text-sm hover:bg-muted rounded-md"
onclick={() => {
editingItem.fa_data = editingItem.fa_data || {};
editingItem.fa_data.search_line = typeof num === 'number' ? num : undefined;
showExportLinePicker = false;
}}
>
Línea {num}
</button>
{/each}
</div>
</Dialog.Content>
</Dialog.Root>
<!-- Diálogo para elegir línea (Impo) -->
<Dialog.Root bind:open={showImportLinePicker}>
<Dialog.Content class="max-w-sm">
<Dialog.Header>
<Dialog.Title class="text-sm">Seleccionar línea</Dialog.Title>
</Dialog.Header>
<div class="max-h-[280px] overflow-y-auto py-2">
{#each importInvoiceLines as lineItem}
{@const num = lineItem.line_number ?? lineItem.id}
<button
type="button"
class="w-full px-3 py-2 text-left text-sm hover:bg-muted rounded-md"
onclick={() => {
editingItem.fa_data = editingItem.fa_data || {};
editingItem.fa_data.search_line = typeof num === 'number' ? num : undefined;
showImportLinePicker = false;
}}
>
Línea {num}
</button>
{/each}
</div>
</Dialog.Content>
</Dialog.Root>

View File

@@ -0,0 +1,309 @@
<script lang="ts">
import * as Dialog from '$lib/components/ui/dialog';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { Loader2, Search, Plus, ArrowLeft } from 'lucide-svelte';
import { companyStore } from '$lib/stores/company.svelte';
import {
getLocations,
createLocation,
type Location,
type LocationSystem
} from '$lib/api/dashboard/a76/general_catalogs/locations';
let {
open = $bindable(false),
system = 'fixed_asset' as LocationSystem,
onSelect
}: {
open: boolean;
system?: LocationSystem;
onSelect: (location: Location) => void;
} = $props();
let locations: Location[] = $state([]);
let filtered: Location[] = $state([]);
let loading = $state(false);
let searchTerm = $state('');
let error = $state('');
let showRegisterForm = $state(false);
// Register form state
let formData = $state({
clave_localizacion: '',
localizacion: '',
department: '',
responsible: '',
observations: ''
});
let formLoading = $state(false);
let formError = $state('');
async function loadLocations() {
const companyId = companyStore?.activeCompany?.id;
if (!companyId) {
error = 'No hay compañía seleccionada';
return;
}
loading = true;
error = '';
try {
const res = await getLocations(companyId, {
system,
page_size: 500
});
locations = res.items ?? [];
filtered = locations;
} catch (e) {
error = 'Error al cargar ubicaciones';
console.error(e);
locations = [];
filtered = [];
} finally {
loading = false;
}
}
function applyFilter() {
if (!searchTerm.trim()) {
filtered = locations;
} else {
const t = searchTerm.toLowerCase();
filtered = locations.filter(
(loc) =>
(loc.clave_localizacion ?? '').toLowerCase().includes(t) ||
(loc.localizacion ?? '').toLowerCase().includes(t)
);
}
}
function handleSelect(loc: Location) {
onSelect(loc);
open = false;
}
function openRegisterForm() {
showRegisterForm = true;
formData = {
clave_localizacion: '',
localizacion: '',
department: '',
responsible: '',
observations: ''
};
formError = '';
}
function closeRegisterForm() {
showRegisterForm = false;
formError = '';
}
async function handleRegisterSubmit() {
const companyId = companyStore?.activeCompany?.id;
if (!companyId) {
formError = 'No hay compañía seleccionada';
return;
}
if (!formData.clave_localizacion.trim()) {
formError = 'La clave es requerida';
return;
}
formLoading = true;
formError = '';
try {
const created = await createLocation(
{
clave_localizacion: formData.clave_localizacion.trim(),
localizacion: formData.localizacion?.trim() || null,
system,
department: formData.department?.trim() || null,
responsible: formData.responsible?.trim() || null,
observations: formData.observations?.trim() || null
},
companyId
);
onSelect(created);
open = false;
} catch (e) {
formError = e instanceof Error ? e.message : 'Error al guardar';
} finally {
formLoading = false;
}
}
$effect(() => {
if (open && !showRegisterForm) loadLocations();
});
$effect(() => {
searchTerm;
applyFilter();
});
$effect(() => {
if (!open) showRegisterForm = false;
});
</script>
<Dialog.Root bind:open>
<Dialog.Content class="!max-w-[60vw] w-[60vw] max-h-[90vh] p-0 flex flex-col">
<Dialog.Header class="px-6 py-4 border-b">
<Dialog.Title class="text-lg font-semibold">Catálogo de ubicaciones (maquinaria y equipo)</Dialog.Title>
</Dialog.Header>
{#if showRegisterForm}
<!-- Registrar nueva ubicación (formulario) -->
<form
class="flex flex-col flex-1 overflow-hidden"
onsubmit={(e) => {
e.preventDefault();
handleRegisterSubmit();
}}
>
<div class="flex-1 overflow-auto px-6 py-4 space-y-4">
{#if formError}
<div class="rounded-md bg-destructive/15 p-3 text-sm text-destructive">
{formError}
</div>
{/if}
<div class="grid gap-4">
<div class="grid grid-cols-4 items-center gap-4">
<Label for="reg-clave" class="text-right">Clave *</Label>
<div class="col-span-3">
<Input
id="reg-clave"
bind:value={formData.clave_localizacion}
maxlength={20}
placeholder="Clave de localización"
class="h-9"
/>
</div>
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="reg-localizacion" class="text-right">Localización</Label>
<div class="col-span-3">
<Input
id="reg-localizacion"
bind:value={formData.localizacion}
maxlength={200}
placeholder="Nombre o descripción"
class="h-9"
/>
</div>
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="reg-department" class="text-right">Departamento</Label>
<div class="col-span-3">
<Input
id="reg-department"
bind:value={formData.department}
maxlength={100}
placeholder="Opcional"
class="h-9"
/>
</div>
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="reg-responsible" class="text-right">Responsable</Label>
<div class="col-span-3">
<Input
id="reg-responsible"
bind:value={formData.responsible}
maxlength={200}
placeholder="Opcional"
class="h-9"
/>
</div>
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="reg-observations" class="text-right">Observaciones</Label>
<div class="col-span-3">
<Input
id="reg-observations"
bind:value={formData.observations}
placeholder="Opcional"
class="h-9"
/>
</div>
</div>
</div>
</div>
<div class="px-6 py-4 border-t flex justify-end gap-2">
<Button type="button" variant="outline" onclick={closeRegisterForm} disabled={formLoading}>
<ArrowLeft class="mr-2 h-4 w-4" />
Volver al listado
</Button>
<Button type="submit" disabled={formLoading}>
{#if formLoading}
<Loader2 class="mr-2 h-4 w-4 animate-spin" />
{/if}
Guardar
</Button>
</div>
</form>
{:else}
<!-- Listado + búsqueda -->
<div class="px-6 py-3 border-b bg-zinc-50 dark:bg-zinc-900 flex items-center gap-2">
<div class="flex items-center gap-2 flex-1">
<Search class="w-4 h-4 text-zinc-400 shrink-0" />
<Input
bind:value={searchTerm}
placeholder="Buscar por clave o localización..."
class="flex-1 h-9"
/>
</div>
<Button type="button" variant="default" size="sm" onclick={openRegisterForm}>
<Plus class="mr-2 h-4 w-4" />
Registrar nueva ubicación
</Button>
</div>
<div class="flex-1 overflow-auto px-6 py-4">
{#if loading}
<div class="flex items-center justify-center py-20">
<Loader2 class="w-8 h-8 animate-spin text-zinc-900 dark:text-zinc-100" />
</div>
{:else if error}
<div class="flex items-center justify-center py-20 text-red-600">
<p>{error}</p>
</div>
{:else}
<div class="border rounded-md overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-zinc-900 dark:bg-zinc-800 text-white">
<tr>
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700">Clave</th>
<th class="px-3 py-2 text-left font-semibold">Localización</th>
</tr>
</thead>
<tbody>
{#each filtered as loc}
<tr
class="border-b hover:bg-zinc-100 dark:hover:bg-zinc-800 cursor-pointer transition-colors"
onclick={() => handleSelect(loc)}
>
<td class="px-3 py-2 border-r">{loc.clave_localizacion ?? '—'}</td>
<td class="px-3 py-2">{loc.localizacion ?? '—'}</td>
</tr>
{/each}
{#if filtered.length === 0}
<tr>
<td colspan="2" class="px-3 py-8 text-center text-zinc-500">
No se encontraron resultados
</td>
</tr>
{/if}
</tbody>
</table>
</div>
{/if}
</div>
<div class="px-6 py-4 border-t flex justify-end gap-2">
<Button variant="outline" onclick={() => (open = false)}>Cancelar</Button>
</div>
{/if}
</Dialog.Content>
</Dialog.Root>

View File

@@ -6,14 +6,18 @@
import { Button } from '$lib/components/ui/button';
import { Folder } from 'lucide-svelte';
import type { Item, LineDescriptions } from '$lib/api/dashboard/a76/items';
import type { InvoiceItemVisibility } from '$lib/config/invoice-item-visibility';
import PaymentMethodDialog from './payment-method-dialog.svelte';
import LocationSelectorDialog from './location-selector-dialog.svelte';
let {
lineItem = $bindable(),
descriptions = $bindable()
descriptions = $bindable(),
visibility
}: {
lineItem: Partial<Item>;
descriptions: LineDescriptions;
visibility: InvoiceItemVisibility;
} = $props();
let taxPaidValue = $derived(lineItem.tax_payment ? 'si' : 'no');
@@ -30,12 +34,25 @@
if (lineItem.is_military_mcia === undefined) {
lineItem.is_military_mcia = false;
}
if (lineItem.has_fda_code === undefined) {
lineItem.has_fda_code = false;
}
if (descriptions.consider_a31 === undefined) {
descriptions.consider_a31 = false;
}
if (!lineItem.fa_data) {
lineItem.fa_data = {};
}
if (lineItem.fa_data.own_equipment === undefined) {
lineItem.fa_data.own_equipment = false;
}
if (lineItem.fa_data.omit_annex31 === undefined) {
lineItem.fa_data.omit_annex31 = false;
}
let paymentMethodDialogOpen = $state(false);
let payment_method_description = $state('');
let locationSelectorOpen = $state(false);
// Load payment method description when payment_method exists
$effect(() => {
@@ -74,11 +91,16 @@
lineItem.payment_method = method.key;
payment_method_description = method.description;
}
function handleLocationSelect(loc: { clave_localizacion: string; localizacion?: string | null }) {
descriptions.machinery_location = loc.localizacion ?? loc.clave_localizacion ?? '';
}
</script>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-2">
<div class="grid grid-cols-1 items-start gap-2 lg:grid-cols-2">
<!-- Left Column -->
<div class="space-y-2">
{#if visibility.showContinuationTaxPayment}
<!-- TAX PAID -->
<div class="grid grid-cols-4 gap-2">
<fieldset class="border rounded p-1.5 space-y-1">
@@ -116,57 +138,144 @@
<Label for="credito_iva" class="text-xs">{payment_method_description || ''}</Label>
</div>
</div>
{/if}
{#if visibility.showContinuationIgi}
<!-- IGI Amount -->
<div class="grid grid-cols-2 gap-2">
<div class="space-y-0.5">
<Label for="monto_igi" class="text-xs">IGI Amount: {lineItem.igi_amount || 0} <span class="text-xs">DOLLARS</span></Label>
<Input id="monto_igi" type="number" bind:value={lineItem.igi_amount} class="h-6 text-xs" />
</div>
<div class="space-y-0.5">
<Label for="igi_payment_method" class="text-xs">IGI Payment Method:</Label>
<Input id="igi_payment_method" bind:value={lineItem.igi_payment_method} class="h-6 text-xs" />
</div>
</div>
{/if}
{#if visibility.showFdaFcc}
<div class="space-y-2 rounded-md border p-2">
<div class="text-xs font-semibold">FDA / FCC</div>
<div class="grid grid-cols-1 gap-2 lg:grid-cols-3">
<div class="flex items-center space-x-1.5">
<Checkbox id="has_fda_code" bind:checked={lineItem.has_fda_code} />
<Label for="has_fda_code" class="text-xs font-normal">Has FDA Code</Label>
</div>
<div class="space-y-0.5">
<Label for="fda_key" class="text-xs">Clave FDA:</Label>
<Input id="fda_key" bind:value={lineItem.fda_key} class="h-6 text-xs" maxlength={20} />
</div>
<div class="space-y-0.5">
<Label for="fcc_key" class="text-xs">Clave FCC:</Label>
<Input id="fcc_key" bind:value={lineItem.fcc_key} class="h-6 text-xs" maxlength={30} />
</div>
</div>
</div>
{/if}
<!-- Certificate of Origin -->
<div class="grid grid-cols-4 gap-2">
<fieldset class="border rounded p-1.5 space-y-1">
<legend class="text-xs font-semibold px-1.5 bg-gray-200 dark:bg-gray-700">Has Certificate of Origin?</legend>
<RadioGroup.Root
{#if visibility.showCertificateOfOrigin}
<!-- Certificate of Origin -->
<div class="grid grid-cols-4 gap-2">
<fieldset class="border rounded p-1.5 space-y-1">
<legend class="text-xs font-semibold px-1.5 bg-gray-200 dark:bg-gray-700">Has Certificate of Origin?</legend>
<RadioGroup.Root
value={hasCertificateValue}
onValueChange={setHasCertificate}
class="flex gap-2">
<div class="flex items-center space-x-1.5">
<RadioGroup.Item value="si" id="cert_origen_si" />
<Label for="cert_origen_si" class="text-xs font-normal">Yes</Label>
</div>
<div class="flex items-center space-x-1.5">
<RadioGroup.Item value="no" id="cert_origen_no" />
<Label for="cert_origen_no" class="text-xs font-normal">No</Label>
</div>
</RadioGroup.Root>
</fieldset>
<div class="space-y-0.5 col-span-3">
<Label for="num_cert_origen" class="text-xs">Certificate of Origin No.:</Label>
<Input id="num_cert_origen" bind:value={lineItem.certificate_number} class="h-6 text-xs" />
<Label for="num_cert_origen" class="text-xs">End Date:</Label>
<div class="flex items-center space-x-1.5">
<RadioGroup.Item value="si" id="cert_origen_si" />
<Label for="cert_origen_si" class="text-xs font-normal">Yes</Label>
</div>
<div class="flex items-center space-x-1.5">
<RadioGroup.Item value="no" id="cert_origen_no" />
<Label for="cert_origen_no" class="text-xs font-normal">No</Label>
</div>
</RadioGroup.Root>
</fieldset>
<div class="col-span-3 space-y-0.5">
<Label for="num_cert_origen" class="text-xs">Certificate of Origin No.:</Label>
<Input id="num_cert_origen" bind:value={lineItem.certificate_number} class="h-6 text-xs" disabled={!lineItem.has_certificate} />
<Label for="cert_end_date" class="text-xs">End Date:</Label>
<Input id="cert_end_date" type="text" bind:value={lineItem.certificate_end_date} class="h-6 text-xs" placeholder="YYYY-MM-DD" disabled={!lineItem.has_certificate} />
</div>
</div>
</div>
{/if}
<!-- Location -->
<div class="space-y-1">
{#if visibility.showExportValuationFields || visibility.showValuationFields}
<!-- Valoración: Met Valor, Valor Det., Motivo De Uso (expo or import CR per legacy). -->
<div class="space-y-2 rounded-md border p-2">
<div class="text-xs font-semibold">Valoración</div>
<div class="grid grid-cols-1 gap-2 lg:grid-cols-3">
<div class="space-y-0.5">
<Label for="met_valor" class="text-xs">Met Valor:</Label>
<Input id="met_valor" bind:value={lineItem.valuation_method} class="h-6 text-xs" maxlength={2} placeholder="Método" />
</div>
<div class="space-y-0.5">
<Label for="valor_det" class="text-xs">Valor Det.:</Label>
<Input id="valor_det" type="number" step="0.00000001" bind:value={lineItem.valuation_determined_value} class="h-6 text-xs" />
</div>
<div class="space-y-0.5 lg:col-span-1">
<Label for="motivo_uso" class="text-xs">Motivo De Uso:</Label>
<textarea
id="motivo_uso"
bind:value={lineItem.valuation_reason}
class="min-h-[60px] w-full rounded-md border border-input bg-background px-2 py-1 text-xs ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
placeholder="Razón valoración"
maxlength={500}
></textarea>
</div>
</div>
</div>
{/if}
{#if visibility.showContinuationLocation}
<!-- Location (catálogo a76, sistema fixed_asset) -->
<div class="space-y-1">
<div class="space-y-0.5">
<Label for="localizacion_maquinaria" class="text-xs">Machinery and equipment location:</Label>
<div class="flex gap-1">
<Input id="localizacion_maquinaria" bind:value={descriptions.machinery_location} class="h-6 text-xs" />
<button
id="localizacion_maquinaria"
type="button"
onclick={() => (locationSelectorOpen = true)}
class="flex h-6 min-w-[120px] flex-1 items-center rounded-md border border-input bg-transparent px-2 text-left text-xs ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
{descriptions.machinery_location || 'Seleccionar ubicación...'}
</button>
</div>
<Label for="localizacion_maquinaria" class="text-xs">Location variable</Label>
<Label for="localizacion_maquinaria" class="text-xs text-muted-foreground">Location variable</Label>
</div>
<LocationSelectorDialog
bind:open={locationSelectorOpen}
system="fixed_asset"
onSelect={handleLocationSelect}
/>
</div>
{/if}
{#if visibility.showContinuationMilitary}
<!-- Military Equipment -->
<div class="flex items-center space-x-1.5 ">
<Checkbox id="equipo_militar" bind:checked={lineItem.is_military_mcia} />
<Label for="equipo_militar" class="text-xs font-normal">Enable if Item Contains Military Equipment</Label>
</div>
{/if}
{#if visibility.showContinuationOwnOmitAnnex}
<div class="grid grid-cols-1 gap-2 lg:grid-cols-2">
<div class="flex items-center space-x-1.5 ">
<Checkbox id="fa_own_equipment" bind:checked={lineItem.fa_data.own_equipment} />
<Label for="fa_own_equipment" class="text-xs font-normal">Own Equipment</Label>
</div>
<div class="flex items-center space-x-1.5 ">
<Checkbox id="fa_omit_annex31" bind:checked={lineItem.fa_data.omit_annex31} />
<Label for="fa_omit_annex31" class="text-xs font-normal">Omit Annex 31</Label>
</div>
</div>
{/if}
{#if visibility.showContinuationLotEntry}
<!-- Lot and Entry Number -->
<div class="grid grid-cols-2 gap-2">
<div class="space-y-0.5">
@@ -177,46 +286,53 @@
<Label for="num_entrada" class="text-xs">Entry No.:</Label>
<Input id="num_entrada" bind:value={descriptions.entry_number} class="h-6 text-xs" />
</div>
</div>
</div>
{/if}
</div>
<!-- Right Column -->
<div class="space-y-2">
<!-- Permit and Eighth Rule Fraction -->
<div class="space-y-1">
<div class="space-y-0.5">
<Label for="permiso_regla_octava" class="text-xs">Eighth Rule Permit:</Label>
<div class="flex gap-1">
<Input id="permiso_regla_octava" bind:value={lineItem.octave_permit} class="h-6 text-xs" />
</div>
</div>
<div class="grid grid-cols-4 gap-2 items-end">
<div class="space-y-0.5 col-span-3">
<Label for="fraccion_regla_octava" class="text-xs">Eighth Rule Fraction:</Label>
<Input id="fraccion_regla_octava" bind:value={descriptions.eighth_rule_fraction} class="h-6 text-xs" />
</div>
<div class="flex h-full flex-col gap-2">
{#if visibility.showEighthRule}
<!-- Permit and Eighth Rule Fraction -->
<div class="space-y-1">
<div class="space-y-0.5">
<Label for="linea_regla" class="text-xs">Line:</Label>
<Input id="linea_regla" type="number" min="0" bind:value={descriptions.eighth_rule_line} class="h-6 text-xs text-right" />
<Label for="permiso_regla_octava" class="text-xs">Eighth Rule Permit:</Label>
<div class="flex gap-1">
<Input id="permiso_regla_octava" bind:value={lineItem.octave_permit} class="h-6 text-xs" />
</div>
</div>
<div class="grid grid-cols-4 items-end gap-2">
<div class="col-span-3 space-y-0.5">
<Label for="fraccion_regla_octava" class="text-xs">Eighth Rule Fraction:</Label>
<Input id="fraccion_regla_octava" bind:value={descriptions.eighth_rule_fraction} class="h-6 text-xs" />
</div>
<div class="space-y-0.5">
<Label for="linea_regla" class="text-xs">Line:</Label>
<Input id="linea_regla" type="number" min="0" bind:value={descriptions.eighth_rule_line} class="h-6 text-xs text-right" />
</div>
</div>
</div>
</div>
{/if}
{#if visibility.showContinuationConsiderA31}
<!-- Consider in a31 -->
<div class="flex items-center space-x-1.5 ">
<Checkbox id="a31" bind:checked={descriptions.consider_a31} />
<Label for="a31" class="text-xs font-normal">Consider in A31</Label>
</div>
{/if}
{#if visibility.showContinuationExtraDescription}
<!-- Extra Description -->
<div class="space-y-0.5">
<div class="flex-1 space-y-0.5">
<Label for="desc_extra_espanol" class="text-xs">Extra Description in Spanish:</Label>
<textarea
id="desc_extra_espanol"
bind:value={descriptions.description_spanish}
class="flex min-h-[60px] w-full rounded-md border border-input bg-background px-2 py-1 text-xs ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
class="flex min-h-[60px] w-full flex-1 rounded-md border border-input bg-background px-2 py-1 text-xs ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
></textarea>
</div>
</div>
{/if}
</div>
</div>
<PaymentMethodDialog bind:open={paymentMethodDialogOpen} onSelect={handlePaymentMethodSelect} />

View File

@@ -12,6 +12,10 @@
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import { obtenerAtajosPestanasItemInv } from '$lib/config/shortcuts/dashboard/invoices/item/inventory';
import PartNumberDialog from '../fa/part-number-dialog.svelte';
import ClassDialog from '../fa/class-dialog.svelte';
import UnitOfMeasureDialog from '../fa/unit-of-measure-dialog.svelte';
import CountryDialog from '../fa/country-dialog.svelte';
import TariffFractionDialog from '../fa/tariff-fraction-dialog.svelte';
let {
open = $bindable(),
@@ -37,6 +41,10 @@
let activeTab = $state('general');
let showPartDialog = $state(false);
let showClassDialog = $state(false);
let showUnitDialog = $state(false);
let showCountryDialog = $state(false);
let showFractionDialog = $state(false);
const tabMapping: Record<string, string> = {
tab1: 'general',
@@ -56,8 +64,69 @@
if (editingItem.customs) {
editingItem.customs.fraction = part.fraction;
}
if (!editingItem.fda_key && part.fda_key) {
editingItem.fda_key = part.fda_key;
}
if (!editingItem.fcc_key && part.fcc_key) {
editingItem.fcc_key = part.fcc_key;
}
}
function handleClassSelect(classItem: any) {
editingItem.class_id = classItem.id;
(editingItem as any).class_code = classItem.class_code;
(editingItem as any).class_description = classItem.description_es || classItem.description_en;
if (editingItem.description) {
if (classItem.description_es) editingItem.description.description_spanish = classItem.description_es;
if (classItem.description_en) editingItem.description.description_english = classItem.description_en;
}
}
function handleUnitSelect(unit: any) {
editingItem.unit_of_measure = unit.id;
(editingItem as any).unit_code = unit.code;
(editingItem as any).unit_description = unit.description || unit.description_en;
if (editingItem.quantity) {
editingItem.quantity.unit_of_measure = unit.code;
}
}
function handleCountrySelect(country: any) {
if (!editingItem.customs) editingItem.customs = {} as any;
editingItem.customs.origin_country = country.m3_key || country.mex_key;
(editingItem.customs as any).origin_country_name = country.description || country.description_en;
}
function handleFractionSelect(fraction: any) {
if (!editingItem.customs) editingItem.customs = {} as any;
const fractionBase = fraction.fraction?.replace(/\./g, '') || '';
const nico = fraction.nico || '';
editingItem.customs.fraction = fractionBase + nico;
(editingItem.customs as any).fraction_description = fraction.description;
}
const fractionDisplay = $derived.by(() => {
const fraction = editingItem.customs?.fraction;
if (!fraction) return '';
if (fraction.includes('.')) return fraction;
if (fraction.length === 8) return `${fraction.slice(0, 4)}.${fraction.slice(4, 6)}.${fraction.slice(6, 8)}`;
if (fraction.length === 10) return `${fraction.slice(0, 4)}.${fraction.slice(4, 6)}.${fraction.slice(6, 8)}.${fraction.slice(8, 10)}`;
return fraction;
});
const systemLabel = $derived.by(() => {
switch ((invoice?.system || '').toLowerCase()) {
case 'fixed_asset':
return 'SCAF (Activo Fijo)';
case 'csv':
return 'CSV';
case 'scaii':
return 'SCAII (Inventario)';
default:
return invoice?.system || 'Inventario';
}
});
useShortcuts(
'Invoice Item Form (Inventory)',
obtenerAtajosPestanasItemInv({
@@ -80,11 +149,16 @@
if (editingItem && !editingItem.financial) editingItem.financial = {} as any;
if (editingItem && !editingItem.customs) editingItem.customs = {} as any;
if (editingItem && !editingItem.description) editingItem.description = {} as any;
if (editingItem.has_fda_code === undefined) editingItem.has_fda_code = false;
}
});
</script>
<PartNumberDialog bind:open={showPartDialog} onSelect={handlePartSelect} />
<ClassDialog bind:open={showClassDialog} onSelect={handleClassSelect} />
<UnitOfMeasureDialog bind:open={showUnitDialog} onSelect={handleUnitSelect} />
<CountryDialog bind:open={showCountryDialog} onSelect={handleCountrySelect} />
<TariffFractionDialog bind:open={showFractionDialog} onSelect={handleFractionSelect} />
<Sheet.Root bind:open>
<Sheet.Content side="right" class="w-[50vw] overflow-hidden p-0 sm:max-w-none">
@@ -93,7 +167,7 @@
>
<div class="space-y-1">
<Sheet.Title class="text-lg font-semibold">
{isEditMode ? 'Editar Item' : 'Agregar Nuevo Item'} - SCAII (Inventario)
{isEditMode ? 'Editar Item' : 'Agregar Nuevo Item'} - {systemLabel}
</Sheet.Title>
<Sheet.Description class="text-sm text-muted-foreground">
{isEditMode
@@ -101,7 +175,7 @@
: 'Completa la información del nuevo item de inventario.'}
{#if editingItem}
<Badge variant="secondary" class="ml-2">
{editingItem} items en esta partida
Línea {editingItem.line_number || 1}
</Badge>
{/if}
</Sheet.Description>
@@ -133,7 +207,7 @@
<!-- Información de la Factura (Solo lectura) -->
{#if !isTargetingPreset}
<div class="space-y-3 rounded-lg border bg-muted/50 p-4">
<h4 class="text-sm font-medium">Información de la Factura (SCAII - Inventario)</h4>
<h4 class="text-sm font-medium">Información de la Factura ({systemLabel})</h4>
{#if !invoice?.id}
<div class="rounded bg-amber-50 p-3 text-sm text-amber-600 dark:bg-amber-950/20">
⚠️ Esta factura aún no se ha guardado. Los items se asociarán cuando guardes la
@@ -157,7 +231,7 @@
<div class="col-span-2">
<span class="text-muted-foreground">Sistema:</span>
<span class="ml-2 rounded bg-blue-100 px-2 py-1 font-medium dark:bg-blue-900/30"
>SCAII (Inventory)</span
>{systemLabel}</span
>
</div>
</div>
@@ -165,6 +239,101 @@
</div>
{/if}
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="space-y-2 md:col-span-2">
<Label for="class_code">Clase</Label>
<div class="flex gap-1">
<Input
id="class_code"
value={(editingItem as any).class_code || ''}
readonly
class="cursor-pointer bg-muted"
placeholder="Selecciona una clase"
onclick={() => (showClassDialog = true)}
/>
<Button variant="outline" size="icon" onclick={() => (showClassDialog = true)}>
<Folder class="h-4 w-4" />
</Button>
</div>
{#if (editingItem as any).class_description}
<p class="text-xs text-muted-foreground">{(editingItem as any).class_description}</p>
{/if}
</div>
<div class="space-y-2">
<Label for="quantity_general">Cantidad</Label>
{#if line?.quantity}
<Input id="quantity_general" type="number" step="0.00000001" min="0" bind:value={line.quantity.quantity} />
{/if}
</div>
<div class="space-y-2">
<Label for="unit_general">U.M.</Label>
<div class="flex gap-1">
<Input
id="unit_general"
value={(editingItem as any).unit_code || line?.quantity?.unit_of_measure || ''}
readonly
class="cursor-pointer bg-muted"
placeholder="Selecciona U.M."
onclick={() => (showUnitDialog = true)}
/>
<Button variant="outline" size="icon" onclick={() => (showUnitDialog = true)}>
<Folder class="h-4 w-4" />
</Button>
</div>
</div>
<div class="space-y-2">
<Label for="unit_cost_capture">Costo Unitario</Label>
{#if line?.financial}
<Input id="unit_cost_capture" type="number" step="0.00000001" min="0" bind:value={line.financial.unit_cost_capture} />
{/if}
</div>
<div class="space-y-2">
<Label for="origin_country_general">País de Origen</Label>
<div class="flex gap-1">
<Input
id="origin_country_general"
value={line?.customs?.origin_country || ''}
readonly
class="cursor-pointer bg-muted"
placeholder="Selecciona país"
onclick={() => (showCountryDialog = true)}
/>
<Button variant="outline" size="icon" onclick={() => (showCountryDialog = true)}>
<Folder class="h-4 w-4" />
</Button>
</div>
</div>
<div class="space-y-2">
<Label for="fraction_general">Fracción</Label>
<div class="flex gap-1">
<Input
id="fraction_general"
value={fractionDisplay}
readonly
class="cursor-pointer bg-muted"
placeholder="Selecciona fracción"
onclick={() => (showFractionDialog = true)}
/>
<Button variant="outline" size="icon" onclick={() => (showFractionDialog = true)}>
<Folder class="h-4 w-4" />
</Button>
</div>
</div>
<div class="space-y-2">
<Label for="fraction_type_general">Tipo de Tarifa</Label>
<select id="fraction_type_general" bind:value={line.customs.fraction_type} class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2">
<option value=""></option>
<option value="GENERAL">GENERAL</option>
<option value="PROSEC">PROSEC</option>
<option value="ALADI">ALADI</option>
<option value="TLCS">TLCS</option>
</select>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="reference_number">Número de Referencia</Label>
@@ -276,7 +445,9 @@
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="country_origin">País de Origen</Label>
<Input id="country_origin" placeholder="Código del país" />
{#if line?.customs}
<Input id="country_origin" placeholder="Código del país" bind:value={line.customs.origin_country} />
{/if}
</div>
<div class="space-y-2">
<Label for="merchandise_category">Categoría de Mercancía</Label>

View File

@@ -27,6 +27,7 @@
import { itemPresetsApi, type ItemPreset } from '$lib/api/dashboard/a76/item-presets';
import { Checkbox } from '$lib/components/ui/checkbox';
import { cleanLineData } from '$lib/utils/items-logic';
import { getVisibility } from '$lib/config/invoice-item-visibility';
let {
invoice,
@@ -99,6 +100,13 @@
);
const invoiceSystem = $derived(invoice?.system || 'scaii');
const itemVisibility = $derived.by(() => getVisibility(invoiceType, operationType));
const showCrTrackingHeader = $derived(itemVisibility.showCrTrackingHeader);
const showTrackingHeaderColumns = $derived(operationType !== 1 && showCrTrackingHeader);
const emptyStateColspan = $derived.by(() => {
if (operationType === 1) return 11;
return showTrackingHeaderColumns ? 12 : 10;
});
const invoiceLabel = $derived.by(() => {
if (invoice?.invoice_number) return `Factura ${invoice.invoice_number}`;
if (invoice?.id) return `Factura ${invoice.id}`;
@@ -256,10 +264,14 @@
alternate_unit: undefined,
permit_number: undefined,
page_line: undefined,
has_fda_code: false,
fda_key: undefined,
fcc_key: undefined,
has_certificate: false,
certificate_number: undefined,
tax_payment: false,
payment_method: undefined,
igi_payment_method: undefined,
igi_amount: undefined,
is_military_mcia: false,
wildcard_field: undefined,
@@ -316,6 +328,14 @@
reference: {
serie_id: undefined
},
fa_data: {
search_invoice: undefined,
search_line: undefined,
search_type: undefined,
movement_type_import: undefined,
down_equipment: false,
omit_annex31: false
},
series: []
};
}
@@ -1033,6 +1053,10 @@
<Table.Header class="bg-background">
<Table.Row>
<Table.Head>Línea</Table.Head>
{#if showTrackingHeaderColumns}
<Table.Head>Factura Impo</Table.Head>
<Table.Head>Línea</Table.Head>
{/if}
<Table.Head>P/S</Table.Head>
<Table.Head>Clase</Table.Head>
<Table.Head>Descripcion Clase</Table.Head>
@@ -1048,7 +1072,7 @@
{#if displayedItems.length === 0}
<Table.Row>
<Table.Cell
colspan={operationType === 1 ? 11 : 10}
colspan={emptyStateColspan}
class="py-8 text-center text-muted-foreground"
>
No hay items disponibles
@@ -1079,20 +1103,21 @@
</Table.Cell>
<Table.Cell>{item.fa_data?.contains_subitems ? 'Sí' : 'No'}</Table.Cell>
<Table.Cell>{item.warehouse || '-'}</Table.Cell>
{:else if invoiceType === 'CR'}
{:else if showCrTrackingHeader}
<Table.Cell>{item.line_number}</Table.Cell>
<Table.Cell>{item.fa_data?.search_invoice || '-'}</Table.Cell>
<Table.Cell>{item.fa_data?.search_line || '-'}</Table.Cell>
<Table.Cell>{item.is_subitem ? 'S' : 'P'}</Table.Cell>
<Table.Cell>{item.quantity?.quantity || '0'}</Table.Cell>
<Table.Cell>{item.class_code || '-'}</Table.Cell>
<Table.Cell>{item.part_number_display || '-'}</Table.Cell>
<Table.Cell
class="max-w-[200px] truncate"
title={item.description?.description_spanish}
>
{item.description?.description_spanish || '-'}
</Table.Cell>
<Table.Cell>{item.quantity?.quantity || '0'}</Table.Cell>
<Table.Cell>{item.unit_of_measure_code || '-'}</Table.Cell>
<Table.Cell>{item.reference_number || '-'}</Table.Cell>
<Table.Cell>{item.fa_data?.contains_subitems ? 'Sí' : 'No'}</Table.Cell>
<Table.Cell>{item.warehouse || '-'}</Table.Cell>
{:else if invoiceType === 'REP' || invoiceType === 'REPAR'}
@@ -1138,7 +1163,7 @@
{/each}
{#if isLoadingMore}
<Table.Row>
<Table.Cell colspan={operationType === 1 ? 11 : 10} class="py-4 text-center">
<Table.Cell colspan={emptyStateColspan} class="py-4 text-center">
<span class="text-sm text-muted-foreground">Cargando más items...</span>
</Table.Cell>
</Table.Row>
@@ -1627,6 +1652,8 @@
{isEditMode}
bind:editingItem
{invoice}
{invoiceType}
{operationType}
{isTargetingPreset}
onSave={saveItem}
onCancel={() => {

View File

@@ -3,24 +3,35 @@ import type { Location } from '$lib/api/dashboard/a76/general_catalogs/locations
import { renderComponent } from '$lib/components/ui/data-table';
import DataTableActions from './data-table-actions.svelte';
const SYSTEM_LABELS: Record<string, string> = {
fixed_asset: 'Activo fijo (FA)',
inventory: 'Inventario'
};
export function createColumns(onSuccess?: () => void): ColumnDef<Location>[] {
return [
{
accessorKey: 'location_code',
header: 'Código',
},
{
accessorKey: 'location_description',
header: 'Descripción',
cell: ({ row }) => row.original.location_description || '—'
},
{
id: 'actions',
header: 'Acciones',
cell: ({ row }) => renderComponent(DataTableActions, {
item: row.original,
onSuccess
})
}
];
return [
{
accessorKey: 'clave_localizacion',
header: 'Clave'
},
{
accessorKey: 'localizacion',
header: 'Localización',
cell: ({ row }) => row.original.localizacion ?? '—'
},
{
accessorKey: 'system',
header: 'Sistema',
cell: ({ row }) => SYSTEM_LABELS[row.original.system] ?? row.original.system
},
{
id: 'actions',
header: 'Acciones',
cell: ({ row }) =>
renderComponent(DataTableActions, {
item: row.original,
onSuccess
})
}
];
}

View File

@@ -0,0 +1,156 @@
<script lang="ts">
import { createColumns } from '$lib/components/dashboard/locations/columns';
import CreateDialog from '$lib/components/dashboard/general_catalogs/locations/create-edit-dialog.svelte';
import DataTable from '$lib/components/dashboard/locations/data-table.svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus } from 'lucide-svelte';
import {
getLocations,
type Location,
type LocationSystem
} from '$lib/api/dashboard/a76/general_catalogs/locations';
let {
companyId,
system: initialSystem = undefined,
showSystemFilter = true,
compact = false
}: {
companyId: number;
/** Filtro por sistema cuando está embebido (ej. solo fixed_asset o solo inventory). */
system?: LocationSystem;
/** Mostrar selector de sistema (FA / Inventario). Por defecto true. */
showSystemFilter?: boolean;
/** Vista compacta sin título ni descripción. */
compact?: boolean;
} = $props();
let items = $state<Location[]>([]);
let total = $state(0);
let page = $state(1);
let pageSize = $state(50);
let pages = $state(0);
let loading = $state(true);
let dialogOpen = $state(false);
let searchClave = $state('');
let searchLocalizacion = $state('');
let filterSystem = $state<LocationSystem | ''>(initialSystem ?? '');
let timeout: ReturnType<typeof setTimeout>;
async function load() {
if (!companyId) return;
loading = true;
try {
const filters: Record<string, string | number> = {
page,
page_size: pageSize
};
if (searchClave) filters.clave_localizacion = searchClave;
if (searchLocalizacion) filters.localizacion = searchLocalizacion;
if (filterSystem) filters.system = filterSystem;
const res = await getLocations(companyId, filters);
items = res.items ?? [];
total = res.total ?? 0;
page = res.page ?? 1;
pageSize = res.page_size ?? 50;
pages = pageSize > 0 ? Math.ceil(total / pageSize) : 0;
} catch (e) {
console.error('Error loading locations:', e);
items = [];
total = 0;
pages = 0;
} finally {
loading = false;
}
}
function handleSuccess() {
load();
}
function handleSearch() {
clearTimeout(timeout);
timeout = setTimeout(() => {
page = 1;
load();
}, 400);
}
$effect(() => {
void companyId;
void page;
load();
});
</script>
<div class="flex flex-col gap-4">
{#if !compact}
<div class="flex items-center justify-between">
<div>
<h2 class="text-lg font-semibold tracking-tight">Ubicaciones</h2>
<p class="text-sm text-muted-foreground">
Clave y localización por sistema (FA / Inventario)
</p>
</div>
<Button onclick={() => (dialogOpen = true)} size="sm">
<Plus class="mr-2 h-4 w-4" />
Nueva
</Button>
</div>
{:else}
<div class="flex items-center justify-end">
<Button onclick={() => (dialogOpen = true)} size="sm" variant="outline">
<Plus class="mr-2 h-4 w-4" />
Nueva ubicación
</Button>
</div>
{/if}
<div class="flex gap-4 items-end flex-wrap">
<div class="grid w-full max-w-[200px] items-center gap-1.5">
<Input
placeholder="Clave..."
bind:value={searchClave}
oninput={handleSearch}
/>
</div>
<div class="grid w-full max-w-[200px] items-center gap-1.5">
<Input
placeholder="Localización..."
bind:value={searchLocalizacion}
oninput={handleSearch}
/>
</div>
{#if showSystemFilter}
<div class="grid w-full max-w-[180px] items-center gap-1.5">
<select
bind:value={filterSystem}
onchange={handleSearch}
class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm"
>
<option value="">Todos</option>
<option value="fixed_asset">Activo fijo (FA)</option>
<option value="inventory">Inventario</option>
</select>
</div>
{/if}
</div>
<div class="rounded-md border">
{#if loading}
<div class="flex items-center justify-center py-12 text-muted-foreground">
Cargando...
</div>
{:else}
<DataTable
data={items}
columns={createColumns(handleSuccess)}
pageCount={pages}
totalItems={total}
/>
{/if}
</div>
<CreateDialog bind:open={dialogOpen} onSuccess={handleSuccess} />
</div>

View File

@@ -278,10 +278,6 @@ export function getSidebarData(): SidebarData {
title: m["sidebar.general_catalogs.customs_warehouses"](),
url: "/dashboard/reference_data/customs_warehouses",
},
{
title: m["sidebar.general_catalogs.locations"](),
url: "/dashboard/general_catalogs/locations",
},
{
title: m["sidebar.general_catalogs.doda"](),
url: "/dashboard/general_catalogs/doda",

View File

@@ -0,0 +1,183 @@
/** Export invoice types that affect item visibility (tipo factura for operation_type === exp). */
export type ExportInvoiceType = 'NODES' | 'AFIJO' | 'DONAC' | 'SCRAP';
export interface InvoiceItemVisibility {
showCrTrackingHeader: boolean;
showEighthRule: boolean;
showFdaFcc: boolean;
showCertificateOfOrigin: boolean;
showIdentifiersTab: boolean;
/** Show block: Genera Descarga? + Tipo Importación + Tipo Búsqueda + Factura de Impo + Línea (expo, or import CR). */
showExportLinkToImportBlock: boolean;
/** Show Met Valor, Valor Det., Motivo De Uso in Continuación tab (expo). */
showExportValuationFields: boolean;
/** Show Met Valor, Valor Det., Motivo De Uso for import (e.g. Cambio de Régimen). */
showValuationFields: boolean;
/** Show block: Genera Descarga? + Factura de Expo + Línea de Expo (importación reparación, REP). */
showRepairLinkToExportBlock: boolean;
/** Continuación tab: TAX PAID + Forma Pago. */
showContinuationTaxPayment: boolean;
/** Continuación tab: IGI Amount + IGI Payment Method. */
showContinuationIgi: boolean;
/** Continuación tab: Machinery and equipment location. */
showContinuationLocation: boolean;
/** Continuación tab: Military Equipment checkbox. */
showContinuationMilitary: boolean;
/** Continuación tab: Own Equipment + Omit Annex 31. */
showContinuationOwnOmitAnnex: boolean;
/** Continuación tab: Lot + Entry No. */
showContinuationLotEntry: boolean;
/** Continuación tab: Consider in A31. */
showContinuationConsiderA31: boolean;
/** Continuación tab: Extra Description in Spanish. */
showContinuationExtraDescription: boolean;
}
const defaultVisibility: InvoiceItemVisibility = {
showCrTrackingHeader: true,
showEighthRule: true,
showFdaFcc: true,
showCertificateOfOrigin: true,
showIdentifiersTab: true,
showExportLinkToImportBlock: false,
showExportValuationFields: false,
showValuationFields: false,
showRepairLinkToExportBlock: false,
showContinuationTaxPayment: true,
showContinuationIgi: true,
showContinuationLocation: true,
showContinuationMilitary: true,
showContinuationOwnOmitAnnex: true,
showContinuationLotEntry: true,
showContinuationConsiderA31: true,
showContinuationExtraDescription: true
};
function normalizeInvoiceType(invoiceType?: string | null): string {
return String(invoiceType || '')
.trim()
.toUpperCase();
}
function normalizeOperationType(operationType?: string | number | null): 'imp' | 'exp' | null {
if (operationType === null || operationType === undefined || operationType === '') {
return null;
}
if (typeof operationType === 'number') {
if (operationType === 1) return 'exp';
if (operationType === 2) return 'imp';
}
const normalized = String(operationType)
.trim()
.toLowerCase();
if (normalized === '1' || normalized === 'exp' || normalized === 'export' || normalized === 'exportacion') {
return 'exp';
}
if (normalized === '2' || normalized === 'imp' || normalized === 'import' || normalized === 'importacion') {
return 'imp';
}
return null;
}
const EXPORT_INVOICE_TYPES: ExportInvoiceType[] = ['NODES', 'AFIJO', 'DONAC', 'SCRAP'];
function normalizeExportInvoiceType(exportInvoiceType?: string | null): ExportInvoiceType {
const normalized = String(exportInvoiceType || '')
.trim()
.toUpperCase();
if (EXPORT_INVOICE_TYPES.includes(normalized as ExportInvoiceType)) {
return normalized as ExportInvoiceType;
}
return 'AFIJO';
}
export function getVisibility(
invoiceType?: string | null,
operationType?: string | number | null
): InvoiceItemVisibility {
if (normalizeOperationType(operationType) === 'exp') {
const exportType = normalizeExportInvoiceType(invoiceType);
// Base visibility for export: existing flags stay true; export-only flags set by type.
const expVisibility: InvoiceItemVisibility = {
...defaultVisibility,
showExportLinkToImportBlock: true,
showExportValuationFields: true
};
// Per-type overrides can be added here (e.g. hide valuation for SCRAP).
switch (exportType) {
case 'NODES':
case 'AFIJO':
case 'DONAC':
case 'SCRAP':
return expVisibility;
default:
return expVisibility;
}
}
switch (normalizeInvoiceType(invoiceType)) {
case 'TEM':
case 'DEF':
return {
...defaultVisibility,
showCrTrackingHeader: false,
showFdaFcc: false
};
case 'CR':
return {
...defaultVisibility,
showEighthRule: false,
showValuationFields: true
};
case 'REP':
case 'REPAR':
// Continuación: solo campos de la captura (sin Valoración, FDA/FCC, Regla Octava, Own/Omit, Lote/Entrada, Consider A31)
return {
...defaultVisibility,
showCrTrackingHeader: false,
showRepairLinkToExportBlock: true,
showValuationFields: false,
showFdaFcc: false,
showCertificateOfOrigin: true,
showEighthRule: false,
showContinuationTaxPayment: true,
showContinuationIgi: true,
showContinuationLocation: true,
showContinuationMilitary: true,
showContinuationOwnOmitAnnex: false,
showContinuationLotEntry: false,
showContinuationConsiderA31: false,
showContinuationExtraDescription: true
};
case 'MEX':
// Compras Mexicanas: lo esencial + localización (como otros tipos); sin IGI, militar, A31, Own/Omit
return {
...defaultVisibility,
showCrTrackingHeader: false,
showEighthRule: false,
showFdaFcc: false,
showCertificateOfOrigin: false,
showIdentifiersTab: false,
showContinuationIgi: false,
showContinuationLocation: true,
showContinuationMilitary: false,
showContinuationOwnOmitAnnex: false,
showContinuationConsiderA31: false
};
default:
return defaultVisibility;
}
}
export function isExportOperation(operationType?: string | number | null): boolean {
return normalizeOperationType(operationType) === 'exp';
}

View File

@@ -1,69 +0,0 @@
import type { PageServerLoad } from './$types';
import { getAuthTokens, authenticatedFetch } from '$lib/server/api';
export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
const parentData = await parent();
const { accessToken } = getAuthTokens(cookies);
if (!accessToken) {
return {
error: 'No authenticated',
locations: { items: [], total: 0, page: 1, page_size: 10, pages: 0 }
};
}
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('page_size')) || 50;
const cookieCompanyId = cookies.get('active_company_id');
const companyId = cookieCompanyId
? parseInt(cookieCompanyId)
: parentData.companies?.[0]?.id;
if (!companyId) {
return {
error: 'No company selected',
locations: { items: [], total: 0, page, page_size: pageSize, pages: 0 }
};
}
const filters: Record<string, string> = {};
const location_code = url.searchParams.get('location_code');
const location_description = url.searchParams.get('location_description');
if (location_code) filters.location_code = location_code;
if (location_description) filters.location_description = location_description;
try {
const queryParams = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
company_id: companyId.toString(),
...filters
});
const response = await authenticatedFetch(
`v1/a76/ports/?${queryParams.toString()}`,
{ method: 'GET', cache: 'no-store' },
cookies,
fetch
);
if (!response.ok) {
return {
error: 'Failed to load',
locations: { items: [], total: 0, page, page_size: pageSize, pages: 0 }
};
}
const data = await response.json();
return { locations: data };
} catch (error) {
console.error('Error loading locations:', error);
return {
error: 'Error loading',
locations: { items: [], total: 0, page: 1, page_size: pageSize, pages: 0 }
};
}
};

View File

@@ -1,89 +0,0 @@
<script lang="ts">
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
import { createColumns } from '$lib/components/dashboard/locations/columns';
import CreateDialog from '$lib/components/dashboard/general_catalogs/locations/create-edit-dialog.svelte';
import DataTable from '$lib/components/dashboard/locations/data-table.svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus } from 'lucide-svelte';
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import { obtenerAtajosListaLocalidades } from '$lib/config/shortcuts/dashboard/general_catalogs/locations/list';
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
let dialogOpen = $state(false);
// Atajos
useShortcuts(
'Lista Localidades',
obtenerAtajosListaLocalidades({
manejarNuevo: () => (dialogOpen = true),
manejarActualizar: handleSuccess
})
);
let searchCode = $state($page.url.searchParams.get('location_code') || '');
let searchDesc = $state($page.url.searchParams.get('location_description') || '');
let timeout: ReturnType<typeof setTimeout>;
function handleSuccess() {
const url = new URL($page.url);
goto(url, { invalidateAll: true });
}
function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(() => {
const url = new URL($page.url);
if (searchCode) url.searchParams.set('location_code', searchCode);
else url.searchParams.delete('location_code');
if (searchDesc) url.searchParams.set('location_description', searchDesc);
else url.searchParams.delete('location_description');
url.searchParams.set('page', '1');
goto(url, { keepFocus: true, noScroll: true });
}, 500);
}
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold tracking-tight">Ubicaciones</h1>
<p class="text-muted-foreground">Catálogo de ubicaciones de puertos</p>
</div>
<Button onclick={() => (dialogOpen = true)}>
<Plus class="mr-2 h-4 w-4" />
Nueva Ubicación
</Button>
</div>
<div class="flex gap-4 items-end">
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input placeholder="Buscar por código..." bind:value={searchCode} oninput={handleSearch} />
</div>
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por descripción..."
bind:value={searchDesc}
oninput={handleSearch}
/>
</div>
</div>
<div class="rounded-md border">
<DataTable
data={data.locations?.items || []}
columns={createColumns(handleSuccess)}
pageCount={data.locations?.pages || 0}
totalItems={data.locations?.total || 0}
/>
</div>
<CreateDialog bind:open={dialogOpen} onSuccess={handleSuccess} />
</div>

View File

@@ -577,22 +577,37 @@
// Esta función se llama cuando el diálogo reporta SUCCESS
try {
if (result.status === 'success') {
const blob = base64ToBlob(result.content, result.media_type);
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = result.file_name;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
toast.success('PDF Descargado exitosamente');
if (result.content) {
// Resultado de generación de PDF: descargar archivo
const blob = base64ToBlob(result.content, result.media_type);
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = result.file_name;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
toast.success('PDF Descargado exitosamente');
} else {
// Resultado de procesamiento de factura
toast.success('Factura procesada correctamente');
reloadData();
}
} else if (result.status === 'validation_error') {
const errors: any[] = result.errors || [];
const preview = errors
.slice(0, 3)
.map((e: any) => `• ${e.message}`)
.join('\n');
const extra = errors.length > 3 ? `\n...y ${errors.length - 3} más` : '';
toast.error(`${errors.length} error(es) de validación:\n${preview}${extra}`);
} else {
toast.error('El worker reportó un error: ' + (result.message || 'Desconocido'));
}
} catch (e) {
console.error('Error al procesar descarga:', e);
toast.error('Error al procesar el archivo descargado');
console.error('Error al procesar resultado:', e);
toast.error('Error al procesar el resultado de la tarea');
} finally {
// Cerrar diálogo después de un breve momento
setTimeout(() => {
@@ -683,6 +698,33 @@
}
}
async function handleProcessInvoice() {
if (!selectedInvoice || !companyStore.activeCompany) {
toast.info('Selecciona una factura para procesar');
return;
}
try {
const response = await invoicesApi.processInvoice(
selectedInvoice.id,
companyStore.activeCompany.id
);
if (response.error) {
toast.error(`Error al iniciar el proceso: ${response.error}`);
return;
}
currentTaskId = response.data!.task_id;
currentStatusFunction = invoicesApi.getProcessStatus;
progressDialogTitle = 'Procesando factura';
showProgressDialog = true;
} catch (e) {
console.error('Error al iniciar proceso de factura:', e);
toast.error('No se pudo iniciar el proceso');
}
}
// Opciones de tipo de operación para el filtro
const operationTypeOptions = [
{ value: '', label: 'Todas' },
@@ -1030,12 +1072,12 @@
<div class="h-6 w-px bg-border"></div>
</div>
{/if}
<!-- Por ahora, Actualizar Estado funciona solo si hay ESTRICTAMENTE UNA seleccionada -->
<!-- Procesa la factura seleccionada mediante tarea Celery -->
<Button
variant="outline"
size="sm"
disabled={loading || selectedInvoiceIds.length !== 1}
onclick={() => handleUpdateStatus(true)}
onclick={handleProcessInvoice}
>
<RefreshCw class="mr-2 h-4 w-4" />
Actualizar