Merge branch 'development' of https://git.aduanasoft.com/ADUANASOFT/anexo76 into feature/partePais_BOM
This commit is contained in:
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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">Sí</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">Sí</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>
|
||||
@@ -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>
|
||||
@@ -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} />
|
||||
@@ -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>
|
||||
|
||||
@@ -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={() => {
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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",
|
||||
|
||||
183
frontend/src/lib/config/invoice-item-visibility.ts
Normal file
183
frontend/src/lib/config/invoice-item-visibility.ts
Normal 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';
|
||||
}
|
||||
@@ -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 }
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user