feature/catalogo-equivalencias

This commit is contained in:
2026-03-18 09:40:52 -06:00
parent d37d6ef699
commit 8b9537b5a9
10 changed files with 379 additions and 286 deletions

View File

@@ -1,43 +1,44 @@
import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
export interface Equivalency {
export interface EquivalencyItem {
id: number;
fraccion_mex: string;
fraccion_us: string;
description?: string;
original_field: string; // from_unit_code
external_field: string; // to_unit_code
conversion_factor: number | null;
equivalency_id: number;
tenant_id: number;
company_id: number;
created_at: string;
updated_at?: string;
}
export interface EquivalencyCreate {
fraccion_mex: string;
fraccion_us: string;
description?: string;
export interface EquivalencyItemCreate {
original_field: string;
external_field: string;
conversion_factor: number;
}
export interface EquivalencyUpdate {
fraccion_mex?: string;
fraccion_us?: string;
description?: string;
export interface EquivalencyItemUpdate {
original_field?: string;
external_field?: string;
conversion_factor?: number | null;
}
export interface EquivalencyListResponse {
items: Equivalency[];
export interface EquivalencyItemListResponse {
items: EquivalencyItem[];
total: number;
page: number;
page_size: number;
pages: number;
}
export async function getEquivalencies(
export async function getEquivalencyItems(
page: number = 1,
pageSize: number = 50,
companyId: number,
filters: Record<string, any> = {}
): Promise<ApiResponse<EquivalencyListResponse>> {
) : Promise<ApiResponse<EquivalencyItemListResponse>> {
const params = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
@@ -45,28 +46,31 @@ export async function getEquivalencies(
...filters
});
return await api.get(`/v1/a76/equivalencies/?${params.toString()}`);
return await api.get(`/v1/a76/equivalencies/items/?${params.toString()}`);
}
export async function getEquivalency(id: number, companyId: number): Promise<ApiResponse<Equivalency>> {
return await api.get(`/v1/a76/equivalencies/${id}/?company_id=${companyId}`);
}
export async function createEquivalency(
data: EquivalencyCreate,
export async function getEquivalencyItem(
id: number,
companyId: number
): Promise<ApiResponse<Equivalency>> {
return await api.post(`/v1/a76/equivalencies/?company_id=${companyId}`, data);
): Promise<ApiResponse<EquivalencyItem>> {
return await api.get(`/v1/a76/equivalencies/items/${id}/?company_id=${companyId}`);
}
export async function updateEquivalency(
id: number,
data: EquivalencyUpdate,
export async function createEquivalencyItem(
data: EquivalencyItemCreate,
companyId: number
): Promise<ApiResponse<Equivalency>> {
return await api.put(`/v1/a76/equivalencies/${id}/?company_id=${companyId}`, data);
): Promise<ApiResponse<EquivalencyItem>> {
return await api.post(`/v1/a76/equivalencies/items/?company_id=${companyId}`, data);
}
export async function deleteEquivalency(id: number, companyId: number): Promise<ApiResponse<void>> {
return await api.delete(`/v1/a76/equivalencies/${id}/?company_id=${companyId}`);
export async function updateEquivalencyItem(
id: number,
data: EquivalencyItemUpdate,
companyId: number
): Promise<ApiResponse<EquivalencyItem>> {
return await api.put(`/v1/a76/equivalencies/items/${id}/?company_id=${companyId}`, data);
}
export async function deleteEquivalencyItem(id: number, companyId: number): Promise<ApiResponse<void>> {
return await api.delete(`/v1/a76/equivalencies/items/${id}?company_id=${companyId}`);
}

View File

@@ -1,25 +1,25 @@
import type { ColumnDef } from '@tanstack/table-core';
import type { Equivalency } from '$lib/api/dashboard/a76/general_catalogs/equivalencies';
import type { EquivalencyItem } from '$lib/api/dashboard/a76/general_catalogs/equivalencies';
import { renderComponent } from '$lib/components/ui/data-table';
import DataTableActions from './data-table-actions.svelte';
import { Header } from '$lib/components/ui/alert-dialog';
export function createColumns(onSuccess?: () => void): ColumnDef<Equivalency>[] {
export function createColumns(onSuccess?: () => void): ColumnDef<EquivalencyItem>[] {
return [
{
accessorKey: 'fraccion_mex',
header: 'Fracción MX',
cell: ({ row }) => row.original.fraccion_mex || 'N/A'
accessorKey: 'original_field',
header: 'Desde código',
cell: ({ row }) => row.original.original_field || 'N/A'
},
{
accessorKey: 'fraccion_us',
header: 'Fracción US',
cell: ({ row }) => row.original.fraccion_us || 'N/A'
accessorKey: 'external_field',
header: 'Hacia código',
cell: ({ row }) => row.original.external_field || 'N/A'
},
{
accessorKey: 'description',
header: 'Descripción',
cell: ({ row }) => row.original.description || 'N/A'
accessorKey: 'conversion_factor',
header: 'Factor de conversión',
cell: ({ row }) => (row.original.conversion_factor ?? 0).toString()
},
{
id: 'actions',

View File

@@ -4,12 +4,13 @@
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { companyStore } from '$lib/stores/company.svelte';
import UnitMeasureSelectorDialog from '$lib/components/dashboard/goods/modales/unit-measure-dialog.svelte';
import { FolderSearch, Scale } from 'lucide-svelte';
import {
createEquivalency,
updateEquivalency,
type Equivalency
createEquivalencyItem,
updateEquivalencyItem,
type EquivalencyItem
} from '$lib/api/dashboard/a76/general_catalogs/equivalencies';
import { obtenerAtajosFormularioEquivalencias } from '$lib/config/shortcuts/dashboard/general_catalogs/equivalencies/edit';
let {
open = $bindable(false),
@@ -17,46 +18,50 @@
onSuccess
}: {
open: boolean;
item?: Equivalency | null;
item?: EquivalencyItem | null;
onSuccess?: () => void;
} = $props();
// Atajos
const isEdit = $derived(!!item);
const title = $derived(
isEdit ? `Editar Equivalencia ${item?.fraccion_mex || ''}` : 'Nueva Equivalencia'
isEdit
? `Editar Equivalencia ${item?.original_field || ''}${item?.external_field || ''}`
: 'Nueva Equivalencia'
);
// Estado del formulario
let formData = $state({
fraccion_mex: '',
fraccion_us: '',
description: ''
original_field: '',
external_field: '',
conversion_factor: ''
});
let loading = $state(false);
let error = $state<string | null>(null);
// Cargar datos
let showFromUomModal = $state(false);
let showToUomModal = $state(false);
$effect(() => {
if (open) {
if (item) {
formData = {
fraccion_mex: item.fraccion_mex || '',
fraccion_us: item.fraccion_us || '',
description: item.description || ''
};
} else {
// Reset
formData = {
fraccion_mex: '',
fraccion_us: '',
description: ''
};
}
error = null;
if (!open) return;
if (item) {
formData = {
original_field: item.original_field || '',
external_field: item.external_field || '',
conversion_factor:
item.conversion_factor !== null && item.conversion_factor !== undefined
? item.conversion_factor.toString()
: ''
};
} else {
formData = {
original_field: '',
external_field: '',
conversion_factor: ''
};
}
error = null;
});
async function handleSubmit() {
@@ -70,15 +75,24 @@
error = null;
try {
let response;
if (isEdit && item) {
response = await updateEquivalency(item.id, formData, companyId);
} else {
response = await createEquivalency(formData, companyId);
}
if (!formData.original_field.trim()) throw new Error('Código origen requerido');
if (!formData.external_field.trim()) throw new Error('Código destino requerido');
if (formData.conversion_factor === '' || formData.conversion_factor === null)
throw new Error('Factor de conversión requerido');
if (response.error) {
throw new Error(response.error);
const factor = parseFloat(formData.conversion_factor);
if (isNaN(factor)) throw new Error('El factor de conversión debe ser un número válido');
const dataToSend = {
original_field: formData.original_field.trim(),
external_field: formData.external_field.trim(),
conversion_factor: factor
};
if (isEdit && item) {
await updateEquivalencyItem(item.id, dataToSend, companyId);
} else {
await createEquivalencyItem(dataToSend, companyId);
}
open = false;
@@ -111,46 +125,104 @@
</div>
{/if}
<div class="grid grid-cols-4 items-center gap-4">
<Label for="fraccion_mex" class="text-right">Fracción MX</Label>
<Input
id="fraccion_mex"
bind:value={formData.fraccion_mex}
class="col-span-3"
maxlength={10}
required
placeholder="Ej. 8544.11.01"
/>
</div>
<div class="grid gap-4 py-4">
<div class="grid gap-2">
<Label for="from_unit_code">
Código origen <span class="text-destructive">*</span>
</Label>
<div class="flex gap-2">
<div class="relative w-full">
<Scale class="absolute top-2.5 left-3 h-4 w-4 text-muted-foreground" />
<Input
id="from_unit_code"
bind:value={formData.original_field}
readonly
onclick={() => (showFromUomModal = true)}
class="cursor-pointer pl-9 font-mono"
placeholder="Seleccione..."
disabled={loading}
required
/>
</div>
<Button
variant="outline"
size="icon"
type="button"
onclick={() => (showFromUomModal = true)}
disabled={loading}
class="shrink-0"
>
<FolderSearch class="h-4 w-4" />
</Button>
</div>
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="fraccion_us" class="text-right">Fracción US</Label>
<Input
id="fraccion_us"
bind:value={formData.fraccion_us}
class="col-span-3"
maxlength={100}
required
placeholder="Ej. 8544.11.00"
/>
</div>
<div class="grid gap-2">
<Label for="to_unit_code">
Código destino <span class="text-destructive">*</span>
</Label>
<div class="flex gap-2">
<div class="relative w-full">
<Scale class="absolute top-2.5 left-3 h-4 w-4 text-muted-foreground" />
<Input
id="to_unit_code"
bind:value={formData.external_field}
readonly
onclick={() => (showToUomModal = true)}
class="cursor-pointer pl-9 font-mono"
placeholder="Seleccione..."
disabled={loading}
required
/>
</div>
<Button
variant="outline"
size="icon"
type="button"
onclick={() => (showToUomModal = true)}
disabled={loading}
class="shrink-0"
>
<FolderSearch class="h-4 w-4" />
</Button>
</div>
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="description" class="text-right">Descripción</Label>
<Input
id="description"
bind:value={formData.description}
class="col-span-3"
maxlength={200}
/>
<div class="grid gap-2">
<Label for="conversion_factor">
Factor de conversión <span class="text-destructive">*</span>
</Label>
<Input
id="conversion_factor"
type="number"
step="0.000001"
bind:value={formData.conversion_factor}
disabled={loading}
required
/>
</div>
</div>
<Dialog.Footer>
<Button type="button" variant="outline" onclick={() => (open = false)}>Cancelar</Button>
<Button type="submit" disabled={loading}>
{loading ? 'Guardando...' : 'Guardar'}
{loading ? 'Guardando...' : isEdit ? 'Actualizar' : 'Crear'}
</Button>
</Dialog.Footer>
</form>
</Dialog.Content>
</Dialog.Root>
<UnitMeasureSelectorDialog
bind:open={showFromUomModal}
onSelect={(u) => {
formData.original_field = u.code;
}}
/>
<UnitMeasureSelectorDialog
bind:open={showToUomModal}
onSelect={(u) => {
formData.external_field = u.code;
}}
/>

View File

@@ -2,8 +2,8 @@
import { Button } from '$lib/components/ui/button';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
import { EllipsisVertical, Pencil, Trash2, LoaderCircle } from 'lucide-svelte';
import type { Equivalency } from '$lib/api/dashboard/a76/general_catalogs/equivalencies';
import { deleteEquivalency } from '$lib/api/dashboard/a76/general_catalogs/equivalencies';
import type { EquivalencyItem } from '$lib/api/dashboard/a76/general_catalogs/equivalencies';
import { deleteEquivalencyItem } from '$lib/api/dashboard/a76/general_catalogs/equivalencies';
import { companyStore } from '$lib/stores/company.svelte';
import CreateEditDialog from './create-edit-dialog.svelte';
@@ -11,14 +11,14 @@
item,
onSuccess
}: {
item: Equivalency;
item: EquivalencyItem;
onSuccess?: () => void;
} = $props();
let loading = $state(false);
let error = $state<string | null>(null);
let dialogOpen = $state(false);
let selectedItem = $state<Equivalency | null>(null);
let selectedItem = $state<EquivalencyItem | null>(null);
async function handleDelete() {
if (!confirm('¿Está seguro de eliminar esta equivalencia?')) {
@@ -33,7 +33,7 @@
loading = true;
try {
const response = await deleteEquivalency(item.id, companyId);
const response = await deleteEquivalencyItem(item.id, companyId);
if (response.error) {
throw new Error(response.error);

View File

@@ -24,11 +24,11 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
}
const filters: Record<string, string> = {};
const identifier = url.searchParams.get('identifier');
const description = url.searchParams.get('description');
const fromUnitCode = url.searchParams.get('from_unit_code');
const toUnitCode = url.searchParams.get('to_unit_code');
if (identifier) filters.identifier = identifier;
if (description) filters.description = description;
if (fromUnitCode) filters.from_unit_code = fromUnitCode;
if (toUnitCode) filters.to_unit_code = toUnitCode;
const queryParams = new URLSearchParams({
page: page.toString(),
@@ -37,7 +37,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
...filters
});
const response = await authenticatedFetch(`v1/a76/equivalencies/?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
const response = await authenticatedFetch(`v1/a76/equivalencies/items/?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
if (!response.ok) {
return { error: 'Failed to load', equivalencies: { items: [], total: 0, page, page_size: pageSize, pages: 0 } };

View File

@@ -26,8 +26,8 @@
);
// Filtros
let searchFraccion = $state($page.url.searchParams.get('fraccion_mex') || '');
let searchDesc = $state($page.url.searchParams.get('description') || '');
let searchFrom = $state($page.url.searchParams.get('from_unit_code') || '');
let searchTo = $state($page.url.searchParams.get('to_unit_code') || '');
let timeout: ReturnType<typeof setTimeout>;
function handleSearch() {
@@ -35,11 +35,11 @@
clearTimeout(timeout);
timeout = setTimeout(() => {
const url = new URL($page.url);
if (searchFraccion) url.searchParams.set('fraccion_mex', searchFraccion);
else url.searchParams.delete('fraccion_mex');
if (searchFrom) url.searchParams.set('from_unit_code', searchFrom);
else url.searchParams.delete('from_unit_code');
if (searchDesc) url.searchParams.set('description', searchDesc);
else url.searchParams.delete('description');
if (searchTo) url.searchParams.set('to_unit_code', searchTo);
else url.searchParams.delete('to_unit_code');
url.searchParams.set('page', '1');
goto(url, { keepFocus: true, noScroll: true });
@@ -67,15 +67,15 @@
<div class="flex gap-4 items-end">
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por fracción MX..."
bind:value={searchFraccion}
placeholder="Buscar por código origen..."
bind:value={searchFrom}
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}
placeholder="Buscar por código destino..."
bind:value={searchTo}
oninput={handleSearch}
/>
</div>