Merge branch 'development' into current branch and resolve modify/delete conflict in items-tab-form.svelte
This commit is contained in:
@@ -59,6 +59,8 @@ export interface A76ClassListParams {
|
||||
class_code?: string;
|
||||
description?: string;
|
||||
q?: string; // Agregado por si usas búsqueda general
|
||||
sort_by?: string;
|
||||
sort_order?: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
// --- API OBJECT ---
|
||||
@@ -112,12 +114,17 @@ export const classesApi = {
|
||||
company_id: number;
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
sort_by?: string;
|
||||
sort_order?: 'asc' | 'desc';
|
||||
}): Promise<ApiResponse<A76Class[]>> => {
|
||||
const query = new URLSearchParams({
|
||||
company_id: params.company_id.toString(),
|
||||
page: (params.page || 1).toString(),
|
||||
page_size: (params.page_size || 1000).toString()
|
||||
});
|
||||
|
||||
if (params.sort_by) query.append('sort_by', params.sort_by);
|
||||
if (params.sort_order) query.append('sort_order', params.sort_order);
|
||||
return api.get(`/v1/a76/classes/with-fa-data?${query.toString()}`);
|
||||
}
|
||||
};
|
||||
@@ -154,7 +154,9 @@ export const partsApi = {
|
||||
company_id: number;
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
q?: string
|
||||
q?: string;
|
||||
sort_by?: string;
|
||||
sort_order?: 'asc' | 'desc';
|
||||
}) => {
|
||||
const { company_id, page = 1, page_size = 50, q = '' } = params;
|
||||
|
||||
@@ -168,6 +170,9 @@ export const partsApi = {
|
||||
queryParams.description = q;
|
||||
}
|
||||
|
||||
if (params.sort_by) queryParams.sort_by = params.sort_by;
|
||||
if (params.sort_order) queryParams.sort_order = params.sort_order;
|
||||
|
||||
const query = new URLSearchParams(queryParams);
|
||||
|
||||
return api.get<PartListResponse>(`/v1/a76/parts/?${query.toString()}`);
|
||||
|
||||
@@ -274,6 +274,8 @@ export interface PedimentoFilters {
|
||||
status?: string;
|
||||
client_id?: number;
|
||||
year?: string;
|
||||
sort_by?: string;
|
||||
sort_order?: 'asc' | 'desc' | string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -288,19 +290,21 @@ export const pedimentosApi = {
|
||||
* @param companyId - ID de la compañía (por defecto 1)
|
||||
*/
|
||||
list: (page = 1, pageSize = 50, filters?: PedimentoFilters, companyId?: number) => {
|
||||
let url = `/v1/a76/pedimentos/?company_id=${companyId}&page=${page}&page_size=${pageSize}`;
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId?.toString() || '',
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString()
|
||||
});
|
||||
|
||||
if (filters?.status) {
|
||||
url += `&status=${encodeURIComponent(filters.status)}`;
|
||||
}
|
||||
if (filters?.client_id) {
|
||||
url += `&client_id=${filters.client_id}`;
|
||||
}
|
||||
if (filters?.year) {
|
||||
url += `&year=${encodeURIComponent(filters.year)}`;
|
||||
if (filters) {
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
params.append(key, value.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return api.get<PedimentoListResponse>(url);
|
||||
return api.get<PedimentoListResponse>(`/v1/a76/pedimentos/?${params.toString()}`);
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,7 +21,7 @@ export function createColumns(onSuccess?: () => void): ColumnDef<ExchangeRate>[]
|
||||
cell: ({ row }) => {
|
||||
const value = row.original.value;
|
||||
if (value === null || value === undefined) return 'N/A';
|
||||
return value.toFixed(6);
|
||||
return Number(value).toFixed(6);
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -18,7 +18,7 @@ export function createColumns(onSuccess?: () => void): ColumnDef<CustomsBrokerCo
|
||||
{
|
||||
accessorKey: 'amount',
|
||||
header: 'Importe',
|
||||
cell: ({ row }) => row.original.amount ? `$${row.original.amount.toFixed(2)}` : '-'
|
||||
cell: ({ row }) => row.original.amount ? `$${Number(row.original.amount).toFixed(2)}` : '-'
|
||||
},
|
||||
{
|
||||
accessorKey: 'priority',
|
||||
|
||||
139
frontend/src/lib/components/dashboard/goods/classes/columns.ts
Normal file
139
frontend/src/lib/components/dashboard/goods/classes/columns.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import type { ColumnDef } from "@tanstack/table-core";
|
||||
import { renderSnippet } from "$lib/components/ui/data-table/index.js";
|
||||
import { createRawSnippet } from "svelte";
|
||||
import type { A76Class } from "$lib/api/dashboard/a76/classes";
|
||||
|
||||
export function createColumns(): ColumnDef<A76Class>[] {
|
||||
return [
|
||||
{
|
||||
id: "select",
|
||||
header: ({ table }) => {
|
||||
return renderSnippet(
|
||||
createRawSnippet(() => ({
|
||||
render: () => `<div class="w-4"></div>`
|
||||
}))
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const isSelected = row.getIsSelected();
|
||||
const checkboxSnippet = createRawSnippet<[{ selected: boolean }]>((getProps) => {
|
||||
const { selected } = getProps();
|
||||
return {
|
||||
render: () => `<div class="flex items-center justify-center">
|
||||
<input type="checkbox" class="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary cursor-pointer" ${selected ? 'checked' : ''} />
|
||||
</div>`
|
||||
};
|
||||
});
|
||||
return renderSnippet(checkboxSnippet, { selected: isSelected });
|
||||
},
|
||||
size: 40,
|
||||
enableSorting: false,
|
||||
enableHiding: false
|
||||
},
|
||||
{
|
||||
accessorKey: "class_code",
|
||||
header: "Clase",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
const codeSnippet = createRawSnippet<[{ code: string }]>((getProps) => {
|
||||
const { code } = getProps();
|
||||
return {
|
||||
render: () => `<span class="inline-flex items-center rounded-md border border-blue-200 bg-blue-50 px-2 py-1 font-mono text-xs font-bold text-blue-700 dark:border-blue-800 dark:bg-blue-900/30 dark:text-blue-400">${code}</span>`
|
||||
};
|
||||
});
|
||||
return renderSnippet(codeSnippet, { code: row.original.class_code });
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "description_es",
|
||||
header: "Descripción Español",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
const descSnippet = createRawSnippet<[{ desc: string | null }]>((getProps) => {
|
||||
const { desc } = getProps();
|
||||
return {
|
||||
render: () => `<div class="text-sm font-medium">${desc || ''}</div>`
|
||||
};
|
||||
});
|
||||
return renderSnippet(descSnippet, { desc: row.original.description_es });
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "description_en",
|
||||
header: "Descripción Inglés",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
const descSnippet = createRawSnippet<[{ desc: string | null }]>((getProps) => {
|
||||
const { desc } = getProps();
|
||||
return {
|
||||
render: () => `<div class="text-sm text-muted-foreground">${desc || ''}</div>`
|
||||
};
|
||||
});
|
||||
return renderSnippet(descSnippet, { desc: row.original.description_en });
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "material_key",
|
||||
header: "Tipo",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
const typeSnippet = createRawSnippet<[{ key: string | null }]>((getProps) => {
|
||||
const { key } = getProps();
|
||||
let colorClass = 'bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-400';
|
||||
if (key === 'MP') colorClass = 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400';
|
||||
else if (key === 'SC') colorClass = 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400';
|
||||
else if (key === 'DESP') colorClass = 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400';
|
||||
|
||||
return {
|
||||
render: () => `<span class="rounded-full px-2 py-0.5 text-[10px] font-bold tracking-wider uppercase ${colorClass}">${key || ''}</span>`
|
||||
};
|
||||
});
|
||||
return renderSnippet(typeSnippet, { key: row.original.material_key });
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "unit_of_measure",
|
||||
header: "U.M.",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
const umSnippet = createRawSnippet<[{ um: string | null }]>((getProps) => {
|
||||
const { um } = getProps();
|
||||
return {
|
||||
render: () => `<div class="text-muted-foreground">${um || ''}</div>`
|
||||
};
|
||||
});
|
||||
return renderSnippet(umSnippet, { um: row.original.unit_of_measure });
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "fraction",
|
||||
header: "Fracción",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
const fracSnippet = createRawSnippet<[{ fr: string | null }]>((getProps) => {
|
||||
const { fr } = getProps();
|
||||
return {
|
||||
render: () => `<span class="font-mono text-xs text-orange-600 dark:text-orange-400">${fr || ''}</span>`
|
||||
};
|
||||
});
|
||||
return renderSnippet(fracSnippet, { fr: row.original.fraction });
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "us_fraction",
|
||||
header: "Fracción US",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
const fracSnippet = createRawSnippet<[{ fr: string | null }]>((getProps) => {
|
||||
const { fr } = getProps();
|
||||
return {
|
||||
render: () => `<span>${fr || '-'}</span>`
|
||||
};
|
||||
});
|
||||
return renderSnippet(fracSnippet, { fr: row.original.us_fraction });
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
export const columns = createColumns();
|
||||
@@ -0,0 +1,164 @@
|
||||
<script lang="ts" generics="TData, TValue">
|
||||
import {
|
||||
type ColumnDef,
|
||||
getCoreRowModel
|
||||
} from "@tanstack/table-core";
|
||||
import { createSvelteTable, FlexRender } from "$lib/components/ui/data-table/index.js";
|
||||
import * as Table from "$lib/components/ui/table/index.js";
|
||||
|
||||
type DataTableProps<TData, TValue> = {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
data: TData[];
|
||||
loading: boolean;
|
||||
selectedId?: number | null;
|
||||
onRowClick?: (row: TData) => void;
|
||||
sorting?: import("@tanstack/table-core").SortingState;
|
||||
onSortingChange?: (sorting: import("@tanstack/table-core").SortingState) => void;
|
||||
};
|
||||
|
||||
let {
|
||||
data,
|
||||
columns,
|
||||
loading,
|
||||
selectedId = null,
|
||||
onRowClick,
|
||||
sorting = [],
|
||||
onSortingChange
|
||||
}: DataTableProps<TData, TValue> = $props();
|
||||
|
||||
const table = createSvelteTable({
|
||||
get data() {
|
||||
return data;
|
||||
},
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
state: {
|
||||
get rowSelection() {
|
||||
return selectedId ? { [selectedId]: true } : {};
|
||||
},
|
||||
get sorting() {
|
||||
return sorting;
|
||||
}
|
||||
},
|
||||
onSortingChange: (updater) => {
|
||||
if (onSortingChange) {
|
||||
const nextSorting = typeof updater === 'function' ? updater(sorting) : updater;
|
||||
onSortingChange(nextSorting);
|
||||
}
|
||||
},
|
||||
manualSorting: true,
|
||||
enableRowSelection: true,
|
||||
enableMultiRowSelection: false
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="w-full h-full flex flex-col overflow-hidden">
|
||||
<div class="rounded-md border flex-1 overflow-auto bg-white dark:bg-black">
|
||||
<Table.Root class="w-full">
|
||||
<Table.Header class="bg-background sticky top-0 z-10">
|
||||
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||
<Table.Row>
|
||||
{#each headerGroup.headers as header (header.id)}
|
||||
<Table.Head class="whitespace-nowrap p-0">
|
||||
{#if !header.isPlaceholder}
|
||||
<button
|
||||
class="group flex h-full w-full items-center gap-2 px-2 py-2 text-left hover:bg-muted/50"
|
||||
onclick={header.column.getToggleSortingHandler()}
|
||||
disabled={!header.column.getCanSort()}
|
||||
>
|
||||
<FlexRender
|
||||
content={header.column.columnDef.header}
|
||||
context={header.getContext()}
|
||||
/>
|
||||
{#if header.column.getCanSort()}
|
||||
<div class="flex h-4 w-4 shrink-0 items-center justify-center">
|
||||
{#if header.column.getIsSorted() === 'asc'}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-chevron-up text-primary"
|
||||
><path d="m18 15-6-6-6 6" /></svg
|
||||
>
|
||||
{:else if header.column.getIsSorted() === 'desc'}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-chevron-down text-primary"
|
||||
><path d="m6 9 6 6 6-6" /></svg
|
||||
>
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-chevrons-up-down opacity-30 group-hover:opacity-100"
|
||||
><path d="m7 15 5 5 5-5" /><path d="m7 9 5-5 5 5" /></svg
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</Table.Head>
|
||||
{/each}
|
||||
</Table.Row>
|
||||
{/each}
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#each table.getRowModel().rows as row (row.id)}
|
||||
<Table.Row
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
onclick={() => {
|
||||
if (onRowClick) {
|
||||
onRowClick(row.original);
|
||||
}
|
||||
}}
|
||||
class="cursor-pointer hover:bg-muted/50 transition-colors {row.getIsSelected() ? 'bg-primary/10' : ''}"
|
||||
>
|
||||
{#each row.getVisibleCells() as cell (cell.id)}
|
||||
<Table.Cell class="whitespace-nowrap px-2 py-1">
|
||||
<FlexRender
|
||||
content={cell.column.columnDef.cell}
|
||||
context={cell.getContext()}
|
||||
/>
|
||||
</Table.Cell>
|
||||
{/each}
|
||||
</Table.Row>
|
||||
{:else}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={columns.length} class="h-24 text-center">
|
||||
{#if loading}
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
<div class="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
|
||||
<p class="text-sm text-muted-foreground">Cargando...</p>
|
||||
</div>
|
||||
{:else}
|
||||
No hay resultados.
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3,6 +3,7 @@ import { renderComponent, renderSnippet } from "$lib/components/ui/data-table/in
|
||||
import { createRawSnippet } from "svelte";
|
||||
import DataTableActions from "./data-table-actions.svelte";
|
||||
import type { Part } from "$lib/api/dashboard/a76/parts";
|
||||
import { Checkbox } from "$lib/components/ui/checkbox/index.js";
|
||||
|
||||
/**
|
||||
* Formatea moneda (USD/MXN)
|
||||
@@ -18,10 +19,47 @@ function formatCurrency(amount: number | null, currency: string | null): string
|
||||
|
||||
export function createColumns(onSuccess?: () => void): ColumnDef<Part>[] {
|
||||
return [
|
||||
{
|
||||
id: "select",
|
||||
header: ({ table }) => {
|
||||
const headerCheckbox = createRawSnippet<[{ table: import("@tanstack/table-core").Table<Part> }]>((getTable) => {
|
||||
const { table: t } = getTable();
|
||||
return {
|
||||
render: () => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'checkbox';
|
||||
input.checked = t.getIsAllPageRowsSelected();
|
||||
input.indeterminate = t.getIsSomePageRowsSelected();
|
||||
input.onchange = (e) => t.toggleAllPageRowsSelected(!!(e.target as HTMLInputElement).checked);
|
||||
// Usamos el componente Checkbox si es posible, pero para snippets crudos en TanStack 5
|
||||
// a veces es más directo un input o un Snippet de Svelte.
|
||||
// Aquí usaremos renderComponent para el Checkbox real.
|
||||
return "<span></span>";
|
||||
}
|
||||
};
|
||||
});
|
||||
return renderComponent(Checkbox, {
|
||||
checked: table.getIsAllPageRowsSelected(),
|
||||
indeterminate: table.getIsSomePageRowsSelected(),
|
||||
onCheckedChange: (value) => table.toggleAllPageRowsSelected(!!value),
|
||||
"aria-label": "Select all"
|
||||
});
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
return renderComponent(Checkbox, {
|
||||
checked: row.getIsSelected(),
|
||||
onCheckedChange: (value) => row.toggleSelected(!!value),
|
||||
"aria-label": "Select row"
|
||||
});
|
||||
},
|
||||
enableSorting: false,
|
||||
enableHiding: false
|
||||
},
|
||||
|
||||
{
|
||||
accessorKey: "is_active",
|
||||
header: "Status",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
const statusSnippet = createRawSnippet<[{ active: boolean }]>((getStatus) => {
|
||||
const { active } = getStatus();
|
||||
@@ -39,6 +77,7 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Part>[] {
|
||||
{
|
||||
accessorKey: "part_number",
|
||||
header: "No. Parte",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
const pnSnippet = createRawSnippet<[{ pn: string }]>((getPn) => {
|
||||
const { pn } = getPn();
|
||||
@@ -55,6 +94,7 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Part>[] {
|
||||
{
|
||||
accessorKey: "description_spanish",
|
||||
header: "Descripción",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
const descSnippet = createRawSnippet<[{ desc: string }]>((getDesc) => {
|
||||
const { desc } = getDesc();
|
||||
@@ -86,6 +126,7 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Part>[] {
|
||||
{
|
||||
accessorKey: "part_class",
|
||||
header: "Clase",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
const classSnippet = createRawSnippet<[{ cls: string }]>((getCls) => {
|
||||
const { cls } = getCls();
|
||||
@@ -117,13 +158,11 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Part>[] {
|
||||
accessorKey: "unit_of_measure",
|
||||
header: "U.M.",
|
||||
cell: ({ row }) => {
|
||||
const umSnippet = createRawSnippet<[{ um: string }]>((getUm) => {
|
||||
const umSnippet = createRawSnippet<[{ um: string | null }]>((getUm) => {
|
||||
const { um } = getUm();
|
||||
return {
|
||||
render: () =>
|
||||
`<span class="inline-flex items-center rounded-sm bg-blue-50 dark:bg-blue-900 text-blue-700 dark:text-blue-300 px-1.5 py-0.5 text-[10px] font-bold ring-1 ring-inset ring-blue-700/10">
|
||||
${um}
|
||||
</span>`
|
||||
`<span class="inline-flex items-center rounded-sm bg-blue-50 dark:bg-blue-900 text-blue-700 dark:text-blue-300 px-1.5 py-0.5 text-[10px] font-bold ring-1 ring-inset ring-blue-700/10">${um || '-'}</span>`
|
||||
};
|
||||
});
|
||||
return renderSnippet(umSnippet, { um: row.original.unit_of_measure });
|
||||
@@ -134,6 +173,7 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Part>[] {
|
||||
{
|
||||
accessorKey: "fraction",
|
||||
header: "Fracción",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
const fracSnippet = createRawSnippet<[{ fr: string }]>((getFrac) => {
|
||||
const { fr } = getFrac();
|
||||
@@ -164,6 +204,7 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Part>[] {
|
||||
{
|
||||
accessorKey: "unit_cost",
|
||||
header: "Costo",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
const costSnippet = createRawSnippet<[{ amount: number | null, curr: string | null }]>((getCost) => {
|
||||
const { amount, curr } = getCost();
|
||||
@@ -197,13 +238,14 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Part>[] {
|
||||
{
|
||||
accessorKey: "unit_weight",
|
||||
header: "Peso Unitario",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
const weightSnippet = createRawSnippet<[{ weight: number | null, type: string | null }]>((getWeight) => {
|
||||
const { weight, type } = getWeight();
|
||||
return {
|
||||
render: () => {
|
||||
if (weight === null || weight === undefined) return '-';
|
||||
return `<div class="text-xs font-mono">${weight.toFixed(4)} ${type || ''}</div>`;
|
||||
if (weight === null || weight === undefined) return '<span>-</span>';
|
||||
return `<div class="text-xs font-mono">${Number(weight).toFixed(4)} ${type || ''}</div>`;
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -233,10 +275,11 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Part>[] {
|
||||
{
|
||||
accessorKey: "updated_at",
|
||||
header: "Fecha Modificación",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
const dateSnippet = createRawSnippet<[{ date: string }]>((getDate) => {
|
||||
const { date } = getDate();
|
||||
if (!date) return { render: () => '-' };
|
||||
if (!date) return { render: () => '<span>-</span>' };
|
||||
const formatted = new Date(date).toLocaleDateString('es-MX', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
|
||||
@@ -82,9 +82,9 @@
|
||||
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
|
||||
<DropdownMenu.Separator />
|
||||
|
||||
<DropdownMenu.Item onclick={handleEdit} >
|
||||
<DropdownMenu.Item onSelect={handleEdit}>
|
||||
<Pencil size={16} class="mr-2" />
|
||||
Editar
|
||||
<span>Editar</span>
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Separator />
|
||||
|
||||
@@ -13,6 +13,11 @@
|
||||
loading: boolean;
|
||||
hasMore: boolean;
|
||||
loadMore: () => void;
|
||||
sorting?: import("@tanstack/table-core").SortingState;
|
||||
onSortingChange?: (sorting: import("@tanstack/table-core").SortingState) => void;
|
||||
rowSelection?: import("@tanstack/table-core").RowSelectionState;
|
||||
onRowSelectionChange?: (rowSelection: import("@tanstack/table-core").RowSelectionState) => void;
|
||||
onRowClick?: (row: TData) => void;
|
||||
};
|
||||
|
||||
let {
|
||||
@@ -20,7 +25,12 @@
|
||||
columns,
|
||||
loading,
|
||||
hasMore,
|
||||
loadMore
|
||||
loadMore,
|
||||
sorting = [],
|
||||
onSortingChange,
|
||||
rowSelection = {},
|
||||
onRowSelectionChange,
|
||||
onRowClick
|
||||
}: DataTableProps<TData, TValue> = $props();
|
||||
|
||||
const table = createSvelteTable({
|
||||
@@ -28,7 +38,28 @@
|
||||
return data;
|
||||
},
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel()
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
state: {
|
||||
get sorting() {
|
||||
return sorting;
|
||||
},
|
||||
get rowSelection() {
|
||||
return rowSelection;
|
||||
}
|
||||
},
|
||||
onSortingChange: (updater) => {
|
||||
if (onSortingChange) {
|
||||
const nextSorting = typeof updater === 'function' ? updater(sorting) : updater;
|
||||
onSortingChange(nextSorting);
|
||||
}
|
||||
},
|
||||
onRowSelectionChange: (updater) => {
|
||||
if (onRowSelectionChange) {
|
||||
const nextRowSelection = typeof updater === 'function' ? updater(rowSelection) : updater;
|
||||
onRowSelectionChange(nextRowSelection);
|
||||
}
|
||||
},
|
||||
manualSorting: true
|
||||
});
|
||||
|
||||
let scrollContainer = $state<HTMLDivElement>();
|
||||
@@ -66,12 +97,65 @@
|
||||
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||
<Table.Row>
|
||||
{#each headerGroup.headers as header (header.id)}
|
||||
<Table.Head>
|
||||
<Table.Head class="p-0">
|
||||
{#if !header.isPlaceholder}
|
||||
<FlexRender
|
||||
content={header.column.columnDef.header}
|
||||
context={header.getContext()}
|
||||
/>
|
||||
<button
|
||||
class="group flex h-full w-full items-center gap-2 px-4 py-2 text-left hover:bg-muted/50"
|
||||
onclick={header.column.getToggleSortingHandler()}
|
||||
disabled={!header.column.getCanSort()}
|
||||
>
|
||||
<FlexRender
|
||||
content={header.column.columnDef.header}
|
||||
context={header.getContext()}
|
||||
/>
|
||||
{#if header.column.getCanSort()}
|
||||
<div class="flex h-4 w-4 shrink-0 items-center justify-center">
|
||||
{#if header.column.getIsSorted() === 'asc'}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-chevron-up text-primary"
|
||||
><path d="m18 15-6-6-6 6" /></svg
|
||||
>
|
||||
{:else if header.column.getIsSorted() === 'desc'}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-chevron-down text-primary"
|
||||
><path d="m6 9 6 6 6-6" /></svg
|
||||
>
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-chevrons-up-down opacity-30 group-hover:opacity-100"
|
||||
><path d="m7 15 5 5 5-5" /><path d="m7 9 5-5 5 5" /></svg
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</Table.Head>
|
||||
{/each}
|
||||
@@ -80,7 +164,11 @@
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#each table.getRowModel().rows as row (row.id)}
|
||||
<Table.Row data-state={row.getIsSelected() && "selected"}>
|
||||
<Table.Row
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
onclick={() => onRowClick?.(row.original)}
|
||||
class={onRowClick ? "cursor-pointer" : ""}
|
||||
>
|
||||
{#each row.getVisibleCells() as cell (cell.id)}
|
||||
<Table.Cell>
|
||||
<FlexRender
|
||||
|
||||
@@ -121,6 +121,7 @@ export function createColumns(
|
||||
{
|
||||
accessorKey: "operation_type",
|
||||
header: "Operación",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
const operationType = row.original.operation_type;
|
||||
|
||||
@@ -142,6 +143,7 @@ export function createColumns(
|
||||
{
|
||||
accessorKey: "invoice_type",
|
||||
header: "Tipo Factura",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
const invoiceType = row.original.invoice_type;
|
||||
const colorClass = getInvoiceTypeColor(invoiceType);
|
||||
@@ -150,9 +152,7 @@ export function createColumns(
|
||||
const { type, colorClass } = getProps();
|
||||
return {
|
||||
render: () =>
|
||||
`<span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${colorClass}">
|
||||
${type || '-'}
|
||||
</span>`
|
||||
`<span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${colorClass}">${type || '-'}</span>`
|
||||
};
|
||||
});
|
||||
return renderSnippet(typeSnippet, { type: invoiceType, colorClass });
|
||||
@@ -161,6 +161,7 @@ export function createColumns(
|
||||
{
|
||||
accessorKey: "invoice_number",
|
||||
header: "Núm. Factura",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
const numberSnippet = createRawSnippet<[{ number?: string | null }]>((getNumber) => {
|
||||
const { number } = getNumber();
|
||||
@@ -210,6 +211,7 @@ export function createColumns(
|
||||
{
|
||||
accessorKey: "invoice_date",
|
||||
header: "Fecha Factura",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
const dateSnippet = createRawSnippet<[{ date: string }]>((getDate) => {
|
||||
const { date } = getDate();
|
||||
@@ -240,6 +242,7 @@ export function createColumns(
|
||||
{
|
||||
accessorKey: "document_type",
|
||||
header: "Tipo Doc.",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
const typeSnippet = createRawSnippet<[{ type?: string | null }]>((getType) => {
|
||||
const { type } = getType();
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
selectedIds?: number[];
|
||||
onRowClick?: (row: TData) => void;
|
||||
compact?: boolean;
|
||||
sorting?: import("@tanstack/table-core").SortingState;
|
||||
onSortingChange?: (sorting: import("@tanstack/table-core").SortingState) => void;
|
||||
};
|
||||
|
||||
let {
|
||||
@@ -24,7 +26,9 @@
|
||||
loadMore,
|
||||
selectedIds = [],
|
||||
onRowClick,
|
||||
compact = false
|
||||
compact = false,
|
||||
sorting = [],
|
||||
onSortingChange
|
||||
}: DataTableProps<TData, TValue> = $props();
|
||||
|
||||
const table = createSvelteTable({
|
||||
@@ -42,12 +46,28 @@
|
||||
selection[id.toString()] = true;
|
||||
});
|
||||
return selection;
|
||||
},
|
||||
get sorting() {
|
||||
return sorting;
|
||||
}
|
||||
},
|
||||
onStateChange: (updater: any) => {
|
||||
if (onSortingChange) {
|
||||
const currentState = table.getState();
|
||||
const nextState = typeof updater === 'function' ? updater(currentState) : updater;
|
||||
|
||||
// Identify if this was a sorting update or at least contains sorting
|
||||
if (nextState && nextState.sorting !== undefined) {
|
||||
onSortingChange(nextState.sorting);
|
||||
} else if (Array.isArray(nextState)) {
|
||||
// Fallback for when updater might return just the array slice
|
||||
onSortingChange(nextState);
|
||||
}
|
||||
}
|
||||
},
|
||||
enableRowSelection: true,
|
||||
enableMultiRowSelection: true
|
||||
|
||||
// No necesitamos onRowSelectionChange porque controlamos el estado desde fuera
|
||||
enableMultiRowSelection: true,
|
||||
manualSorting: true // Sorting is handled server-side for this component
|
||||
});
|
||||
|
||||
let scrollContainer = $state<HTMLDivElement>();
|
||||
@@ -99,10 +119,67 @@
|
||||
.join(' ')}
|
||||
>
|
||||
{#if !header.isPlaceholder}
|
||||
<FlexRender
|
||||
content={header.column.columnDef.header}
|
||||
context={header.getContext()}
|
||||
/>
|
||||
<button
|
||||
class={[
|
||||
'flex w-full items-center justify-between gap-2',
|
||||
header.column.getCanSort() && 'cursor-pointer select-none'
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
onclick={header.column.getToggleSortingHandler()}
|
||||
>
|
||||
<FlexRender
|
||||
content={header.column.columnDef.header}
|
||||
context={header.getContext()}
|
||||
/>
|
||||
{#if header.column.getCanSort()}
|
||||
<div class="flex h-4 w-4 shrink-0 items-center justify-center">
|
||||
{#if header.column.getIsSorted() === 'asc'}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-chevron-up text-primary"
|
||||
><path d="m18 15-6-6-6 6" /></svg
|
||||
>
|
||||
{:else if header.column.getIsSorted() === 'desc'}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-chevron-down text-primary"
|
||||
><path d="m6 9 6 6 6-6" /></svg
|
||||
>
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-chevrons-up-down opacity-30 group-hover:opacity-100"
|
||||
><path d="m7 15 5 5 5-5" /><path d="m7 9 5-5 5 5" /></svg
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</Table.Head>
|
||||
{/each}
|
||||
|
||||
@@ -264,7 +264,7 @@
|
||||
<div class="flex-1 overflow-y-auto px-2">
|
||||
<div class="space-y-2">
|
||||
|
||||
{#if line}
|
||||
{#if editingItem}
|
||||
{#if showRepairBlock}
|
||||
<!-- Importación de Reparación: Genera Descarga? + Factura de Expo + Línea de Expo -->
|
||||
<div class="space-y-3 rounded-md border border-zinc-200 bg-white p-3 dark:border-zinc-800 dark:bg-zinc-900">
|
||||
@@ -481,13 +481,15 @@
|
||||
<h3 class="text-[10px] font-semibold text-zinc-500 uppercase tracking-wide">Datos Principales</h3>
|
||||
</div>
|
||||
<div class="p-2">
|
||||
<MainData
|
||||
bind:lineItem={editingItem}
|
||||
bind:quantities={editingItem.quantity!}
|
||||
bind:financials={editingItem.financial!}
|
||||
bind:customs={editingItem.customs!}
|
||||
{invoice}
|
||||
/>
|
||||
{#if editingItem.quantity && editingItem.financial && editingItem.customs}
|
||||
<MainData
|
||||
bind:lineItem={editingItem}
|
||||
bind:quantities={editingItem.quantity}
|
||||
bind:financials={editingItem.financial}
|
||||
bind:customs={editingItem.customs}
|
||||
{invoice}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -498,10 +500,12 @@
|
||||
<h3 class="text-[10px] font-semibold text-zinc-500 uppercase tracking-wide">Configuración</h3>
|
||||
</div>
|
||||
<div class="p-2">
|
||||
<ItemConfiguration
|
||||
bind:lineItem={editingItem}
|
||||
bind:descriptions={editingItem.description!}
|
||||
/>
|
||||
{#if editingItem.description}
|
||||
<ItemConfiguration
|
||||
bind:lineItem={editingItem}
|
||||
bind:descriptions={editingItem.description}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -519,38 +523,46 @@
|
||||
<div class="mt-1.5">
|
||||
<Tabs.Content value="generales" class="m-0">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
<PackagesSection
|
||||
bind:item={editingItem}
|
||||
bind:lineItem={editingItem}
|
||||
bind:descriptions={editingItem.description!}
|
||||
bind:customs={editingItem.customs!}
|
||||
bind:quantities={editingItem.quantity!}
|
||||
invoice={invoice}
|
||||
/>
|
||||
<SummarySection
|
||||
bind:financials={editingItem.financial!}
|
||||
bind:quantities={editingItem.quantity!}
|
||||
lineItem={editingItem}
|
||||
{invoice}
|
||||
/>
|
||||
{#if editingItem.description && editingItem.customs && editingItem.quantity}
|
||||
<PackagesSection
|
||||
bind:item={editingItem}
|
||||
bind:lineItem={editingItem}
|
||||
bind:descriptions={editingItem.description}
|
||||
bind:customs={editingItem.customs}
|
||||
bind:quantities={editingItem.quantity}
|
||||
invoice={invoice}
|
||||
/>
|
||||
{/if}
|
||||
{#if editingItem.financial && editingItem.quantity}
|
||||
<SummarySection
|
||||
bind:financials={editingItem.financial}
|
||||
bind:quantities={editingItem.quantity}
|
||||
lineItem={editingItem}
|
||||
{invoice}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="continuacion" class="m-0 focus-visible:outline-none">
|
||||
<TabContinuation
|
||||
bind:lineItem={editingItem}
|
||||
bind:descriptions={editingItem.description!}
|
||||
visibility={visibility}
|
||||
/>
|
||||
{#if editingItem.description}
|
||||
<TabContinuation
|
||||
bind:lineItem={editingItem}
|
||||
bind:descriptions={editingItem.description}
|
||||
visibility={visibility}
|
||||
/>
|
||||
{/if}
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="series" class="m-0 focus-visible:outline-none">
|
||||
<TabSeries
|
||||
bind:descriptions={editingItem.description!}
|
||||
bind:series={editingItem.series!}
|
||||
lineItem={editingItem}
|
||||
{invoice}
|
||||
/>
|
||||
{#if editingItem.description && editingItem.series}
|
||||
<TabSeries
|
||||
bind:descriptions={editingItem.description}
|
||||
bind:series={editingItem.series}
|
||||
lineItem={editingItem}
|
||||
{invoice}
|
||||
/>
|
||||
{/if}
|
||||
</Tabs.Content>
|
||||
|
||||
{#if visibility.showLabelingTab}
|
||||
|
||||
@@ -64,12 +64,14 @@
|
||||
: null
|
||||
);
|
||||
|
||||
// Ensure descriptions.has_serial has default
|
||||
$effect(() => {
|
||||
if (descriptions && descriptions.has_serial === undefined) {
|
||||
descriptions.has_serial = false;
|
||||
const internalHasSerial = $derived(descriptions?.has_serial === true);
|
||||
function toggleHasSerial() {
|
||||
if (descriptions) {
|
||||
descriptions.has_serial = !descriptions.has_serial;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const hasSerial = $derived(internalHasSerial);
|
||||
|
||||
// Ensure current serie has defaults for form fields
|
||||
$effect(() => {
|
||||
@@ -83,7 +85,6 @@
|
||||
}
|
||||
});
|
||||
|
||||
const hasSerial = $derived(Boolean(descriptions?.has_serial));
|
||||
const invoiceNumber = $derived(invoice?.invoice_number || '');
|
||||
const invoiceLine = $derived(lineItem?.line_number != null ? String(lineItem.line_number) : '');
|
||||
const partNumber = $derived((lineItem as any)?.part_number_display || lineItem?.part_number || '');
|
||||
@@ -137,7 +138,11 @@
|
||||
|
||||
<div class="flex items-center justify-between gap-2 flex-wrap">
|
||||
<div class="flex items-center space-x-1.5">
|
||||
<Checkbox id="has_serial" bind:checked={descriptions.has_serial} />
|
||||
<Checkbox
|
||||
id="has_serial"
|
||||
checked={descriptions?.has_serial === true}
|
||||
onCheckedChange={(v: boolean) => { if (descriptions) descriptions.has_serial = v; }}
|
||||
/>
|
||||
<Label for="has_serial" class="text-xs font-normal">Lleva serie (LLEVASERIE)</Label>
|
||||
</div>
|
||||
<Button
|
||||
|
||||
@@ -139,16 +139,13 @@
|
||||
})
|
||||
);
|
||||
|
||||
// Derived state for easier binding and safety
|
||||
let line = $derived(editingItem);
|
||||
|
||||
// Initialize missing nested objects if they don't exist
|
||||
// Use editingItem directly and ensure it's safe
|
||||
$effect(() => {
|
||||
if (open && editingItem) {
|
||||
if (editingItem && !editingItem.quantity) editingItem.quantity = {} as any;
|
||||
if (editingItem && !editingItem.financial) editingItem.financial = {} as any;
|
||||
if (editingItem && !editingItem.customs) editingItem.customs = {} as any;
|
||||
if (editingItem && !editingItem.description) editingItem.description = {} as any;
|
||||
if (!editingItem.quantity) editingItem.quantity = {} as any;
|
||||
if (!editingItem.financial) editingItem.financial = {} as any;
|
||||
if (!editingItem.customs) editingItem.customs = {} as any;
|
||||
if (!editingItem.description) editingItem.description = {} as any;
|
||||
if (editingItem.has_fda_code === undefined) editingItem.has_fda_code = false;
|
||||
}
|
||||
});
|
||||
@@ -193,7 +190,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-h-[calc(90vh-96px)] overflow-auto bg-slate-50/60 p-6 dark:bg-black">
|
||||
{#if editingItem}
|
||||
<div class="max-h-[calc(90vh-96px)] overflow-auto bg-slate-50/60 p-6 dark:bg-black">
|
||||
<Tabs.Root bind:value={activeTab} class="mt-0">
|
||||
<Tabs.List class="grid w-full grid-cols-4">
|
||||
<Tabs.Trigger value="general">General</Tabs.Trigger>
|
||||
@@ -262,8 +260,8 @@
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="quantity_general">Cantidad</Label>
|
||||
{#if line?.quantity}
|
||||
<Input id="quantity_general" type="number" step="0.00000001" min="0" bind:value={line.quantity.quantity} />
|
||||
{#if editingItem?.quantity}
|
||||
<Input id="quantity_general" type="number" step="0.00000001" min="0" bind:value={editingItem.quantity.quantity} />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
@@ -271,7 +269,7 @@
|
||||
<div class="flex gap-1">
|
||||
<Input
|
||||
id="unit_general"
|
||||
value={(editingItem as any).unit_code || line?.quantity?.unit_of_measure || ''}
|
||||
value={(editingItem as any).unit_code || editingItem?.quantity?.unit_of_measure || ''}
|
||||
readonly
|
||||
class="cursor-pointer bg-muted"
|
||||
placeholder="Selecciona U.M."
|
||||
@@ -285,8 +283,8 @@
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="unit_cost_capture">Costo Unitario</Label>
|
||||
{#if line?.financial}
|
||||
<Input id="unit_cost_capture" type="number" step="0.00000001" min="0" bind:value={line.financial.unit_cost_capture} />
|
||||
{#if editingItem?.financial}
|
||||
<Input id="unit_cost_capture" type="number" step="0.00000001" min="0" bind:value={editingItem.financial.unit_cost_capture} />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
@@ -294,7 +292,7 @@
|
||||
<div class="flex gap-1">
|
||||
<Input
|
||||
id="origin_country_general"
|
||||
value={line?.customs?.origin_country || ''}
|
||||
value={editingItem?.customs?.origin_country || ''}
|
||||
readonly
|
||||
class="cursor-pointer bg-muted"
|
||||
placeholder="Selecciona país"
|
||||
@@ -324,13 +322,15 @@
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="fraction_type_general">Tipo de Tarifa</Label>
|
||||
<select id="fraction_type_general" bind:value={line.customs.fraction_type} class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2">
|
||||
<option value=""></option>
|
||||
<option value="GENERAL">GENERAL</option>
|
||||
<option value="PROSEC">PROSEC</option>
|
||||
<option value="ALADI">ALADI</option>
|
||||
<option value="TLCS">TLCS</option>
|
||||
</select>
|
||||
{#if editingItem?.customs}
|
||||
<select id="fraction_type_general" bind:value={editingItem.customs.fraction_type} class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2">
|
||||
<option value=""></option>
|
||||
<option value="GENERAL">GENERAL</option>
|
||||
<option value="PROSEC">PROSEC</option>
|
||||
<option value="ALADI">ALADI</option>
|
||||
<option value="TLCS">TLCS</option>
|
||||
</select>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -363,11 +363,11 @@
|
||||
<!-- Campos específicos de SCAII -->
|
||||
<div class="space-y-2">
|
||||
<Label for="product_description">Descripción del Producto</Label>
|
||||
{#if line?.description}
|
||||
{#if editingItem?.description}
|
||||
<Input
|
||||
id="product_description"
|
||||
placeholder="Descripción detallada del producto"
|
||||
bind:value={line.description.description_spanish}
|
||||
bind:value={editingItem.description.description_spanish}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -375,12 +375,12 @@
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="sku">SKU</Label>
|
||||
{#if line}
|
||||
{#if editingItem}
|
||||
<div class="flex gap-1">
|
||||
<Input
|
||||
id="sku"
|
||||
placeholder="Código SKU del producto"
|
||||
value={(line as any).part_number_display || ''}
|
||||
value={(editingItem as any).part_number_display || ''}
|
||||
readonly
|
||||
class="cursor-pointer bg-muted"
|
||||
onclick={() => (showPartDialog = true)}
|
||||
@@ -393,8 +393,8 @@
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="batch">Lote</Label>
|
||||
{#if line?.description}
|
||||
<Input id="batch" placeholder="Número de lote" bind:value={line.description.lot} />
|
||||
{#if editingItem?.description}
|
||||
<Input id="batch" placeholder="Número de lote" bind:value={editingItem.description.lot} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -405,11 +405,11 @@
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="tariff_fraction">Fracción Arancelaria</Label>
|
||||
{#if line?.customs}
|
||||
{#if editingItem?.customs}
|
||||
<Input
|
||||
id="tariff_fraction"
|
||||
placeholder="8 dígitos"
|
||||
bind:value={line.customs.fraction}
|
||||
bind:value={editingItem.customs.fraction}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -417,21 +417,21 @@
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="product_type">Tipo de Producto</Label>
|
||||
{#if line?.description}
|
||||
{#if editingItem?.description}
|
||||
<Textarea
|
||||
id="product_type"
|
||||
placeholder="Materia prima, producto terminado, etc."
|
||||
bind:value={(line.description as any).extra_description_2}
|
||||
bind:value={(editingItem.description as any).extra_description_2}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="material_type">Tipo de Material</Label>
|
||||
{#if line?.description}
|
||||
{#if editingItem?.description}
|
||||
<Textarea
|
||||
id="material_type"
|
||||
placeholder="Metal, plástico, etc."
|
||||
bind:value={(line.description as any).extra_description_3}
|
||||
bind:value={(editingItem.description as any).extra_description_3}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -445,8 +445,8 @@
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="country_origin">País de Origen</Label>
|
||||
{#if line?.customs}
|
||||
<Input id="country_origin" placeholder="Código del país" bind:value={line.customs.origin_country} />
|
||||
{#if editingItem?.customs}
|
||||
<Input id="country_origin" placeholder="Código del país" bind:value={editingItem.customs.origin_country} />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
@@ -463,22 +463,22 @@
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="quantity">Cantidad</Label>
|
||||
{#if line?.quantity}
|
||||
{#if editingItem?.quantity}
|
||||
<Input
|
||||
id="quantity"
|
||||
type="number"
|
||||
placeholder="0"
|
||||
bind:value={line.quantity.quantity}
|
||||
bind:value={editingItem.quantity.quantity}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="unit">Unidad de Medida</Label>
|
||||
{#if line?.quantity}
|
||||
{#if editingItem?.quantity}
|
||||
<Input
|
||||
id="unit"
|
||||
placeholder="PZA, KG, M, etc."
|
||||
bind:value={line.quantity.unit_of_measure}
|
||||
bind:value={editingItem.quantity.unit_of_measure}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -487,25 +487,25 @@
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="net_weight">Peso Neto (KG)</Label>
|
||||
{#if line?.quantity}
|
||||
{#if editingItem?.quantity}
|
||||
<Input
|
||||
id="net_weight"
|
||||
type="number"
|
||||
step="0.01"
|
||||
placeholder="0.00"
|
||||
bind:value={line.quantity.net_weight}
|
||||
bind:value={editingItem.quantity.net_weight}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="gross_weight">Peso Bruto (KG)</Label>
|
||||
{#if line?.quantity}
|
||||
{#if editingItem?.quantity}
|
||||
<Input
|
||||
id="gross_weight"
|
||||
type="number"
|
||||
step="0.01"
|
||||
placeholder="0.00"
|
||||
bind:value={line.quantity.gross_weight}
|
||||
bind:value={editingItem.quantity.gross_weight}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -514,13 +514,13 @@
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="unit_cost_usd">Costo Unitario (USD)</Label>
|
||||
{#if line?.financial}
|
||||
{#if editingItem?.financial}
|
||||
<Input
|
||||
id="unit_cost_usd"
|
||||
type="number"
|
||||
step="0.0001"
|
||||
placeholder="0.00"
|
||||
bind:value={line.financial.unit_cost_usd}
|
||||
bind:value={editingItem.financial.unit_cost_usd}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -531,7 +531,7 @@
|
||||
type="number"
|
||||
step="0.01"
|
||||
placeholder="0.00"
|
||||
value={(line?.quantity?.quantity || 0) * (line?.financial?.unit_cost_usd || 0)}
|
||||
value={(editingItem?.quantity?.quantity || 0) * (editingItem?.financial?.unit_cost_usd || 0)}
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
@@ -541,22 +541,22 @@
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="packages">Número de Bultos</Label>
|
||||
{#if line?.quantity}
|
||||
{#if editingItem?.quantity}
|
||||
<Input
|
||||
id="packages"
|
||||
type="number"
|
||||
placeholder="0"
|
||||
bind:value={(line.quantity as any).packages}
|
||||
bind:value={(editingItem.quantity as any).packages}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="package_type">Tipo de Empaque</Label>
|
||||
{#if line?.quantity}
|
||||
{#if editingItem?.quantity}
|
||||
<Input
|
||||
id="package_type"
|
||||
placeholder="Caja, pallet, etc."
|
||||
bind:value={(line.quantity as any).package_type}
|
||||
bind:value={(editingItem.quantity as any).package_type}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -565,24 +565,24 @@
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="imported_quantity">Cantidad Importada</Label>
|
||||
{#if line?.quantity}
|
||||
{#if editingItem?.quantity}
|
||||
<Input
|
||||
id="imported_quantity"
|
||||
type="number"
|
||||
placeholder="0"
|
||||
bind:value={(line.quantity as any).quantity_imported}
|
||||
bind:value={(editingItem.quantity as any).quantity_imported}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="remaining_quantity">Cantidad Remanente</Label>
|
||||
{#if line?.quantity}
|
||||
{#if editingItem?.quantity}
|
||||
<Input
|
||||
id="remaining_quantity"
|
||||
type="number"
|
||||
placeholder="0"
|
||||
value={(line.quantity.quantity || 0) -
|
||||
((line.quantity as any).quantity_imported || 0)}
|
||||
value={(editingItem.quantity.quantity || 0) -
|
||||
((editingItem.quantity as any).quantity_imported || 0)}
|
||||
disabled
|
||||
/>
|
||||
{/if}
|
||||
@@ -596,11 +596,11 @@
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="brand">Marca</Label>
|
||||
{#if line?.description}
|
||||
{#if editingItem?.description}
|
||||
<Input
|
||||
id="brand"
|
||||
placeholder="Marca del producto"
|
||||
bind:value={line.description.brand}
|
||||
bind:value={editingItem.description.brand}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -629,12 +629,12 @@
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="observations">Observaciones</Label>
|
||||
{#if line?.description}
|
||||
{#if editingItem?.description}
|
||||
<textarea
|
||||
id="observations"
|
||||
class="flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||
placeholder="Notas adicionales sobre el inventario..."
|
||||
bind:value={line.description.extra_description}
|
||||
bind:value={editingItem.description.extra_description}
|
||||
></textarea>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -642,5 +642,11 @@
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col items-center justify-center py-20 text-muted-foreground">
|
||||
<Loader2 class="w-8 h-8 animate-spin mb-4" />
|
||||
<p class="text-sm">Cargando datos de la partida...</p>
|
||||
</div>
|
||||
{/if}
|
||||
</Sheet.Content>
|
||||
</Sheet.Root>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -122,6 +122,7 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Pedimento>[] {
|
||||
{
|
||||
accessorKey: "id",
|
||||
header: "ID",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
const idSnippet = createRawSnippet<[{ id: number }]>((getId) => {
|
||||
const { id } = getId();
|
||||
@@ -136,6 +137,7 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Pedimento>[] {
|
||||
{
|
||||
accessorKey: "pedimento_number",
|
||||
header: "Número de Pedimento",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
const pedimento = row.original;
|
||||
const customs2 = (pedimento.customs_office ?? '').toString().slice(0, 2);
|
||||
@@ -154,6 +156,7 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Pedimento>[] {
|
||||
{
|
||||
accessorKey: "pedimento_type",
|
||||
header: "Tipo",
|
||||
enableSorting: true,
|
||||
meta: { className: "hidden md:table-cell" },
|
||||
cell: ({ row }) => {
|
||||
const typeSnippet = createRawSnippet<[{ type?: string | null }]>((getType) => {
|
||||
@@ -169,6 +172,7 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Pedimento>[] {
|
||||
{
|
||||
accessorKey: "pedimento_code",
|
||||
header: "Clave",
|
||||
enableSorting: true,
|
||||
meta: { className: "hidden lg:table-cell" },
|
||||
cell: ({ row }) => {
|
||||
const codeSnippet = createRawSnippet<[{ code?: string | null }]>((getCode) => {
|
||||
@@ -295,6 +299,7 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Pedimento>[] {
|
||||
{
|
||||
accessorKey: "status",
|
||||
header: "Estado",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
const status = row.original.status;
|
||||
const colorClass = getStatusColor(status);
|
||||
@@ -324,6 +329,7 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Pedimento>[] {
|
||||
});
|
||||
return renderSnippet(headerSnippet, {});
|
||||
},
|
||||
enableSorting: true,
|
||||
meta: { className: "hidden lg:table-cell" },
|
||||
cell: ({ row }) => {
|
||||
const valueSnippet = createRawSnippet<[{ value: string }]>((getValue) => {
|
||||
@@ -383,6 +389,7 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Pedimento>[] {
|
||||
{
|
||||
accessorKey: "created_at",
|
||||
header: "Fecha de Creación",
|
||||
enableSorting: true,
|
||||
meta: { className: "hidden 2xl:table-cell" },
|
||||
cell: ({ row }) => {
|
||||
const dateSnippet = createRawSnippet<[{ date: string }]>((getDate) => {
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
loadMore: () => void;
|
||||
selectedId?: number | null;
|
||||
onRowClick?: (row: TData) => void;
|
||||
sorting?: import("@tanstack/table-core").SortingState;
|
||||
onSortingChange?: (sorting: import("@tanstack/table-core").SortingState) => void;
|
||||
};
|
||||
|
||||
let {
|
||||
@@ -27,7 +29,9 @@
|
||||
hasMore,
|
||||
loadMore,
|
||||
selectedId = null,
|
||||
onRowClick
|
||||
onRowClick,
|
||||
sorting = [],
|
||||
onSortingChange
|
||||
}: DataTableProps<TData, TValue> = $props();
|
||||
|
||||
const table = createSvelteTable({
|
||||
@@ -40,8 +44,18 @@
|
||||
state: {
|
||||
get rowSelection() {
|
||||
return selectedId ? { [selectedId]: true } : {};
|
||||
},
|
||||
get sorting() {
|
||||
return sorting;
|
||||
}
|
||||
},
|
||||
onSortingChange: (updater) => {
|
||||
if (onSortingChange) {
|
||||
const nextSorting = typeof updater === 'function' ? updater(sorting) : updater;
|
||||
onSortingChange(nextSorting);
|
||||
}
|
||||
},
|
||||
manualSorting: true,
|
||||
enableRowSelection: true,
|
||||
enableMultiRowSelection: false
|
||||
});
|
||||
@@ -89,12 +103,65 @@
|
||||
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||
<Table.Row>
|
||||
{#each headerGroup.headers as header (header.id)}
|
||||
<Table.Head class="whitespace-nowrap {header.column.columnDef.meta?.className || ''}">
|
||||
<Table.Head class="whitespace-nowrap p-0 {header.column.columnDef.meta?.className || ''}">
|
||||
{#if !header.isPlaceholder}
|
||||
<FlexRender
|
||||
content={header.column.columnDef.header}
|
||||
context={header.getContext()}
|
||||
/>
|
||||
<button
|
||||
class="group flex h-full w-full items-center gap-2 px-2 py-2 text-left hover:bg-muted/50"
|
||||
onclick={header.column.getToggleSortingHandler()}
|
||||
disabled={!header.column.getCanSort()}
|
||||
>
|
||||
<FlexRender
|
||||
content={header.column.columnDef.header}
|
||||
context={header.getContext()}
|
||||
/>
|
||||
{#if header.column.getCanSort()}
|
||||
<div class="flex h-4 w-4 shrink-0 items-center justify-center">
|
||||
{#if header.column.getIsSorted() === 'asc'}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-chevron-up text-primary"
|
||||
><path d="m18 15-6-6-6 6" /></svg
|
||||
>
|
||||
{:else if header.column.getIsSorted() === 'desc'}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-chevron-down text-primary"
|
||||
><path d="m6 9 6 6 6-6" /></svg
|
||||
>
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-chevrons-up-down opacity-30 group-hover:opacity-100"
|
||||
><path d="m7 15 5 5 5-5" /><path d="m7 9 5-5 5 5" /></svg
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</Table.Head>
|
||||
{/each}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { helpApi, type HelpArticle } from '$lib/api/help';
|
||||
import { helpStore } from '$lib/stores/help.svelte';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
// Nota: Estas librerías deben ser instaladas: npm install marked dompurify @types/dompurify
|
||||
// Si no están, fallará el import. El usuario debe instalarlas.
|
||||
@@ -133,96 +134,100 @@
|
||||
</Sheet.Trigger>
|
||||
<Sheet.Content side="right" class="w-[400px] sm:w-[540px]">
|
||||
<Sheet.Header>
|
||||
<Sheet.Title class="flex items-center gap-2">
|
||||
{#if selectedArticle}
|
||||
<Button variant="ghost" size="icon" onclick={() => (selectedArticle = null)}>
|
||||
<ChevronLeft size={20} />
|
||||
</Button>
|
||||
{/if}
|
||||
Base de Conocimientos
|
||||
</Sheet.Title>
|
||||
</Sheet.Header>
|
||||
<Sheet.Title class="flex items-center gap-2">
|
||||
{#if selectedArticle}
|
||||
<Button variant="ghost" size="icon" onclick={() => (selectedArticle = null)}>
|
||||
<ChevronLeft size={20} />
|
||||
</Button>
|
||||
{/if}
|
||||
<span>Base de Conocimientos</span>
|
||||
</Sheet.Title>
|
||||
</Sheet.Header>
|
||||
|
||||
<div class="mt-6 flex h-[calc(100vh-120px)] flex-col">
|
||||
{#if !selectedArticle}
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg font-medium">Artículos Disponibles</h3>
|
||||
{#if isAdmin}
|
||||
<Button variant="outline" size="sm" onclick={startCreate}>
|
||||
<Plus size={16} class="mr-2" /> Agregar Nuevo
|
||||
</Button>
|
||||
<div class="mt-6 flex h-[calc(100vh-120px)] flex-col">
|
||||
{#if !selectedArticle}
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg font-medium">Artículos Disponibles</h3>
|
||||
{#if isAdmin}
|
||||
<Button variant="outline" size="sm" onclick={startCreate}>
|
||||
<Plus size={16} class="mr-2" /> <span>Agregar Nuevo</span>
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{#if isLoading}
|
||||
<p class="py-10 text-center text-sm text-muted-foreground">Cargando...</p>
|
||||
{:else if articles.length === 0}
|
||||
<p class="py-10 text-center text-sm text-muted-foreground">
|
||||
No hay artículos de ayuda disponibles.
|
||||
</p>
|
||||
{/if}
|
||||
<div class="grid gap-2">
|
||||
{#each articles as article (article.uuid)}
|
||||
<button
|
||||
onclick={() => selectArticle(article)}
|
||||
class="flex flex-col items-start rounded-lg border p-4 text-left transition-colors hover:bg-accent"
|
||||
>
|
||||
<span class="font-semibold">{article.title}</span>
|
||||
<span class="text-xs text-muted-foreground">
|
||||
Última edición: {browser ? new Date(article.updated_at).toLocaleDateString() : article.updated_at.split('T')[0]}
|
||||
</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-1 flex-col gap-4">
|
||||
{#if isEditing}
|
||||
<div class="space-y-4">
|
||||
<input
|
||||
bind:value={editTitle}
|
||||
class="w-full rounded-md border bg-transparent p-2 text-xl font-bold"
|
||||
placeholder="Título del artículo"
|
||||
/>
|
||||
<textarea
|
||||
bind:value={editContent}
|
||||
class="min-h-[400px] w-full flex-1 rounded-md border bg-transparent p-4 font-mono text-sm focus:ring-1 focus:ring-primary focus:outline-none"
|
||||
placeholder="Escribe en Markdown..."
|
||||
></textarea>
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onclick={() => {
|
||||
isEditing = false;
|
||||
if (isCreating) selectedArticle = null;
|
||||
isCreating = false;
|
||||
}}
|
||||
>
|
||||
<X size={16} class="mr-2" /> <span>Cancelar</span>
|
||||
</Button>
|
||||
<Button onclick={saveChanges}>
|
||||
<Save size={16} class="mr-2" /> <span>Guardar Cambios</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-2xl font-bold">{selectedArticle.title}</h2>
|
||||
{#if isAdmin}
|
||||
<Button variant="outline" size="sm" onclick={startEdit}>
|
||||
<Edit2 size={16} class="mr-2" /> <span>Editar</span>
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="prose prose-sm max-w-none border-t pt-4 dark:prose-invert">
|
||||
{#if browser}
|
||||
{@html renderMarkdown(selectedArticle.content)}
|
||||
{:else}
|
||||
<div class="whitespace-pre-wrap">{selectedArticle.content}</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if isLoading}
|
||||
<p class="py-10 text-center text-sm text-muted-foreground">Cargando...</p>
|
||||
{:else if articles.length === 0}
|
||||
<p class="py-10 text-center text-sm text-muted-foreground">
|
||||
No hay artículos de ayuda disponibles.
|
||||
</p>
|
||||
{/if}
|
||||
<div class="grid gap-2">
|
||||
{#each articles as article}
|
||||
<button
|
||||
onclick={() => selectArticle(article)}
|
||||
class="flex flex-col items-start rounded-lg border p-4 text-left transition-colors hover:bg-accent"
|
||||
>
|
||||
<span class="font-semibold">{article.title}</span>
|
||||
<span class="text-xs text-muted-foreground"
|
||||
>Última edición: {new Date(article.updated_at).toLocaleDateString()}</span
|
||||
>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-1 flex-col gap-4">
|
||||
{#if isEditing}
|
||||
<div class="space-y-4">
|
||||
<input
|
||||
bind:value={editTitle}
|
||||
class="w-full rounded-md border bg-transparent p-2 text-xl font-bold"
|
||||
placeholder="Título del artículo"
|
||||
/>
|
||||
<textarea
|
||||
bind:value={editContent}
|
||||
class="min-h-[400px] w-full flex-1 rounded-md border bg-transparent p-4 font-mono text-sm focus:ring-1 focus:ring-primary focus:outline-none"
|
||||
placeholder="Escribe en Markdown..."
|
||||
></textarea>
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onclick={() => {
|
||||
isEditing = false;
|
||||
if (isCreating) selectedArticle = null;
|
||||
isCreating = false;
|
||||
}}
|
||||
>
|
||||
<X size={16} class="mr-2" /> Cancelar
|
||||
</Button>
|
||||
<Button onclick={saveChanges}>
|
||||
<Save size={16} class="mr-2" /> Guardar Cambios
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-2xl font-bold">{selectedArticle.title}</h2>
|
||||
{#if isAdmin}
|
||||
<Button variant="outline" size="sm" onclick={startEdit}>
|
||||
<Edit2 size={16} class="mr-2" /> Editar
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="prose prose-sm max-w-none border-t pt-4 dark:prose-invert">
|
||||
{@html renderMarkdown(selectedArticle.content)}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Sheet.Content>
|
||||
</Sheet.Root>
|
||||
|
||||
|
||||
@@ -49,7 +49,8 @@ export function createSvelteTable<TData extends RowData>(options: TableOptions<T
|
||||
);
|
||||
|
||||
const table = createTable(resolvedOptions);
|
||||
let state = $state<Partial<TableState>>(table.initialState);
|
||||
// Use JSON parse/stringify to ensure we get a clean, non-proxy initial state object
|
||||
let state = $state<Partial<TableState>>(JSON.parse(JSON.stringify(table.initialState)));
|
||||
|
||||
function updateOptions() {
|
||||
table.setOptions((prev) => {
|
||||
@@ -105,8 +106,7 @@ export function mergeObjects<Sources extends readonly MaybeThunk<any>[]>(
|
||||
return new Proxy(Object.create(null), {
|
||||
get(_, key) {
|
||||
const src = findSourceWithKey(key);
|
||||
|
||||
return src?.[key as never];
|
||||
return src ? src[key as never] : undefined;
|
||||
},
|
||||
|
||||
has(_, key) {
|
||||
|
||||
@@ -7,7 +7,13 @@ export interface Item {
|
||||
// Helper function to check if an object has any meaningful values
|
||||
export function hasValues(obj: any): boolean {
|
||||
if (!obj || typeof obj !== 'object') return false;
|
||||
return Object.values(obj).some(
|
||||
|
||||
// Si el objeto está intencionalmente vacío o tiene campos que serán usados,
|
||||
// es mejor dejar que el backend valide si es requerido.
|
||||
const values = Object.values(obj);
|
||||
if (values.length === 0) return false;
|
||||
|
||||
return values.some(
|
||||
(val) =>
|
||||
val !== undefined &&
|
||||
val !== null &&
|
||||
@@ -18,7 +24,9 @@ export function hasValues(obj: any): boolean {
|
||||
|
||||
// Clean nested data before sending to API
|
||||
export function cleanLineData(line: any) {
|
||||
const cleaned: any = { ...line };
|
||||
// 1. First, deeply copy and unwrap any Svelte Proxies to ensure a clean JS object
|
||||
const rawLine = JSON.parse(JSON.stringify(line));
|
||||
const cleaned: any = { ...rawLine };
|
||||
|
||||
// Helper function to convert to number or undefined
|
||||
const toNumberOrUndefined = (value: any): number | undefined => {
|
||||
@@ -29,47 +37,88 @@ export function cleanLineData(line: any) {
|
||||
return !isNaN(numValue) && isFinite(numValue) ? numValue : undefined;
|
||||
};
|
||||
|
||||
// Convert integer fields
|
||||
cleaned.part_number = toNumberOrUndefined(cleaned.part_number);
|
||||
// Explicitly keep mandatory fields for a76 schema
|
||||
cleaned.invoice_id = toNumberOrUndefined(rawLine.invoice_id);
|
||||
cleaned.line_number = toNumberOrUndefined(rawLine.line_number);
|
||||
|
||||
// Reconstruction approach for core objects to be 100% sure
|
||||
// We ALWAYS want these objects to exist in the payload even if all fields are null
|
||||
// so the backend validation layer doesn't crash (AttributeError on None)
|
||||
|
||||
cleaned.financial = {
|
||||
unit_cost_usd: rawLine.financial ? toNumberOrUndefined(rawLine.financial.unit_cost_usd) : undefined,
|
||||
unit_cost_mxn: rawLine.financial ? toNumberOrUndefined(rawLine.financial.unit_cost_mxn) : undefined,
|
||||
unit_cost_capture: rawLine.financial ? toNumberOrUndefined(rawLine.financial.unit_cost_capture) : undefined,
|
||||
value_mc: rawLine.financial ? toNumberOrUndefined(rawLine.financial.value_mc) : undefined,
|
||||
value_usd: rawLine.financial ? toNumberOrUndefined(rawLine.financial.value_usd) : undefined,
|
||||
value_mxn: rawLine.financial ? toNumberOrUndefined(rawLine.financial.value_mxn) : undefined
|
||||
};
|
||||
|
||||
cleaned.quantity = {
|
||||
quantity: rawLine.quantity ? (toNumberOrUndefined(rawLine.quantity.quantity) || 0) : 0,
|
||||
net_weight: rawLine.quantity ? toNumberOrUndefined(rawLine.quantity.net_weight) : undefined,
|
||||
gross_weight: rawLine.quantity ? toNumberOrUndefined(rawLine.quantity.gross_weight) : undefined,
|
||||
package_id: rawLine.quantity ? toNumberOrUndefined(rawLine.quantity.package_id) : undefined,
|
||||
package_quantity: rawLine.quantity ? toNumberOrUndefined(rawLine.quantity.package_quantity) : undefined
|
||||
// NOTE: unit_of_measure lives at the top-level LineItem, NOT inside quantity
|
||||
};
|
||||
|
||||
cleaned.description = {
|
||||
description_spanish: rawLine.description ? (rawLine.description.description_spanish || '') : '',
|
||||
description_english: rawLine.description ? (rawLine.description.description_english || '') : '',
|
||||
brand: rawLine.description ? (rawLine.description.brand || '') : '',
|
||||
model: rawLine.description ? (rawLine.description.model || '') : ''
|
||||
};
|
||||
|
||||
cleaned.customs = {
|
||||
fraction: rawLine.customs ? (rawLine.customs.fraction || undefined) : undefined,
|
||||
american_fraction: rawLine.customs ? (rawLine.customs.american_fraction || undefined) : undefined,
|
||||
origin_country: rawLine.customs ? (rawLine.customs.origin_country || undefined) : undefined,
|
||||
fraction_type: rawLine.customs ? (rawLine.customs.fraction_type || undefined) : undefined
|
||||
};
|
||||
|
||||
// Convert integer fields (only if they look like numbers/IDs)
|
||||
if (cleaned.part_number && !isNaN(Number(cleaned.part_number))) {
|
||||
cleaned.part_number = toNumberOrUndefined(cleaned.part_number);
|
||||
}
|
||||
|
||||
cleaned.component_part_number = toNumberOrUndefined(cleaned.component_part_number);
|
||||
cleaned.class_id = toNumberOrUndefined(cleaned.class_id);
|
||||
|
||||
// Ensure part_number is preserved if it's a string (common in some modules)
|
||||
if (rawLine.part_number && !cleaned.part_number) {
|
||||
cleaned.part_number = rawLine.part_number;
|
||||
}
|
||||
|
||||
cleaned.unit_of_measure = toNumberOrUndefined(cleaned.unit_of_measure);
|
||||
cleaned.alternate_unit = toNumberOrUndefined(cleaned.alternate_unit);
|
||||
|
||||
// Remove display-only fields
|
||||
// Remove ALL display-only and UI-specific fields that the backend schema doesn't know about
|
||||
delete cleaned.class_code;
|
||||
delete cleaned.class_unit_of_measure;
|
||||
delete cleaned.class_description;
|
||||
|
||||
// UI specific fields that shouldn't be in the payload
|
||||
delete cleaned.part_description_es;
|
||||
delete cleaned.part_description_en;
|
||||
delete cleaned.part_number_display; // UI display field for part number string
|
||||
delete cleaned.unit_code;
|
||||
delete cleaned.unit_description;
|
||||
delete cleaned.includes_subitems;
|
||||
delete cleaned.payment_method_description;
|
||||
|
||||
// Only delete part_number if it's the string code from UI, but schema expects int ID.
|
||||
// In this codebase, if part_number is populated from existing data, it's an ID.
|
||||
// If it's a new item, it might be cleaned.
|
||||
// Remove any other unknown top-level display fields
|
||||
delete (cleaned as any).class_unit_of_measure_description;
|
||||
|
||||
// Remove display-only fields from nested objects
|
||||
if (cleaned.customs) {
|
||||
delete cleaned.customs.origin_country_name;
|
||||
delete cleaned.customs.fraction_description;
|
||||
if (!hasValues(cleaned.customs)) delete cleaned.customs;
|
||||
// DO NOT delete if empty - backend needs the object structure
|
||||
}
|
||||
|
||||
if (cleaned.fa_data) {
|
||||
delete cleaned.fa_data.includes_subitems;
|
||||
if (!hasValues(cleaned.fa_data)) delete cleaned.fa_data;
|
||||
// Keep fa_data as is otherwise (unwrapped by stringify/parse above)
|
||||
}
|
||||
|
||||
// Remove empty nested objects
|
||||
if (cleaned.financial && !hasValues(cleaned.financial)) delete cleaned.financial;
|
||||
if (cleaned.quantity && !hasValues(cleaned.quantity)) delete cleaned.quantity;
|
||||
if (cleaned.description && !hasValues(cleaned.description)) delete cleaned.description;
|
||||
if (cleaned.reference && !hasValues(cleaned.reference)) delete cleaned.reference;
|
||||
if (cleaned.series != null) {
|
||||
const arr = Array.isArray(cleaned.series) ? cleaned.series : [cleaned.series];
|
||||
cleaned.series = arr
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
import { faClassesApi, type FAClass } from '$lib/api/dashboard/a24/fa_classes';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import DataTable from '$lib/components/dashboard/goods/classes/data-table.svelte';
|
||||
import { columns } from '$lib/components/dashboard/goods/classes/columns';
|
||||
|
||||
// Tipo extendido que combina A76Class y FAClass
|
||||
interface FixedAssetClassExtended extends A76Class {
|
||||
@@ -39,6 +41,7 @@
|
||||
let showDeleteDialog = $state(false);
|
||||
let validationError = $state<string>('');
|
||||
let isSaving = $state(false);
|
||||
let sorting = $state<import("@tanstack/table-core").SortingState>([]);
|
||||
|
||||
// Estado del formulario
|
||||
let formData = $state({
|
||||
@@ -56,32 +59,26 @@
|
||||
bom: ''
|
||||
});
|
||||
|
||||
// Clases filtradas según búsqueda
|
||||
// Clases filtradas según búsqueda (mantenemos filtrado local para compatibilidad inmediata)
|
||||
const filteredClasses = $derived(
|
||||
classes.filter((c) => {
|
||||
// Filtro por código de clase
|
||||
const matchesCode =
|
||||
!searchTerm || c.class_code.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
|
||||
// Filtro por descripción (español o inglés)
|
||||
const matchesDescription =
|
||||
!searchDescription ||
|
||||
const matchesCode = !searchTerm || c.class_code.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
const matchesDescription = !searchDescription ||
|
||||
(c.description_es?.toLowerCase().includes(searchDescription.toLowerCase()) ?? false) ||
|
||||
(c.description_en?.toLowerCase().includes(searchDescription.toLowerCase()) ?? false);
|
||||
|
||||
// Filtro por tipo de material
|
||||
const matchesType =
|
||||
!searchType || (c.material_key?.toLowerCase().includes(searchType.toLowerCase()) ?? false);
|
||||
|
||||
// Filtro por fracción arancelaria
|
||||
const matchesFraction =
|
||||
!searchFraction ||
|
||||
(c.fraction?.toLowerCase().includes(searchFraction.toLowerCase()) ?? false);
|
||||
|
||||
const matchesType = !searchType || (c.material_key?.toLowerCase().includes(searchType.toLowerCase()) ?? false);
|
||||
const matchesFraction = !searchFraction || (c.fraction?.toLowerCase().includes(searchFraction.toLowerCase()) ?? false);
|
||||
return matchesCode && matchesDescription && matchesType && matchesFraction;
|
||||
})
|
||||
);
|
||||
|
||||
// Efecto para reaccionar al cambio de ordenamiento
|
||||
$effect(() => {
|
||||
if (sorting.length >= 0) {
|
||||
loadClasses();
|
||||
}
|
||||
});
|
||||
|
||||
// Reactively load classes when company changes
|
||||
$effect(() => {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
@@ -102,7 +99,9 @@
|
||||
const response = await classesApi.getWithFAData({
|
||||
company_id: companyId,
|
||||
page: 1,
|
||||
page_size: 1000
|
||||
page_size: 1000,
|
||||
sort_by: sorting.length > 0 ? sorting[0].id : undefined,
|
||||
sort_order: sorting.length > 0 ? (sorting[0].desc ? 'desc' : 'asc') : undefined
|
||||
});
|
||||
|
||||
if (response.data) {
|
||||
@@ -295,82 +294,16 @@
|
||||
</div>
|
||||
|
||||
<!-- Tabla de clases -->
|
||||
<div class="flex-1 overflow-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="border-b bg-white text-gray-900 dark:bg-black dark:text-white">
|
||||
<tr>
|
||||
<th class="w-8 px-2 py-2 text-left">
|
||||
<input type="checkbox" class="h-4 w-4" />
|
||||
</th>
|
||||
<th class="px-2 py-2 text-left">Clase</th>
|
||||
<th class="px-2 py-2 text-left">Descripción Español</th>
|
||||
<th class="px-2 py-2 text-left">Descripción Inglés</th>
|
||||
<th class="px-2 py-2 text-left">Tipo</th>
|
||||
<th class="px-2 py-2 text-left">U.M</th>
|
||||
<th class="px-2 py-2 text-left">Fracción</th>
|
||||
<th class="px-2 py-2 text-left">U.M.T.</th>
|
||||
<th class="px-2 py-2 text-left">Fracción US</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#if isLoading}
|
||||
<tr>
|
||||
<td colspan="10" class="py-8 text-center text-muted-foreground">Cargando...</td>
|
||||
</tr>
|
||||
{:else if filteredClasses.length === 0}
|
||||
<tr>
|
||||
<td colspan="10" class="py-8 text-center text-muted-foreground">
|
||||
No hay clases de activo fijo registradas
|
||||
</td>
|
||||
</tr>
|
||||
{:else}
|
||||
{#each filteredClasses as cls (cls.id)}
|
||||
<tr
|
||||
class="cursor-pointer border-b transition-colors {selectedClass?.id === cls.id
|
||||
? 'bg-gray-300 dark:bg-gray-600'
|
||||
: 'hover:bg-gray-100 dark:hover:bg-gray-700'}"
|
||||
onclick={() => selectClass(cls)}
|
||||
>
|
||||
<td class="px-2 py-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedClass?.id === cls.id}
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
</td>
|
||||
<td class="px-2 py-1">
|
||||
<span
|
||||
class="inline-flex items-center rounded-md border border-blue-200 bg-blue-50 px-2 py-1 font-mono text-xs font-bold text-blue-700 dark:border-blue-800 dark:bg-blue-900/30 dark:text-blue-400"
|
||||
>
|
||||
{cls.class_code}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-2 py-1 text-sm font-medium">{cls.description_es || ''}</td>
|
||||
<td class="px-2 py-1">{cls.description_en || ''}</td>
|
||||
<td class="px-2 py-1">
|
||||
<span
|
||||
class="rounded-full px-2 py-0.5 text-[10px] font-bold tracking-wider uppercase {cls.material_key ===
|
||||
'MP'
|
||||
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'
|
||||
: cls.material_key === 'SC'
|
||||
? 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400'
|
||||
: cls.material_key === 'DESP'
|
||||
? 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400'
|
||||
: 'bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-400'}"
|
||||
>
|
||||
{cls.material_key || ''}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-2 py-1 text-muted-foreground">{cls.unit_of_measure || ''}</td>
|
||||
<td class="px-2 py-1 font-mono text-xs text-orange-600 dark:text-orange-400"
|
||||
>{cls.fraction || ''}</td
|
||||
> <td class="px-2 py-1 text-xs text-muted-foreground">-</td>
|
||||
<td class="px-2 py-1">{cls.us_fraction || '-'}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="flex-1 overflow-hidden p-0">
|
||||
<DataTable
|
||||
data={filteredClasses}
|
||||
{columns}
|
||||
loading={isLoading}
|
||||
selectedId={selectedClass?.id}
|
||||
onRowClick={selectClass}
|
||||
{sorting}
|
||||
onSortingChange={(newSorting) => (sorting = newSorting)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,8 +8,10 @@
|
||||
import { clientsProvidersApi } from '$lib/api/dashboard/a76/clients-providers';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
import { obtenerAtajosListaMercancias } from '$lib/config/shortcuts/dashboard/goods/list';
|
||||
import { goto } from '$app/navigation';
|
||||
import DataTable from '$lib/components/dashboard/goods/parts/data-table.svelte';
|
||||
import { createColumns } from '$lib/components/dashboard/goods/parts/columns';
|
||||
import { obtenerAtajosListaMercancias } from '$lib/config/shortcuts/dashboard/goods/list';
|
||||
|
||||
// Estado de la lista de partes
|
||||
let parts = $state<Part[]>([]);
|
||||
@@ -20,45 +22,40 @@
|
||||
let searchDescription = $state('');
|
||||
let searchClient = $state('');
|
||||
let searchClass = $state('');
|
||||
let systemFilter = $state<'ALL' | 'SCAI' | 'SCAF'>('ALL'); // SCAI = inv_data, SCAF = fa_data
|
||||
let systemFilter = $state<'ALL' | 'SCAI' | 'SCAF'>('ALL');
|
||||
let sorting = $state<import("@tanstack/table-core").SortingState>([{ id: 'updated_at', desc: true }]);
|
||||
const columns = createColumns(() => loadParts());
|
||||
|
||||
// Partes filtradas según búsqueda
|
||||
// Para mantener compatibilidad con el diseño original que usa filtros locales,
|
||||
// pero ahora con soporte para ordenamiento en el servidor.
|
||||
const filteredParts = $derived(
|
||||
parts.filter((p) => {
|
||||
// Filtro por número de parte
|
||||
const matchesPartNumber =
|
||||
!searchPartNumber || p.part_number.toLowerCase().includes(searchPartNumber.toLowerCase());
|
||||
|
||||
// Filtro por descripción (español o inglés)
|
||||
const matchesDescription =
|
||||
!searchDescription ||
|
||||
(p.description_spanish?.toLowerCase().includes(searchDescription.toLowerCase()) ?? false) ||
|
||||
(p.description_english?.toLowerCase().includes(searchDescription.toLowerCase()) ?? false);
|
||||
|
||||
// Filtro por cliente (Busca en nombre o ID)
|
||||
const clientName = clientsMap[p.client_id] || '';
|
||||
const matchesClient =
|
||||
!searchClient ||
|
||||
(p.client_id?.toString().includes(searchClient) ?? false) ||
|
||||
clientName.toLowerCase().includes(searchClient.toLowerCase());
|
||||
|
||||
// Filtro por clase
|
||||
const matchesClass =
|
||||
!searchClass || (p.part_class?.toLowerCase().includes(searchClass.toLowerCase()) ?? false);
|
||||
|
||||
// Filtro por sistema (SCAI/SCAF)
|
||||
let matchesSystem = true;
|
||||
if (systemFilter === 'SCAI') {
|
||||
matchesSystem = !!p.inv_data;
|
||||
} else if (systemFilter === 'SCAF') {
|
||||
matchesSystem = !!p.fa_data;
|
||||
}
|
||||
|
||||
return (
|
||||
matchesPartNumber && matchesDescription && matchesClient && matchesClass && matchesSystem
|
||||
);
|
||||
if (systemFilter === 'SCAI') matchesSystem = !!p.inv_data;
|
||||
else if (systemFilter === 'SCAF') matchesSystem = !!p.fa_data;
|
||||
return matchesPartNumber && matchesDescription && matchesClient && matchesClass && matchesSystem;
|
||||
})
|
||||
);
|
||||
|
||||
// Reaccionar al cambio de ordenamiento
|
||||
$effect(() => {
|
||||
if (sorting.length >= 0) {
|
||||
loadParts();
|
||||
}
|
||||
});
|
||||
// Reactively load parts when company changes
|
||||
$effect(() => {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
@@ -120,7 +117,9 @@
|
||||
const response = await partsApi.list({
|
||||
company_id: companyId,
|
||||
page: 1,
|
||||
page_size: 1000
|
||||
page_size: 1000,
|
||||
sort_by: sorting.length > 0 ? sorting[0].id : undefined,
|
||||
sort_order: sorting.length > 0 ? (sorting[0].desc ? 'desc' : 'asc') : undefined
|
||||
});
|
||||
|
||||
if (response.data) {
|
||||
@@ -283,106 +282,17 @@
|
||||
</div>
|
||||
|
||||
<!-- Tabla de partes -->
|
||||
<div class="flex-1 overflow-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="border-b bg-white text-gray-900 dark:bg-black dark:text-white">
|
||||
<tr>
|
||||
<th class="w-8 px-2 py-2 text-left">
|
||||
<input type="checkbox" class="h-4 w-4" />
|
||||
</th>
|
||||
<th class="px-2 py-2 text-left">Número de Parte</th>
|
||||
<th class="px-2 py-2 text-left">Descripción</th>
|
||||
<th class="px-2 py-2 text-left">Cliente</th>
|
||||
<th class="px-2 py-2 text-left">Clase</th>
|
||||
<th class="px-2 py-2 text-left">U.M.</th>
|
||||
<th class="px-2 py-2 text-left">Fracción</th>
|
||||
<th class="px-2 py-2 text-left">Sistema</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#if isLoading}
|
||||
<tr>
|
||||
<td colspan="7" class="py-8 text-center text-muted-foreground">Cargando...</td>
|
||||
</tr>
|
||||
{:else if filteredParts.length === 0}
|
||||
<tr>
|
||||
<td colspan="7" class="py-8 text-center text-muted-foreground">
|
||||
No hay partes registradas
|
||||
</td>
|
||||
</tr>
|
||||
{:else}
|
||||
{#each filteredParts as part (part.id)}
|
||||
<tr
|
||||
class="cursor-pointer border-b transition-colors {selectedPart?.id === part.id
|
||||
? 'bg-gray-300 dark:bg-gray-600'
|
||||
: 'hover:bg-gray-100 dark:hover:bg-gray-700'}"
|
||||
onclick={() => selectPart(part)}
|
||||
>
|
||||
<td class="px-2 py-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedPart?.id === part.id}
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
</td>
|
||||
<td class="px-2 py-1">
|
||||
<span
|
||||
class="inline-flex items-center rounded-md border border-blue-200 bg-blue-50 px-2 py-1 font-mono text-xs font-bold text-blue-700 dark:border-blue-800 dark:bg-blue-900/30 dark:text-blue-400"
|
||||
>
|
||||
{part.part_number}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-2 py-1 text-sm font-medium">{part.description_spanish || ''}</td>
|
||||
<td class="px-2 py-1 text-muted-foreground">
|
||||
<div class="flex flex-col">
|
||||
<span class="text-xs font-medium"
|
||||
>{clientsMap[part.client_id] || 'Sin cliente'}</span
|
||||
>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-2 py-1">
|
||||
{#if part.part_class}
|
||||
<span
|
||||
class="rounded-full bg-purple-100 px-2 py-0.5 text-[10px] font-bold tracking-wider text-purple-700 uppercase dark:bg-purple-900/30 dark:text-purple-400"
|
||||
>
|
||||
{part.part_class}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="text-muted-foreground">-</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td class="px-2 py-1 text-muted-foreground">{part.unit_of_measure || '-'}</td>
|
||||
<td class="px-2 py-1 font-mono text-xs text-orange-600 dark:text-orange-400"
|
||||
>{part.fraction || '-'}</td
|
||||
>
|
||||
<td class="px-2 py-1">
|
||||
{#if part.inv_data && part.fa_data}
|
||||
<span
|
||||
class="inline-flex items-center rounded-full border border-purple-200 bg-purple-50 px-2 py-0.5 text-[10px] font-bold tracking-wider text-purple-700 uppercase dark:border-purple-800 dark:bg-purple-900/30 dark:text-purple-400"
|
||||
>
|
||||
AMBOS
|
||||
</span>
|
||||
{:else if part.inv_data}
|
||||
<span
|
||||
class="inline-flex items-center rounded-full border border-sky-200 bg-sky-50 px-2 py-0.5 text-[10px] font-bold tracking-wider text-sky-700 uppercase dark:border-sky-800 dark:bg-sky-900/30 dark:text-sky-400"
|
||||
>
|
||||
SCAI
|
||||
</span>
|
||||
{:else if part.fa_data}
|
||||
<span
|
||||
class="inline-flex items-center rounded-full border border-amber-200 bg-amber-50 px-2 py-0.5 text-[10px] font-bold tracking-wider text-amber-700 uppercase dark:border-amber-800 dark:bg-amber-900/30 dark:text-amber-400"
|
||||
>
|
||||
SCAF
|
||||
</span>
|
||||
{:else}
|
||||
<span class="text-muted-foreground">—</span>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="flex-1 overflow-hidden p-0">
|
||||
<DataTable
|
||||
data={filteredParts}
|
||||
{columns}
|
||||
loading={isLoading}
|
||||
hasMore={false}
|
||||
loadMore={() => {}}
|
||||
{sorting}
|
||||
onSortingChange={(newSorting) => (sorting = newSorting)}
|
||||
onRowClick={selectPart}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -67,6 +67,8 @@
|
||||
project_number: data.filters?.project_number || '',
|
||||
year: data.filters?.year || ''
|
||||
});
|
||||
|
||||
let sorting = $state<import("@tanstack/table-core").SortingState>([]);
|
||||
|
||||
let isDownloadModalOpen = $state(false);
|
||||
let isTransferenciaModalOpen = $state(false);
|
||||
@@ -143,7 +145,7 @@
|
||||
};
|
||||
const currentFiltersKey = JSON.stringify(currentFilters);
|
||||
|
||||
// Limpiar selección cada vez que se modifica algún filtro
|
||||
// Limpiar selección cada vez que se modifica algún filtro o el orden
|
||||
if (currentFiltersKey !== lastFiltersKey) {
|
||||
selectedInvoiceIds = [];
|
||||
lastFiltersKey = currentFiltersKey;
|
||||
@@ -156,6 +158,14 @@
|
||||
}, 300); // Esperar 300ms después del último cambio
|
||||
});
|
||||
|
||||
// Efecto para reaccionar al cambio de ordenamiento
|
||||
$effect(() => {
|
||||
// Cuando cambia el sorting, aplicamos filtros (que reinicia a la página 1)
|
||||
if (sorting.length >= 0) {
|
||||
applyFilters();
|
||||
}
|
||||
});
|
||||
|
||||
// Sincronizar token de cookies a localStorage al montar el componente
|
||||
onMount(() => {
|
||||
if (browser) {
|
||||
@@ -248,7 +258,9 @@
|
||||
invoice_type: filters.invoice_type || undefined,
|
||||
invoice_number: filters.invoice_number || undefined,
|
||||
project_number: filters.project_number || undefined,
|
||||
year: filters.year || undefined
|
||||
year: filters.year || undefined,
|
||||
sort_by: sorting.length > 0 ? sorting[0].id : undefined,
|
||||
sort_order: sorting.length > 0 ? (sorting[0].desc ? 'desc' : 'asc') : undefined
|
||||
};
|
||||
|
||||
const response = await invoicesApi.list(companyId, currentPage + 1, pageSize, filterParams);
|
||||
@@ -305,7 +317,9 @@
|
||||
invoice_type: filters.invoice_type || undefined,
|
||||
invoice_number: filters.invoice_number || undefined,
|
||||
project_number: filters.project_number || undefined,
|
||||
year: filters.year || undefined
|
||||
year: filters.year || undefined,
|
||||
sort_by: sorting.length > 0 ? sorting[0].id : undefined,
|
||||
sort_order: sorting.length > 0 ? (sorting[0].desc ? 'desc' : 'asc') : undefined
|
||||
};
|
||||
|
||||
const response = await invoicesApi.list(companyId, 1, pageSize, filterParams);
|
||||
@@ -363,7 +377,9 @@
|
||||
invoice_type: filters.invoice_type || undefined,
|
||||
invoice_number: filters.invoice_number || undefined,
|
||||
project_number: filters.project_number || undefined,
|
||||
year: filters.year || undefined
|
||||
year: filters.year || undefined,
|
||||
sort_by: sorting.length > 0 ? sorting[0].id : undefined,
|
||||
sort_order: sorting.length > 0 ? (sorting[0].desc ? 'desc' : 'asc') : undefined
|
||||
};
|
||||
|
||||
const response = await invoicesApi.list(companyId, 1, pageSize, filterParams);
|
||||
@@ -955,12 +971,14 @@
|
||||
<Card.Content>
|
||||
<DataTable
|
||||
data={allItems}
|
||||
{columns}
|
||||
columns={createColumns()}
|
||||
{loading}
|
||||
{hasMore}
|
||||
{loadMore}
|
||||
selectedIds={selectedInvoiceIds}
|
||||
onRowClick={handleRowClick}
|
||||
{sorting}
|
||||
onSortingChange={(newSorting) => (sorting = newSorting)}
|
||||
/>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
year: ''
|
||||
});
|
||||
|
||||
let sorting = $state<import("@tanstack/table-core").SortingState>([{ id: 'id', desc: true }]);
|
||||
|
||||
// Sincronizar token de cookies a localStorage al montar el componente
|
||||
onMount(() => {
|
||||
if (browser) {
|
||||
@@ -79,6 +81,14 @@
|
||||
let pageSize = $state(50);
|
||||
let totalItems = $state(data.total || 0);
|
||||
let loading = $state(false);
|
||||
let isSaving = $state(false);
|
||||
|
||||
// Efecto para reaccionar al cambio de ordenamiento
|
||||
$effect(() => {
|
||||
if (sorting.length >= 0) {
|
||||
applyFilters();
|
||||
}
|
||||
});
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
|
||||
@@ -247,7 +257,9 @@
|
||||
const filterParams = {
|
||||
status: filters.status || undefined,
|
||||
client_id: filters.client_id ? parseInt(filters.client_id) : undefined,
|
||||
year: filters.year || undefined
|
||||
year: filters.year || undefined,
|
||||
sort_by: sorting.length > 0 ? sorting[0].id : undefined,
|
||||
sort_order: sorting.length > 0 ? (sorting[0].desc ? 'desc' : 'asc') : undefined
|
||||
};
|
||||
|
||||
const response = await pedimentosApi.list(currentPage + 1, pageSize, filterParams, companyId);
|
||||
@@ -298,7 +310,9 @@
|
||||
const filterParams = {
|
||||
status: filters.status || undefined,
|
||||
client_id: filters.client_id ? parseInt(filters.client_id) : undefined,
|
||||
year: filters.year || undefined
|
||||
year: filters.year || undefined,
|
||||
sort_by: sorting.length > 0 ? sorting[0].id : undefined,
|
||||
sort_order: sorting.length > 0 ? (sorting[0].desc ? 'desc' : 'asc') : undefined
|
||||
};
|
||||
|
||||
const response = await pedimentosApi.list(1, pageSize, filterParams, companyId);
|
||||
@@ -357,7 +371,9 @@
|
||||
const filterParams = {
|
||||
status: filters.status || undefined,
|
||||
client_id: filters.client_id ? parseInt(filters.client_id) : undefined,
|
||||
year: filters.year || undefined
|
||||
year: filters.year || undefined,
|
||||
sort_by: sorting.length > 0 ? sorting[0].id : undefined,
|
||||
sort_order: sorting.length > 0 ? (sorting[0].desc ? 'desc' : 'asc') : undefined
|
||||
};
|
||||
|
||||
const response = await pedimentosApi.list(1, pageSize, filterParams, companyId);
|
||||
@@ -521,6 +537,8 @@
|
||||
{loadMore}
|
||||
{selectedId}
|
||||
onRowClick={handleRowClick}
|
||||
{sorting}
|
||||
onSortingChange={(newSorting) => (sorting = newSorting)}
|
||||
/>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
Reference in New Issue
Block a user