feat: Implement general catalogs for INPC, Legends, Multi-Currency Types, Packages, Ports, Prevalidators, Signatures, Unit Conversions, and Units of Measure

- Added INPC management page with search functionality.
- Created Legends management page with filters for code and description.
- Implemented Multi-Currency Types management with search capabilities.
- Developed Packages management page with filtering options.
- Introduced Ports management page with authentication and data fetching.
- Added Prevalidators management page with search filters.
- Implemented Signatures management page with search by name and position.
- Created Unit Conversions management page for conversion factors.
- Developed Units of Measure management pages for ACE, American, and OMA with data tables and create/edit dialogs.
This commit is contained in:
2025-12-08 14:17:47 -06:00
parent f1bd2d63e8
commit 6acbd48e9e
152 changed files with 7334 additions and 2747 deletions

View File

@@ -0,0 +1,61 @@
import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
export interface ClassificationConcept {
id: number;
classification: string;
description?: string;
tenant_id: string;
company_id?: string;
created_at: string;
updated_at?: string;
}
export interface ClassificationConceptCreate {
classification: string;
description?: string;
}
export interface ClassificationConceptUpdate extends Partial<ClassificationConceptCreate> {}
export interface ClassificationConceptListResponse {
items: ClassificationConcept[];
total: number;
page: number;
page_size: number;
pages: number;
}
export async function getClassificationConcepts(
page: number = 1,
pageSize: number = 50,
filters: Record<string, any> = {}
): Promise<ApiResponse<ClassificationConceptListResponse>> {
const params = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
const response = await api.get(`/a76/classification_concepts?${params.toString()}`);
return response.data;
}
export async function getClassificationConcept(id: number): Promise<ClassificationConcept> {
const response = await api.get(`/a76/classification_concepts/${id}`);
return response.data;
}
export async function createClassificationConcept(data: ClassificationConceptCreate): Promise<ClassificationConcept> {
const response = await api.post('/a76/classification_concepts', data);
return response.data;
}
export async function updateClassificationConcept(id: number, data: ClassificationConceptUpdate): Promise<ClassificationConcept> {
const response = await api.patch(`/a76/classification_concepts/${id}`, data);
return response.data;
}
export async function deleteClassificationConcept(id: number): Promise<void> {
await api.delete(`/a76/classification_concepts/${id}`);
}

View File

@@ -0,0 +1,91 @@
import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
export interface Company {
id: number;
tenant_id: number;
name: string | null;
rfc: string | null;
main_activity: string | null;
program: string | null;
program_number: string | null;
prosec: number | null;
prosec_authorization: string | null;
manufacturer_id: string | null;
broker_company: string | null;
responsible: string | null;
responsible_name: string | null;
responsible_last_name: string | null;
responsible_mother_last_name: string | null;
created_at: string | null;
updated_at: string | null;
}
export interface CompanyCreate {
name?: string | null;
rfc?: string | null;
main_activity?: string | null;
program?: string | null;
program_number?: string | null;
prosec?: number | null;
prosec_authorization?: string | null;
manufacturer_id?: string | null;
broker_company?: string | null;
responsible?: string | null;
responsible_name?: string | null;
responsible_last_name?: string | null;
responsible_mother_last_name?: string | null;
}
export interface CompanyUpdate {
name?: string | null;
rfc?: string | null;
main_activity?: string | null;
program?: string | null;
program_number?: string | null;
prosec?: number | null;
prosec_authorization?: string | null;
manufacturer_id?: string | null;
broker_company?: string | null;
responsible?: string | null;
responsible_name?: string | null;
responsible_last_name?: string | null;
responsible_mother_last_name?: string | null;
}
export interface CompanyListResponse {
items: Company[];
total: number;
page: number;
page_size: number;
pages: number;
}
export async function getCompanies(
page = 1,
pageSize = 50,
filters: Record<string, any> = {}
): Promise<ApiResponse<CompanyListResponse>> {
const queryParams = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
return await api.get(`/a76/company?${queryParams.toString()}`);
}
export async function getCompany(id: number): Promise<ApiResponse<Company>> {
return await api.get(`/a76/company/${id}`);
}
export async function createCompany(data: CompanyCreate): Promise<ApiResponse<Company>> {
return await api.post(`/a76/company`, data);
}
export async function updateCompany(id: number, data: CompanyUpdate): Promise<ApiResponse<Company>> {
return await api.put(`/a76/company/${id}`, data);
}
export async function deleteCompany(id: number): Promise<ApiResponse<void>> {
return await api.delete(`/a76/company/${id}`);
}

View File

@@ -0,0 +1,79 @@
import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
export interface Concept {
id: number;
code: string;
description?: string;
description_en?: string;
detailed_description?: string;
priority?: number;
priority_ame?: number;
first_total?: boolean;
type?: string;
is_printed?: boolean;
section?: number;
classification?: string;
tenant_id: string;
company_id?: string;
created_at: string;
updated_at?: string;
}
export interface ConceptCreate {
code: string;
description?: string;
description_en?: string;
detailed_description?: string;
priority?: number;
priority_ame?: number;
first_total?: boolean;
type?: string;
is_printed?: boolean;
section?: number;
classification?: string;
}
export interface ConceptUpdate extends Partial<ConceptCreate> {}
export interface ConceptListResponse {
items: Concept[];
total: number;
page: number;
page_size: number;
pages: number;
}
export async function getConcepts(
page: number = 1,
pageSize: number = 50,
filters: Record<string, any> = {}
): Promise<ApiResponse<ConceptListResponse>> {
const params = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
const response = await api.get(`/a76/concepts?${params.toString()}`);
return response.data;
}
export async function getConcept(id: number): Promise<Concept> {
const response = await api.get(`/a76/concepts/${id}`);
return response.data;
}
export async function createConcept(data: ConceptCreate): Promise<Concept> {
const response = await api.post('/a76/concepts', data);
return response.data;
}
export async function updateConcept(id: number, data: ConceptUpdate): Promise<Concept> {
const response = await api.patch(`/a76/concepts/${id}`, data);
return response.data;
}
export async function deleteConcept(id: number): Promise<void> {
await api.delete(`/a76/concepts/${id}`);
}

View File

@@ -0,0 +1,77 @@
import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
export interface CustomsBrokerConcept {
id: number;
code: string;
description?: string;
description_en?: string;
detailed_description?: string;
priority?: number;
first_total?: boolean;
type?: string;
is_printed?: boolean;
section?: number;
classification?: string;
tenant_id: string;
company_id?: string;
created_at: string;
updated_at?: string;
}
export interface CustomsBrokerConceptCreate {
code: string;
description?: string;
description_en?: string;
detailed_description?: string;
priority?: number;
first_total?: boolean;
type?: string;
is_printed?: boolean;
section?: number;
classification?: string;
}
export interface CustomsBrokerConceptUpdate extends Partial<CustomsBrokerConceptCreate> {}
export interface CustomsBrokerConceptListResponse {
items: CustomsBrokerConcept[];
total: number;
page: number;
page_size: number;
pages: number;
}
export async function getCustomsBrokerConcepts(
page: number = 1,
pageSize: number = 50,
filters: Record<string, any> = {}
): Promise<ApiResponse<CustomsBrokerConceptListResponse>> {
const params = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
const response = await api.get(`/a76/customs_broker_concepts?${params.toString()}`);
return response.data;
}
export async function getCustomsBrokerConcept(id: number): Promise<CustomsBrokerConcept> {
const response = await api.get(`/a76/customs_broker_concepts/${id}`);
return response.data;
}
export async function createCustomsBrokerConcept(data: CustomsBrokerConceptCreate): Promise<CustomsBrokerConcept> {
const response = await api.post('/a76/customs_broker_concepts', data);
return response.data;
}
export async function updateCustomsBrokerConcept(id: number, data: CustomsBrokerConceptUpdate): Promise<CustomsBrokerConcept> {
const response = await api.patch(`/a76/customs_broker_concepts/${id}`, data);
return response.data;
}
export async function deleteCustomsBrokerConcept(id: number): Promise<void> {
await api.delete(`/a76/customs_broker_concepts/${id}`);
}

View File

@@ -0,0 +1,61 @@
import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
export interface DODA {
id: number;
code: string;
description?: string;
tenant_id: string;
company_id?: string;
created_at: string;
updated_at?: string;
}
export interface DODACreate {
code: string;
description?: string;
}
export interface DODAUpdate extends Partial<DODACreate> {}
export interface DODAListResponse {
items: DODA[];
total: number;
page: number;
page_size: number;
pages: number;
}
export async function getDODAs(
page: number = 1,
pageSize: number = 50,
filters: Record<string, any> = {}
): Promise<ApiResponse<DODAListResponse>> {
const params = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
const response = await api.get(`/a76/doda?${params.toString()}`);
return response.data;
}
export async function getDODA(id: number): Promise<DODA> {
const response = await api.get(`/a76/doda/${id}`);
return response.data;
}
export async function createDODA(data: DODACreate): Promise<DODA> {
const response = await api.post('/a76/doda', data);
return response.data;
}
export async function updateDODA(id: number, data: DODAUpdate): Promise<DODA> {
const response = await api.patch(`/a76/doda/${id}`, data);
return response.data;
}
export async function deleteDODA(id: number): Promise<void> {
await api.delete(`/a76/doda/${id}`);
}

View File

@@ -0,0 +1,61 @@
import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
export interface ElectronicNotice {
id: number;
code: string;
description?: string;
tenant_id: string;
company_id?: string;
created_at: string;
updated_at?: string;
}
export interface ElectronicNoticeCreate {
code: string;
description?: string;
}
export interface ElectronicNoticeUpdate extends Partial<ElectronicNoticeCreate> {}
export interface ElectronicNoticeListResponse {
items: ElectronicNotice[];
total: number;
page: number;
page_size: number;
pages: number;
}
export async function getElectronicNotices(
page: number = 1,
pageSize: number = 50,
filters: Record<string, any> = {}
): Promise<ApiResponse<ElectronicNoticeListResponse>> {
const params = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
const response = await api.get(`/a76/electronic_notices?${params.toString()}`);
return response.data;
}
export async function getElectronicNotice(id: number): Promise<ElectronicNotice> {
const response = await api.get(`/a76/electronic_notices/${id}`);
return response.data;
}
export async function createElectronicNotice(data: ElectronicNoticeCreate): Promise<ElectronicNotice> {
const response = await api.post('/a76/electronic_notices', data);
return response.data;
}
export async function updateElectronicNotice(id: number, data: ElectronicNoticeUpdate): Promise<ElectronicNotice> {
const response = await api.patch(`/a76/electronic_notices/${id}`, data);
return response.data;
}
export async function deleteElectronicNotice(id: number): Promise<void> {
await api.delete(`/a76/electronic_notices/${id}`);
}

View File

@@ -0,0 +1,63 @@
import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
export interface Equivalency {
id: number;
fraccion_mex: string;
fraccion_us: string;
description?: string;
tenant_id: string;
company_id?: string;
created_at: string;
updated_at?: string;
}
export interface EquivalencyCreate {
fraccion_mex: string;
fraccion_us: string;
description?: string;
}
export interface EquivalencyUpdate extends Partial<EquivalencyCreate> {}
export interface EquivalencyListResponse {
items: Equivalency[];
total: number;
page: number;
page_size: number;
pages: number;
}
export async function getEquivalencies(
page: number = 1,
pageSize: number = 50,
filters: Record<string, any> = {}
): Promise<ApiResponse<EquivalencyListResponse>> {
const params = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
const response = await api.get(`/a76/equivalencies?${params.toString()}`);
return response.data;
}
export async function getEquivalency(id: number): Promise<Equivalency> {
const response = await api.get(`/a76/equivalencies/${id}`);
return response.data;
}
export async function createEquivalency(data: EquivalencyCreate): Promise<Equivalency> {
const response = await api.post('/a76/equivalencies', data);
return response.data;
}
export async function updateEquivalency(id: number, data: EquivalencyUpdate): Promise<Equivalency> {
const response = await api.patch(`/a76/equivalencies/${id}`, data);
return response.data;
}
export async function deleteEquivalency(id: number): Promise<void> {
await api.delete(`/a76/equivalencies/${id}`);
}

View File

@@ -0,0 +1,61 @@
import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
export interface ErrorCatalog {
id: number;
code: string;
description?: string;
tenant_id: string;
company_id?: string;
created_at: string;
updated_at?: string;
}
export interface ErrorCatalogCreate {
code: string;
description?: string;
}
export interface ErrorCatalogUpdate extends Partial<ErrorCatalogCreate> {}
export interface ErrorCatalogListResponse {
items: ErrorCatalog[];
total: number;
page: number;
page_size: number;
pages: number;
}
export async function getErrorCatalogs(
page: number = 1,
pageSize: number = 50,
filters: Record<string, any> = {}
): Promise<ApiResponse<ErrorCatalogListResponse>> {
const params = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
const response = await api.get(`/a76/error_catalogs?${params.toString()}`);
return response.data;
}
export async function getErrorCatalog(id: number): Promise<ErrorCatalog> {
const response = await api.get(`/a76/error_catalogs/${id}`);
return response.data;
}
export async function createErrorCatalog(data: ErrorCatalogCreate): Promise<ErrorCatalog> {
const response = await api.post('/a76/error_catalogs', data);
return response.data;
}
export async function updateErrorCatalog(id: number, data: ErrorCatalogUpdate): Promise<ErrorCatalog> {
const response = await api.patch(`/a76/error_catalogs/${id}`, data);
return response.data;
}
export async function deleteErrorCatalog(id: number): Promise<void> {
await api.delete(`/a76/error_catalogs/${id}`);
}

View File

@@ -0,0 +1,62 @@
import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
export interface Identifier {
id: number;
code: string;
description: string | null;
level: string | null;
complement: string | null;
company_id: number;
tenant_id: number;
created_at: string | null;
updated_at: string | null;
}
export interface IdentifierCreate {
code: string;
description?: string | null;
level?: string | null;
complement?: string | null;
company_id: number;
}
export interface IdentifierUpdate {
code?: string;
description?: string | null;
level?: string | null;
complement?: string | null;
}
export interface IdentifierListResponse {
items: Identifier[];
total: number;
page: number;
page_size: number;
pages: number;
}
export async function getIdentifiers(
page = 1,
pageSize = 50,
filters: Record<string, any> = {}
): Promise<ApiResponse<IdentifierListResponse>> {
const queryParams = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
return await api.get(`/a76/identifiers?${queryParams.toString()}`);
}
export async function createIdentifier(data: IdentifierCreate): Promise<ApiResponse<Identifier>> {
return await api.post('/a76/identifiers', data);
}
export async function updateIdentifier(id: number, data: IdentifierUpdate): Promise<ApiResponse<Identifier>> {
return await api.put(`/a76/identifiers/${id}`, data);
}
export async function deleteIdentifier(id: number): Promise<ApiResponse<void>> {
return await api.delete(`/a76/identifiers/${id}`);
}

View File

@@ -4,3 +4,16 @@
export * from './classes';
export * from './packages';
export * from './exchange-rate';
export * from './concepts';
export * from './customs-broker-concepts';
export * from './classification-concepts';
export * from './unit-conversions';
export * from './equivalencies';
export * from './multi-currency-types';
export * from './inpc';
export * from './legends';
export * from './signatures';
export * from './error-catalogs';
export * from './doda';
export * from './prevalidators';
export * from './electronic-notices';

View File

@@ -0,0 +1,63 @@
import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
export interface INPC {
id: number;
year: string;
month: string;
value?: number;
tenant_id: string;
company_id?: string;
created_at: string;
updated_at?: string;
}
export interface INPCCreate {
year: string;
month: string;
value?: number;
}
export interface INPCUpdate extends Partial<INPCCreate> {}
export interface INPCListResponse {
items: INPC[];
total: number;
page: number;
page_size: number;
pages: number;
}
export async function getINPCs(
page: number = 1,
pageSize: number = 50,
filters: Record<string, any> = {}
): Promise<ApiResponse<INPCListResponse>> {
const params = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
const response = await api.get(`/a76/inpc?${params.toString()}`);
return response.data;
}
export async function getINPC(id: number): Promise<INPC> {
const response = await api.get(`/a76/inpc/${id}`);
return response.data;
}
export async function createINPC(data: INPCCreate): Promise<INPC> {
const response = await api.post('/a76/inpc', data);
return response.data;
}
export async function updateINPC(id: number, data: INPCUpdate): Promise<INPC> {
const response = await api.patch(`/a76/inpc/${id}`, data);
return response.data;
}
export async function deleteINPC(id: number): Promise<void> {
await api.delete(`/a76/inpc/${id}`);
}

