Un comit de guardado de seguridad

This commit is contained in:
2026-01-07 10:32:24 -06:00
parent 02d6d2bac7
commit 7249607fd7
4 changed files with 87 additions and 378 deletions

View File

@@ -12,6 +12,7 @@ export interface Part {
part_number: string;
commercial_part_number: string | null;
part_class: string | null;
material_type?: string | null;
// Descripciones
description_spanish: string | null;
@@ -59,6 +60,7 @@ export interface PartCreate {
description_english?: string | null;
commercial_part_number?: string | null;
part_class?: string | null;
material_type?: string | null;
unit_of_measure: string;
alternate_unit_measure?: string | null;

View File

@@ -1,346 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import { classesApi, type A76Class } from '$lib/api/dashboard/a76/classes';
import { companyStore } from '$lib/stores/company.svelte';
import DataTable from '$lib/components/dashboard/classes/data-table.svelte';
import { createColumns } from '$lib/components/dashboard/classes/columns.js';
import CreateEditDialog from '$lib/components/dashboard/classes/create-edit-dialog.svelte';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { Plus, Filter, Trash2, RefreshCw } from 'lucide-svelte';
import { browser } from '$app/environment';
// Estado para la lista de classes
let allItems = $state<A76Class[]>([]);
let currentPage = $state(1);
let pageSize = $state(50);
let totalItems = $state(0);
let loading = $state(false);
let hasMore = $derived(allItems.length < totalItems);
let error = $state<string | null>(null);
// Estado para filtros
let filters = $state({
class_code: '',
client_id: ''
});
// Estado para el dialog de crear
let createDialogOpen = $state(false);
// Sincronizar token de cookies a localStorage al montar el componente
onMount(() => {
if (browser) {
// Función para obtener el valor de una cookie
const getCookie = (name: string): string | null => {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
return null;
};
// Verificar si hay token en las cookies
const cookieToken = getCookie('access_token');
const localToken = localStorage.getItem('access_token');
if (cookieToken && cookieToken !== localToken) {
localStorage.setItem('access_token', cookieToken);
}
// También sincronizar refresh_token si existe
const cookieRefreshToken = getCookie('refresh_token');
const localRefreshToken = localStorage.getItem('refresh_token');
if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) {
localStorage.setItem('refresh_token', cookieRefreshToken);
}
}
// Esperar a que el companyStore esté inicializado antes de cargar datos
const checkAndLoad = () => {
if (companyStore.activeCompany) {
loadInitialData();
} else {
// Si no hay compañía, esperar un poco y reintentar
setTimeout(checkAndLoad, 100);
}
};
checkAndLoad();
// También escuchar cambios de compañía para recargar
const handleCompanyChange = () => {
loadInitialData();
};
window.addEventListener('companyChanged', handleCompanyChange);
return () => {
window.removeEventListener('companyChanged', handleCompanyChange);
};
});
async function loadInitialData() {
const companyId = companyStore.activeCompany?.id;
if (!companyId) {
error = 'No hay compañía seleccionada. Por favor selecciona una compañía en el sidebar.';
loading = false;
return;
}
loading = true;
error = null;
try {
const response = await classesApi.list({
company_id: companyId,
page: 1,
page_size: pageSize
});
if (response.error) {
console.error('📊 [Classes Page] Error en loadInitialData:', response.error);
if (response.status === 401 || response.status === 403) {
error = 'Sesión expirada. Recargando página...';
setTimeout(() => {
window.location.reload();
}, 2000);
} else {
error = response.error;
}
return;
}
if (response.data) {
allItems = response.data.items;
currentPage = response.data.page;
totalItems = response.data.total;
}
} catch (e) {
error = 'Error cargando los datos';
console.error('📊 [Classes Page] Error loading initial data:', e);
} finally {
loading = false;
}
}
async function loadMore() {
if (loading || !hasMore) return;
const companyId = companyStore.activeCompany?.id;
if (!companyId) return;
loading = true;
error = null;
try {
const response = await classesApi.list({
company_id: companyId,
page: currentPage + 1,
page_size: pageSize
});
if (response.error) {
console.error('📊 [Classes Page] Error en loadMore:', response.error);
if (response.status === 401 || response.status === 403) {
error = 'Sesión expirada. Recargando página...';
setTimeout(() => {
window.location.reload();
}, 2000);
} else {
error = response.error;
}
return;
}
if (response.data?.items) {
allItems = [...allItems, ...response.data.items];
currentPage++;
totalItems = response.data.total;
}
} catch (e) {
error = 'Error cargando más datos';
console.error('📊 [Classes Page] Error loading more:', e);
} finally {
loading = false;
}
}
async function applyFilters() {
// Reset y recargar con filtros
const companyId = companyStore.activeCompany?.id;
if (!companyId) {
error = 'No hay compañía seleccionada';
return;
}
loading = true;
error = null;
try {
// Aquí podrías agregar filtros adicionales al endpoint si el backend los soporta
const response = await classesApi.list({
company_id: companyId,
page: 1,
page_size: pageSize
});
if (response.error) {
console.error('📊 [Classes Page] Error aplicando filtros:', response.error);
if (response.status === 401 || response.status === 403) {
error = 'Sesión expirada. Recargando página...';
setTimeout(() => {
window.location.reload();
}, 2000);
} else {
error = response.error;
}
return;
}
if (response.data) {
allItems = response.data.items;
currentPage = 1;
totalItems = response.data.total;
}
} catch (e) {
error = 'Error aplicando filtros';
console.error('📊 [Classes Page] Error applying filters:', e);
} finally {
loading = false;
}
}
function clearFilters() {
filters = {
class_code: '',
client_id: ''
};
loadInitialData();
}
function reloadData() {
loadInitialData();
}
function handleCreateClick() {
createDialogOpen = true;
}
function handleSuccess() {
// Recargar datos después de crear/editar/eliminar
reloadData();
}
function handleDialogSuccess() {
createDialogOpen = false;
reloadData();
}
// Crear columnas con el callback onSuccess
const columns = createColumns(handleSuccess);
</script>
<div class="space-y-6">
<!-- Header -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold tracking-tight">Clases A76</h1>
<p class="text-muted-foreground">
Gestiona las clases de materiales del sistema
</p>
</div>
<Button onclick={handleCreateClick}>
<Plus class="mr-2" size={16} />
Nueva Clase
</Button>
</div>
<!-- Filtros -->
<Card.Root>
<Card.Header>
<Card.Title>Filtros</Card.Title>
<Card.Description>Filtra las clases por diferentes criterios</Card.Description>
</Card.Header>
<Card.Content>
<form onsubmit={(e) => { e.preventDefault(); applyFilters(); }} class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div class="space-y-2">
<Label for="filter-class-code">Código de Clase</Label>
<Input
id="filter-class-code"
bind:value={filters.class_code}
placeholder="Ej: A76"
/>
</div>
<div class="space-y-2">
<Label for="filter-client">ID del Cliente</Label>
<Input
id="filter-client"
type="number"
bind:value={filters.client_id}
placeholder="Ej: 123"
/>
</div>
<div class="flex items-end gap-2 md:col-span-2">
<Button type="submit" disabled={loading} class="flex-1">
<Filter class="mr-2" size={16} />
Filtrar
</Button>
<Button type="button" variant="outline" onclick={clearFilters} disabled={loading}>
<Trash2 size={16} />
</Button>
</div>
</form>
</Card.Content>
</Card.Root>
<!-- Error Message -->
{#if error}
<Card.Root class="border-destructive">
<Card.Header>
<Card.Title class="text-destructive">Error</Card.Title>
<Card.Description>{error}</Card.Description>
</Card.Header>
</Card.Root>
{/if}
<!-- Data Table -->
<Card.Root>
<Card.Header>
<div class="flex items-center justify-between">
<div>
<Card.Title>Listado de Clases</Card.Title>
<Card.Description>
Mostrando {allItems.length} de {totalItems} registros
</Card.Description>
</div>
<Button variant="outline" onclick={reloadData}>
<RefreshCw class="mr-2" size={16} />
Actualizar
</Button>
</div>
</Card.Header>
<Card.Content>
<!-- TanStack DataTable con Infinite Scroll -->
<DataTable
data={allItems}
{columns}
{loading}
{hasMore}
{loadMore}
/>
</Card.Content>
</Card.Root>
</div>
<!-- Dialog de crear nueva clase -->
<CreateEditDialog bind:open={createDialogOpen} onSuccess={handleDialogSuccess} />

View File

@@ -4,7 +4,7 @@
import { browser } from '$app/environment';
import { createColumns } from '$lib/components/dashboard/ports/columns';
import CreateEditDialog from '$lib/components/dashboard/ports/create-edit-dialog.svelte';
import DataTable from '$lib/components/dashboard/units_of_measure/ace/data-table.svelte';
import DataTable from '$lib/components/dashboard/general_catalogs/units_of_measure/ace/data-table.svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus } from 'lucide-svelte';

View File

@@ -17,7 +17,7 @@
import {
ArrowLeft, LoaderCircle, Save, Package, DollarSign,
FileText, Settings, Image as ImageIcon, Search,
UserCheck, CheckCircle2, XCircle, Tag, Layers
UserCheck, CheckCircle2, XCircle, Tag, Layers, Scale
} from 'lucide-svelte';
// Stores & APIs
@@ -25,18 +25,19 @@
import { partsApi, type PartCreate } from '$lib/api/dashboard/a76/parts';
import { clientsProvidersApi } from '$lib/api/dashboard/a76/clients-providers';
import { classesApi } from '$lib/api/dashboard/a76/classes';
import { materialTypesApi } from '$lib/api/dashboard/a76/material-types';
import { materialTypesApi } from '$lib/api/dashboard/a76/material-types';
// Modales
import ClientSelectorDialog from '$lib/components/dashboard/parts/client-selector-dialog.svelte';
import ClassSelectorDialog from '$lib/components/dashboard/parts/class-selector-dialog.svelte';
import MaterialTypeSelectorDialog from '$lib/components/dashboard/parts/material-type-selector-dialog.svelte';
// --- 1. IDENTIFICACIÓN ---
let id = $derived($page.params.id === 'new' ? null : Number($page.params.id));
let isEdit = $derived(!!id);
let title = $derived(isEdit ? "Editar Parte" : "Nueva Parte");
// --- 2. ESTADOS ---
let loading = $state(false);
let error = $state<string | null>(null);
@@ -59,10 +60,10 @@
// General
description_spanish: '',
description_english: '',
part_class: '',
material_type_key: '',
part_class: '',
material_type: '', // Corregido: coincide con backend
country_of_origin: 'MEX',
unit_of_measure: 'PZ',
unit_of_measure: 'PZ', // U.M. TIGIE
// Costos y Pesos
unit_weight: 0,
@@ -81,14 +82,17 @@
// Opcionales 2
fda_key: '',
// Otros
// Otros (Comerciales)
commercial_part_number: '',
alternate_unit_measure: '', // U.M. Comercial
// Regulatorios
fraction: '',
eccn: '',
license_code: '',
export_code: '',
exclusion_symbol: '',
alternate_unit_measure: '',
is_active: true
});
@@ -122,8 +126,7 @@
description_english: d.description_english || '',
part_class: d.part_class || '',
// OJO: Asegúrate que tu backend devuelva este campo si existe en BD
material_type_key: (d as any).material_type_key || '',
material_type: d.material_type || '', // Corregido
country_of_origin: d.country_of_origin || 'MEX',
unit_of_measure: d.unit_of_measure || 'PZ',
@@ -151,7 +154,7 @@
// Cargar datos visuales
if (d.client_id) await fetchClientName(d.client_id, companyId);
if (d.part_class) await fetchClassDesc(d.part_class, companyId);
if ((d as any).material_type_key) await fetchMaterialName((d as any).material_type_key);
if (d.material_type) await fetchMaterialName(d.material_type);
}
} catch (e) {
error = "Error al cargar la parte";
@@ -161,6 +164,7 @@
}
}
// --- HELPERS VISUALES ---
async function fetchClientName(clientId: number, companyId: number) {
try {
const res = await clientsProvidersApi.get(clientId, companyId);
@@ -194,8 +198,7 @@
} catch (e) { console.log("Error visual material", e); }
}
// --- HANDLERS ---
function handleClientSelect(client: any) {
formData.client_id = client.id;
selectedClientName = client.name;
@@ -208,10 +211,11 @@
}
function handleMaterialSelect(item: any) {
formData.material_type_key = item.key;
formData.material_type = item.key;
selectedMaterialDesc = item.description;
}
// --- SUBMIT ---
async function handleSubmit() {
error = null;
const activeCompanyId = companyStore.activeCompany?.id;
@@ -221,14 +225,12 @@
loading = true;
try {
const commonData = {
description_spanish: formData.description_spanish || null,
description_english: formData.description_english || null,
part_class: formData.part_class || null,
material_type_key: formData.material_type_key || null,
part_class: formData.part_class || null,
material_type: formData.material_type || null, // Corregido
country_of_origin: formData.country_of_origin || 'MEX',
unit_of_measure: formData.unit_of_measure,
@@ -253,11 +255,9 @@
};
if (isEdit && id) {
const response = await partsApi.update(id, commonData, activeCompanyId);
if (response.error) throw new Error(response.error);
} else {
const createData: PartCreate = {
...commonData,
company_id: activeCompanyId,
@@ -366,7 +366,7 @@
</div>
<Input
id="part_class_material"
bind:value={formData.material_type_key}
bind:value={formData.material_type}
maxlength={8}
placeholder="Seleccione Material..."
class="pl-9 font-mono cursor-pointer"
@@ -387,9 +387,30 @@
<p class="text-[10px] text-muted-foreground">Opcional: Clasificación adicional por tipo de material.</p>
</div>
<div class="grid gap-2 md:col-span-2">
<!-- <div class="grid gap-2">
<Label for="country">País Origen (ISO)</Label>
<Input id="country" bind:value={formData.country_of_origin} maxlength={3} placeholder="MEX"/>
<Input id="country" bind:value={formData.country_of_origin} maxlength={3} placeholder="MEX" class="font-mono"/>
</div> -->
<div class="grid gap-2">
<Label for="uom" class="required">Comercial</Label>
<Select.Root type="single" bind:value={formData.unit_of_measure}>
<Select.Trigger id="uom" class="font-mono">
{formData.unit_of_measure || 'Seleccione...'}
</Select.Trigger>
<Select.Content class="max-h-[300px]">
<Select.Group>
<Select.Label>Comunes</Select.Label>
<Select.Item value="PZ">Pieza (PZ)</Select.Item>
<Select.Item value="KG">Kilogramo (KG)</Select.Item>
<Select.Item value="L">Litro (L)</Select.Item>
<Select.Item value="M">Metro Lineal (M)</Select.Item>
<Select.Item value="M2">Metro Cuadrado (M2)</Select.Item>
<Select.Item value="JGO">Juego (JGO)</Select.Item>
<Select.Item value="PAR">Par (PAR)</Select.Item>
</Select.Group>
</Select.Content>
</Select.Root>
</div>
</div>
@@ -498,6 +519,7 @@
</Tabs.Content>
<Tabs.Content value="otros" class="space-y-6 pt-4 animate-in fade-in duration-300">
<div class="grid gap-2">
<Label for="client_id" class="required">Cliente Asignado</Label>
<div class="flex gap-2">
@@ -528,15 +550,45 @@
</div>
{/if}
</div>
<div class="grid gap-2">
<Label for="comm_pn">Número de Parte Comercial</Label>
<Input id="comm_pn" bind:value={formData.commercial_part_number} maxlength={70} />
<div class="p-4 border rounded-lg bg-slate-50 dark:bg-slate-900/30 space-y-4">
<h3 class="font-medium text-sm text-muted-foreground flex items-center gap-2">
<Package class="h-4 w-4"/> Datos Comerciales (Factura)
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="grid gap-2">
<Label for="comm_pn">Número de Parte Comercial</Label>
<Input id="comm_pn" bind:value={formData.commercial_part_number} maxlength={70} placeholder="Código en factura" />
</div>
<div class="grid gap-2">
<Label for="alt_um">Unidad de Medida Comercial</Label>
<Select.Root type="single" bind:value={formData.alternate_unit_measure}>
<Select.Trigger id="alt_um" class="font-mono">
{formData.alternate_unit_measure || 'Seleccione...'}
</Select.Trigger>
<Select.Content class="max-h-[300px]">
<Select.Group>
<Select.Label>Comunes</Select.Label>
<Select.Item value="PZ">Pieza (PZ)</Select.Item>
<Select.Item value="KG">Kilogramo (KG)</Select.Item>
<Select.Item value="L">Litro (L)</Select.Item>
<Select.Item value="M">Metro Lineal (M)</Select.Item>
<Select.Item value="M2">Metro Cuadrado (M2)</Select.Item>
<Select.Item value="JGO">Juego (JGO)</Select.Item>
<Select.Item value="PAR">Par (PAR)</Select.Item>
<Select.Item value="SET">Set (SET)</Select.Item>
<Select.Item value="CAJA">Caja (CAJA)</Select.Item>
<Select.Item value="PK">Paquete (PK)</Select.Item>
</Select.Group>
</Select.Content>
</Select.Root>
</div>
</div>
</div>
<div class="p-4 border rounded-lg space-y-4">
<Label for="alt_um">UM Conversión (Alterna)</Label>
<Input id="alt_um" bind:value={formData.alternate_unit_measure} maxlength={14} />
</div>
<div class="p-4 border rounded-lg space-y-4 bg-slate-50 dark:bg-slate-900/30">
<h3 class="font-medium text-sm text-muted-foreground">Datos Regulatorios</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="grid gap-2">
@@ -561,6 +613,7 @@
</div>
</div>
</div>
<div class="flex items-center gap-3 p-4 border rounded-lg bg-card">
<Switch id="is_active" bind:checked={formData.is_active} disabled={loading} />
<Label for="is_active">Parte Activa en Sistema</Label>