feat: implement package and payment method dialogs with API integration
This commit is contained in:
@@ -211,18 +211,18 @@ def validate_create(
|
||||
|
||||
# Calcular peso neto en kilogramos (estándar interno)
|
||||
if unit_is_kgs:
|
||||
if invoice_weight_type == "kgs":
|
||||
if invoice_weight_type == "KGS":
|
||||
line.quantity.net_weight = quantity
|
||||
else: # invoice en libras
|
||||
line.quantity.net_weight = quantity * Decimal("2.204624")
|
||||
elif unit_is_lbs:
|
||||
if invoice_weight_type == "kgs":
|
||||
if invoice_weight_type == "KGS":
|
||||
line.quantity.net_weight = quantity / Decimal("2.204624")
|
||||
else: # invoice en libras
|
||||
line.quantity.net_weight = quantity
|
||||
else:
|
||||
# Otra unidad de medida - usar peso capturado y convertir si es necesario
|
||||
if invoice_weight_type == "kgs":
|
||||
if invoice_weight_type == "KGS":
|
||||
# El peso capturado está en kilos
|
||||
line.quantity.net_weight = net_weight_input
|
||||
else:
|
||||
@@ -252,7 +252,7 @@ def validate_create(
|
||||
|
||||
# Si no se proporcionó peso bruto, calcularlo
|
||||
if not gross_weight_input or gross_weight_input == 0:
|
||||
if invoice_weight_type == "kgs":
|
||||
if invoice_weight_type == "KGS":
|
||||
line.quantity.gross_weight = line.quantity.net_weight + (
|
||||
package_weight_unit * package_quantity
|
||||
)
|
||||
@@ -262,7 +262,7 @@ def validate_create(
|
||||
)
|
||||
else:
|
||||
# Convertir peso bruto capturado según tipo de factura
|
||||
if invoice_weight_type == "kgs":
|
||||
if invoice_weight_type == "KGS":
|
||||
line.quantity.gross_weight = gross_weight_input
|
||||
else: # libras
|
||||
line.quantity.gross_weight = gross_weight_input / Decimal("2.204624")
|
||||
|
||||
@@ -73,7 +73,7 @@ def validate_update(
|
||||
# Se proporcionó nuevo peso neto, convertir según tipo
|
||||
net_weight_input = line.quantity.net_weight
|
||||
|
||||
if invoice_weight_type == "kgs":
|
||||
if invoice_weight_type == "KGS":
|
||||
line.quantity.net_weight = net_weight_input
|
||||
else: # libras, convertir a kilos
|
||||
line.quantity.net_weight = net_weight_input / Decimal("2.204624")
|
||||
@@ -85,7 +85,7 @@ def validate_update(
|
||||
if line.quantity.gross_weight is not None:
|
||||
gross_weight_input = line.quantity.gross_weight
|
||||
|
||||
if invoice_weight_type == "kgs":
|
||||
if invoice_weight_type == "KGS":
|
||||
line.quantity.gross_weight = gross_weight_input
|
||||
else: # libras, convertir a kilos
|
||||
line.quantity.gross_weight = gross_weight_input / Decimal("2.204624")
|
||||
@@ -98,8 +98,8 @@ def validate_update(
|
||||
line.quantity.package_quantity = existing_line.quantity.package_quantity
|
||||
|
||||
# Clave de bultos
|
||||
if not line.quantity.package_key:
|
||||
line.quantity.package_key = existing_line.quantity.package_key
|
||||
if not line.quantity.package_id:
|
||||
line.quantity.package_id = existing_line.quantity.package_id
|
||||
|
||||
# País de origen
|
||||
if not line.customs.origin_country:
|
||||
|
||||
@@ -53,9 +53,9 @@ export interface LineQuantities {
|
||||
gross_weight?: number;
|
||||
|
||||
// Packaging
|
||||
package_key?: string;
|
||||
package_id?: number; // Changed from string to number - this is the ID from packages catalog
|
||||
package_quantity?: number;
|
||||
package_description?: string;
|
||||
package_description?: string; // This is for display purposes only
|
||||
}
|
||||
|
||||
export interface LineDescriptions {
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
}
|
||||
|
||||
function handlePartSelect(part: any) {
|
||||
lineItem.part_number_id = part.id;
|
||||
lineItem.part_number = part.id;
|
||||
// Store part number for display
|
||||
(lineItem as any).part_number = part.part_number;
|
||||
(lineItem as any).part_description_es = part.description_spanish;
|
||||
@@ -110,9 +110,6 @@
|
||||
{#if (lineItem as any).part_description_es}
|
||||
<p class="text-xs text-muted-foreground truncate">{(lineItem as any).part_description_es}</p>
|
||||
{/if}
|
||||
{#if lineItem.part_number_id}
|
||||
<p class="text-xs text-muted-foreground italic">ID: {lineItem.part_number_id}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="space-y-1">
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="flex-1 overflow-y-auto px-2 py-1.5">
|
||||
<div class="flex-1 overflow-y-auto px-2">
|
||||
<div class="space-y-2">
|
||||
|
||||
{#if line}
|
||||
@@ -129,6 +129,7 @@
|
||||
bind:descriptions={editingItem.lines![0].description!}
|
||||
bind:customs={editingItem.lines![0].customs!}
|
||||
bind:quantities={editingItem.lines![0].quantity!}
|
||||
invoice={invoice}
|
||||
/>
|
||||
<SummarySection
|
||||
bind:financials={editingItem.lines![0].financial!}
|
||||
|
||||
@@ -151,9 +151,6 @@
|
||||
{#if (lineItem as any).class_description}
|
||||
<p class="text-xs text-muted-foreground">{(lineItem as any).class_description}</p>
|
||||
{/if}
|
||||
{#if lineItem.class_id}
|
||||
<p class="text-xs text-muted-foreground italic">ID: {lineItem.class_id}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Quantity and U.M. on the same row -->
|
||||
@@ -183,9 +180,6 @@
|
||||
<Folder class="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{#if (lineItem as any).unit_description}
|
||||
<p class="text-xs text-muted-foreground truncate">{(lineItem as any).unit_description}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Unit Cost and Fraction -->
|
||||
@@ -217,9 +211,6 @@
|
||||
<Folder class="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{#if (customs as any).fraction_description}
|
||||
<p class="text-xs text-muted-foreground truncate">{(customs as any).fraction_description}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Origin Country and Tariff Type -->
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
<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 { Loader2, Search } from 'lucide-svelte';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let {
|
||||
open = $bindable(),
|
||||
onSelect
|
||||
}: {
|
||||
open: boolean;
|
||||
onSelect: (pkg: any) => void;
|
||||
} = $props();
|
||||
|
||||
let packages: any[] = $state([]);
|
||||
let filteredPackages: any[] = $state([]);
|
||||
let loading = $state(false);
|
||||
let searchTerm = $state('');
|
||||
let error = $state('');
|
||||
|
||||
async function loadPackages() {
|
||||
loading = true;
|
||||
error = '';
|
||||
try {
|
||||
const response = await fetch('/api-sveltekit/packages', {
|
||||
credentials: 'include'
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
// El backend puede devolver { items: [...] } o directamente un array
|
||||
if (Array.isArray(data)) {
|
||||
packages = data;
|
||||
} else if (data.items && Array.isArray(data.items)) {
|
||||
packages = data.items;
|
||||
} else if (data.data && Array.isArray(data.data)) {
|
||||
packages = data.data;
|
||||
} else {
|
||||
console.error('Unexpected data format:', data);
|
||||
packages = [];
|
||||
}
|
||||
filteredPackages = packages;
|
||||
} else {
|
||||
error = `Error: ${response.status} - ${response.statusText}`;
|
||||
console.error('Error response:', await response.text());
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error loading packages';
|
||||
console.error('Error loading packages:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function filterPackages() {
|
||||
if (!searchTerm.trim()) {
|
||||
filteredPackages = packages;
|
||||
} else {
|
||||
const term = searchTerm.toLowerCase();
|
||||
filteredPackages = packages.filter(
|
||||
(pkg) =>
|
||||
pkg.key?.toLowerCase().includes(term) ||
|
||||
pkg.description_es?.toLowerCase().includes(term) ||
|
||||
pkg.description_en?.toLowerCase().includes(term)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelect(pkg: any) {
|
||||
onSelect(pkg);
|
||||
open = false;
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
loadPackages();
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
filterPackages();
|
||||
});
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="!max-w-[70vw] w-[70vw] max-h-[90vh] p-0 flex flex-col">
|
||||
<Dialog.Header class="px-6 py-4 border-b">
|
||||
<Dialog.Title class="text-lg font-semibold">CATALOGO DE BULTOS</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<div class="px-6 py-3 border-b bg-zinc-50 dark:bg-zinc-900">
|
||||
<div class="flex items-center gap-2">
|
||||
<Search class="w-4 h-4 text-zinc-400" />
|
||||
<Input
|
||||
bind:value={searchTerm}
|
||||
placeholder="Buscando..."
|
||||
class="flex-1 h-9"
|
||||
/>
|
||||
</div>
|
||||
</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 border-r border-zinc-700"
|
||||
>Descripción Español</th
|
||||
>
|
||||
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700"
|
||||
>Descripción Inglés</th
|
||||
>
|
||||
<th class="px-3 py-2 text-left font-semibold border-r border-zinc-700"
|
||||
>Peso Unitario</th
|
||||
>
|
||||
<th class="px-3 py-2 text-left font-semibold">Código ACE</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each filteredPackages as pkg, i}
|
||||
<tr
|
||||
class="border-b hover:bg-zinc-100 dark:hover:bg-zinc-800 cursor-pointer transition-colors"
|
||||
onclick={() => handleSelect(pkg)}
|
||||
>
|
||||
<td class="px-3 py-2 border-r">{pkg.key || ''}</td>
|
||||
<td class="px-3 py-2 border-r">{pkg.description_es || ''}</td>
|
||||
<td class="px-3 py-2 border-r">{pkg.description_en || ''}</td>
|
||||
<td class="px-3 py-2 border-r text-right">{pkg.weight_unit || ''}</td>
|
||||
<td class="px-3 py-2 text-center">{pkg.code_ace || ''}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{#if filteredPackages.length === 0}
|
||||
<tr>
|
||||
<td colspan="5" 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>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -1,21 +1,95 @@
|
||||
<script lang="ts">
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Folder } from 'lucide-svelte';
|
||||
import type { Item, LineItem, LineDescriptions, LineCustoms, LineQuantities } from '$lib/api/dashboard/a76/items';
|
||||
import type { Invoice } from '$lib/api/dashboard/a76/invoices';
|
||||
import PackageDialog from './package-dialog.svelte';
|
||||
|
||||
let {
|
||||
item = $bindable(),
|
||||
lineItem = $bindable(),
|
||||
descriptions = $bindable(),
|
||||
customs = $bindable(),
|
||||
quantities = $bindable()
|
||||
quantities = $bindable(),
|
||||
invoice
|
||||
}: {
|
||||
item: Partial<Item>;
|
||||
lineItem: LineItem;
|
||||
descriptions: LineDescriptions;
|
||||
customs: LineCustoms;
|
||||
quantities: LineQuantities;
|
||||
invoice: Invoice | null;
|
||||
} = $props();
|
||||
|
||||
let packageDialogOpen = $state(false);
|
||||
let package_key = $state('');
|
||||
let package_weight_unit = $state<number>(0);
|
||||
let isLoadingPackage = $state(false);
|
||||
|
||||
// Get weight unit from invoice logistics or default to 'KILOS'
|
||||
const weightUnitLabel = $derived.by(() => {
|
||||
const weightType = invoice?.logistics?.weight_type?.toUpperCase();
|
||||
if (weightType === 'KGS') return 'KILOS';
|
||||
if (weightType === 'LBS') return 'LIBRAS';
|
||||
return 'KILOS'; // Default
|
||||
});
|
||||
|
||||
// Calculate total package weight
|
||||
const totalPackageWeight = $derived.by(() => {
|
||||
const qty = quantities.package_quantity || 0;
|
||||
const weightPerUnit = package_weight_unit || 0;
|
||||
return (qty * weightPerUnit).toFixed(4);
|
||||
});
|
||||
|
||||
// Load or sync package data when package_id exists
|
||||
$effect(() => {
|
||||
async function loadPackageData() {
|
||||
// Si ya tiene package_key en quantities (cargado por enrichItemData), usarlo
|
||||
if ((quantities as any).package_key) {
|
||||
package_key = (quantities as any).package_key;
|
||||
package_weight_unit = (quantities as any).package_weight_unit || 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// Si no, pero tiene package_id, cargar los datos
|
||||
if (quantities.package_id && !package_key && !isLoadingPackage) {
|
||||
isLoadingPackage = true;
|
||||
try {
|
||||
const response = await fetch('/api-sveltekit/packages', {
|
||||
credentials: 'include'
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const packages = data.items || data.data || data;
|
||||
if (Array.isArray(packages)) {
|
||||
const pkg = packages.find((p: any) => p.id === quantities.package_id);
|
||||
if (pkg) {
|
||||
package_key = pkg.key;
|
||||
package_weight_unit = pkg.weight_unit || 0;
|
||||
if (!quantities.package_description) {
|
||||
quantities.package_description = pkg.description_es || pkg.description_en || pkg.key;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading package data:', error);
|
||||
} finally {
|
||||
isLoadingPackage = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
loadPackageData();
|
||||
});
|
||||
|
||||
function handlePackageSelect(pkg: any) {
|
||||
quantities.package_id = pkg.id;
|
||||
package_key = pkg.key;
|
||||
package_weight_unit = pkg.weight_unit || 0;
|
||||
quantities.package_description = pkg.description_es || pkg.description_en || pkg.key;
|
||||
}
|
||||
</script>
|
||||
|
||||
<fieldset class="border rounded-md p-2 space-y-2">
|
||||
@@ -28,7 +102,24 @@
|
||||
</div>
|
||||
<div class="col-span-3 space-y-1">
|
||||
<Label for="clave_bultos" class="text-xs">Package Code:</Label>
|
||||
<Input id="clave_bultos" bind:value={quantities.package_key} class="h-7 text-xs" />
|
||||
<div class="flex gap-1">
|
||||
<Input
|
||||
id="clave_bultos"
|
||||
bind:value={package_key}
|
||||
class="h-7 text-xs flex-1"
|
||||
readonly
|
||||
placeholder="Seleccionar..."
|
||||
/>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
class="h-7 w-7 shrink-0"
|
||||
onclick={() => packageDialogOpen = true}
|
||||
type="button"
|
||||
>
|
||||
<Folder class="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-1 flex items-end">
|
||||
</div>
|
||||
@@ -36,10 +127,10 @@
|
||||
|
||||
<div class="grid grid-cols-12 gap-2 items-end">
|
||||
<div class="col-span-2 space-y-1">
|
||||
<Label for="peso_bultos" class="text-xs">Weight: 0.0000</Label>
|
||||
<Label for="peso_bultos" class="text-xs">Weight: {totalPackageWeight}</Label>
|
||||
</div>
|
||||
<div class="col-span-4 space-y-1">
|
||||
<Label for="descripcion_bultos" class="text-xs">Description: {quantities.package_description}</Label>
|
||||
<Label for="descripcion_bultos" class="text-xs">Description: {quantities.package_description || ''}</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -57,7 +148,7 @@
|
||||
</div>
|
||||
<div class="col-span-2 space-y-1">
|
||||
<Label class="text-xs invisible">Space</Label>
|
||||
<span class="text-xs text-gray-900 dark:text-gray-100 font-semibold">KILOS</span>
|
||||
<span class="text-xs text-gray-900 dark:text-gray-100 font-semibold">{weightUnitLabel}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -99,3 +190,5 @@
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<PackageDialog bind:open={packageDialogOpen} onSelect={handlePackageSelect} />
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
<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 { Loader2, Search } from 'lucide-svelte';
|
||||
|
||||
let {
|
||||
open = $bindable(),
|
||||
onSelect
|
||||
}: {
|
||||
open: boolean;
|
||||
onSelect: (method: any) => void;
|
||||
} = $props();
|
||||
|
||||
let paymentMethods: any[] = $state([]);
|
||||
let filteredMethods: any[] = $state([]);
|
||||
let loading = $state(false);
|
||||
let searchTerm = $state('');
|
||||
let error = $state('');
|
||||
|
||||
async function loadPaymentMethods() {
|
||||
loading = true;
|
||||
error = '';
|
||||
try {
|
||||
const response = await fetch('/api-sveltekit/payment-methods', {
|
||||
credentials: 'include'
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
// El backend puede devolver { items: [...] } o directamente un array
|
||||
if (Array.isArray(data)) {
|
||||
paymentMethods = data;
|
||||
} else if (data.items && Array.isArray(data.items)) {
|
||||
paymentMethods = data.items;
|
||||
} else if (data.data && Array.isArray(data.data)) {
|
||||
paymentMethods = data.data;
|
||||
} else {
|
||||
console.error('Unexpected data format:', data);
|
||||
paymentMethods = [];
|
||||
}
|
||||
filteredMethods = paymentMethods;
|
||||
} else {
|
||||
error = `Error: ${response.status} - ${response.statusText}`;
|
||||
console.error('Error response:', await response.text());
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error loading payment methods';
|
||||
console.error('Error loading payment methods:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function filterMethods() {
|
||||
if (!searchTerm.trim()) {
|
||||
filteredMethods = paymentMethods;
|
||||
} else {
|
||||
const term = searchTerm.toLowerCase();
|
||||
filteredMethods = paymentMethods.filter(
|
||||
(method) =>
|
||||
method.key?.toLowerCase().includes(term) ||
|
||||
method.description?.toLowerCase().includes(term)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelect(method: any) {
|
||||
onSelect(method);
|
||||
open = false;
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
loadPaymentMethods();
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
filterMethods();
|
||||
});
|
||||
</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">CATALOGO DE FORMAS DE PAGO</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<div class="px-6 py-3 border-b bg-zinc-50 dark:bg-zinc-900">
|
||||
<div class="flex items-center gap-2">
|
||||
<Search class="w-4 h-4 text-zinc-400" />
|
||||
<Input
|
||||
bind:value={searchTerm}
|
||||
placeholder="Buscando..."
|
||||
class="flex-1 h-9"
|
||||
/>
|
||||
</div>
|
||||
</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"
|
||||
>Descripción</th
|
||||
>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each filteredMethods as method, i}
|
||||
<tr
|
||||
class="border-b hover:bg-zinc-100 dark:hover:bg-zinc-800 cursor-pointer transition-colors"
|
||||
onclick={() => handleSelect(method)}
|
||||
>
|
||||
<td class="px-3 py-2 border-r text-center">{method.key || ''}</td>
|
||||
<td class="px-3 py-2">{method.description || ''}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{#if filteredMethods.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>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -3,7 +3,10 @@
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { Checkbox } from '$lib/components/ui/checkbox';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Folder } from 'lucide-svelte';
|
||||
import type { LineItem, LineDescriptions } from '$lib/api/dashboard/a76/items';
|
||||
import PaymentMethodDialog from './payment-method-dialog.svelte';
|
||||
|
||||
let {
|
||||
lineItem = $bindable(),
|
||||
@@ -22,6 +25,47 @@
|
||||
function setHasCertificate(val: string) {
|
||||
lineItem.has_certificate = val === 'si';
|
||||
}
|
||||
|
||||
let paymentMethodDialogOpen = $state(false);
|
||||
let payment_method_description = $state('');
|
||||
|
||||
// Load payment method description when payment_method exists
|
||||
$effect(() => {
|
||||
async function loadPaymentMethodData() {
|
||||
// Si ya tiene la descripción cargada por enrichItemData, usarla
|
||||
if ((lineItem as any).payment_method_description) {
|
||||
payment_method_description = (lineItem as any).payment_method_description;
|
||||
return;
|
||||
}
|
||||
|
||||
// Si tiene payment_method pero no descripción, cargarla
|
||||
if (lineItem.payment_method && !payment_method_description) {
|
||||
try {
|
||||
const response = await fetch('/api-sveltekit/payment-methods', {
|
||||
credentials: 'include'
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const methods = data.items || data.data || data;
|
||||
if (Array.isArray(methods)) {
|
||||
const method = methods.find((m: any) => m.key === lineItem.payment_method);
|
||||
if (method) {
|
||||
payment_method_description = method.description;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading payment method data:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
loadPaymentMethodData();
|
||||
});
|
||||
|
||||
function handlePaymentMethodSelect(method: any) {
|
||||
lineItem.payment_method = method.key;
|
||||
payment_method_description = method.description;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-2">
|
||||
@@ -49,10 +93,19 @@
|
||||
<div class="space-y-0.5">
|
||||
<Label for="forma_pago" class="text-xs">Payment Method:</Label>
|
||||
<div class="flex gap-1">
|
||||
<Input id="forma_pago" bind:value={lineItem.payment_method} class="h-6 text-xs" />
|
||||
<Input id="forma_pago" bind:value={lineItem.payment_method} class="h-6 text-xs flex-1" />
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
class="h-6 w-6 shrink-0"
|
||||
onclick={() => paymentMethodDialogOpen = true}
|
||||
type="button"
|
||||
>
|
||||
<Folder class="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Label for="credito_iva" class="text-xs">VAT AND EXCISE TAX CREDITS.</Label>
|
||||
<Label for="credito_iva" class="text-xs">{payment_method_description || ''}</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -158,3 +211,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PaymentMethodDialog bind:open={paymentMethodDialogOpen} onSelect={handlePaymentMethodSelect} />
|
||||
@@ -290,7 +290,7 @@
|
||||
quantity_returned: undefined,
|
||||
net_weight: undefined,
|
||||
gross_weight: undefined,
|
||||
package_key: undefined,
|
||||
package_id: undefined,
|
||||
package_quantity: undefined,
|
||||
package_description: undefined
|
||||
},
|
||||
@@ -664,6 +664,53 @@
|
||||
console.error('Error loading fraction data:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Load package data (if needed)
|
||||
const packageId = line.quantity?.package_id;
|
||||
if (packageId && line.quantity) {
|
||||
try {
|
||||
const response = await fetch('/api-sveltekit/packages', {
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const packages = data.items || data.data || data;
|
||||
if (Array.isArray(packages)) {
|
||||
const pkg = packages.find((p: any) => p.id === packageId);
|
||||
if (pkg) {
|
||||
(line.quantity as any).package_description = pkg.description_es || pkg.description_en || pkg.key;
|
||||
(line.quantity as any).package_key = pkg.key;
|
||||
(line.quantity as any).package_weight_unit = pkg.weight_unit || 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading package data:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Load payment method description (if needed)
|
||||
if (line.payment_method) {
|
||||
try {
|
||||
const response = await fetch('/api-sveltekit/payment-methods', {
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const methods = data.items || data.data || data;
|
||||
if (Array.isArray(methods)) {
|
||||
const method = methods.find((m: any) => m.key === line.payment_method);
|
||||
if (method) {
|
||||
(line as any).payment_method_description = method.description;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading payment method data:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize numeric values from strings to numbers
|
||||
|
||||
80
frontend/src/routes/api-sveltekit/packages/+server.ts
Normal file
80
frontend/src/routes/api-sveltekit/packages/+server.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const GET: RequestHandler = async ({ cookies, url }) => {
|
||||
const token = cookies.get('access_token');
|
||||
|
||||
// Obtener company_id de la cookie
|
||||
const companyId = cookies.get('active_company_id');
|
||||
|
||||
if (!companyId) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'No company selected' }),
|
||||
{
|
||||
status: 400,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Configurar la URL de la API usando las variables de entorno
|
||||
let apiUrl = process.env.INTERNAL_API_URL;
|
||||
if (!apiUrl) {
|
||||
apiUrl = process.env.VITE_API_URL;
|
||||
// Reemplazar 'localhost' con 'backend' para llamadas desde el servidor (SSR)
|
||||
apiUrl = apiUrl?.replace('localhost', 'backend').replace('127.0.0.1', 'backend');
|
||||
}
|
||||
|
||||
// Normalizar la URL
|
||||
const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`;
|
||||
|
||||
// Get query parameters and add company_id
|
||||
const searchParams = new URLSearchParams(url.search);
|
||||
searchParams.set('company_id', companyId);
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
try {
|
||||
const fetchUrl = `${baseUrl}v1/a76/packages?${queryString}`;
|
||||
|
||||
const response = await fetch(
|
||||
fetchUrl,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
return new Response(JSON.stringify(data), {
|
||||
status: response.status,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify(data), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching packages:', error);
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Failed to fetch packages' }),
|
||||
{
|
||||
status: 500,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
64
frontend/src/routes/api-sveltekit/payment-methods/+server.ts
Normal file
64
frontend/src/routes/api-sveltekit/payment-methods/+server.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const GET: RequestHandler = async ({ cookies, url }) => {
|
||||
const token = cookies.get('access_token');
|
||||
|
||||
// Configurar la URL de la API usando las variables de entorno
|
||||
let apiUrl = process.env.INTERNAL_API_URL;
|
||||
if (!apiUrl) {
|
||||
apiUrl = process.env.VITE_API_URL;
|
||||
// Reemplazar 'localhost' con 'backend' para llamadas desde el servidor (SSR)
|
||||
apiUrl = apiUrl?.replace('localhost', 'backend').replace('127.0.0.1', 'backend');
|
||||
}
|
||||
|
||||
// Normalizar la URL
|
||||
const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`;
|
||||
|
||||
// Get query parameters
|
||||
const searchParams = new URLSearchParams(url.search);
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
try {
|
||||
const fetchUrl = `${baseUrl}v1/public/reference_data/payment-methods${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const response = await fetch(
|
||||
fetchUrl,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
return new Response(JSON.stringify(data), {
|
||||
status: response.status,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify(data), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching payment methods:', error);
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Failed to fetch payment methods' }),
|
||||
{
|
||||
status: 500,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user