View File

@@ -0,0 +1,61 @@
import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
export interface Legend {
id: number;
code: string;
description?: string;
tenant_id: string;
company_id?: string;
created_at: string;
updated_at?: string;
}
export interface LegendCreate {
code: string;
description?: string;
}
export interface LegendUpdate extends Partial<LegendCreate> {}
export interface LegendListResponse {
items: Legend[];
total: number;
page: number;
page_size: number;
pages: number;
}
export async function getLegends(
page: number = 1,
pageSize: number = 50,
filters: Record<string, any> = {}
): Promise<ApiResponse<LegendListResponse>> {
const params = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
const response = await api.get(`/a76/legends?${params.toString()}`);
return response.data;
}
export async function getLegend(id: number): Promise<Legend> {
const response = await api.get(`/a76/legends/${id}`);
return response.data;
}
export async function createLegend(data: LegendCreate): Promise<Legend> {
const response = await api.post('/a76/legends', data);
return response.data;
}
export async function updateLegend(id: number, data: LegendUpdate): Promise<Legend> {
const response = await api.patch(`/a76/legends/${id}`, data);
return response.data;
}
export async function deleteLegend(id: number): Promise<void> {
await api.delete(`/a76/legends/${id}`);
}

View File

@@ -0,0 +1,61 @@
import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
export interface MultiCurrencyType {
id: number;
key: string;
description?: string;
tenant_id: string;
company_id?: string;
created_at: string;
updated_at?: string;
}
export interface MultiCurrencyTypeCreate {
key: string;
description?: string;
}
export interface MultiCurrencyTypeUpdate extends Partial<MultiCurrencyTypeCreate> {}
export interface MultiCurrencyTypeListResponse {
items: MultiCurrencyType[];
total: number;
page: number;
page_size: number;
pages: number;
}
export async function getMultiCurrencyTypes(
page: number = 1,
pageSize: number = 50,
filters: Record<string, any> = {}
): Promise<ApiResponse<MultiCurrencyTypeListResponse>> {
const params = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
const response = await api.get(`/a76/multi_currency_types?${params.toString()}`);
return response.data;
}
export async function getMultiCurrencyType(id: number): Promise<MultiCurrencyType> {
const response = await api.get(`/a76/multi_currency_types/${id}`);
return response.data;
}
export async function createMultiCurrencyType(data: MultiCurrencyTypeCreate): Promise<MultiCurrencyType> {
const response = await api.post('/a76/multi_currency_types', data);
return response.data;
}
export async function updateMultiCurrencyType(id: number, data: MultiCurrencyTypeUpdate): Promise<MultiCurrencyType> {
const response = await api.patch(`/a76/multi_currency_types/${id}`, data);
return response.data;
}
export async function deleteMultiCurrencyType(id: number): Promise<void> {
await api.delete(`/a76/multi_currency_types/${id}`);
}

View File

