Se esta trabajando en el formulario de partes y sus relaciones

This commit is contained in:
2026-01-06 18:02:45 -06:00
parent 2182f0b739
commit 7c5f78f258
9 changed files with 595 additions and 252 deletions

View File

@@ -0,0 +1,166 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
import * as Dialog from "$lib/components/ui/dialog";
import { Search, Loader2, Tag, Ruler } from "lucide-svelte";
import { classesApi, type A76Class } from "$lib/api/dashboard/a76/classes"; // Ajusta ruta
import { companyStore } from "$lib/stores/company.svelte";
// --- PROPS ---
let {
open = $bindable(false),
onSelect
}: {
open: boolean,
onSelect: (item: A76Class) => void
} = $props();
// --- ESTADO ---
let items = $state<A76Class[]>([]);
let loading = $state(false);
let searchTerm = $state("");
let loaded = $state(false);
// Filtro local: Busca por Clave (class_code) o Descripción (description_es)
let filteredItems = $derived(
items.filter(i =>
(i.class_code || "").toLowerCase().includes(searchTerm.toLowerCase()) ||
(i.description_es || "").toLowerCase().includes(searchTerm.toLowerCase())
)
);
// Cargar datos al abrir
$effect(() => {
if (open && !loaded && companyStore.activeCompany?.id) {
loadClasses();
}
});
async function loadClasses() {
if (!companyStore.activeCompany?.id) return;
loading = true;
try {
const res = await classesApi.list({
company_id: companyStore.activeCompany.id,
page: 1,
page_size: 100
});
const data = (res as any).data || res;
const list = data.items || data.classes || [];
if (Array.isArray(list)) {
items = list;
loaded = true;
} else {
console.warn("No encontré lista de clases en la respuesta:", data);
}
} catch (e) {
console.error("Error cargando clases:", e);
} finally {
loading = false;
}
}
function handleSelect(item: A76Class) {
if (onSelect) onSelect(item);
open = false;
}
</script>
<Dialog.Root bind:open={open}>
<Dialog.Content class="sm:max-w-[700px] max-h-[80vh] flex flex-col">
<Dialog.Header>
<Dialog.Title>Seleccionar Clase (Anexo 24)</Dialog.Title>
<Dialog.Description>
Seleccione la clasificación del material.
</Dialog.Description>
</Dialog.Header>
<div class="relative w-full my-2">
<Search class="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
type="search"
placeholder="Buscar por Clave o Descripción..."
class="pl-9"
bind:value={searchTerm}
/>
</div>
<div class="flex-1 overflow-y-auto border rounded-md min-h-[300px]">
{#if loading}
<div class="flex flex-col items-center justify-center h-48 gap-2 text-muted-foreground">
<Loader2 class="h-8 w-8 animate-spin text-primary" />
<p>Cargando catálogo...</p>
</div>
{:else if filteredItems.length === 0}
<div class="flex flex-col items-center justify-center h-48 text-muted-foreground">
<p>No se encontraron clases.</p>
</div>
{:else}
<table class="w-full text-sm">
<thead class="bg-muted/50 sticky top-0 backdrop-blur-sm">
<tr class="text-left border-b">
<th class="p-3 font-medium text-muted-foreground w-[100px]">Clave</th>
<th class="p-3 font-medium text-muted-foreground">Descripción</th>
<th class="p-3 font-medium text-muted-foreground w-[80px]">UM</th>
<th class="p-3 font-medium text-muted-foreground w-[80px]">Acción</th>
</tr>
</thead>
<tbody>
{#each filteredItems as item}
<tr class="border-b hover:bg-muted/50 transition-colors">
<td class="p-3">
<span class="font-mono font-bold text-primary bg-primary/10 px-2 py-1 rounded text-xs">
{item.class_code}
</span>
</td>
<td class="p-3">
<div class="flex items-center gap-2">
<Tag class="h-3 w-3 text-muted-foreground shrink-0" />
<span class="truncate max-w-[300px]" title={item.description_es || ''}>
{item.description_es || 'Sin descripción'}
</span>
</div>
</td>
<td class="p-3">
<div class="flex items-center gap-1 text-xs text-muted-foreground">
<Ruler class="h-3 w-3" />
{item.unit_of_measure || '-'}
</div>
</td>
<td class="p-2">
<Button
type="button"
size="sm"
variant="ghost"
class="h-8 w-full"
onclick={() => handleSelect(item)}
>
Usar
</Button>
</td>
</tr>
{/each}
</tbody>
</table>
{/if}
</div>
<Dialog.Footer>
<div class="text-xs text-muted-foreground self-center mr-auto">
{filteredItems.length} registros encontrados
</div>
<Button variant="outline" onclick={() => open = false}>Cancelar</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>

View File

@@ -2,20 +2,26 @@
import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
import * as Dialog from "$lib/components/ui/dialog";
import { Search, Loader2, User, Building2, CheckCircle2, XCircle } from "lucide-svelte";
import { Search, Loader2, User, Building2 } from "lucide-svelte";
import { clientsProvidersApi, type ClientProvider } from "$lib/api/dashboard/a76/clients-providers";
import { companyStore } from "$lib/stores/company.svelte";
// Props
let { open = $bindable(false), onSelect }: { open: boolean, onSelect: (client: ClientProvider) => void } = $props();
// Estado
// --- PROPS Y BINDING ---
let {
open = $bindable(false),
onSelect
}: {
open: boolean,
onSelect: (client: ClientProvider) => void
} = $props();
// --- ESTADO LOCAL ---
let clients = $state<ClientProvider[]>([]);
let loading = $state(false);
let searchTerm = $state("");
let loaded = $state(false);
let loaded = $state(false);
// Filtramos localmente para que sea instantáneo
// Filtro reactivo local
let filteredClients = $derived(
clients.filter(c =>
c.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
@@ -24,7 +30,7 @@
)
);
// Cargar clientes al abrir el modal
// Efecto para cargar datos cuando se abre el modal
$effect(() => {
if (open && !loaded && companyStore.activeCompany?.id) {
loadClients();
@@ -36,19 +42,20 @@
loading = true;
try {
// Petición a la API
const res = await clientsProvidersApi.list(companyStore.activeCompany.id, 1, 100, {
type: 'client'
});
// Normalización de respuesta
const responseData = (res as any).data || res;
if (responseData && responseData.items) {
clients = responseData.items;
loaded = true;
} else {
console.warn("La API respondió pero no trajo items:", responseData);
console.warn("La API no trajo items:", responseData);
}
} catch (e) {
console.error("Error cargando clientes:", e);
} finally {
@@ -56,9 +63,17 @@
}
}
// --- FUNCIÓN DE SELECCIÓN ---
function handleSelect(client: ClientProvider) {
console.log("Seleccionando cliente:", client.name);
if (onSelect) {
onSelect(client);
}
open = false; // Cerrar el modal
}
</script>
<Dialog.Root bind:open>
<Dialog.Root bind:open={open}>
<Dialog.Content class="sm:max-w-[700px] max-h-[80vh] flex flex-col">
<Dialog.Header>
<Dialog.Title>Seleccionar Cliente</Dialog.Title>
@@ -125,7 +140,13 @@
{/if}
</td>
<td class="p-2">
<Button size="sm" variant="ghost" class="h-8 w-full" onclick={() => handleSelect(client)}>
<Button
type="button"
size="sm"
variant="ghost"
class="h-8 w-full"
onclick={() => handleSelect(client)}
>
Usar
</Button>
</td>

View File

@@ -18,7 +18,7 @@ function formatCurrency(amount: number | null, currency: string | null): string
export function createColumns(onSuccess?: () => void): ColumnDef<Part>[] {
return [
// 1. STATUS (Corregido a Texto)
{
accessorKey: "is_active",
header: "Status",
@@ -35,7 +35,7 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Part>[] {
}
},
// 2. NUMERO PARTE
{
accessorKey: "part_number",
header: "No. Parte",
@@ -51,7 +51,7 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Part>[] {
}
},
// 3. DESCRIPCION (Español)
{
accessorKey: "description_spanish",
header: "Descripción",
@@ -67,7 +67,6 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Part>[] {
}
},
// 4. DESCRIPCION INGLES
{
accessorKey: "description_english",
header: "Desc. Inglés",
@@ -83,7 +82,7 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Part>[] {
}
},
// 5. CLASE
{
accessorKey: "part_class",
header: "Clase",
@@ -98,7 +97,7 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Part>[] {
}
},
// 6. TIPO (Commercial Part Number)
{
accessorKey: "commercial_part_number",
header: "Tipo",

View File

@@ -0,0 +1,133 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
import * as Dialog from "$lib/components/ui/dialog";
import { Search, Loader2, Layers, Tag, Box } from "lucide-svelte";
// Importamos la interfaz corregida
import { materialTypesApi, type MaterialType } from "$lib/api/dashboard/a76/material-types";
// --- PROPS ---
let {
open = $bindable(false),
onSelect
}: {
open: boolean,
onSelect: (item: MaterialType) => void
} = $props();
let items = $state<MaterialType[]>([]);
let loading = $state(false);
let searchTerm = $state("");
let loaded = $state(false);
let filteredItems = $derived(
items.filter(i =>
i.key.toLowerCase().includes(searchTerm.toLowerCase()) ||
i.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
i.type.toLowerCase().includes(searchTerm.toLowerCase())
)
);
$effect(() => {
if (open && !loaded) loadMaterials();
});
async function loadMaterials() {
loading = true;
try {
const res = await materialTypesApi.list(1, 100);
const responseData = (res as any).data || res;
if (responseData && responseData.items) {
items = responseData.items;
loaded = true;
} else {
console.error("Estructura inesperada:", responseData);
}
} catch (e) {
console.error("Error cargando materiales:", e);
} finally {
loading = false;
}
}
function handleSelect(item: MaterialType) {
if (onSelect) onSelect(item);
open = false;
}
function getCategoryIcon(type: string) {
if (type.includes('PRODUCTOS')) return Box;
if (type.includes('ACTIVO')) return Layers;
return Tag;
}
</script>
<Dialog.Root bind:open={open}>
<Dialog.Content class="sm:max-w-[650px] max-h-[80vh] flex flex-col">
<Dialog.Header>
<Dialog.Title>Seleccionar Tipo de Material</Dialog.Title>
<Dialog.Description>Catálogo general.</Dialog.Description>
</Dialog.Header>
<div class="relative w-full my-2">
<Search class="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input type="search" placeholder="Buscar..." class="pl-9" bind:value={searchTerm} />
</div>
<div class="flex-1 overflow-y-auto border rounded-md min-h-[300px]">
{#if loading}
<div class="flex flex-col items-center justify-center h-48 gap-2 text-muted-foreground">
<Loader2 class="h-8 w-8 animate-spin text-primary" />
<p>Cargando catálogo...</p>
</div>
{:else if filteredItems.length === 0}
<div class="flex flex-col items-center justify-center h-48 text-muted-foreground">
<p>No se encontraron resultados.</p>
</div>
{:else}
<table class="w-full text-sm">
<thead class="bg-muted/50 sticky top-0 backdrop-blur-sm">
<tr class="text-left border-b">
<th class="p-3 font-medium text-muted-foreground w-[80px]">Clave</th>
<th class="p-3 font-medium text-muted-foreground">Descripción</th>
<th class="p-3 font-medium text-muted-foreground w-[120px]">Tipo</th>
<th class="p-3 font-medium text-muted-foreground w-[80px]">Acción</th>
</tr>
</thead>
<tbody>
{#each filteredItems as item}
<tr class="border-b hover:bg-muted/50 transition-colors">
<td class="p-3 font-mono font-bold text-primary">{item.key}</td>
<td class="p-3 font-medium">{item.description}</td>
<td class="p-3">
<div class="flex items-center gap-1 text-xs text-muted-foreground">
<svelte:component this={getCategoryIcon(item.type)} class="h-3 w-3" />
{item.type}
</div>
</td>
<td class="p-2">
<Button type="button" size="sm" variant="ghost" class="h-8 w-full" onclick={() => handleSelect(item)}>
Usar
</Button>
</td>
</tr>
{/each}
</tbody>
</table>
{/if}
</div>
<Dialog.Footer>
<Button variant="outline" onclick={() => open = false}>Cancelar</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>