Se esta trabajando en el formulario de partes y sus relaciones
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user