@@ -1,122 +1,78 @@
/**
* API para gestión de Packages (Bultos/Embalajes A76)
*/
import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
export interface Package {
id: number;
tenant_id: number;
company_id: number;
key: string;
description_es: string | null;
description_en: string | null;
weight_unit: number | null;
plurals: string | null;
plural_in: string | null;
code_ace: string | null;
code_aamex: string | null;
created_at: string | null;
updated_at: string | null;
id: number;
tenant_id: number;
company_id: number;
key: string;
description_es: string | null;
description_en: string | null;
weight_unit: number | null;
plurals: string | null;
plural_in: string | null;
code_ace: string | null;
code_aamex: string | null;
created_at: string | null;
updated_at: string | null;
}
export interface PackageCreate {
key: string;
description_es?: string | null;
description_en?: string | null;
weight_unit?: number | null;
plurals?: string | null;
plural_in?: string | null;
code_ace?: string | null;
code_aamex?: string | null;
key: string;
description_es?: string | null;
description_en?: string | null;
weight_unit?: number | null;
plurals?: string | null;
plural_in?: string | null;
code_ace?: string | null;
code_aamex?: string | null;
company_id: number;
}
export interface PackageUpdate {
key?: string;
description_es?: string | null;
description_en?: string | null;
weight_unit?: number | null;
plurals?: string | null;
plural_in?: string | null;
code_ace?: string | null;
code_aamex?: string | null;
key?: string;
description_es?: string | null;
description_en?: string | null;
weight_unit?: number | null;
plurals?: string | null;
plural_in?: string | null;
code_ace?: string | null;
code_aamex?: string | null;
}
export interface PackageListResponse {
items: Package[];
total: number;
page: number;
page_size: number;
pages: number;
items: Package[];
total: number;
page: number;
page_size: number;
pages: number;
}
export interface PackageFilters {
key?: string;
description_es?: string;
}
/**
* Obtener lista de packages con paginación
*/
export async function getPackages(
companyId: number,
page: number = 1,
pageSize: number = 50,
filters?: PackageFilters
page = 1,
pageSize = 50,
filters: Record<string, any> = {}
): Promise<ApiResponse<PackageListResponse>> {
const params = new URLSearchParams({
company_id: companyId.toString(),
page: page.toString(),
page_size: pageSize.toString()
});
if (filters?.key) {
params.append('key', filters.key);
}
if (filters?.description_es) {
params.append('description_es', filters.description_es);
}
return api.get<PackageListResponse>(`/v1/a76/packages/?${params.toString()}`);
const queryParams = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
return await api.get(`/a76/packages?${queryParams.toString()}`);
}
/**
* Obtener un package por ID
*/
export async function getPackage(
packageId: number,
companyId: number
): Promise<ApiResponse<Package>> {
return api.get<Package>(`/v1/a76/packages/${packageId}?company_id=${companyId}`);
export async function getPackage(id: number): Promise<ApiResponse<Package>> {
return await api.get(`/a76/packages/${id}`);
}
/**
* Crear un nuevo package
*/
export async function createPackage(
data: PackageCreate,
companyId: number
): Promise<ApiResponse<Package>> {
return api.post<Package>(`/v1/a76/packages/?company_id=${companyId}`, data);
export async function createPackage(data: PackageCreate): Promise<ApiResponse<Package>> {
return await api.post(`/a76/packages`, data);
}
/**
* Actualizar un package existente
*/
export async function updatePackage(
packageId: number,
data: PackageUpdate,
companyId: number
): Promise<ApiResponse<Package>> {
return api.put<Package>(`/v1/a76/packages/${packageId}?company_id=${companyId}`, data);
export async function updatePackage(id: number, data: PackageUpdate): Promise<ApiResponse<Package>> {
return await api.put(`/a76/packages/${id}`, data);
}
/**
* Eliminar un package
*/
export async function deletePackage(
packageId: number,
companyId: number
): Promise<ApiResponse<void>> {
return api.delete<void>(`/v1/a76/packages/${packageId}?company_id=${companyId}`);
export async function deletePackage(id: number): Promise<ApiResponse<void>> {
return await api.delete(`/a76/packages/${id}`);
}

View File

@@ -0,0 +1,68 @@
import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
export enum PortType {
ENTRY = 'ENTRY',
EXIT = 'EXIT',
BOTH = 'BOTH'
}
export interface Port {
id: number;
port_code: string;
description: string | null;
location_code: string;
location_description: string | null;
port_type: PortType;
created_at: string | null;
updated_at: string | null;
}
export interface PortCreate {
port_code: string;
description?: string | null;
location_code: string;
location_description?: string | null;
port_type?: PortType;
}
export interface PortUpdate {
port_code?: string;
description?: string | null;
location_code?: string;
location_description?: string | null;
port_type?: PortType;
}
export interface PortListResponse {
items: Port[];
total: number;
page: number;
page_size: number;
pages: number;
}
export async function getPorts(
page = 1,
pageSize = 50,
filters: Record<string, any> = {}
): Promise<ApiResponse<PortListResponse>> {
const queryParams = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
return await api.get(`/a76/ports?${queryParams.toString()}`);
}
export async function createPort(data: PortCreate): Promise<ApiResponse<Port>> {
return await api.post('/a76/ports', data);
}
export async function updatePort(id: number, data: PortUpdate): Promise<ApiResponse<Port>> {
return await api.put(`/a76/ports/${id}`, data);
}
export async function deletePort(id: number): Promise<ApiResponse<void>> {
return await api.delete(`/a76/ports/${id}`);
}

View File

@@ -0,0 +1,61 @@
import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
export interface Prevalidator {
id: number;
code: string;
description?: string;
tenant_id: string;
company_id?: string;
created_at: string;
updated_at?: string;
}
export interface PrevalidatorCreate {
code: string;
description?: string;
}
export interface PrevalidatorUpdate extends Partial<PrevalidatorCreate> {}
export interface PrevalidatorListResponse {
items: Prevalidator[];
total: number;
page: number;
page_size: number;
pages: number;
}
export async function getPrevalidators(
page: number = 1,
pageSize: number = 50,
filters: Record<string, any> = {}
): Promise<ApiResponse<PrevalidatorListResponse>> {
const params = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
const response = await api.get(`/a76/prevalidators?${params.toString()}`);
return response.data;
}
export async function getPrevalidator(id: number): Promise<Prevalidator> {
const response = await api.get(`/a76/prevalidators/${id}`);
return response.data;
}
export async function createPrevalidator(data: PrevalidatorCreate): Promise<Prevalidator> {
const response = await api.post('/a76/prevalidators', data);
return response.data;
}
export async function updatePrevalidator(id: number, data: PrevalidatorUpdate): Promise<Prevalidator> {
const response = await api.patch(`/a76/prevalidators/${id}`, data);
return response.data;
}
export async function deletePrevalidator(id: number): Promise<void> {
await api.delete(`/a76/prevalidators/${id}`);
}

View File

@@ -0,0 +1,63 @@
import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
export interface Signature {
id: number;
name: string;
position?: string;
certificate?: string;
tenant_id: string;
company_id?: string;
created_at: string;
updated_at?: string;
}
export interface SignatureCreate {
name: string;
position?: string;
certificate?: string;
}
export interface SignatureUpdate extends Partial<SignatureCreate> {}
export interface SignatureListResponse {
items: Signature[];
total: number;
page: number;
page_size: number;
pages: number;
}
export async function getSignatures(
page: number = 1,
pageSize: number = 50,
filters: Record<string, any> = {}
): Promise<ApiResponse<SignatureListResponse>> {
const params = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
const response = await api.get(`/a76/signatures?${params.toString()}`);
return response.data;
}
export async function getSignature(id: number): Promise<Signature> {
const response = await api.get(`/a76/signatures/${id}`);
return response.data;
}
export async function createSignature(data: SignatureCreate): Promise<Signature> {
const response = await api.post('/a76/signatures', data);
return response.data;
}
export async function updateSignature(id: number, data: SignatureUpdate): Promise<Signature> {
const response = await api.patch(`/a76/signatures/${id}`, data);
return response.data;
}
export async function deleteSignature(id: number): Promise<void> {
await api.delete(`/a76/signatures/${id}`);
}

View File

@@ -0,0 +1,63 @@
import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
export interface UnitConversion {
id: number;
from_unit_id: number;
to_unit_id: number;
conversion_factor: number;
tenant_id: string;
company_id?: string;
created_at: string;
updated_at?: string;
}
export interface UnitConversionCreate {
from_unit_id: number;
to_unit_id: number;
conversion_factor: number;
}
export interface UnitConversionUpdate extends Partial<UnitConversionCreate> {}
export interface UnitConversionListResponse {
items: UnitConversion[];
total: number;
page: number;
page_size: number;
pages: number;
}
export async function getUnitConversions(
page: number = 1,
pageSize: number = 50,
filters: Record<string, any> = {}
): Promise<ApiResponse<UnitConversionListResponse>> {
const params = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
const response = await api.get(`/a76/unit_conversions?${params.toString()}`);
return response.data;
}
export async function getUnitConversion(id: number): Promise<UnitConversion> {
const response = await api.get(`/a76/unit_conversions/${id}`);
return response.data;
}
export async function createUnitConversion(data: UnitConversionCreate): Promise<UnitConversion> {
const response = await api.post('/a76/unit_conversions', data);
return response.data;
}
export async function updateUnitConversion(id: number, data: UnitConversionUpdate): Promise<UnitConversion> {
const response = await api.patch(`/a76/unit_conversions/${id}`, data);
return response.data;
}
export async function deleteUnitConversion(id: number): Promise<void> {
await api.delete(`/a76/unit_conversions/${id}`);
}

View File

@@ -0,0 +1,158 @@
import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
// --- ACE ---
export interface UnitOfMeasureACE {
id: number;
code: string;
description: string | null;
created_at: string | null;
updated_at: string | null;
}
export interface UnitOfMeasureACECreate {
code: string;
description?: string | null;
}
export interface UnitOfMeasureACEUpdate {
code?: string;
description?: string | null;
}
export interface UnitOfMeasureACEListResponse {
items: UnitOfMeasureACE[];
total: number;
page: number;
page_size: number;
pages: number;
}
export async function getUnitsOfMeasureACE(
page = 1,
pageSize = 50,
filters: Record<string, any> = {}
): Promise<ApiResponse<UnitOfMeasureACEListResponse>> {
const queryParams = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
return await api.get(`/a76/units-of-measure/ace?${queryParams.toString()}`);
}
export async function createUnitOfMeasureACE(data: UnitOfMeasureACECreate): Promise<ApiResponse<UnitOfMeasureACE>> {
return await api.post('/a76/units-of-measure/ace', data);
}
export async function updateUnitOfMeasureACE(id: number, data: UnitOfMeasureACEUpdate): Promise<ApiResponse<UnitOfMeasureACE>> {
return await api.put(`/a76/units-of-measure/ace/${id}`, data);
}
export async function deleteUnitOfMeasureACE(id: number): Promise<ApiResponse<void>> {
return await api.delete(`/a76/units-of-measure/ace/${id}`);
}
// --- OMA ---
export interface UnitOfMeasureOMA {
id: number;
code: string;
description: string | null;
created_at: string | null;
updated_at: string | null;
}
export interface UnitOfMeasureOMACreate {
code: string;
description?: string | null;
}
export interface UnitOfMeasureOMAUpdate {
code?: string;
description?: string | null;
}
export interface UnitOfMeasureOMAListResponse {
items: UnitOfMeasureOMA[];
total: number;
page: number;
page_size: number;
pages: number;
}
export async function getUnitsOfMeasureOMA(
page = 1,
pageSize = 50,
filters: Record<string, any> = {}
): Promise<ApiResponse<UnitOfMeasureOMAListResponse>> {
const queryParams = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
return await api.get(`/a76/units-of-measure/oma?${queryParams.toString()}`);
}
export async function createUnitOfMeasureOMA(data: UnitOfMeasureOMACreate): Promise<ApiResponse<UnitOfMeasureOMA>> {
return await api.post('/a76/units-of-measure/oma', data);
}
export async function updateUnitOfMeasureOMA(id: number, data: UnitOfMeasureOMAUpdate): Promise<ApiResponse<UnitOfMeasureOMA>> {
return await api.put(`/a76/units-of-measure/oma/${id}`, data);
}
export async function deleteUnitOfMeasureOMA(id: number): Promise<ApiResponse<void>> {
return await api.delete(`/a76/units-of-measure/oma/${id}`);
}
// --- American ---
export interface UnitOfMeasureAmerican {
id: number;
code: string;
description: string | null;
created_at: string | null;
updated_at: string | null;
}
export interface UnitOfMeasureAmericanCreate {
code: string;
description?: string | null;
}
export interface UnitOfMeasureAmericanUpdate {
code?: string;
description?: string | null;
}
export interface UnitOfMeasureAmericanListResponse {
items: UnitOfMeasureAmerican[];
total: number;
page: number;
page_size: number;
pages: number;
}
export async function getUnitsOfMeasureAmerican(
page = 1,
pageSize = 50,
filters: Record<string, any> = {}
): Promise<ApiResponse<UnitOfMeasureAmericanListResponse>> {
const queryParams = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
...filters
});
return await api.get(`/a76/units-of-measure/american?${queryParams.toString()}`);
}
export async function createUnitOfMeasureAmerican(data: UnitOfMeasureAmericanCreate): Promise<ApiResponse<UnitOfMeasureAmerican>> {
return await api.post('/a76/units-of-measure/american', data);
}
export async function updateUnitOfMeasureAmerican(id: number, data: UnitOfMeasureAmericanUpdate): Promise<ApiResponse<UnitOfMeasureAmerican>> {
return await api.put(`/a76/units-of-measure/american/${id}`, data);
}
export async function deleteUnitOfMeasureAmerican(id: number): Promise<ApiResponse<void>> {
return await api.delete(`/a76/units-of-measure/american/${id}`);
}

View File

@@ -0,0 +1,38 @@
import type { Company } from '$lib/api/dashboard/a76/company';
import type { ColumnDef } from '@tanstack/table-core';
import { renderComponent } from '$lib/components/ui/data-table';
import DataTableActions from './data-table-actions.svelte';
export function createColumns(onSuccess?: () => void): ColumnDef<Company>[] {
return [
{
accessorKey: 'name',
header: 'Nombre',
cell: ({ row }) => row.original.name || '-'
},
{
accessorKey: 'rfc',
header: 'RFC',
cell: ({ row }) => row.original.rfc || '-'
},
{
accessorKey: 'program',
header: 'Programa',
cell: ({ row }) => row.original.program || '-'
},
{
accessorKey: 'program_number',
header: 'No. Programa',
cell: ({ row }) => row.original.program_number || '-'
},
{
id: 'actions',
cell: ({ row }) => {
return renderComponent(DataTableActions, {
item: row.original,
onSuccess
});
}
}
];
}

View File

@@ -0,0 +1,127 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import * as Dialog from "$lib/components/ui/dialog";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import { createCompany, updateCompany, type Company } from "$lib/api/dashboard/a76/company";
let {
open = $bindable(false),
item = null,
onSuccess
}: {
open: boolean;
item?: Company | null;
onSuccess?: () => void;
} = $props();
const isEdit = $derived(!!item);
const title = $derived(isEdit ? "Editar Empresa" : "Nueva Empresa");
let formData = $state({
name: item?.name || '',
rfc: item?.rfc || '',
main_activity: item?.main_activity || '',
program: item?.program || '',
program_number: item?.program_number || ''
});
let loading = $state(false);
let error = $state<string | null>(null);
$effect(() => {
if (item) {
formData = {
name: item.name || '',
rfc: item.rfc || '',
main_activity: item.main_activity || '',
program: item.program || '',
program_number: item.program_number || ''
};
} else {
formData = {
name: '',
rfc: '',
main_activity: '',
program: '',
program_number: ''
};
}
});
async function handleSubmit() {
error = null;
loading = true;
try {
if (!formData.name.trim()) throw new Error('El nombre es requerido');
const dataToSend = {
name: formData.name.trim(),
rfc: formData.rfc.trim() || null,
main_activity: formData.main_activity.trim() || null,
program: formData.program.trim() || null,
program_number: formData.program_number.trim() || null
};
let response;
if (isEdit && item) {
response = await updateCompany(item.id, dataToSend);
} else {
response = await createCompany(dataToSend);
}
if (response.error) {
throw new Error(response.error);
}
open = false;
if (onSuccess) onSuccess();
} catch (e) {
error = e instanceof Error ? e.message : 'Error al guardar';
} finally {
loading = false;
}
}
</script>
<Dialog.Root bind:open>
<Dialog.Content class="max-w-md">
<Dialog.Header>
<Dialog.Title>{title}</Dialog.Title>
</Dialog.Header>
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="space-y-4">
{#if error}
<div class="text-destructive text-sm">{error}</div>
{/if}
<div class="grid gap-2">
<Label for="name">Nombre <span class="text-destructive">*</span></Label>
<Input id="name" bind:value={formData.name} />
</div>
<div class="grid gap-2">
<Label for="rfc">RFC</Label>
<Input id="rfc" bind:value={formData.rfc} />
</div>
<div class="grid gap-2">
<Label for="program">Programa</Label>
<Input id="program" bind:value={formData.program} />
</div>
<div class="grid gap-2">
<Label for="program_number">No. Programa</Label>
<Input id="program_number" bind:value={formData.program_number} />
</div>
<Dialog.Footer>
<Button type="button" variant="outline" onclick={() => open = false}>Cancelar</Button>
<Button type="submit" disabled={loading}>
{loading ? 'Guardando...' : 'Guardar'}
</Button>
</Dialog.Footer>
</form>
</Dialog.Content>
</Dialog.Root>

View File

@@ -0,0 +1,78 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
import { deleteCompany, type Company } from "$lib/api/dashboard/a76/company";
import { EllipsisVertical, Pencil, LoaderCircle, Trash2 } from 'lucide-svelte';
import CreateEditDialog from "./create-edit-dialog.svelte";
let {
item,
onSuccess
}: {
item: Company;
onSuccess?: () => void;
} = $props();
let loading = $state(false);
let error = $state<string | null>(null);
let dialogOpen = $state(false);
async function handleDelete() {
if (!confirm(`¿Estás seguro de eliminar la empresa "${item.name}"?`)) {
return;
}
loading = true;
error = null;
try {
const response = await deleteCompany(item.id);
if (response.error) {
alert(`Error al eliminar: ${response.error}`);
return;
}
if (onSuccess) {
onSuccess();
}
} catch (e) {
alert('Error al eliminar el registro');
} finally {
loading = false;
}
}
</script>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
{#snippet child({ props })}
<Button {...props} variant="ghost" size="icon" class="h-8 w-8 p-0">
<span class="sr-only">Abrir menú</span>
<EllipsisVertical class="h-4 w-4" />
</Button>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Item onclick={() => dialogOpen = true}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item class="text-destructive focus:text-destructive" onclick={handleDelete} disabled={loading}>
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<Trash2 class="mr-2 h-4 w-4" />
{/if}
Eliminar
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
<CreateEditDialog
bind:open={dialogOpen}
item={item}
onSuccess={onSuccess}
/>

View File

@@ -0,0 +1,93 @@
<script lang="ts" generics="T extends Record<string, any>">
import * as Table from "$lib/components/ui/table/index.js";
import { Button } from "$lib/components/ui/button";
import { goto } from "$app/navigation";
import { page } from "$app/stores";
type Column = {
key: string;
label: string;
format?: (value: any, row: T) => string;
};
type SimpleDataTableProps<T> = {
columns: Column[];
data: T[];
pageCount: number;
totalItems: number;
};
let {
data,
columns,
pageCount,
totalItems
}: SimpleDataTableProps<T> = $props();
function handlePageChange(newPage: number) {
const url = new URL($page.url);
url.searchParams.set('page', newPage.toString());
goto(url);
}
function getCellValue(row: T, column: Column): string {
const value = row[column.key];
if (column.format) {
return column.format(value, row);
}
return value !== null && value !== undefined ? String(value) : '-';
}
</script>
<div class="rounded-md border">
<Table.Root>
<Table.Header>
<Table.Row>
{#each columns as column (column.key)}
<Table.Head>{column.label}</Table.Head>
{/each}
</Table.Row>
</Table.Header>
<Table.Body>
{#each data as row, i (i)}
<Table.Row>
{#each columns as column (column.key)}
<Table.Cell>
{getCellValue(row, column)}
</Table.Cell>
{/each}
</Table.Row>
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center">
No hay resultados.
</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
</div>
<div class="flex items-center justify-end space-x-2 py-4">
<div class="flex-1 text-sm text-muted-foreground">
Total: {totalItems}
</div>
<div class="space-x-2">
<Button
variant="outline"
size="sm"
onclick={() => handlePageChange(Number($page.url.searchParams.get('page') || 1) - 1)}
disabled={Number($page.url.searchParams.get('page') || 1) <= 1}
>
Anterior
</Button>
<Button
variant="outline"
size="sm"
onclick={() => handlePageChange(Number($page.url.searchParams.get('page') || 1) + 1)}
disabled={Number($page.url.searchParams.get('page') || 1) >= pageCount}
>
Siguiente
</Button>
</div>
</div>

View File

@@ -0,0 +1,37 @@
import type { Identifier } from '$lib/api/dashboard/a76/identifiers';
import type { ColumnDef } from '@tanstack/table-core';
import { renderComponent } from '$lib/components/ui/data-table';
import DataTableActions from './data-table-actions.svelte';
export function createColumns(onSuccess?: () => void): ColumnDef<Identifier>[] {
return [
{
accessorKey: 'code',
header: 'Clave',
},
{
accessorKey: 'description',
header: 'Descripción',
cell: ({ row }) => row.original.description || '-'
},
{
accessorKey: 'level',
header: 'Nivel',
cell: ({ row }) => row.original.level || '-'
},
{
accessorKey: 'complement',
header: 'Complemento',
cell: ({ row }) => row.original.complement || '-'
},
{
id: 'actions',
cell: ({ row }) => {
return renderComponent(DataTableActions, {
item: row.original,
onSuccess
});
}
}
];
}

View File

@@ -0,0 +1,139 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import * as Dialog from "$lib/components/ui/dialog";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import { Textarea } from "$lib/components/ui/textarea";
import { createIdentifier, updateIdentifier, type Identifier } from "$lib/api/dashboard/a76/identifiers";
import { companyStore } from "$lib/stores/company.svelte";
let {
open = $bindable(false),
mode = 'create',
item = null,
onSuccess
}: {
open: boolean;
mode?: 'create' | 'edit';
item?: Identifier | null;
onSuccess?: () => void;
} = $props();
const isEdit = $derived(mode === 'edit');
const title = $derived(isEdit ? "Editar Identificador" : "Nuevo Identificador");
let formData = $state({
code: '',
description: '',
level: '',
complement: ''
});
let loading = $state(false);
let error = $state<string | null>(null);
$effect(() => {
if (open) {
if (isEdit && item) {
formData = {
code: item.code,
description: item.description || '',
level: item.level || '',
complement: item.complement || ''
};
} else {
formData = {
code: '',
description: '',
level: '',
complement: ''
};
}
error = null;
}
});
async function handleSubmit() {
const companyId = companyStore.activeCompany?.id;
if (!companyId) {
error = 'No hay compañía seleccionada';
return;
}
loading = true;
error = null;
try {
let response;
if (isEdit && item) {
response = await updateIdentifier(item.id, {
code: formData.code,
description: formData.description || null,
level: formData.level || null,
complement: formData.complement || null
});
} else {
response = await createIdentifier({
code: formData.code,
description: formData.description || null,
level: formData.level || null,
complement: formData.complement || null,
company_id: companyId
});
}
if (response.error) {
error = response.error;
return;
}
open = false;
if (onSuccess) onSuccess();
} catch (e) {
error = 'Error de conexión';
console.error(e);
} finally {
loading = false;
}
}
</script>
<Dialog.Root bind:open>
<Dialog.Content class="sm:max-w-[425px]">
<Dialog.Header>
<Dialog.Title>{title}</Dialog.Title>
</Dialog.Header>
<div class="grid gap-4 py-4">
{#if error}
<div class="text-red-500 text-sm mb-2">{error}</div>
{/if}
<div class="grid grid-cols-4 items-center gap-4">
<Label for="code" class="text-right">Clave</Label>
<Input id="code" bind:value={formData.code} class="col-span-3" disabled={loading} />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="description" class="text-right">Descripción</Label>
<Textarea id="description" bind:value={formData.description} class="col-span-3" disabled={loading} />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="level" class="text-right">Nivel</Label>
<Input id="level" bind:value={formData.level} class="col-span-3" disabled={loading} />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="complement" class="text-right">Complemento</Label>
<Textarea id="complement" bind:value={formData.complement} class="col-span-3" disabled={loading} />
</div>
</div>
<Dialog.Footer>
<Button type="submit" onclick={handleSubmit} disabled={loading}>
{loading ? 'Guardando...' : 'Guardar'}
</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>

View File

@@ -0,0 +1,79 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
import { deleteIdentifier, type Identifier } from "$lib/api/dashboard/a76/identifiers";
import { EllipsisVertical, Pencil, LoaderCircle, Trash2 } from 'lucide-svelte';
import CreateEditDialog from "./create-edit-dialog.svelte";
let {
item,
onSuccess
}: {
item: Identifier;
onSuccess?: () => void;
} = $props();
let loading = $state(false);
let error = $state<string | null>(null);
let dialogOpen = $state(false);
async function handleDelete() {
if (!confirm(`¿Estás seguro de eliminar el identificador "${item.code}"?`)) {
return;
}
loading = true;
error = null;
try {
const response = await deleteIdentifier(item.id);
if (response.error) {
error = response.error;
alert(`Error al eliminar: ${response.error}`);
return;
}
if (onSuccess) onSuccess();
} catch (e) {
error = 'Error de conexión';
console.error(e);
alert('Error de conexión al eliminar');
} finally {
loading = false;
}
}
</script>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
{#snippet child({ props })}
<Button {...props} variant="ghost" size="icon" class="h-8 w-8 p-0">
<span class="sr-only">Abrir menú</span>
<EllipsisVertical class="h-4 w-4" />
</Button>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Item onclick={() => dialogOpen = true}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleDelete} class="text-red-600">
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<Trash2 class="mr-2 h-4 w-4" />
{/if}
Eliminar
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
<CreateEditDialog
bind:open={dialogOpen}
mode="edit"
{item}
{onSuccess}
/>

View File

@@ -0,0 +1,106 @@
<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";
import { Button } from "$lib/components/ui/button";
import { goto } from "$app/navigation";
import { page } from "$app/stores";
type DataTableProps<TData, TValue> = {
columns: ColumnDef<TData, TValue>[];
data: TData[];
pageCount: number;
totalItems: number;
};
let {
data,
columns,
pageCount,
totalItems
}: DataTableProps<TData, TValue> = $props();
const table = createSvelteTable({
get data() {
return data;
},
columns,
getCoreRowModel: getCoreRowModel(),
manualPagination: true,
pageCount: pageCount,
});
function handlePageChange(newPage: number) {
const url = new URL($page.url);
url.searchParams.set('page', newPage.toString());
goto(url);
}
</script>
<div class="rounded-md border">
<Table.Root>
<Table.Header>
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
{#each headerGroup.headers as header (header.id)}
<Table.Head>
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{/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"}>
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell>
<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">
No hay resultados.
</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
</div>
<div class="flex items-center justify-end space-x-2 py-4">
<div class="flex-1 text-sm text-muted-foreground">
Total: {totalItems}
</div>
<div class="space-x-2">
<Button
variant="outline"
size="sm"
onclick={() => handlePageChange(Number($page.url.searchParams.get('page') || 1) - 1)}
disabled={Number($page.url.searchParams.get('page') || 1) <= 1}
>
Anterior
</Button>
<Button
variant="outline"
size="sm"
onclick={() => handlePageChange(Number($page.url.searchParams.get('page') || 1) + 1)}
disabled={Number($page.url.searchParams.get('page') || 1) >= pageCount}
>
Siguiente
</Button>
</div>
</div>

View File

@@ -95,9 +95,9 @@
let response;
if (isEdit && item) {
response = await updatePackage(item.id, dataToSend as PackageUpdate, companyId);
response = await updatePackage(item.id, dataToSend);
} else {
response = await createPackage(dataToSend as PackageCreate, companyId);
response = await createPackage({ ...dataToSend, company_id: companyId });
}
if (response.error) {

View File

@@ -7,10 +7,10 @@
import CreateEditDialog from "./create-edit-dialog.svelte";
let {
package: item,
item,
onSuccess
}: {
package: Package;
item: Package;
onSuccess?: () => void;
} = $props();
@@ -24,17 +24,11 @@
return;
}
const companyId = companyStore.activeCompany?.id;
if (!companyId) {
alert('No hay compañía seleccionada');
return;
}
loading = true;
error = null;
try {
const response = await deletePackage(item.id, companyId);
const response = await deletePackage(item.id);
if (response.error) {
if (response.status === 401) {

View File

@@ -0,0 +1,47 @@
import type { Port } from '$lib/api/dashboard/a76/ports';
import type { ColumnDef } from '@tanstack/table-core';
import { renderComponent } from '$lib/components/ui/data-table';
import DataTableActions from './data-table-actions.svelte';
export function createColumns(onSuccess?: () => void): ColumnDef<Port>[] {
return [
{
accessorKey: 'port_code',
header: 'Código Puerto',
},
{
accessorKey: 'description',
header: 'Descripción',
cell: ({ row }) => row.original.description || '-'
},
{
accessorKey: 'location_code',
header: 'Código Ubicación',
},
{
accessorKey: 'location_description',
header: 'Ubicación',
cell: ({ row }) => row.original.location_description || '-'
},
{
accessorKey: 'port_type',
header: 'Tipo',
cell: ({ row }) => {
const type = row.original.port_type;
if (type === 'ENTRY') return 'Entrada';
if (type === 'EXIT') return 'Salida';
if (type === 'BOTH') return 'Ambos';
return type;
}
},
{
id: 'actions',
cell: ({ row }) => {
return renderComponent(DataTableActions, {
item: row.original,
onSuccess
});
}
}
];
}

View File

@@ -0,0 +1,154 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import * as Dialog from "$lib/components/ui/dialog";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import * as Select from "$lib/components/ui/select";
import { createPort, updatePort, type Port, PortType } from "$lib/api/dashboard/a76/ports";
let {
open = $bindable(false),
mode = 'create',
item = null,
onSuccess
}: {
open: boolean;
mode?: 'create' | 'edit';
item?: Port | null;
onSuccess?: () => void;
} = $props();
const isEdit = $derived(mode === 'edit');
const title = $derived(isEdit ? "Editar Puerto" : "Nuevo Puerto");
let formData = $state({
port_code: '',
description: '',
location_code: '',
location_description: '',
port_type: PortType.ENTRY
});
let loading = $state(false);
let error = $state<string | null>(null);
$effect(() => {
if (open) {
if (isEdit && item) {
formData = {
port_code: item.port_code,
description: item.description || '',
location_code: item.location_code,
location_description: item.location_description || '',
port_type: item.port_type
};
} else {
formData = {
port_code: '',
description: '',
location_code: '',
location_description: '',
port_type: PortType.ENTRY
};
}
error = null;
}
});
async function handleSubmit() {
loading = true;
error = null;
try {
let response;
if (isEdit && item) {
response = await updatePort(item.id, {
port_code: formData.port_code,
description: formData.description || null,
location_code: formData.location_code,
location_description: formData.location_description || null,
port_type: formData.port_type
});
} else {
response = await createPort({
port_code: formData.port_code,
description: formData.description || null,
location_code: formData.location_code,
location_description: formData.location_description || null,
port_type: formData.port_type
});
}
if (response.error) {
error = response.error;
return;
}
open = false;
if (onSuccess) onSuccess();
} catch (e) {
error = 'Error de conexión';
console.error(e);
} finally {
loading = false;
}
}
</script>
<Dialog.Root bind:open>
<Dialog.Content class="sm:max-w-[425px]">
<Dialog.Header>
<Dialog.Title>{title}</Dialog.Title>
</Dialog.Header>
<div class="grid gap-4 py-4">
{#if error}
<div class="text-red-500 text-sm mb-2">{error}</div>
{/if}
<div class="grid grid-cols-4 items-center gap-4">
<Label for="port_code" class="text-right">Código Puerto</Label>
<Input id="port_code" bind:value={formData.port_code} class="col-span-3" disabled={loading} />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="description" class="text-right">Descripción</Label>
<Input id="description" bind:value={formData.description} class="col-span-3" disabled={loading} />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="location_code" class="text-right">Código Ubicación</Label>
<Input id="location_code" bind:value={formData.location_code} class="col-span-3" disabled={loading} />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="location_description" class="text-right">Ubicación</Label>
<Input id="location_description" bind:value={formData.location_description} class="col-span-3" disabled={loading} />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="port_type" class="text-right">Tipo</Label>
<div class="col-span-3">
<Select.Root type="single" bind:value={formData.port_type}>
<Select.Trigger>
{formData.port_type === 'ENTRY' ? 'Entrada' :
formData.port_type === 'EXIT' ? 'Salida' :
formData.port_type === 'BOTH' ? 'Ambos' : 'Seleccionar'}
</Select.Trigger>
<Select.Content>
<Select.Item value="ENTRY">Entrada</Select.Item>
<Select.Item value="EXIT">Salida</Select.Item>
<Select.Item value="BOTH">Ambos</Select.Item>
</Select.Content>
</Select.Root>
</div>
</div>
</div>
<Dialog.Footer>
<Button type="submit" onclick={handleSubmit} disabled={loading}>
{loading ? 'Guardando...' : 'Guardar'}
</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>

View File

@@ -0,0 +1,79 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
import { deletePort, type Port } from "$lib/api/dashboard/a76/ports";
import { EllipsisVertical, Pencil, LoaderCircle, Trash2 } from 'lucide-svelte';
import CreateEditDialog from "./create-edit-dialog.svelte";
let {
item,
onSuccess
}: {
item: Port;
onSuccess?: () => void;
} = $props();
let loading = $state(false);
let error = $state<string | null>(null);
let dialogOpen = $state(false);
async function handleDelete() {
if (!confirm(`¿Estás seguro de eliminar el puerto "${item.port_code}"?`)) {
return;
}
loading = true;
error = null;
try {
const response = await deletePort(item.id);
if (response.error) {
error = response.error;
alert(`Error al eliminar: ${response.error}`);
return;
}
if (onSuccess) onSuccess();
} catch (e) {
error = 'Error de conexión';
console.error(e);
alert('Error de conexión al eliminar');
} finally {
loading = false;
}
}
</script>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
{#snippet child({ props })}
<Button {...props} variant="ghost" size="icon" class="h-8 w-8 p-0">
<span class="sr-only">Abrir menú</span>
<EllipsisVertical class="h-4 w-4" />
</Button>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Item onclick={() => dialogOpen = true}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleDelete} class="text-red-600">
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<Trash2 class="mr-2 h-4 w-4" />
{/if}
Eliminar
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
<CreateEditDialog
bind:open={dialogOpen}
mode="edit"
{item}
{onSuccess}
/>

View File

@@ -0,0 +1,27 @@
import type { UnitOfMeasureACE } from '$lib/api/dashboard/a76/units-of-measure';
import type { ColumnDef } from '@tanstack/table-core';
import { renderComponent } from '$lib/components/ui/data-table';
import DataTableActions from './data-table-actions.svelte';
export function createColumns(onSuccess?: () => void): ColumnDef<UnitOfMeasureACE>[] {
return [
{
accessorKey: 'code',
header: 'Código',
},
{
accessorKey: 'description',
header: 'Descripción',
cell: ({ row }) => row.original.description || '-'
},
{
id: 'actions',
cell: ({ row }) => {
return renderComponent(DataTableActions, {
item: row.original,
onSuccess
});
}
}
];
}

View File

@@ -0,0 +1,110 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import * as Dialog from "$lib/components/ui/dialog";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import { createUnitOfMeasureACE, updateUnitOfMeasureACE, type UnitOfMeasureACE } from "$lib/api/dashboard/a76/units-of-measure";
let {
open = $bindable(false),
mode = 'create',
item = null,
onSuccess
}: {
open: boolean;
mode?: 'create' | 'edit';
item?: UnitOfMeasureACE | null;
onSuccess?: () => void;
} = $props();
const isEdit = $derived(mode === 'edit');
const title = $derived(isEdit ? "Editar Unidad ACE" : "Nueva Unidad ACE");
let formData = $state({
code: '',
description: ''
});
let loading = $state(false);
let error = $state<string | null>(null);
$effect(() => {
if (open) {
if (isEdit && item) {
formData = {
code: item.code,
description: item.description || ''
};
} else {
formData = {
code: '',
description: ''
};
}
error = null;
}
});
async function handleSubmit() {
loading = true;
error = null;
try {
let response;
if (isEdit && item) {
response = await updateUnitOfMeasureACE(item.id, {
code: formData.code,
description: formData.description || null
});
} else {
response = await createUnitOfMeasureACE({
code: formData.code,
description: formData.description || null
});
}
if (response.error) {
error = response.error;
return;
}
open = false;
if (onSuccess) onSuccess();
} catch (e) {
error = 'Error de conexión';
console.error(e);
} finally {
loading = false;
}
}
</script>
<Dialog.Root bind:open>
<Dialog.Content class="sm:max-w-[425px]">
<Dialog.Header>
<Dialog.Title>{title}</Dialog.Title>
</Dialog.Header>
<div class="grid gap-4 py-4">
{#if error}
<div class="text-red-500 text-sm mb-2">{error}</div>
{/if}
<div class="grid grid-cols-4 items-center gap-4">
<Label for="code" class="text-right">Código</Label>
<Input id="code" bind:value={formData.code} class="col-span-3" disabled={loading} />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="description" class="text-right">Descripción</Label>
<Input id="description" bind:value={formData.description} class="col-span-3" disabled={loading} />
</div>
</div>
<Dialog.Footer>
<Button type="submit" onclick={handleSubmit} disabled={loading}>
{loading ? 'Guardando...' : 'Guardar'}
</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>

View File

@@ -0,0 +1,79 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
import { deleteUnitOfMeasureACE, type UnitOfMeasureACE } from "$lib/api/dashboard/a76/units-of-measure";
import { EllipsisVertical, Pencil, LoaderCircle, Trash2 } from 'lucide-svelte';
import CreateEditDialog from "./create-edit-dialog.svelte";
let {
item,
onSuccess
}: {
item: UnitOfMeasureACE;
onSuccess?: () => void;
} = $props();
let loading = $state(false);
let error = $state<string | null>(null);
let dialogOpen = $state(false);
async function handleDelete() {
if (!confirm(`¿Estás seguro de eliminar la unidad "${item.code}"?`)) {
return;
}
loading = true;
error = null;
try {
const response = await deleteUnitOfMeasureACE(item.id);
if (response.error) {
error = response.error;
alert(`Error al eliminar: ${response.error}`);
return;
}
if (onSuccess) onSuccess();
} catch (e) {
error = 'Error de conexión';
console.error(e);
alert('Error de conexión al eliminar');
} finally {
loading = false;
}
}
</script>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
{#snippet child({ props })}
<Button {...props} variant="ghost" size="icon" class="h-8 w-8 p-0">
<span class="sr-only">Abrir menú</span>
<EllipsisVertical class="h-4 w-4" />
</Button>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Item onclick={() => dialogOpen = true}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleDelete} class="text-red-600">
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<Trash2 class="mr-2 h-4 w-4" />
{/if}
Eliminar
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
<CreateEditDialog
bind:open={dialogOpen}
mode="edit"
{item}
{onSuccess}
/>

View File

@@ -0,0 +1,106 @@
<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";
import { Button } from "$lib/components/ui/button";
import { goto } from "$app/navigation";
import { page } from "$app/stores";
type DataTableProps<TData, TValue> = {
columns: ColumnDef<TData, TValue>[];
data: TData[];
pageCount: number;
totalItems: number;
};
let {
data,
columns,
pageCount,
totalItems
}: DataTableProps<TData, TValue> = $props();
const table = createSvelteTable({
get data() {
return data;
},
columns,
getCoreRowModel: getCoreRowModel(),
manualPagination: true,
pageCount: pageCount,
});
function handlePageChange(newPage: number) {
const url = new URL($page.url);
url.searchParams.set('page', newPage.toString());
goto(url);
}
</script>
<div class="rounded-md border">
<Table.Root>
<Table.Header>
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
{#each headerGroup.headers as header (header.id)}
<Table.Head>
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{/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"}>
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell>
<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">
No hay resultados.
</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
</div>
<div class="flex items-center justify-end space-x-2 py-4">
<div class="flex-1 text-sm text-muted-foreground">
Total: {totalItems}
</div>
<div class="space-x-2">
<Button
variant="outline"
size="sm"
onclick={() => handlePageChange(Number($page.url.searchParams.get('page') || 1) - 1)}
disabled={Number($page.url.searchParams.get('page') || 1) <= 1}
>
Anterior
</Button>
<Button
variant="outline"
size="sm"
onclick={() => handlePageChange(Number($page.url.searchParams.get('page') || 1) + 1)}
disabled={Number($page.url.searchParams.get('page') || 1) >= pageCount}
>
Siguiente
</Button>
</div>
</div>

View File

@@ -0,0 +1,27 @@
import type { UnitOfMeasureAmerican } from '$lib/api/dashboard/a76/units-of-measure';
import type { ColumnDef } from '@tanstack/table-core';
import { renderComponent } from '$lib/components/ui/data-table';
import DataTableActions from './data-table-actions.svelte';
export function createColumns(onSuccess?: () => void): ColumnDef<UnitOfMeasureAmerican>[] {
return [
{
accessorKey: 'code',
header: 'Código',
},
{
accessorKey: 'description',
header: 'Descripción',
cell: ({ row }) => row.original.description || '-'
},
{
id: 'actions',
cell: ({ row }) => {
return renderComponent(DataTableActions, {
item: row.original,
onSuccess
});
}
}
];
}

View File

@@ -0,0 +1,110 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import * as Dialog from "$lib/components/ui/dialog";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import { createUnitOfMeasureAmerican, updateUnitOfMeasureAmerican, type UnitOfMeasureAmerican } from "$lib/api/dashboard/a76/units-of-measure";
let {
open = $bindable(false),
mode = 'create',
item = null,
onSuccess
}: {
open: boolean;
mode?: 'create' | 'edit';
item?: UnitOfMeasureAmerican | null;
onSuccess?: () => void;
} = $props();
const isEdit = $derived(mode === 'edit');
const title = $derived(isEdit ? "Editar Unidad Americana" : "Nueva Unidad Americana");
let formData = $state({
code: '',
description: ''
});
let loading = $state(false);
let error = $state<string | null>(null);
$effect(() => {
if (open) {
if (isEdit && item) {
formData = {
code: item.code,
description: item.description || ''
};
} else {
formData = {
code: '',
description: ''
};
}
error = null;
}
});
async function handleSubmit() {
loading = true;
error = null;
try {
let response;
if (isEdit && item) {
response = await updateUnitOfMeasureAmerican(item.id, {
code: formData.code,
description: formData.description || null
});
} else {
response = await createUnitOfMeasureAmerican({
code: formData.code,
description: formData.description || null
});
}
if (response.error) {
error = response.error;
return;
}
open = false;
if (onSuccess) onSuccess();
} catch (e) {
error = 'Error de conexión';
console.error(e);
} finally {
loading = false;
}
}
</script>
<Dialog.Root bind:open>
<Dialog.Content class="sm:max-w-[425px]">
<Dialog.Header>
<Dialog.Title>{title}</Dialog.Title>
</Dialog.Header>
<div class="grid gap-4 py-4">
{#if error}
<div class="text-red-500 text-sm mb-2">{error}</div>
{/if}
<div class="grid grid-cols-4 items-center gap-4">
<Label for="code" class="text-right">Código</Label>
<Input id="code" bind:value={formData.code} class="col-span-3" disabled={loading} />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="description" class="text-right">Descripción</Label>
<Input id="description" bind:value={formData.description} class="col-span-3" disabled={loading} />
</div>
</div>
<Dialog.Footer>
<Button type="submit" onclick={handleSubmit} disabled={loading}>
{loading ? 'Guardando...' : 'Guardar'}
</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>

View File

@@ -0,0 +1,79 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
import { deleteUnitOfMeasureAmerican, type UnitOfMeasureAmerican } from "$lib/api/dashboard/a76/units-of-measure";
import { EllipsisVertical, Pencil, LoaderCircle, Trash2 } from 'lucide-svelte';
import CreateEditDialog from "./create-edit-dialog.svelte";
let {
item,
onSuccess
}: {
item: UnitOfMeasureAmerican;
onSuccess?: () => void;
} = $props();
let loading = $state(false);
let error = $state<string | null>(null);
let dialogOpen = $state(false);
async function handleDelete() {
if (!confirm(`¿Estás seguro de eliminar la unidad "${item.code}"?`)) {
return;
}
loading = true;
error = null;
try {
const response = await deleteUnitOfMeasureAmerican(item.id);
if (response.error) {
error = response.error;
alert(`Error al eliminar: ${response.error}`);
return;
}
if (onSuccess) onSuccess();
} catch (e) {
error = 'Error de conexión';
console.error(e);
alert('Error de conexión al eliminar');
} finally {
loading = false;
}
}
</script>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
{#snippet child({ props })}
<Button {...props} variant="ghost" size="icon" class="h-8 w-8 p-0">
<span class="sr-only">Abrir menú</span>
<EllipsisVertical class="h-4 w-4" />
</Button>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Item onclick={() => dialogOpen = true}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleDelete} class="text-red-600">
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<Trash2 class="mr-2 h-4 w-4" />
{/if}
Eliminar
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
<CreateEditDialog
bind:open={dialogOpen}
mode="edit"
{item}
{onSuccess}
/>

View File

@@ -0,0 +1,27 @@
import type { UnitOfMeasureOMA } from '$lib/api/dashboard/a76/units-of-measure';
import type { ColumnDef } from '@tanstack/table-core';
import { renderComponent } from '$lib/components/ui/data-table';
import DataTableActions from './data-table-actions.svelte';
export function createColumns(onSuccess?: () => void): ColumnDef<UnitOfMeasureOMA>[] {
return [
{
accessorKey: 'code',
header: 'Código',
},
{
accessorKey: 'description',
header: 'Descripción',
cell: ({ row }) => row.original.description || '-'
},
{
id: 'actions',
cell: ({ row }) => {
return renderComponent(DataTableActions, {
item: row.original,
onSuccess
});
}
}
];
}

View File

@@ -0,0 +1,110 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import * as Dialog from "$lib/components/ui/dialog";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import { createUnitOfMeasureOMA, updateUnitOfMeasureOMA, type UnitOfMeasureOMA } from "$lib/api/dashboard/a76/units-of-measure";
let {
open = $bindable(false),
mode = 'create',
item = null,
onSuccess
}: {
open: boolean;
mode?: 'create' | 'edit';
item?: UnitOfMeasureOMA | null;
onSuccess?: () => void;
} = $props();
const isEdit = $derived(mode === 'edit');
const title = $derived(isEdit ? "Editar Unidad OMA" : "Nueva Unidad OMA");
let formData = $state({
code: '',
description: ''
});
let loading = $state(false);
let error = $state<string | null>(null);
$effect(() => {
if (open) {
if (isEdit && item) {
formData = {
code: item.code,
description: item.description || ''
};
} else {
formData = {
code: '',
description: ''
};
}
error = null;
}
});
async function handleSubmit() {
loading = true;
error = null;
try {
let response;
if (isEdit && item) {
response = await updateUnitOfMeasureOMA(item.id, {
code: formData.code,
description: formData.description || null
});
} else {
response = await createUnitOfMeasureOMA({
code: formData.code,
description: formData.description || null
});
}
if (response.error) {
error = response.error;
return;
}
open = false;
if (onSuccess) onSuccess();
} catch (e) {
error = 'Error de conexión';
console.error(e);
} finally {
loading = false;
}
}
</script>
<Dialog.Root bind:open>
<Dialog.Content class="sm:max-w-[425px]">
<Dialog.Header>
<Dialog.Title>{title}</Dialog.Title>
</Dialog.Header>
<div class="grid gap-4 py-4">
{#if error}
<div class="text-red-500 text-sm mb-2">{error}</div>
{/if}
<div class="grid grid-cols-4 items-center gap-4">
<Label for="code" class="text-right">Código</Label>
<Input id="code" bind:value={formData.code} class="col-span-3" disabled={loading} />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label for="description" class="text-right">Descripción</Label>
<Input id="description" bind:value={formData.description} class="col-span-3" disabled={loading} />
</div>
</div>
<Dialog.Footer>
<Button type="submit" onclick={handleSubmit} disabled={loading}>
{loading ? 'Guardando...' : 'Guardar'}
</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>

View File

@@ -0,0 +1,79 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
import { deleteUnitOfMeasureOMA, type UnitOfMeasureOMA } from "$lib/api/dashboard/a76/units-of-measure";
import { EllipsisVertical, Pencil, LoaderCircle, Trash2 } from 'lucide-svelte';
import CreateEditDialog from "./create-edit-dialog.svelte";
let {
item,
onSuccess
}: {
item: UnitOfMeasureOMA;
onSuccess?: () => void;
} = $props();
let loading = $state(false);
let error = $state<string | null>(null);
let dialogOpen = $state(false);
async function handleDelete() {
if (!confirm(`¿Estás seguro de eliminar la unidad "${item.code}"?`)) {
return;
}
loading = true;
error = null;
try {
const response = await deleteUnitOfMeasureOMA(item.id);
if (response.error) {
error = response.error;
alert(`Error al eliminar: ${response.error}`);
return;
}
if (onSuccess) onSuccess();
} catch (e) {
error = 'Error de conexión';
console.error(e);
alert('Error de conexión al eliminar');
} finally {
loading = false;
}
}
</script>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
{#snippet child({ props })}
<Button {...props} variant="ghost" size="icon" class="h-8 w-8 p-0">
<span class="sr-only">Abrir menú</span>
<EllipsisVertical class="h-4 w-4" />
</Button>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Item onclick={() => dialogOpen = true}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleDelete} class="text-red-600">
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<Trash2 class="mr-2 h-4 w-4" />
{/if}
Eliminar
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
<CreateEditDialog
bind:open={dialogOpen}
mode="edit"
{item}
{onSuccess}
/>

View File

@@ -155,7 +155,7 @@ export function getSidebarData(): SidebarData {
items: [
{
title: m["sidebar.general_catalogs.company_information"](),
url: "#",
url: "/dashboard/general_catalogs/company_information",
},
{
title: m["sidebar.general_catalogs.packages"](),
@@ -163,15 +163,19 @@ export function getSidebarData(): SidebarData {
},
{
title: m["sidebar.general_catalogs.concepts"](),
url: "#",
url: "/dashboard/general_catalogs/concepts",
},
{
title: "Conceptos de Agente Aduanal",
url: "/dashboard/general_catalogs/customs_broker_concepts",
},
{
title: m["sidebar.general_catalogs.classification"](),
url: "/dashboard/general_catalogs/classes",
url: "/dashboard/general_catalogs/classification_concepts",
},
{
title: m["sidebar.general_catalogs.identifiers"](),
url: "#",
url: "/dashboard/general_catalogs/identifiers",
},
{
title: m["sidebar.general_catalogs.incoterms"](),
@@ -179,11 +183,11 @@ export function getSidebarData(): SidebarData {
},
{
title: m["sidebar.general_catalogs.inpc"](),
url: "#",
url: "/dashboard/general_catalogs/inpc",
},
{
title: m["sidebar.general_catalogs.fixed_legends"](),
url: "#",
url: "/dashboard/general_catalogs/legends",
},
{
title: m["sidebar.general_catalogs.seals"](),
@@ -199,11 +203,11 @@ export function getSidebarData(): SidebarData {
},
{
title: m["sidebar.general_catalogs.ports"](),
url: "#",
url: "/dashboard/general_catalogs/ports",
},
{
title: m["sidebar.general_catalogs.unit_measures"](),
url: "#",
url: "/dashboard/general_catalogs/units_of_measures",
},
{
title: m["sidebar.general_catalogs.um_customs_mex"](),
@@ -223,11 +227,11 @@ export function getSidebarData(): SidebarData {
},
{
title: m["sidebar.general_catalogs.conversions"](),
url: "#",
url: "/dashboard/general_catalogs/unit_conversions",
},
{
title: m["sidebar.general_catalogs.equivalences"](),
url: "#",
url: "/dashboard/general_catalogs/equivalencies",
},
{
title: m["sidebar.general_catalogs.exchange_rates"](),
@@ -239,7 +243,7 @@ export function getSidebarData(): SidebarData {
},
{
title: m["sidebar.general_catalogs.multi_currency"](),
url: "#",
url: "/dashboard/general_catalogs/multi_currency_types",
},
{
title: m["sidebar.general_catalogs.invoice_types"](),
@@ -247,11 +251,11 @@ export function getSidebarData(): SidebarData {
},
{
title: m["sidebar.general_catalogs.electronic_signatures"](),
url: "#",
url: "/dashboard/general_catalogs/signatures",
},
{
title: m["sidebar.general_catalogs.billing_errors"](),
url: "#",
url: "/dashboard/general_catalogs/error_catalogs",
},
{
title: m["sidebar.general_catalogs.customs_warehouses"](),
@@ -263,7 +267,7 @@ export function getSidebarData(): SidebarData {
},
{
title: m["sidebar.general_catalogs.doda"](),
url: "#",
url: "/dashboard/general_catalogs/doda",
},
{
title: m["sidebar.general_catalogs.packing_list"](),
@@ -271,11 +275,11 @@ export function getSidebarData(): SidebarData {
},
{
title: m["sidebar.general_catalogs.prevalidators"](),
url: "#",
url: "/dashboard/general_catalogs/prevalidators",
},
{
title: m["sidebar.general_catalogs.electronic_notices"](),
url: "#",
url: "/dashboard/general_catalogs/electronic_notices",
},
{
title: m["sidebar.general_catalogs.back_flush"](),

View File

@@ -0,0 +1,20 @@
import { getClassificationConcepts } from '$lib/api/dashboard/a76/classification-concepts';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ url }) => {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const filters: Record<string, any> = {};
const classification = url.searchParams.get('classification');
const description = url.searchParams.get('description');
if (classification) filters.classification = classification;
if (description) filters.description = description;
const response = await getClassificationConcepts(page, pageSize, filters);
return {
classifications: response.data
};
};

View File

@@ -0,0 +1,84 @@
<script lang="ts">
import { page } from '$app/stores';
import SimpleDataTable from '$lib/components/dashboard/general_catalogs/simple-data-table.svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus } from 'lucide-svelte';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
let { data } = $props();
let dialogOpen = $state(false);
// Filtros
let searchClassification = $state($page.url.searchParams.get('classification') || '');
let searchDesc = $state($page.url.searchParams.get('description') || '');
let timeout: ReturnType<typeof setTimeout>;
const columns = [
{ key: 'classification', label: 'Clasificación' },
{ key: 'description', label: 'Descripción' },
];
function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(() => {
const url = new URL($page.url);
if (searchClassification) url.searchParams.set('classification', searchClassification);
else url.searchParams.delete('classification');
if (searchDesc) url.searchParams.set('description', searchDesc);
else url.searchParams.delete('description');
url.searchParams.set('page', '1');
goto(url, { keepFocus: true, noScroll: true });
}, 500);
}
function handleSuccess() {
const url = new URL($page.url);
goto(url, { invalidateAll: true });
}
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold tracking-tight">Clasificaciones de Conceptos</h1>
<p class="text-muted-foreground">
Gestión del catálogo de clasificaciones de conceptos
</p>
</div>
<Button onclick={() => dialogOpen = true}>
<Plus class="mr-2 h-4 w-4" />
Nueva Clasificación
</Button>
</div>
<div class="flex gap-4 items-end">
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por clasificación..."
bind:value={searchClassification}
oninput={handleSearch}
/>
</div>
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por descripción..."
bind:value={searchDesc}
oninput={handleSearch}
/>
</div>
</div>
<div class="rounded-md border">
<SimpleDataTable
data={data.classifications?.items || []}
columns={columns}
pageCount={data.classifications?.pages || 0}
totalItems={data.classifications?.total || 0}
/>
</div>
</div>

View File

@@ -0,0 +1,20 @@
import { getCompanies } from '$lib/api/dashboard/a76/company';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ url }) => {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const filters: Record<string, any> = {};
const name = url.searchParams.get('name');
const rfc = url.searchParams.get('rfc');
if (name) filters.name = name;
if (rfc) filters.rfc = rfc;
const response = await getCompanies(page, pageSize, filters);
return {
companies: response.data
};
};

View File

@@ -0,0 +1,87 @@
<script lang="ts">
import { page } from '$app/stores';
import DataTable from '$lib/components/dashboard/units_of_measure/ace/data-table.svelte';
import { createColumns } from '$lib/components/dashboard/company/columns';
import CreateEditDialog from '$lib/components/dashboard/company/create-edit-dialog.svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus } from 'lucide-svelte';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
import * as m from "$lib/paraglide/messages.js";
let { data } = $props();
let dialogOpen = $state(false);
// Filtros
let searchName = $state($page.url.searchParams.get('name') || '');
let searchRfc = $state($page.url.searchParams.get('rfc') || '');
let timeout: ReturnType<typeof setTimeout>;
function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(() => {
const url = new URL($page.url);
if (searchName) url.searchParams.set('name', searchName);
else url.searchParams.delete('name');
if (searchRfc) url.searchParams.set('rfc', searchRfc);
else url.searchParams.delete('rfc');
url.searchParams.set('page', '1');
goto(url, { keepFocus: true, noScroll: true });
}, 500);
}
function handleSuccess() {
const url = new URL($page.url);
goto(url, { invalidateAll: true });
}
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold tracking-tight">company_information</h1> <!-- {m["sidebar.general_catalogs.company_information"]()} -->
<p class="text-muted-foreground">
Gestión de información de empresas
</p>
</div>
<Button onclick={() => dialogOpen = true}>
<Plus class="mr-2 h-4 w-4" />
Nueva Empresa
</Button>
</div>
<div class="flex gap-4 items-end">
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por nombre..."
bind:value={searchName}
oninput={handleSearch}
/>
</div>
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por RFC..."
bind:value={searchRfc}
oninput={handleSearch}
/>
</div>
</div>
<div class="rounded-md border">
<DataTable
data={data.companies?.items || []}
columns={createColumns(handleSuccess)}
pageCount={data.companies?.pages || 0}
totalItems={data.companies?.total || 0}
/>
</div>
<CreateEditDialog
bind:open={dialogOpen}
onSuccess={handleSuccess}
/>
</div>

View File

@@ -0,0 +1,20 @@
import { getConcepts } from '$lib/api/dashboard/a76/concepts';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ url }) => {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const filters: Record<string, any> = {};
const code = url.searchParams.get('code');
const description = url.searchParams.get('description');
if (code) filters.code = code;
if (description) filters.description = description;
const response = await getConcepts(page, pageSize, filters);
return {
concepts: response.data
};
};

View File

@@ -0,0 +1,85 @@
<script lang="ts">
import { page } from '$app/stores';
import SimpleDataTable from '$lib/components/dashboard/general_catalogs/simple-data-table.svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus } from 'lucide-svelte';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
let { data } = $props();
let dialogOpen = $state(false);
// Filtros
let searchCode = $state($page.url.searchParams.get('code') || '');
let searchDesc = $state($page.url.searchParams.get('description') || '');
let timeout: ReturnType<typeof setTimeout>;
const columns = [
{ key: 'code', label: 'Código' },
{ key: 'description', label: 'Descripción' },
{ key: 'type', label: 'Tipo' },
];
function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(() => {
const url = new URL($page.url);
if (searchCode) url.searchParams.set('code', searchCode);
else url.searchParams.delete('code');
if (searchDesc) url.searchParams.set('description', searchDesc);
else url.searchParams.delete('description');
url.searchParams.set('page', '1');
goto(url, { keepFocus: true, noScroll: true });
}, 500);
}
function handleSuccess() {
const url = new URL($page.url);
goto(url, { invalidateAll: true });
}
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold tracking-tight">Conceptos</h1>
<p class="text-muted-foreground">
Gestión del catálogo de conceptos
</p>
</div>
<Button onclick={() => dialogOpen = true}>
<Plus class="mr-2 h-4 w-4" />
Nuevo Concepto
</Button>
</div>
<div class="flex gap-4 items-end">
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por código..."
bind:value={searchCode}
oninput={handleSearch}
/>
</div>
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por descripción..."
bind:value={searchDesc}
oninput={handleSearch}
/>
</div>
</div>
<div class="rounded-md border">
<SimpleDataTable
data={data.concepts?.items || []}
columns={columns}
pageCount={data.concepts?.pages || 0}
totalItems={data.concepts?.total || 0}
/>
</div>
</div>

View File

@@ -0,0 +1,20 @@
import { getCustomsBrokerConcepts } from '$lib/api/dashboard/a76/customs-broker-concepts';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ url }) => {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const filters: Record<string, any> = {};
const code = url.searchParams.get('code');
const description = url.searchParams.get('description');
if (code) filters.code = code;
if (description) filters.description = description;
const response = await getCustomsBrokerConcepts(page, pageSize, filters);
return {
concepts: response.data
};
};

View File

@@ -0,0 +1,85 @@
<script lang="ts">
import { page } from '$app/stores';
import SimpleDataTable from '$lib/components/dashboard/general_catalogs/simple-data-table.svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus } from 'lucide-svelte';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
let { data } = $props();
let dialogOpen = $state(false);
// Filtros
let searchCode = $state($page.url.searchParams.get('code') || '');
let searchDesc = $state($page.url.searchParams.get('description') || '');
let timeout: ReturnType<typeof setTimeout>;
const columns = [
{ key: 'code', label: 'Código' },
{ key: 'description', label: 'Descripción' },
{ key: 'type', label: 'Tipo' },
];
function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(() => {
const url = new URL($page.url);
if (searchCode) url.searchParams.set('code', searchCode);
else url.searchParams.delete('code');
if (searchDesc) url.searchParams.set('description', searchDesc);
else url.searchParams.delete('description');
url.searchParams.set('page', '1');
goto(url, { keepFocus: true, noScroll: true });
}, 500);
}
function handleSuccess() {
const url = new URL($page.url);
goto(url, { invalidateAll: true });
}
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold tracking-tight">Conceptos de Agente Aduanal</h1>
<p class="text-muted-foreground">
Gestión del catálogo de conceptos de agente aduanal
</p>
</div>
<Button onclick={() => dialogOpen = true}>
<Plus class="mr-2 h-4 w-4" />
Nuevo Concepto
</Button>
</div>
<div class="flex gap-4 items-end">
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por código..."
bind:value={searchCode}
oninput={handleSearch}
/>
</div>
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por descripción..."
bind:value={searchDesc}
oninput={handleSearch}
/>
</div>
</div>
<div class="rounded-md border">
<SimpleDataTable
data={data.concepts?.items || []}
columns={columns}
pageCount={data.concepts?.pages || 0}
totalItems={data.concepts?.total || 0}
/>
</div>
</div>

View File

@@ -0,0 +1,20 @@
import { getDODAs } from '$lib/api/dashboard/a76/doda';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ url }) => {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const filters: Record<string, any> = {};
const code = url.searchParams.get('code');
const description = url.searchParams.get('description');
if (code) filters.code = code;
if (description) filters.description = description;
const response = await getDODAs(page, pageSize, filters);
return {
dodas: response.data
};
};

View File

@@ -0,0 +1,84 @@
<script lang="ts">
import { page } from '$app/stores';
import SimpleDataTable from '$lib/components/dashboard/general_catalogs/simple-data-table.svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus } from 'lucide-svelte';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
let { data } = $props();
let dialogOpen = $state(false);
// Filtros
let searchCode = $state($page.url.searchParams.get('code') || '');
let searchDesc = $state($page.url.searchParams.get('description') || '');
let timeout: ReturnType<typeof setTimeout>;
const columns = [
{ key: 'code', label: 'Código' },
{ key: 'description', label: 'Descripción' },
];
function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(() => {
const url = new URL($page.url);
if (searchCode) url.searchParams.set('code', searchCode);
else url.searchParams.delete('code');
if (searchDesc) url.searchParams.set('description', searchDesc);
else url.searchParams.delete('description');
url.searchParams.set('page', '1');
goto(url, { keepFocus: true, noScroll: true });
}, 500);
}
function handleSuccess() {
const url = new URL($page.url);
goto(url, { invalidateAll: true });
}
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold tracking-tight">DODA</h1>
<p class="text-muted-foreground">
Gestión del catálogo de DODA
</p>
</div>
<Button onclick={() => dialogOpen = true}>
<Plus class="mr-2 h-4 w-4" />
Nuevo DODA
</Button>
</div>
<div class="flex gap-4 items-end">
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por código..."
bind:value={searchCode}
oninput={handleSearch}
/>
</div>
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por descripción..."
bind:value={searchDesc}
oninput={handleSearch}
/>
</div>
</div>
<div class="rounded-md border">
<SimpleDataTable
data={data.dodas?.items || []}
columns={columns}
pageCount={data.dodas?.pages || 0}
totalItems={data.dodas?.total || 0}
/>
</div>
</div>

View File

@@ -0,0 +1,20 @@
import { getElectronicNotices } from '$lib/api/dashboard/a76/electronic-notices';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ url }) => {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const filters: Record<string, any> = {};
const code = url.searchParams.get('code');
const description = url.searchParams.get('description');
if (code) filters.code = code;
if (description) filters.description = description;
const response = await getElectronicNotices(page, pageSize, filters);
return {
notices: response.data
};
};

View File

@@ -0,0 +1,84 @@
<script lang="ts">
import { page } from '$app/stores';
import SimpleDataTable from '$lib/components/dashboard/general_catalogs/simple-data-table.svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus } from 'lucide-svelte';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
let { data } = $props();
let dialogOpen = $state(false);
// Filtros
let searchCode = $state($page.url.searchParams.get('code') || '');
let searchDesc = $state($page.url.searchParams.get('description') || '');
let timeout: ReturnType<typeof setTimeout>;
const columns = [
{ key: 'code', label: 'Código' },
{ key: 'description', label: 'Descripción' },
];
function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(() => {
const url = new URL($page.url);
if (searchCode) url.searchParams.set('code', searchCode);
else url.searchParams.delete('code');
if (searchDesc) url.searchParams.set('description', searchDesc);
else url.searchParams.delete('description');
url.searchParams.set('page', '1');
goto(url, { keepFocus: true, noScroll: true });
}, 500);
}
function handleSuccess() {
const url = new URL($page.url);
goto(url, { invalidateAll: true });
}
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold tracking-tight">Avisos Electrónicos</h1>
<p class="text-muted-foreground">
Gestión del catálogo de avisos electrónicos
</p>
</div>
<Button onclick={() => dialogOpen = true}>
<Plus class="mr-2 h-4 w-4" />
Nuevo Aviso
</Button>
</div>
<div class="flex gap-4 items-end">
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por código..."
bind:value={searchCode}
oninput={handleSearch}
/>
</div>
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por descripción..."
bind:value={searchDesc}
oninput={handleSearch}
/>
</div>
</div>
<div class="rounded-md border">
<SimpleDataTable
data={data.notices?.items || []}
columns={columns}
pageCount={data.notices?.pages || 0}
totalItems={data.notices?.total || 0}
/>
</div>
</div>

View File

@@ -0,0 +1,20 @@
import { getEquivalencies } from '$lib/api/dashboard/a76/equivalencies';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ url }) => {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const filters: Record<string, any> = {};
const fraccion_mex = url.searchParams.get('fraccion_mex');
const fraccion_us = url.searchParams.get('fraccion_us');
if (fraccion_mex) filters.fraccion_mex = fraccion_mex;
if (fraccion_us) filters.fraccion_us = fraccion_us;
const response = await getEquivalencies(page, pageSize, filters);
return {
equivalencies: response.data
};
};

View File

@@ -0,0 +1,85 @@
<script lang="ts">
import { page } from '$app/stores';
import SimpleDataTable from '$lib/components/dashboard/general_catalogs/simple-data-table.svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus } from 'lucide-svelte';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
let { data } = $props();
let dialogOpen = $state(false);
// Filtros
let searchMex = $state($page.url.searchParams.get('fraccion_mex') || '');
let searchUS = $state($page.url.searchParams.get('fraccion_us') || '');
let timeout: ReturnType<typeof setTimeout>;
const columns = [
{ key: 'fraccion_mex', label: 'Fracción MEX' },
{ key: 'fraccion_us', label: 'Fracción US' },
{ key: 'description', label: 'Descripción' },
];
function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(() => {
const url = new URL($page.url);
if (searchMex) url.searchParams.set('fraccion_mex', searchMex);
else url.searchParams.delete('fraccion_mex');
if (searchUS) url.searchParams.set('fraccion_us', searchUS);
else url.searchParams.delete('fraccion_us');
url.searchParams.set('page', '1');
goto(url, { keepFocus: true, noScroll: true });
}, 500);
}
function handleSuccess() {
const url = new URL($page.url);
goto(url, { invalidateAll: true });
}
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold tracking-tight">Equivalencias Arancelarias</h1>
<p class="text-muted-foreground">
Gestión del catálogo de equivalencias entre fracciones arancelarias
</p>
</div>
<Button onclick={() => dialogOpen = true}>
<Plus class="mr-2 h-4 w-4" />
Nueva Equivalencia
</Button>
</div>
<div class="flex gap-4 items-end">
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por fracción MEX..."
bind:value={searchMex}
oninput={handleSearch}
/>
</div>
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por fracción US..."
bind:value={searchUS}
oninput={handleSearch}
/>
</div>
</div>
<div class="rounded-md border">
<SimpleDataTable
data={data.equivalencies?.items || []}
columns={columns}
pageCount={data.equivalencies?.pages || 0}
totalItems={data.equivalencies?.total || 0}
/>
</div>
</div>

View File

@@ -0,0 +1,20 @@
import { getErrorCatalogs } from '$lib/api/dashboard/a76/error-catalogs';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ url }) => {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const filters: Record<string, any> = {};
const code = url.searchParams.get('code');
const description = url.searchParams.get('description');
if (code) filters.code = code;
if (description) filters.description = description;
const response = await getErrorCatalogs(page, pageSize, filters);
return {
errors: response.data
};
};

View File

@@ -0,0 +1,84 @@
<script lang="ts">
import { page } from '$app/stores';
import SimpleDataTable from '$lib/components/dashboard/general_catalogs/simple-data-table.svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus } from 'lucide-svelte';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
let { data } = $props();
let dialogOpen = $state(false);
// Filtros
let searchCode = $state($page.url.searchParams.get('code') || '');
let searchDesc = $state($page.url.searchParams.get('description') || '');
let timeout: ReturnType<typeof setTimeout>;
const columns = [
{ key: 'code', label: 'Código' },
{ key: 'description', label: 'Descripción' },
];
function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(() => {
const url = new URL($page.url);
if (searchCode) url.searchParams.set('code', searchCode);
else url.searchParams.delete('code');
if (searchDesc) url.searchParams.set('description', searchDesc);
else url.searchParams.delete('description');
url.searchParams.set('page', '1');
goto(url, { keepFocus: true, noScroll: true });
}, 500);
}
function handleSuccess() {
const url = new URL($page.url);
goto(url, { invalidateAll: true });
}
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold tracking-tight">Errores de Facturación</h1>
<p class="text-muted-foreground">
Gestión del catálogo de errores de facturación
</p>
</div>
<Button onclick={() => dialogOpen = true}>
<Plus class="mr-2 h-4 w-4" />
Nuevo Error
</Button>
</div>
<div class="flex gap-4 items-end">
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por código..."
bind:value={searchCode}
oninput={handleSearch}
/>
</div>
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por descripción..."
bind:value={searchDesc}
oninput={handleSearch}
/>
</div>
</div>
<div class="rounded-md border">
<SimpleDataTable
data={data.errors?.items || []}
columns={columns}
pageCount={data.errors?.pages || 0}
totalItems={data.errors?.total || 0}
/>
</div>
</div>

View File

@@ -0,0 +1,56 @@
import { getServerApiUrl, getAuthTokens } from '$lib/server/api';
import type { PageServerLoad } from './$types';
import { redirect } from '@sveltejs/kit';
export const load: PageServerLoad = async ({ cookies, fetch, url }) => {
const { accessToken } = getAuthTokens(cookies);
if (!accessToken) {
throw redirect(302, '/login');
}
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('page_size')) || 50;
const apiUrl = getServerApiUrl();
const endpoint = `${apiUrl}api/v1/a76/identifiers?page=${page}&page_size=${pageSize}`;
try {
const response = await fetch(endpoint, {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
if (!response.ok) {
console.error(`Error fetching identifiers: ${response.status} ${response.statusText}`);
return {
items: [],
total: 0,
page,
pageSize,
pages: 0,
error: `Error: ${response.statusText}`
};
}
const data = await response.json();
return {
items: data.items,
total: data.total,
page: data.page,
pageSize: data.page_size,
pages: data.pages
};
} catch (error) {
console.error('Error fetching identifiers:', error);
return {
items: [],
total: 0,
page,
pageSize,
pages: 0,
error: 'Failed to connect to server'
};
}
};

View File

@@ -0,0 +1,61 @@
<script lang="ts">
import { invalidateAll } from '$app/navigation';
import { createColumns } from '$lib/components/dashboard/identifiers/columns';
import CreateEditDialog from '$lib/components/dashboard/identifiers/create-edit-dialog.svelte';
import DataTable from '$lib/components/dashboard/identifiers/data-table.svelte';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import { Plus, RefreshCw } from 'lucide-svelte';
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
let createDialogOpen = $state(false);
let loading = $state(false);
const columns = createColumns(() => {
refreshData();
});
async function refreshData() {
loading = true;
await invalidateAll();
loading = false;
}
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex items-center justify-between">
<div>
<h2 class="text-2xl font-bold tracking-tight">Identificadores</h2>
<p class="text-muted-foreground">
Catálogo de identificadores
</p>
</div>
<div class="flex items-center gap-2">
<Button variant="outline" size="icon" onclick={refreshData} disabled={loading}>
<RefreshCw class="h-4 w-4 {loading ? 'animate-spin' : ''}" />
</Button>
<Button onclick={() => createDialogOpen = true}>
<Plus class="mr-2 h-4 w-4" />
Nuevo
</Button>
</div>
</div>
<Card.Root>
<Card.Content class="p-0">
<DataTable
data={data.items}
{columns}
pageCount={data.pages}
totalItems={data.total}
/>
</Card.Content>
</Card.Root>
<CreateEditDialog
bind:open={createDialogOpen}
onSuccess={refreshData}
/>
</div>

View File

@@ -0,0 +1,20 @@
import { getINPCs } from '$lib/api/dashboard/a76/inpc';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ url }) => {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const filters: Record<string, any> = {};
const year = url.searchParams.get('year');
const month = url.searchParams.get('month');
if (year) filters.year = year;
if (month) filters.month = month;
const response = await getINPCs(page, pageSize, filters);
return {
inpcs: response.data
};
};

View File

@@ -0,0 +1,85 @@
<script lang="ts">
import { page } from '$app/stores';
import SimpleDataTable from '$lib/components/dashboard/general_catalogs/simple-data-table.svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus } from 'lucide-svelte';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
let { data } = $props();
let dialogOpen = $state(false);
// Filtros
let searchYear = $state($page.url.searchParams.get('year') || '');
let searchMonth = $state($page.url.searchParams.get('month') || '');
let timeout: ReturnType<typeof setTimeout>;
const columns = [
{ key: 'year', label: 'Año' },
{ key: 'month', label: 'Mes' },
{ key: 'value', label: 'Valor' },
];
function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(() => {
const url = new URL($page.url);
if (searchYear) url.searchParams.set('year', searchYear);
else url.searchParams.delete('year');
if (searchMonth) url.searchParams.set('month', searchMonth);
else url.searchParams.delete('month');
url.searchParams.set('page', '1');
goto(url, { keepFocus: true, noScroll: true });
}, 500);
}
function handleSuccess() {
const url = new URL($page.url);
goto(url, { invalidateAll: true });
}
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold tracking-tight">INPC</h1>
<p class="text-muted-foreground">
Gestión del catálogo de Índice Nacional de Precios al Consumidor
</p>
</div>
<Button onclick={() => dialogOpen = true}>
<Plus class="mr-2 h-4 w-4" />
Nuevo INPC
</Button>
</div>
<div class="flex gap-4 items-end">
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por año..."
bind:value={searchYear}
oninput={handleSearch}
/>
</div>
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por mes..."
bind:value={searchMonth}
oninput={handleSearch}
/>
</div>
</div>
<div class="rounded-md border">
<SimpleDataTable
data={data.inpcs?.items || []}
columns={columns}
pageCount={data.inpcs?.pages || 0}
totalItems={data.inpcs?.total || 0}
/>
</div>
</div>

View File

@@ -0,0 +1,20 @@
import { getLegends } from '$lib/api/dashboard/a76/legends';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ url }) => {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const filters: Record<string, any> = {};
const code = url.searchParams.get('code');
const description = url.searchParams.get('description');
if (code) filters.code = code;
if (description) filters.description = description;
const response = await getLegends(page, pageSize, filters);
return {
legends: response.data
};
};

View File

@@ -0,0 +1,84 @@
<script lang="ts">
import { page } from '$app/stores';
import SimpleDataTable from '$lib/components/dashboard/general_catalogs/simple-data-table.svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus } from 'lucide-svelte';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
let { data } = $props();
let dialogOpen = $state(false);
// Filtros
let searchCode = $state($page.url.searchParams.get('code') || '');
let searchDesc = $state($page.url.searchParams.get('description') || '');
let timeout: ReturnType<typeof setTimeout>;
const columns = [
{ key: 'code', label: 'Código' },
{ key: 'description', label: 'Descripción' },
];
function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(() => {
const url = new URL($page.url);
if (searchCode) url.searchParams.set('code', searchCode);
else url.searchParams.delete('code');
if (searchDesc) url.searchParams.set('description', searchDesc);
else url.searchParams.delete('description');
url.searchParams.set('page', '1');
goto(url, { keepFocus: true, noScroll: true });
}, 500);
}
function handleSuccess() {
const url = new URL($page.url);
goto(url, { invalidateAll: true });
}
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold tracking-tight">Leyendas Fijas</h1>
<p class="text-muted-foreground">
Gestión del catálogo de leyendas fijas
</p>
</div>
<Button onclick={() => dialogOpen = true}>
<Plus class="mr-2 h-4 w-4" />
Nueva Leyenda
</Button>
</div>
<div class="flex gap-4 items-end">
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por código..."
bind:value={searchCode}
oninput={handleSearch}
/>
</div>
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por descripción..."
bind:value={searchDesc}
oninput={handleSearch}
/>
</div>
</div>
<div class="rounded-md border">
<SimpleDataTable
data={data.legends?.items || []}
columns={columns}
pageCount={data.legends?.pages || 0}
totalItems={data.legends?.total || 0}
/>
</div>
</div>

View File

@@ -0,0 +1,20 @@
import { getMultiCurrencyTypes } from '$lib/api/dashboard/a76/multi-currency-types';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ url }) => {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const filters: Record<string, any> = {};
const key = url.searchParams.get('key');
const description = url.searchParams.get('description');
if (key) filters.key = key;
if (description) filters.description = description;
const response = await getMultiCurrencyTypes(page, pageSize, filters);
return {
types: response.data
};
};

View File

@@ -0,0 +1,84 @@
<script lang="ts">
import { page } from '$app/stores';
import SimpleDataTable from '$lib/components/dashboard/general_catalogs/simple-data-table.svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus } from 'lucide-svelte';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
let { data } = $props();
let dialogOpen = $state(false);
// Filtros
let searchKey = $state($page.url.searchParams.get('key') || '');
let searchDesc = $state($page.url.searchParams.get('description') || '');
let timeout: ReturnType<typeof setTimeout>;
const columns = [
{ key: 'key', label: 'Clave' },
{ key: 'description', label: 'Descripción' },
];
function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(() => {
const url = new URL($page.url);
if (searchKey) url.searchParams.set('key', searchKey);
else url.searchParams.delete('key');
if (searchDesc) url.searchParams.set('description', searchDesc);
else url.searchParams.delete('description');
url.searchParams.set('page', '1');
goto(url, { keepFocus: true, noScroll: true });
}, 500);
}
function handleSuccess() {
const url = new URL($page.url);
goto(url, { invalidateAll: true });
}
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold tracking-tight">Tipos de Moneda Múltiple</h1>
<p class="text-muted-foreground">
Gestión del catálogo de tipos de moneda múltiple
</p>
</div>
<Button onclick={() => dialogOpen = true}>
<Plus class="mr-2 h-4 w-4" />
Nuevo Tipo
</Button>
</div>
<div class="flex gap-4 items-end">
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por clave..."
bind:value={searchKey}
oninput={handleSearch}
/>
</div>
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por descripción..."
bind:value={searchDesc}
oninput={handleSearch}
/>
</div>
</div>
<div class="rounded-md border">
<SimpleDataTable
data={data.types?.items || []}
columns={columns}
pageCount={data.types?.pages || 0}
totalItems={data.types?.total || 0}
/>
</div>
</div>

View File

@@ -0,0 +1,20 @@
import { getPackages } from '$lib/api/dashboard/a76/packages';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ url }) => {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const filters: Record<string, any> = {};
const key = url.searchParams.get('key');
const description_es = url.searchParams.get('description_es');
if (key) filters.key = key;
if (description_es) filters.description_es = description_es;
const response = await getPackages(page, pageSize, filters);
return {
packages: response.data
};
};

View File

@@ -1,326 +1,88 @@
<script lang="ts">
import { onMount } from 'svelte';
import { getPackages, type Package } from '$lib/api/dashboard/a76/packages';
import { companyStore } from '$lib/stores/company.svelte';
import DataTable from '$lib/components/dashboard/packages/data-table.svelte';
import { createColumns } from '$lib/components/dashboard/packages/columns';
import CreateEditDialog from '$lib/components/dashboard/packages/create-edit-dialog.svelte';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { Plus, Filter, Trash2, RefreshCw } from 'lucide-svelte';
import { browser } from '$app/environment';
import { page } from '$app/stores';
import DataTable from '$lib/components/dashboard/units_of_measure/ace/data-table.svelte';
import { createColumns } from '$lib/components/dashboard/packages/columns';
import CreateEditDialog from '$lib/components/dashboard/packages/create-edit-dialog.svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus } from 'lucide-svelte';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
import * as m from "$lib/paraglide/messages.js";
// Estado para la lista de packages
let allItems = $state<Package[]>([]);
let currentPage = $state(1);
let pageSize = $state(50);
let totalItems = $state(0);
let loading = $state(false);
let hasMore = $derived(allItems.length < totalItems);
let error = $state<string | null>(null);
let { data } = $props();
let dialogOpen = $state(false);
// Filtros
let searchKey = $state($page.url.searchParams.get('key') || '');
let searchDesc = $state($page.url.searchParams.get('description_es') || '');
let timeout: ReturnType<typeof setTimeout>;
// Estado para filtros
let filters = $state({
key: '',
description_es: ''
});
let showFilters = $state(false);
function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(() => {
const url = new URL($page.url);
if (searchKey) url.searchParams.set('key', searchKey);
else url.searchParams.delete('key');
if (searchDesc) url.searchParams.set('description_es', searchDesc);
else url.searchParams.delete('description_es');
url.searchParams.set('page', '1');
goto(url, { keepFocus: true, noScroll: true });
}, 500);
}
// Estado para el dialog de crear
let createDialogOpen = $state(false);
// Sincronizar token de cookies a localStorage al montar el componente
onMount(() => {
if (browser) {
// Función para obtener el valor de una cookie
const getCookie = (name: string): string | null => {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
return null;
};
// Verificar si hay token en las cookies
const cookieToken = getCookie('access_token');
const localToken = localStorage.getItem('access_token');
if (cookieToken && cookieToken !== localToken) {
localStorage.setItem('access_token', cookieToken);
}
// También sincronizar refresh_token si existe
const cookieRefreshToken = getCookie('refresh_token');
const localRefreshToken = localStorage.getItem('refresh_token');
if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) {
localStorage.setItem('refresh_token', cookieRefreshToken);
}
}
// Esperar a que el companyStore esté inicializado antes de cargar datos
const checkAndLoad = () => {
if (companyStore.activeCompany) {
loadInitialData();
} else {
// Si no hay compañía, esperar un poco y reintentar
setTimeout(checkAndLoad, 100);
}
};
checkAndLoad();
// También escuchar cambios de compañía para recargar
const handleCompanyChange = () => {
loadInitialData();
};
window.addEventListener('companyChanged', handleCompanyChange);
return () => {
window.removeEventListener('companyChanged', handleCompanyChange);
};
});
async function loadInitialData() {
const companyId = companyStore.activeCompany?.id;
if (!companyId) {
error = 'No hay compañía seleccionada. Por favor selecciona una compañía en el sidebar.';
loading = false;
return;
}
loading = true;
error = null;
try {
const activeFilters = {
...(filters.key && { key: filters.key }),
...(filters.description_es && { description_es: filters.description_es })
};
const response = await getPackages(companyId, 1, pageSize, activeFilters);
if (response.error) {
console.error('📦 [Packages Page] Error en loadInitialData:', response.error);
if (response.status === 401 || response.status === 403) {
error = 'Sesión expirada. Recargando página...';
setTimeout(() => {
window.location.reload();
}, 2000);
} else {
error = response.error;
}
return;
}
if (response.data) {
allItems = response.data.items;
currentPage = response.data.page;
totalItems = response.data.total;
}
} catch (e) {
error = 'Error cargando los datos';
console.error('📦 [Packages Page] Error loading initial data:', e);
} finally {
loading = false;
}
}
async function loadMore() {
if (loading || !hasMore) return;
const companyId = companyStore.activeCompany?.id;
if (!companyId) return;
loading = true;
error = null;
try {
const activeFilters = {
...(filters.key && { key: filters.key }),
...(filters.description_es && { description_es: filters.description_es })
};
const response = await getPackages(companyId, currentPage + 1, pageSize, activeFilters);
if (response.error) {
console.error('📦 [Packages Page] Error en loadMore:', response.error);
if (response.status === 401 || response.status === 403) {
error = 'Sesión expirada. Recargando página...';
setTimeout(() => {
window.location.reload();
}, 2000);
} else {
error = response.error;
}
return;
}
if (response.data) {
allItems = [...allItems, ...response.data.items];
currentPage = response.data.page;
}
} catch (e) {
error = 'Error cargando más datos';
console.error('📦 [Packages Page] Error loading more:', e);
} finally {
loading = false;
}
}
function handleCreateSuccess() {
createDialogOpen = false;
loadInitialData();
}
function handleItemSuccess() {
loadInitialData();
}
function handleApplyFilters() {
currentPage = 1;
allItems = [];
loadInitialData();
}
function handleClearFilters() {
filters = {
key: '',
description_es: ''
};
currentPage = 1;
allItems = [];
loadInitialData();
}
// Crear columnas con el callback de éxito
const tableColumns = createColumns(handleItemSuccess);
function handleSuccess() {
// Recargar datos
const url = new URL($page.url);
goto(url, { invalidateAll: true });
}
</script>
<svelte:head>
<title>Bultos / Embalajes - Catálogos Generales</title>
</svelte:head>
<div class="flex flex-col gap-4 p-4">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold tracking-tight">packages</h1> <!-- {m["sidebar.general_catalogs.packages"]()} -->
<p class="text-muted-foreground">
Gestión del catálogo de bultos y embalajes
</p>
</div>
<Button onclick={() => dialogOpen = true}>
<Plus class="mr-2 h-4 w-4" />
Nuevo Bulto
</Button>
</div>
<div class="flex flex-col gap-4 p-4 md:p-6">
<!-- Header -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold tracking-tight">Bultos / Embalajes</h1>
<p class="text-muted-foreground">
Gestiona los tipos de bultos y embalajes utilizados en tus operaciones
</p>
</div>
<Button onclick={() => createDialogOpen = true}>
<Plus size={16} class="mr-2" />
Nuevo Bulto
</Button>
</div>
<div class="flex gap-4 items-end">
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por clave..."
bind:value={searchKey}
oninput={handleSearch}
/>
</div>
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por descripción..."
bind:value={searchDesc}
oninput={handleSearch}
/>
</div>
</div>
<!-- Filters Card -->
<Card.Root>
<Card.Header>
<div class="flex items-center justify-between">
<Card.Title class="flex items-center gap-2">
<Filter size={20} />
Filtros
</Card.Title>
<Button
variant="ghost"
size="sm"
onclick={() => showFilters = !showFilters}
>
{showFilters ? 'Ocultar' : 'Mostrar'}
</Button>
</div>
</Card.Header>
{#if showFilters}
<Card.Content>
<form onsubmit={(e) => { e.preventDefault(); handleApplyFilters(); }} class="space-y-4">
<div class="grid gap-4 md:grid-cols-2">
<div class="grid gap-2">
<Label for="filter-key">Clave</Label>
<Input
id="filter-key"
bind:value={filters.key}
placeholder="Ej: CAJA"
/>
</div>
<div class="rounded-md border">
<DataTable
data={data.packages?.items || []}
columns={createColumns(handleSuccess)}
pageCount={data.packages?.pages || 0}
totalItems={data.packages?.total || 0}
/>
</div>
<div class="grid gap-2">
<Label for="filter-description">Descripción</Label>
<Input
id="filter-description"
bind:value={filters.description_es}
placeholder="Buscar en descripción..."
/>
</div>
</div>
<div class="flex gap-2">
<Button type="submit">
<Filter size={16} class="mr-2" />
Aplicar Filtros
</Button>
<Button type="button" variant="outline" onclick={handleClearFilters}>
<Trash2 size={16} class="mr-2" />
Limpiar
</Button>
<Button type="button" variant="outline" onclick={loadInitialData}>
<RefreshCw size={16} class="mr-2" />
Refrescar
</Button>
</div>
</form>
</Card.Content>
{/if}
</Card.Root>
<!-- Error Alert -->
{#if error}
<Card.Root class="border-destructive">
<Card.Content class="pt-6">
<div class="flex items-center gap-2 text-destructive">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
</svg>
<p class="font-medium">{error}</p>
</div>
</Card.Content>
</Card.Root>
{/if}
<!-- Data Table Card -->
<Card.Root>
<Card.Header>
<div class="flex items-center justify-between">
<div>
<Card.Title>Lista de Bultos</Card.Title>
<Card.Description>
Total: {totalItems} bulto{totalItems !== 1 ? 's' : ''} |
Mostrando: {allItems.length}
</Card.Description>
</div>
</div>
</Card.Header>
<Card.Content>
<DataTable
data={allItems}
columns={tableColumns}
{loading}
{hasMore}
{loadMore}
/>
</Card.Content>
</Card.Root>
<CreateEditDialog
bind:open={dialogOpen}
onSuccess={handleSuccess}
/>
</div>
<!-- Create Dialog -->
<CreateEditDialog
bind:open={createDialogOpen}
onSuccess={handleCreateSuccess}
/>

View File

@@ -0,0 +1,56 @@
import { getServerApiUrl, getAuthTokens } from '$lib/server/api';
import type { PageServerLoad } from './$types';
import { redirect } from '@sveltejs/kit';
export const load: PageServerLoad = async ({ cookies, fetch, url }) => {
const { accessToken } = getAuthTokens(cookies);
if (!accessToken) {
throw redirect(302, '/login');
}
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('page_size')) || 50;
const apiUrl = getServerApiUrl();
const endpoint = `${apiUrl}api/v1/a76/ports?page=${page}&page_size=${pageSize}`;
try {
const response = await fetch(endpoint, {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
if (!response.ok) {
console.error(`Error fetching ports: ${response.status} ${response.statusText}`);
return {
items: [],
total: 0,
page,
pageSize,
pages: 0,
error: `Error: ${response.statusText}`
};
}
const data = await response.json();
return {
items: data.items,
total: data.total,
page: data.page,
pageSize: data.page_size,
pages: data.pages
};
} catch (error) {
console.error('Error fetching ports:', error);
return {
items: [],
total: 0,
page,
pageSize,
pages: 0,
error: 'Failed to connect to server'
};
}
};

View File

@@ -0,0 +1,61 @@
<script lang="ts">
import { invalidateAll } from '$app/navigation';
import { createColumns } from '$lib/components/dashboard/ports/columns';
import CreateEditDialog from '$lib/components/dashboard/ports/create-edit-dialog.svelte';
import DataTable from '$lib/components/dashboard/units_of_measure/ace/data-table.svelte'; // Reusing generic data table
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import { Plus, RefreshCw } from 'lucide-svelte';
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
let createDialogOpen = $state(false);
let loading = $state(false);
const columns = createColumns(() => {
refreshData();
});
async function refreshData() {
loading = true;
await invalidateAll();
loading = false;
}
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex items-center justify-between">
<div>
<h2 class="text-2xl font-bold tracking-tight">Puertos</h2>
<p class="text-muted-foreground">
Catálogo de puertos
</p>
</div>
<div class="flex items-center gap-2">
<Button variant="outline" size="icon" onclick={refreshData} disabled={loading}>
<RefreshCw class="h-4 w-4 {loading ? 'animate-spin' : ''}" />
</Button>
<Button onclick={() => createDialogOpen = true}>
<Plus class="mr-2 h-4 w-4" />
Nuevo
</Button>
</div>
</div>
<Card.Root>
<Card.Content class="p-0">
<DataTable
data={data.items}
{columns}
pageCount={data.pages}
totalItems={data.total}
/>
</Card.Content>
</Card.Root>
<CreateEditDialog
bind:open={createDialogOpen}
onSuccess={refreshData}
/>
</div>

View File

@@ -0,0 +1,20 @@
import { getPrevalidators } from '$lib/api/dashboard/a76/prevalidators';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ url }) => {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const filters: Record<string, any> = {};
const code = url.searchParams.get('code');
const description = url.searchParams.get('description');
if (code) filters.code = code;
if (description) filters.description = description;
const response = await getPrevalidators(page, pageSize, filters);
return {
prevalidators: response.data
};
};

View File

@@ -0,0 +1,84 @@
<script lang="ts">
import { page } from '$app/stores';
import SimpleDataTable from '$lib/components/dashboard/general_catalogs/simple-data-table.svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus } from 'lucide-svelte';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
let { data } = $props();
let dialogOpen = $state(false);
// Filtros
let searchCode = $state($page.url.searchParams.get('code') || '');
let searchDesc = $state($page.url.searchParams.get('description') || '');
let timeout: ReturnType<typeof setTimeout>;
const columns = [
{ key: 'code', label: 'Código' },
{ key: 'description', label: 'Descripción' },
];
function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(() => {
const url = new URL($page.url);
if (searchCode) url.searchParams.set('code', searchCode);
else url.searchParams.delete('code');
if (searchDesc) url.searchParams.set('description', searchDesc);
else url.searchParams.delete('description');
url.searchParams.set('page', '1');
goto(url, { keepFocus: true, noScroll: true });
}, 500);
}
function handleSuccess() {
const url = new URL($page.url);
goto(url, { invalidateAll: true });
}
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold tracking-tight">Prevalidadores</h1>
<p class="text-muted-foreground">
Gestión del catálogo de prevalidadores
</p>
</div>
<Button onclick={() => dialogOpen = true}>
<Plus class="mr-2 h-4 w-4" />
Nuevo Prevalidador
</Button>
</div>
<div class="flex gap-4 items-end">
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por código..."
bind:value={searchCode}
oninput={handleSearch}
/>
</div>
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por descripción..."
bind:value={searchDesc}
oninput={handleSearch}
/>
</div>
</div>
<div class="rounded-md border">
<SimpleDataTable
data={data.prevalidators?.items || []}
columns={columns}
pageCount={data.prevalidators?.pages || 0}
totalItems={data.prevalidators?.total || 0}
/>
</div>
</div>

View File

@@ -0,0 +1,20 @@
import { getSignatures } from '$lib/api/dashboard/a76/signatures';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ url }) => {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const filters: Record<string, any> = {};
const name = url.searchParams.get('name');
const position = url.searchParams.get('position');
if (name) filters.name = name;
if (position) filters.position = position;
const response = await getSignatures(page, pageSize, filters);
return {
signatures: response.data
};
};

View File

@@ -0,0 +1,85 @@
<script lang="ts">
import { page } from '$app/stores';
import SimpleDataTable from '$lib/components/dashboard/general_catalogs/simple-data-table.svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus } from 'lucide-svelte';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
let { data } = $props();
let dialogOpen = $state(false);
// Filtros
let searchName = $state($page.url.searchParams.get('name') || '');
let searchPosition = $state($page.url.searchParams.get('position') || '');
let timeout: ReturnType<typeof setTimeout>;
const columns = [
{ key: 'name', label: 'Nombre' },
{ key: 'position', label: 'Cargo' },
{ key: 'certificate', label: 'Certificado' },
];
function handleSearch() {
if (!browser) return;
clearTimeout(timeout);
timeout = setTimeout(() => {
const url = new URL($page.url);
if (searchName) url.searchParams.set('name', searchName);
else url.searchParams.delete('name');
if (searchPosition) url.searchParams.set('position', searchPosition);
else url.searchParams.delete('position');
url.searchParams.set('page', '1');
goto(url, { keepFocus: true, noScroll: true });
}, 500);
}
function handleSuccess() {
const url = new URL($page.url);
goto(url, { invalidateAll: true });
}
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold tracking-tight">Firmas Electrónicas</h1>
<p class="text-muted-foreground">
Gestión del catálogo de firmas electrónicas
</p>
</div>
<Button onclick={() => dialogOpen = true}>
<Plus class="mr-2 h-4 w-4" />
Nueva Firma
</Button>
</div>
<div class="flex gap-4 items-end">
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por nombre..."
bind:value={searchName}
oninput={handleSearch}
/>
</div>
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por cargo..."
bind:value={searchPosition}
oninput={handleSearch}
/>
</div>
</div>
<div class="rounded-md border">
<SimpleDataTable
data={data.signatures?.items || []}
columns={columns}
pageCount={data.signatures?.pages || 0}
totalItems={data.signatures?.total || 0}
/>
</div>
</div>

View File

@@ -0,0 +1,15 @@
import { getUnitConversions } from '$lib/api/dashboard/a76/unit-conversions';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ url }) => {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const filters: Record<string, any> = {};
const response = await getUnitConversions(page, pageSize, filters);
return {
conversions: response.data
};
};

View File

@@ -0,0 +1,46 @@
<script lang="ts">
import { page } from '$app/stores';
import SimpleDataTable from '$lib/components/dashboard/general_catalogs/simple-data-table.svelte';
import { Button } from '$lib/components/ui/button';
import { Plus } from 'lucide-svelte';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
let { data } = $props();
let dialogOpen = $state(false);
const columns = [
{ key: 'from_unit_id', label: 'De Unidad' },
{ key: 'to_unit_id', label: 'A Unidad' },
{ key: 'conversion_factor', label: 'Factor de Conversión' },
];
function handleSuccess() {
const url = new URL($page.url);
goto(url, { invalidateAll: true });
}
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold tracking-tight">Conversiones de Unidades</h1>
<p class="text-muted-foreground">
Gestión del catálogo de conversiones de unidades de medida
</p>
</div>
<Button onclick={() => dialogOpen = true}>
<Plus class="mr-2 h-4 w-4" />
Nueva Conversión
</Button>
</div>
<div class="rounded-md border">
<SimpleDataTable
data={data.conversions?.items || []}
columns={columns}
pageCount={data.conversions?.pages || 0}
totalItems={data.conversions?.total || 0}
/>
</div>
</div>

View File

@@ -0,0 +1,57 @@
import { getServerApiUrl, getAuthTokens } from '$lib/server/api';
import type { PageServerLoad } from './$types';
import { redirect } from '@sveltejs/kit';
export const load: PageServerLoad = async ({ cookies, fetch, url }) => {
const { accessToken } = getAuthTokens(cookies);
if (!accessToken) {
throw redirect(302, '/login');
}
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('page_size')) || 50;
const apiUrl = getServerApiUrl();
// Note: apiUrl already ends with /
const endpoint = `${apiUrl}api/v1/a76/units-of-measure/ace?page=${page}&page_size=${pageSize}`;
try {
const response = await fetch(endpoint, {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
if (!response.ok) {
console.error(`Error fetching ACE units: ${response.status} ${response.statusText}`);
return {
items: [],
total: 0,
page,
pageSize,
pages: 0,
error: `Error: ${response.statusText}`
};
}
const data = await response.json();
return {
items: data.items,
total: data.total,
page: data.page,
pageSize: data.page_size,
pages: data.pages
};
} catch (error) {
console.error('Error fetching ACE units:', error);
return {
items: [],
total: 0,
page,
pageSize,
pages: 0,
error: 'Failed to connect to server'
};
}
};

View File

@@ -0,0 +1,61 @@
<script lang="ts">
import { invalidateAll } from '$app/navigation';
import { createColumns } from '$lib/components/dashboard/units_of_measure/ace/columns';
import CreateEditDialog from '$lib/components/dashboard/units_of_measure/ace/create-edit-dialog.svelte';
import DataTable from '$lib/components/dashboard/units_of_measure/ace/data-table.svelte';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import { Plus, RefreshCw } from 'lucide-svelte';
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
let createDialogOpen = $state(false);
let loading = $state(false);
const columns = createColumns(() => {
refreshData();
});
async function refreshData() {
loading = true;
await invalidateAll();
loading = false;
}
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex items-center justify-between">
<div>
<h2 class="text-2xl font-bold tracking-tight">Unidades de Medida ACE</h2>
<p class="text-muted-foreground">
Catálogo de unidades de medida ACE
</p>
</div>
<div class="flex items-center gap-2">
<Button variant="outline" size="icon" onclick={refreshData} disabled={loading}>
<RefreshCw class="h-4 w-4 {loading ? 'animate-spin' : ''}" />
</Button>
<Button onclick={() => createDialogOpen = true}>
<Plus class="mr-2 h-4 w-4" />
Nuevo
</Button>
</div>
</div>
<Card.Root>
<Card.Content class="p-0">
<DataTable
data={data.items}
{columns}
pageCount={data.pages}
totalItems={data.total}
/>
</Card.Content>
</Card.Root>
<CreateEditDialog
bind:open={createDialogOpen}
onSuccess={refreshData}
/>
</div>

View File

@@ -0,0 +1,56 @@
import { getServerApiUrl, getAuthTokens } from '$lib/server/api';
import type { PageServerLoad } from './$types';
import { redirect } from '@sveltejs/kit';
export const load: PageServerLoad = async ({ cookies, fetch, url }) => {
const { accessToken } = getAuthTokens(cookies);
if (!accessToken) {
throw redirect(302, '/login');
}
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('page_size')) || 50;
const apiUrl = getServerApiUrl();
const endpoint = `${apiUrl}api/v1/a76/units-of-measure/american?page=${page}&page_size=${pageSize}`;
try {
const response = await fetch(endpoint, {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
if (!response.ok) {
console.error(`Error fetching American units: ${response.status} ${response.statusText}`);
return {
items: [],
total: 0,
page,
pageSize,
pages: 0,
error: `Error: ${response.statusText}`
};
}
const data = await response.json();
return {
items: data.items,
total: data.total,
page: data.page,
pageSize: data.page_size,
pages: data.pages
};
} catch (error) {
console.error('Error fetching American units:', error);
return {
items: [],
total: 0,
page,
pageSize,
pages: 0,
error: 'Failed to connect to server'
};
}
};

View File

@@ -0,0 +1,61 @@
<script lang="ts">
import { invalidateAll } from '$app/navigation';
import { createColumns } from '$lib/components/dashboard/units_of_measure/american/columns';
import CreateEditDialog from '$lib/components/dashboard/units_of_measure/american/create-edit-dialog.svelte';
import DataTable from '$lib/components/dashboard/units_of_measure/ace/data-table.svelte'; // Reusing generic data table from ACE
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import { Plus, RefreshCw } from 'lucide-svelte';
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
let createDialogOpen = $state(false);
let loading = $state(false);
const columns = createColumns(() => {
refreshData();
});
async function refreshData() {
loading = true;
await invalidateAll();
loading = false;
}
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex items-center justify-between">
<div>
<h2 class="text-2xl font-bold tracking-tight">Unidades de Medida Americanas</h2>
<p class="text-muted-foreground">
Catálogo de unidades de medida Americanas
</p>
</div>
<div class="flex items-center gap-2">
<Button variant="outline" size="icon" onclick={refreshData} disabled={loading}>
<RefreshCw class="h-4 w-4 {loading ? 'animate-spin' : ''}" />
</Button>
<Button onclick={() => createDialogOpen = true}>
<Plus class="mr-2 h-4 w-4" />
Nuevo
</Button>
</div>
</div>
<Card.Root>
<Card.Content class="p-0">
<DataTable
data={data.items}
{columns}
pageCount={data.pages}
totalItems={data.total}
/>
</Card.Content>
</Card.Root>
<CreateEditDialog
bind:open={createDialogOpen}
onSuccess={refreshData}
/>
</div>

View File

@@ -0,0 +1,56 @@
import { getServerApiUrl, getAuthTokens } from '$lib/server/api';
import type { PageServerLoad } from './$types';
import { redirect } from '@sveltejs/kit';
export const load: PageServerLoad = async ({ cookies, fetch, url }) => {
const { accessToken } = getAuthTokens(cookies);
if (!accessToken) {
throw redirect(302, '/login');
}
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('page_size')) || 50;
const apiUrl = getServerApiUrl();
const endpoint = `${apiUrl}api/v1/a76/units-of-measure/oma?page=${page}&page_size=${pageSize}`;
try {
const response = await fetch(endpoint, {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
if (!response.ok) {
console.error(`Error fetching OMA units: ${response.status} ${response.statusText}`);
return {
items: [],
total: 0,
page,
pageSize,
pages: 0,
error: `Error: ${response.statusText}`
};
}
const data = await response.json();
return {
items: data.items,
total: data.total,
page: data.page,
pageSize: data.page_size,
pages: data.pages
};
} catch (error) {
console.error('Error fetching OMA units:', error);
return {
items: [],
total: 0,
page,
pageSize,
pages: 0,
error: 'Failed to connect to server'
};
}
};

View File

@@ -0,0 +1,61 @@
<script lang="ts">
import { invalidateAll } from '$app/navigation';
import { createColumns } from '$lib/components/dashboard/units_of_measure/oma/columns';
import CreateEditDialog from '$lib/components/dashboard/units_of_measure/oma/create-edit-dialog.svelte';
import DataTable from '$lib/components/dashboard/units_of_measure/ace/data-table.svelte'; // Reusing generic data table from ACE
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import { Plus, RefreshCw } from 'lucide-svelte';
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
let createDialogOpen = $state(false);
let loading = $state(false);
const columns = createColumns(() => {
refreshData();
});
async function refreshData() {
loading = true;
await invalidateAll();
loading = false;
}
</script>
<div class="flex flex-col gap-4 p-4">
<div class="flex items-center justify-between">
<div>
<h2 class="text-2xl font-bold tracking-tight">Unidades de Medida OMA</h2>
<p class="text-muted-foreground">
Catálogo de unidades de medida OMA
</p>
</div>
<div class="flex items-center gap-2">
<Button variant="outline" size="icon" onclick={refreshData} disabled={loading}>
<RefreshCw class="h-4 w-4 {loading ? 'animate-spin' : ''}" />
</Button>
<Button onclick={() => createDialogOpen = true}>
<Plus class="mr-2 h-4 w-4" />
Nuevo
</Button>
</div>
</div>
<Card.Root>
<Card.Content class="p-0">
<DataTable
data={data.items}
{columns}
pageCount={data.pages}
totalItems={data.total}
/>
</Card.Content>
</Card.Root>
<CreateEditDialog
bind:open={createDialogOpen}
onSuccess={refreshData}
/>
</div>