Merge branch 'features/Creacion-formularios_catalogos_generales' into development
This commit is contained in:
@@ -194,6 +194,14 @@ async function fetchApi<T = any>(
|
||||
}
|
||||
}
|
||||
|
||||
// Manejar respuestas sin contenido (204 No Content)
|
||||
if (response.status === 204) {
|
||||
return {
|
||||
data: null as T,
|
||||
status: response.status
|
||||
};
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
/**
|
||||
* API Client para Agentes Aduanales (Customs Brokers)
|
||||
* Gestiona las operaciones CRUD para agentes aduanales
|
||||
*/
|
||||
|
||||
import { api } from '$lib/api';
|
||||
|
||||
export interface CustomsBroker {
|
||||
|
||||
@@ -2,60 +2,63 @@ 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;
|
||||
id: number;
|
||||
classification: string;
|
||||
tenant_id: number;
|
||||
company_id: number;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface ClassificationConceptCreate {
|
||||
classification: string;
|
||||
description?: string;
|
||||
classification: string;
|
||||
}
|
||||
|
||||
export interface ClassificationConceptUpdate extends Partial<ClassificationConceptCreate> {}
|
||||
|
||||
export interface ClassificationConceptListResponse {
|
||||
items: ClassificationConcept[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
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> = {}
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
companyId: number,
|
||||
filters: Record<string, any> = {}
|
||||
): Promise<ApiResponse<ClassificationConceptListResponse>> {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
...filters
|
||||
});
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId.toString(),
|
||||
...filters
|
||||
});
|
||||
|
||||
const response = await api.get(`/a76/classification_concepts?${params.toString()}`);
|
||||
return response.data;
|
||||
return await api.get(`/v1/a76/classification-concepts/?${params.toString()}`);
|
||||
}
|
||||
|
||||
export async function getClassificationConcept(id: number): Promise<ClassificationConcept> {
|
||||
const response = await api.get(`/a76/classification_concepts/${id}`);
|
||||
return response.data;
|
||||
export async function getClassificationConcept(id: number, companyId: number): Promise<ApiResponse<ClassificationConcept>> {
|
||||
return await api.get(`/v1/a76/classification-concepts/${id}/?company_id=${companyId}`);
|
||||
}
|
||||
|
||||
export async function createClassificationConcept(data: ClassificationConceptCreate): Promise<ClassificationConcept> {
|
||||
const response = await api.post('/a76/classification_concepts', data);
|
||||
return response.data;
|
||||
export async function createClassificationConcept(
|
||||
data: ClassificationConceptCreate,
|
||||
companyId: number
|
||||
): Promise<ApiResponse<ClassificationConcept>> {
|
||||
return await api.post(`/v1/a76/classification-concepts/?company_id=${companyId}`, 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 updateClassificationConcept(
|
||||
id: number,
|
||||
data: ClassificationConceptUpdate,
|
||||
companyId: number
|
||||
): Promise<ApiResponse<ClassificationConcept>> {
|
||||
return await api.put(`/v1/a76/classification-concepts/${id}/?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function deleteClassificationConcept(id: number): Promise<void> {
|
||||
await api.delete(`/a76/classification_concepts/${id}`);
|
||||
}
|
||||
export async function deleteClassificationConcept(id: number, companyId: number): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/v1/a76/classification-concepts/${id}/?company_id=${companyId}`);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ export interface Company {
|
||||
tenant_id: number;
|
||||
name: string | null;
|
||||
rfc: string | null;
|
||||
curp?: string | null;
|
||||
main_activity: string | null;
|
||||
program: string | null;
|
||||
program_number: string | null;
|
||||
@@ -17,6 +18,13 @@ export interface Company {
|
||||
responsible_name: string | null;
|
||||
responsible_last_name: string | null;
|
||||
responsible_mother_last_name: string | null;
|
||||
responsible_rfc?: string | null;
|
||||
position?: string | null;
|
||||
has_express_line?: boolean;
|
||||
is_service_company?: boolean;
|
||||
order_format_type?: string | null;
|
||||
ctpat_svi?: string | null;
|
||||
trusted_exporter_number?: string | null;
|
||||
created_at: string | null;
|
||||
updated_at: string | null;
|
||||
}
|
||||
@@ -24,6 +32,7 @@ export interface Company {
|
||||
export interface CompanyCreate {
|
||||
name?: string | null;
|
||||
rfc?: string | null;
|
||||
curp?: string | null;
|
||||
main_activity?: string | null;
|
||||
program?: string | null;
|
||||
program_number?: string | null;
|
||||
@@ -35,11 +44,19 @@ export interface CompanyCreate {
|
||||
responsible_name?: string | null;
|
||||
responsible_last_name?: string | null;
|
||||
responsible_mother_last_name?: string | null;
|
||||
responsible_rfc?: string | null;
|
||||
position?: string | null;
|
||||
has_express_line?: boolean;
|
||||
is_service_company?: boolean;
|
||||
order_format_type?: string | null;
|
||||
ctpat_svi?: string | null;
|
||||
trusted_exporter_number?: string | null;
|
||||
}
|
||||
|
||||
export interface CompanyUpdate {
|
||||
name?: string | null;
|
||||
rfc?: string | null;
|
||||
curp?: string | null;
|
||||
main_activity?: string | null;
|
||||
program?: string | null;
|
||||
program_number?: string | null;
|
||||
@@ -51,6 +68,13 @@ export interface CompanyUpdate {
|
||||
responsible_name?: string | null;
|
||||
responsible_last_name?: string | null;
|
||||
responsible_mother_last_name?: string | null;
|
||||
responsible_rfc?: string | null;
|
||||
position?: string | null;
|
||||
has_express_line?: boolean;
|
||||
is_service_company?: boolean;
|
||||
order_format_type?: string | null;
|
||||
ctpat_svi?: string | null;
|
||||
trusted_exporter_number?: string | null;
|
||||
}
|
||||
|
||||
export interface CompanyListResponse {
|
||||
@@ -71,21 +95,21 @@ export async function getCompanies(
|
||||
page_size: pageSize.toString(),
|
||||
...filters
|
||||
});
|
||||
return await api.get(`/a76/company?${queryParams.toString()}`);
|
||||
return await api.get(`/v1/a76/company?${queryParams.toString()}`);
|
||||
}
|
||||
|
||||
export async function getCompany(id: number): Promise<ApiResponse<Company>> {
|
||||
return await api.get(`/a76/company/${id}`);
|
||||
return await api.get(`/v1/a76/company/${id}`);
|
||||
}
|
||||
|
||||
export async function createCompany(data: CompanyCreate): Promise<ApiResponse<Company>> {
|
||||
return await api.post(`/a76/company`, data);
|
||||
return await api.post(`/v1/a76/company`, data);
|
||||
}
|
||||
|
||||
export async function updateCompany(id: number, data: CompanyUpdate): Promise<ApiResponse<Company>> {
|
||||
return await api.put(`/a76/company/${id}`, data);
|
||||
return await api.put(`/v1/a76/company/${id}`, data);
|
||||
}
|
||||
|
||||
export async function deleteCompany(id: number): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/a76/company/${id}`);
|
||||
return await api.delete(`/v1/a76/company/${id}`);
|
||||
}
|
||||
|
||||
@@ -2,78 +2,80 @@ 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;
|
||||
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;
|
||||
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;
|
||||
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> = {}
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
companyId: number,
|
||||
filters: Record<string, any> = {},
|
||||
): Promise<ApiResponse<ConceptListResponse>> {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
...filters
|
||||
});
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId.toString(),
|
||||
...filters
|
||||
});
|
||||
|
||||
const response = await api.get(`/a76/concepts?${params.toString()}`);
|
||||
return response.data;
|
||||
return await api.get(`/v1/a76/concepts?${params.toString()}`);
|
||||
}
|
||||
|
||||
export async function getConcept(id: number): Promise<Concept> {
|
||||
const response = await api.get(`/a76/concepts/${id}`);
|
||||
return response.data;
|
||||
|
||||
export async function getConcept(id: number, companyId: number): Promise<ApiResponse<Concept>> {
|
||||
return await api.get(`/v1/a76/concepts/${id}?company_id=${companyId}`);
|
||||
}
|
||||
|
||||
export async function createConcept(data: ConceptCreate): Promise<Concept> {
|
||||
const response = await api.post('/a76/concepts', data);
|
||||
return response.data;
|
||||
|
||||
export async function createConcept(data: ConceptCreate, companyId: number): Promise<ApiResponse<Concept>> {
|
||||
return await api.post(`/v1/a76/concepts?company_id=${companyId}`, 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 updateConcept(id: number, data: ConceptUpdate, companyId: number): Promise<ApiResponse<Concept>> {
|
||||
return await api.put(`/v1/a76/concepts/${id}?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function deleteConcept(id: number): Promise<void> {
|
||||
await api.delete(`/a76/concepts/${id}`);
|
||||
}
|
||||
|
||||
export async function deleteConcept(id: number, companyId: number): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/v1/a76/concepts/${id}?company_id=${companyId}`);
|
||||
}
|
||||
@@ -2,76 +2,74 @@ 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;
|
||||
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;
|
||||
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;
|
||||
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> = {}
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
companyId: number,
|
||||
filters: Record<string, any> = {},
|
||||
): Promise<ApiResponse<CustomsBrokerConceptListResponse>> {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
...filters
|
||||
});
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId.toString(),
|
||||
...filters
|
||||
});
|
||||
|
||||
const response = await api.get(`/a76/customs_broker_concepts?${params.toString()}`);
|
||||
return response.data;
|
||||
return await api.get(`/v1/a76/customs-broker-concepts?${params.toString()}`);
|
||||
}
|
||||
|
||||
export async function getCustomsBrokerConcept(id: number): Promise<CustomsBrokerConcept> {
|
||||
const response = await api.get(`/a76/customs_broker_concepts/${id}`);
|
||||
return response.data;
|
||||
export async function getCustomsBrokerConcept(id: number, companyId: number): Promise<ApiResponse<CustomsBrokerConcept>> {
|
||||
return await api.get(`/v1/a76/customs-broker-concepts/${id}?company_id=${companyId}`);
|
||||
}
|
||||
|
||||
export async function createCustomsBrokerConcept(data: CustomsBrokerConceptCreate): Promise<CustomsBrokerConcept> {
|
||||
const response = await api.post('/a76/customs_broker_concepts', data);
|
||||
return response.data;
|
||||
export async function createCustomsBrokerConcept(data: CustomsBrokerConceptCreate, companyId: number): Promise<ApiResponse<CustomsBrokerConcept>> {
|
||||
return await api.post(`/v1/a76/customs-broker-concepts?company_id=${companyId}`, 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 updateCustomsBrokerConcept(id: number, data: CustomsBrokerConceptUpdate, companyId: number): Promise<ApiResponse<CustomsBrokerConcept>> {
|
||||
return await api.put(`/v1/a76/customs-broker-concepts/${id}?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function deleteCustomsBrokerConcept(id: number): Promise<void> {
|
||||
await api.delete(`/a76/customs_broker_concepts/${id}`);
|
||||
export async function deleteCustomsBrokerConcept(id: number, companyId: number): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/v1/a76/customs-broker-concepts/${id}?company_id=${companyId}`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
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,
|
||||
companyId: number,
|
||||
filters: Record<string, any> = {},
|
||||
): Promise<ApiResponse<CustomsBrokerConceptListResponse>> {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId.toString(),
|
||||
...filters
|
||||
});
|
||||
|
||||
return await api.get(`/v1/a76/customs-broker-concepts?${params.toString()}`);
|
||||
}
|
||||
|
||||
export async function getCustomsBrokerConcept(id: number, companyId: number): Promise<ApiResponse<CustomsBrokerConcept>> {
|
||||
return await api.get(`/v1/a76/customs-broker-concepts/${id}?company_id=${companyId}`);
|
||||
}
|
||||
|
||||
export async function createCustomsBrokerConcept(data: CustomsBrokerConceptCreate, companyId: number): Promise<ApiResponse<CustomsBrokerConcept>> {
|
||||
return await api.post(`/v1/a76/customs-broker-concepts?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function updateCustomsBrokerConcept(id: number, data: CustomsBrokerConceptUpdate, companyId: number): Promise<ApiResponse<CustomsBrokerConcept>> {
|
||||
return await api.put(`/v1/a76/customs-broker-concepts/${id}?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function deleteCustomsBrokerConcept(id: number, companyId: number): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/v1/a76/customs-broker-concepts/${id}?company_id=${companyId}`);
|
||||
}
|
||||
@@ -1,61 +1,186 @@
|
||||
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 DodaContainerSeal {
|
||||
id: number;
|
||||
doda_sys_id: number;
|
||||
seal_line: number;
|
||||
seal_value?: string;
|
||||
}
|
||||
|
||||
export interface DODACreate {
|
||||
code: string;
|
||||
description?: string;
|
||||
export interface DodaContainerSealCreate {
|
||||
seal_value?: string;
|
||||
}
|
||||
|
||||
export interface DODAUpdate extends Partial<DODACreate> {}
|
||||
|
||||
export interface DODAListResponse {
|
||||
items: DODA[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
export interface DodaContainer {
|
||||
id: number;
|
||||
doda_sys_id: number;
|
||||
container_line: number;
|
||||
container_value?: string;
|
||||
seals?: string;
|
||||
seals_detail?: DodaContainerSeal[];
|
||||
}
|
||||
|
||||
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 interface DodaContainerCreate {
|
||||
container_value?: string;
|
||||
seals?: string;
|
||||
seals_detail?: DodaContainerSealCreate[];
|
||||
}
|
||||
|
||||
export async function getDODA(id: number): Promise<DODA> {
|
||||
const response = await api.get(`/a76/doda/${id}`);
|
||||
return response.data;
|
||||
|
||||
export interface DodaAmericanPedimento {
|
||||
id: number;
|
||||
doda_sys_id: number;
|
||||
american_pedimento_line: number;
|
||||
american_pedimento_type?: string;
|
||||
american_pedimento_value?: string;
|
||||
}
|
||||
|
||||
export async function createDODA(data: DODACreate): Promise<DODA> {
|
||||
const response = await api.post('/a76/doda', data);
|
||||
return response.data;
|
||||
export interface DodaAmericanPedimentoCreate {
|
||||
american_pedimento_type?: string;
|
||||
american_pedimento_value?: string;
|
||||
}
|
||||
|
||||
export async function updateDODA(id: number, data: DODAUpdate): Promise<DODA> {
|
||||
const response = await api.patch(`/a76/doda/${id}`, data);
|
||||
return response.data;
|
||||
export interface DodaPedimento {
|
||||
id: number;
|
||||
doda_sys_id: number;
|
||||
pedimento_line: number;
|
||||
authorization_patent?: string;
|
||||
document?: string;
|
||||
shipment?: string;
|
||||
cove?: string;
|
||||
umc?: string;
|
||||
effective_amount_usd?: number;
|
||||
difference_amount_usd?: number;
|
||||
dta_niu?: string;
|
||||
article_7?: boolean;
|
||||
pedimento_sys_id?: number;
|
||||
invoice_line?: number;
|
||||
part_ii_line?: number;
|
||||
pedimento_type?: string;
|
||||
zero_packaging_validation?: boolean;
|
||||
}
|
||||
|
||||
export async function deleteDODA(id: number): Promise<void> {
|
||||
await api.delete(`/a76/doda/${id}`);
|
||||
export interface DodaPedimentoCreate {
|
||||
authorization_patent?: string;
|
||||
document?: string;
|
||||
shipment?: string;
|
||||
|
||||
effective_amount_usd?: number;
|
||||
}
|
||||
|
||||
|
||||
export interface Doda {
|
||||
id: number;
|
||||
|
||||
integration_number?: string;
|
||||
doda_date?: number;
|
||||
doda_time?: number;
|
||||
dispatch_customs?: string;
|
||||
customs_sections?: string;
|
||||
patent?: string;
|
||||
pedimentos?: string;
|
||||
caat?: string;
|
||||
transport_identification?: string;
|
||||
fast_id?: string;
|
||||
operation_type?: string;
|
||||
status?: string;
|
||||
|
||||
|
||||
containers?: DodaContainer[];
|
||||
american_pedimentos?: DodaAmericanPedimento[];
|
||||
pedimentos_detail?: DodaPedimento[];
|
||||
|
||||
|
||||
tenant_id?: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
|
||||
export interface DodaCreate {
|
||||
integration_number?: string;
|
||||
doda_date?: number;
|
||||
doda_time?: number;
|
||||
dispatch_customs?: string;
|
||||
customs_sections?: string;
|
||||
patent?: string;
|
||||
pedimentos?: string;
|
||||
caat?: string;
|
||||
transport_identification?: string;
|
||||
fast_id?: string;
|
||||
operation_type?: string;
|
||||
selected?: boolean;
|
||||
user_selected?: string;
|
||||
last_user?: string;
|
||||
responsible?: string;
|
||||
carrier?: string;
|
||||
shipments?: string;
|
||||
pedimento_type?: string;
|
||||
original_chain?: string;
|
||||
serial_number?: string;
|
||||
electronic_signature?: string;
|
||||
transaction_number?: string;
|
||||
status?: string;
|
||||
linq_sat_qr?: string;
|
||||
sat_certificate?: string;
|
||||
sat_digital_seal?: string;
|
||||
xml_doda_sent_path?: string;
|
||||
xml_doda_response_path?: string;
|
||||
sat_original_chain?: string;
|
||||
customs_clearance?: number;
|
||||
unique_badge_number?: 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> = {},
|
||||
companyId?: number
|
||||
): Promise<ApiResponse<DodaListResponse>> {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
...filters
|
||||
});
|
||||
if (companyId) {
|
||||
params.append('company_id', companyId.toString());
|
||||
}
|
||||
const response = await api.get(`/v1/a76/doda?${params.toString()}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function getDoda(id: number, companyId?: number): Promise<Doda> {
|
||||
const params = new URLSearchParams();
|
||||
if (companyId) {
|
||||
params.append('company_id', companyId.toString());
|
||||
}
|
||||
const response = await api.get(`/v1/a76/doda/${id}?${params.toString()}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function createDoda(data: DodaCreate, companyId: number): Promise<Doda> {
|
||||
const response = await api.post(`/v1/a76/doda?company_id=${companyId}`, data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function updateDoda(id: number, data: DodaUpdate, companyId: number): Promise<Doda> {
|
||||
const response = await api.patch(`/v1/a76/doda/${id}?company_id=${companyId}`, data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function deleteDoda(id: number, companyId: number): Promise<void> {
|
||||
await api.delete(`/v1/a76/doda/${id}?company_id=${companyId}`);
|
||||
}
|
||||
@@ -2,60 +2,89 @@ 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;
|
||||
id: number;
|
||||
// Campos del Modelo Python
|
||||
notice_number?: string;
|
||||
year?: string;
|
||||
patent?: string;
|
||||
pedimento?: string;
|
||||
file_sent?: string;
|
||||
file_response?: string;
|
||||
status?: string;
|
||||
invoice?: string;
|
||||
validation_acknowledgment?: string;
|
||||
fea?: string;
|
||||
certificate_number?: string;
|
||||
|
||||
// Mixins
|
||||
tenant_id: number;
|
||||
company_id: number;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface ElectronicNoticeCreate {
|
||||
code: string;
|
||||
description?: string;
|
||||
notice_number?: string;
|
||||
year?: string;
|
||||
patent?: string;
|
||||
pedimento?: string;
|
||||
file_sent?: string;
|
||||
file_response?: string;
|
||||
status?: string;
|
||||
invoice?: string;
|
||||
validation_acknowledgment?: string;
|
||||
fea?: string;
|
||||
certificate_number?: string;
|
||||
}
|
||||
|
||||
export interface ElectronicNoticeUpdate extends Partial<ElectronicNoticeCreate> {}
|
||||
|
||||
export interface ElectronicNoticeListResponse {
|
||||
items: ElectronicNotice[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
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> = {}
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
filters: Record<string, any> = {},
|
||||
companyId?: number
|
||||
): Promise<ApiResponse<ElectronicNoticeListResponse>> {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
...filters
|
||||
});
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
...filters
|
||||
});
|
||||
if (companyId) {
|
||||
params.append('company_id', companyId.toString());
|
||||
}
|
||||
|
||||
const response = await api.get(`/a76/electronic_notices?${params.toString()}`);
|
||||
return response.data;
|
||||
const response = await api.get(`/v1/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 getElectronicNotice(id: number, companyId?: number): Promise<ElectronicNotice> {
|
||||
const params = new URLSearchParams();
|
||||
if (companyId) {
|
||||
params.append('company_id', companyId.toString());
|
||||
}
|
||||
const response = await api.get(`/v1/a76/electronic-notices/${id}?${params.toString()}`);
|
||||
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 createElectronicNotice(data: ElectronicNoticeCreate, companyId: number): Promise<ElectronicNotice> {
|
||||
const response = await api.post(`/v1/a76/electronic-notices/?company_id=${companyId}`, 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 updateElectronicNotice(id: number, data: ElectronicNoticeUpdate, companyId: number): Promise<ElectronicNotice> {
|
||||
const response = await api.put(`/v1/a76/electronic-notices/${id}?company_id=${companyId}`, data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function deleteElectronicNotice(id: number): Promise<void> {
|
||||
await api.delete(`/a76/electronic_notices/${id}`);
|
||||
}
|
||||
export async function deleteElectronicNotice(id: number, companyId: number): Promise<void> {
|
||||
await api.delete(`/v1/a76/electronic-notices/${id}?company_id=${companyId}`);
|
||||
}
|
||||
@@ -2,62 +2,71 @@ 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;
|
||||
id: number;
|
||||
fraccion_mex: string;
|
||||
fraccion_us: string;
|
||||
description?: string;
|
||||
tenant_id: number;
|
||||
company_id: number;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface EquivalencyCreate {
|
||||
fraccion_mex: string;
|
||||
fraccion_us: string;
|
||||
description?: string;
|
||||
fraccion_mex: string;
|
||||
fraccion_us: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface EquivalencyUpdate extends Partial<EquivalencyCreate> {}
|
||||
export interface EquivalencyUpdate {
|
||||
fraccion_mex?: string;
|
||||
fraccion_us?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface EquivalencyListResponse {
|
||||
items: Equivalency[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
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> = {}
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
companyId: number,
|
||||
filters: Record<string, any> = {}
|
||||
): Promise<ApiResponse<EquivalencyListResponse>> {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
...filters
|
||||
});
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId.toString(),
|
||||
...filters
|
||||
});
|
||||
|
||||
const response = await api.get(`/a76/equivalencies?${params.toString()}`);
|
||||
return response.data;
|
||||
return await api.get(`/v1/a76/equivalencies/?${params.toString()}`);
|
||||
}
|
||||
|
||||
export async function getEquivalency(id: number): Promise<Equivalency> {
|
||||
const response = await api.get(`/a76/equivalencies/${id}`);
|
||||
return response.data;
|
||||
export async function getEquivalency(id: number, companyId: number): Promise<ApiResponse<Equivalency>> {
|
||||
return await api.get(`/v1/a76/equivalencies/${id}/?company_id=${companyId}`);
|
||||
}
|
||||
|
||||
export async function createEquivalency(data: EquivalencyCreate): Promise<Equivalency> {
|
||||
const response = await api.post('/a76/equivalencies', data);
|
||||
return response.data;
|
||||
export async function createEquivalency(
|
||||
data: EquivalencyCreate,
|
||||
companyId: number
|
||||
): Promise<ApiResponse<Equivalency>> {
|
||||
return await api.post(`/v1/a76/equivalencies/?company_id=${companyId}`, 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 updateEquivalency(
|
||||
id: number,
|
||||
data: EquivalencyUpdate,
|
||||
companyId: number
|
||||
): Promise<ApiResponse<Equivalency>> {
|
||||
return await api.put(`/v1/a76/equivalencies/${id}/?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function deleteEquivalency(id: number): Promise<void> {
|
||||
await api.delete(`/a76/equivalencies/${id}`);
|
||||
export async function deleteEquivalency(id: number, companyId: number): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/v1/a76/equivalencies/${id}/?company_id=${companyId}`);
|
||||
}
|
||||
|
||||
@@ -1,61 +1,145 @@
|
||||
import { api } from '$lib/api';
|
||||
import type { ApiResponse } from '$lib/api';
|
||||
|
||||
// ==========================================
|
||||
// ERROR CLASSIFICATION
|
||||
// ==========================================
|
||||
|
||||
export interface ErrorClassification {
|
||||
id: number;
|
||||
code: string;
|
||||
level?: string;
|
||||
errors?: ErrorCatalog[];
|
||||
tenant_id?: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface ErrorClassificationCreate {
|
||||
code: string;
|
||||
level?: string;
|
||||
}
|
||||
|
||||
export interface ErrorClassificationUpdate {
|
||||
level?: string;
|
||||
}
|
||||
|
||||
export interface ErrorClassificationListResponse {
|
||||
items: ErrorClassification[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
}
|
||||
|
||||
|
||||
export interface ErrorCatalog {
|
||||
id: number;
|
||||
code: string;
|
||||
description?: string;
|
||||
tenant_id: string;
|
||||
company_id?: string;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
id: number;
|
||||
code: string;
|
||||
description?: string;
|
||||
classification_id?: number;
|
||||
classification?: ErrorClassification;
|
||||
tenant_id?: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface ErrorCatalogCreate {
|
||||
code: string;
|
||||
description?: string;
|
||||
code: string;
|
||||
description?: string;
|
||||
classification_id?: number;
|
||||
}
|
||||
|
||||
export interface ErrorCatalogUpdate extends Partial<ErrorCatalogCreate> {}
|
||||
export interface ErrorCatalogUpdate {
|
||||
description?: string;
|
||||
classification_id?: number;
|
||||
}
|
||||
|
||||
export interface ErrorCatalogListResponse {
|
||||
items: ErrorCatalog[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
items: ErrorCatalog[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
}
|
||||
|
||||
export async function getErrorClassifications(
|
||||
companyId: number,
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
filters: Record<string, any> = {}
|
||||
): Promise<ErrorClassificationListResponse> {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId.toString(),
|
||||
...filters
|
||||
});
|
||||
|
||||
const response = await api.get(`/v1/a76/error-catalogs/classifications/?${params.toString()}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function getErrorClassification(id: number, companyId: number): Promise<ErrorClassification> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
const response = await api.get(`/v1/a76/error-catalogs/classifications/${id}?${params.toString()}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function createErrorClassification(data: ErrorClassificationCreate, companyId: number): Promise<ErrorClassification> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
const response = await api.post(`/v1/a76/error-catalogs/classifications/?${params.toString()}`, data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function updateErrorClassification(id: number, data: ErrorClassificationUpdate, companyId: number): Promise<ErrorClassification> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
const response = await api.put(`/v1/a76/error-catalogs/classifications/${id}?${params.toString()}`, data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function deleteErrorClassification(id: number, companyId: number): Promise<void> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
await api.delete(`/v1/a76/error-catalogs/classifications/${id}?${params.toString()}`);
|
||||
}
|
||||
|
||||
// --- Catalogs ---
|
||||
|
||||
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
|
||||
});
|
||||
companyId: number,
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
filters: Record<string, any> = {}
|
||||
): Promise<ErrorCatalogListResponse> {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId.toString(),
|
||||
...filters
|
||||
});
|
||||
|
||||
const response = await api.get(`/a76/error_catalogs?${params.toString()}`);
|
||||
return response.data;
|
||||
const response = await api.get(`/v1/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 getErrorCatalog(id: number, companyId: number): Promise<ErrorCatalog> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
const response = await api.get(`/v1/a76/error-catalogs/${id}?${params.toString()}`);
|
||||
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 createErrorCatalog(data: ErrorCatalogCreate, companyId: number): Promise<ErrorCatalog> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
const response = await api.post(`/v1/a76/error-catalogs/?${params.toString()}`, 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 updateErrorCatalog(id: number, data: ErrorCatalogUpdate, companyId: number): Promise<ErrorCatalog> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
const response = await api.put(`/v1/a76/error-catalogs/${id}?${params.toString()}`, data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function deleteErrorCatalog(id: number): Promise<void> {
|
||||
await api.delete(`/a76/error_catalogs/${id}`);
|
||||
}
|
||||
export async function deleteErrorCatalog(id: number, companyId: number): Promise<void> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
await api.delete(`/v1/a76/error-catalogs/${id}?${params.toString()}`);
|
||||
}
|
||||
@@ -7,8 +7,8 @@ export interface Identifier {
|
||||
description: string | null;
|
||||
level: string | null;
|
||||
complement: string | null;
|
||||
company_id: number;
|
||||
tenant_id: number;
|
||||
company_id: number;
|
||||
tenant_id: number;
|
||||
created_at: string | null;
|
||||
updated_at: string | null;
|
||||
}
|
||||
@@ -18,7 +18,6 @@ export interface IdentifierCreate {
|
||||
description?: string | null;
|
||||
level?: string | null;
|
||||
complement?: string | null;
|
||||
company_id: number;
|
||||
}
|
||||
|
||||
export interface IdentifierUpdate {
|
||||
@@ -39,24 +38,37 @@ export interface IdentifierListResponse {
|
||||
export async function getIdentifiers(
|
||||
page = 1,
|
||||
pageSize = 50,
|
||||
companyId: number,
|
||||
filters: Record<string, any> = {}
|
||||
): Promise<ApiResponse<IdentifierListResponse>> {
|
||||
const queryParams = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId.toString(),
|
||||
...filters
|
||||
});
|
||||
return await api.get(`/a76/identifiers?${queryParams.toString()}`);
|
||||
|
||||
return await api.get(`/v1/a76/identifiers/?${queryParams.toString()}`);
|
||||
}
|
||||
|
||||
export async function createIdentifier(data: IdentifierCreate): Promise<ApiResponse<Identifier>> {
|
||||
return await api.post('/a76/identifiers', data);
|
||||
export async function createIdentifier(
|
||||
data: IdentifierCreate,
|
||||
companyId: number
|
||||
): Promise<ApiResponse<Identifier>> {
|
||||
return await api.post(`/v1/a76/identifiers/?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function updateIdentifier(id: number, data: IdentifierUpdate): Promise<ApiResponse<Identifier>> {
|
||||
return await api.put(`/a76/identifiers/${id}`, data);
|
||||
export async function updateIdentifier(
|
||||
id: number,
|
||||
data: IdentifierUpdate,
|
||||
companyId: number
|
||||
): Promise<ApiResponse<Identifier>> {
|
||||
return await api.put(`/v1/a76/identifiers/${id}/?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function deleteIdentifier(id: number): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/a76/identifiers/${id}`);
|
||||
}
|
||||
export async function deleteIdentifier(
|
||||
id: number,
|
||||
companyId: number
|
||||
): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/v1/a76/identifiers/${id}/?company_id=${companyId}`);
|
||||
}
|
||||
@@ -2,62 +2,67 @@ 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;
|
||||
id: number;
|
||||
year: string;
|
||||
month: string;
|
||||
value?: number;
|
||||
tenant_id: number;
|
||||
company_id: number;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface INPCCreate {
|
||||
year: string;
|
||||
month: string;
|
||||
value?: number;
|
||||
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;
|
||||
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> = {}
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
companyId: number,
|
||||
filters: Record<string, any> = {}
|
||||
): Promise<ApiResponse<INPCListResponse>> {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
...filters
|
||||
});
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId.toString(),
|
||||
...filters
|
||||
});
|
||||
|
||||
const response = await api.get(`/a76/inpc?${params.toString()}`);
|
||||
return response.data;
|
||||
return await api.get(`/v1/a76/inpc/?${params.toString()}`);
|
||||
}
|
||||
|
||||
export async function getINPC(id: number): Promise<INPC> {
|
||||
const response = await api.get(`/a76/inpc/${id}`);
|
||||
return response.data;
|
||||
export async function getINPC(id: number, companyId: number): Promise<ApiResponse<INPC>> {
|
||||
return await api.get(`/v1/a76/inpc/${id}/?company_id=${companyId}`);
|
||||
}
|
||||
|
||||
export async function createINPC(data: INPCCreate): Promise<INPC> {
|
||||
const response = await api.post('/a76/inpc', data);
|
||||
return response.data;
|
||||
export async function createINPC(
|
||||
data: INPCCreate,
|
||||
companyId: number
|
||||
): Promise<ApiResponse<INPC>> {
|
||||
return await api.post(`/v1/a76/inpc/?company_id=${companyId}`, 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 updateINPC(
|
||||
id: number,
|
||||
data: INPCUpdate,
|
||||
companyId: number
|
||||
): Promise<ApiResponse<INPC>> {
|
||||
return await api.put(`/v1/a76/inpc/${id}/?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function deleteINPC(id: number): Promise<void> {
|
||||
await api.delete(`/a76/inpc/${id}`);
|
||||
export async function deleteINPC(id: number, companyId: number): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/v1/a76/inpc/${id}/?company_id=${companyId}`);
|
||||
}
|
||||
|
||||
@@ -2,60 +2,64 @@ 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;
|
||||
id: number;
|
||||
code: number;
|
||||
description?: string;
|
||||
tenant_id: number;
|
||||
company_id: number;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface LegendCreate {
|
||||
code: string;
|
||||
description?: string;
|
||||
code: number;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface LegendUpdate extends Partial<LegendCreate> {}
|
||||
|
||||
export interface LegendListResponse {
|
||||
items: Legend[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
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> = {}
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
companyId: number,
|
||||
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;
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId.toString(),
|
||||
...filters
|
||||
});
|
||||
return await api.get(`/v1/a76/legends/?${params.toString()}`);
|
||||
}
|
||||
|
||||
export async function getLegend(id: number): Promise<Legend> {
|
||||
const response = await api.get(`/a76/legends/${id}`);
|
||||
return response.data;
|
||||
export async function getLegend(id: number, companyId: number): Promise<ApiResponse<Legend>> {
|
||||
return await api.get(`/v1/a76/legends/${id}/?company_id=${companyId}`);
|
||||
}
|
||||
|
||||
export async function createLegend(data: LegendCreate): Promise<Legend> {
|
||||
const response = await api.post('/a76/legends', data);
|
||||
return response.data;
|
||||
export async function createLegend(
|
||||
data: LegendCreate,
|
||||
companyId: number
|
||||
): Promise<ApiResponse<Legend>> {
|
||||
return await api.post(`/v1/a76/legends/?company_id=${companyId}`, 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 updateLegend(
|
||||
id: number,
|
||||
data: LegendUpdate,
|
||||
companyId: number
|
||||
): Promise<ApiResponse<Legend>> {
|
||||
return await api.put(`/v1/a76/legends/${id}/?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function deleteLegend(id: number): Promise<void> {
|
||||
await api.delete(`/a76/legends/${id}`);
|
||||
}
|
||||
export async function deleteLegend(id: number, companyId: number): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/v1/a76/legends/${id}/?company_id=${companyId}`);
|
||||
}
|
||||
@@ -1,52 +1,90 @@
|
||||
/**
|
||||
* API Client para Locations - Ubicaciones relacionadas con puertos
|
||||
* Basado en los campos location_code y location_description del módulo de puertos
|
||||
*/
|
||||
import type { PaginatedResponse } from '$lib/types';
|
||||
import { api } from '$lib/api';
|
||||
import type { ApiResponse } from '$lib/api';
|
||||
|
||||
export interface Location {
|
||||
location_code: string;
|
||||
location_description: string | null;
|
||||
id: number;
|
||||
location_code: string;
|
||||
location_description: string | null;
|
||||
company_id: number;
|
||||
tenant_id: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Nota: Las ubicaciones están integradas en el módulo de puertos.
|
||||
* Este archivo proporciona tipos para trabajar con ubicaciones,
|
||||
* pero las operaciones se realizan a través del módulo de puertos.
|
||||
*
|
||||
* Ver: /a76/ports para operaciones relacionadas con ubicaciones
|
||||
*/
|
||||
|
||||
/**
|
||||
* Obtiene ubicaciones únicas de los puertos
|
||||
* Esta función extrae las ubicaciones únicas de la lista de puertos
|
||||
*/
|
||||
export async function getLocationsFromPorts(): Promise<ApiResponse<Location[]>> {
|
||||
const portsResponse = await api.get('/a76/ports?page_size=1000');
|
||||
|
||||
if (portsResponse.error || !portsResponse.data) {
|
||||
return {
|
||||
error: portsResponse.error || 'Error al obtener puertos',
|
||||
status: portsResponse.status
|
||||
};
|
||||
}
|
||||
|
||||
// Extraer ubicaciones únicas
|
||||
const locationMap = new Map<string, Location>();
|
||||
const ports = portsResponse.data.items || [];
|
||||
|
||||
ports.forEach((port: any) => {
|
||||
if (port.location_code && !locationMap.has(port.location_code)) {
|
||||
locationMap.set(port.location_code, {
|
||||
location_code: port.location_code,
|
||||
location_description: port.location_description
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
data: Array.from(locationMap.values()),
|
||||
status: 200
|
||||
};
|
||||
export interface LocationCreate {
|
||||
location_code: string;
|
||||
location_description?: string | null;
|
||||
}
|
||||
|
||||
export interface LocationUpdate {
|
||||
location_description?: string | null;
|
||||
}
|
||||
|
||||
export interface LocationListResponse extends PaginatedResponse {
|
||||
items: Location[];
|
||||
}
|
||||
|
||||
export interface LocationFilters {
|
||||
location_code?: string;
|
||||
location_description?: string;
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
}
|
||||
|
||||
export async function getLocations(
|
||||
companyId: number,
|
||||
filters?: LocationFilters
|
||||
): Promise<LocationListResponse> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
|
||||
if (filters) {
|
||||
if (filters.location_code) params.append('location_code', filters.location_code);
|
||||
if (filters.location_description) params.append('location_description', filters.location_description);
|
||||
if (filters.page) params.append('page', filters.page.toString());
|
||||
if (filters.page_size) params.append('page_size', filters.page_size.toString());
|
||||
}
|
||||
|
||||
return api.get<LocationListResponse>(`/v1/a76/ports/?${params.toString()}`);
|
||||
}
|
||||
|
||||
export async function getLocation(
|
||||
locationId: number,
|
||||
companyId: number
|
||||
): Promise<Location> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
return api.get<Location>(`/v1/a76/ports/${locationId}?${params.toString()}`);
|
||||
}
|
||||
|
||||
export async function createLocation(
|
||||
data: LocationCreate,
|
||||
companyId: number
|
||||
): Promise<Location> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
return api.post<Location>(`/v1/a76/ports/?${params.toString()}`, {
|
||||
port_code: data.location_code,
|
||||
location_code: data.location_code,
|
||||
description: null,
|
||||
location_description: data.location_description || null,
|
||||
port_type: 'ENTRY'
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateLocation(
|
||||
locationId: number,
|
||||
data: LocationUpdate,
|
||||
companyId: number
|
||||
): Promise<Location> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
return api.put<Location>(
|
||||
`/v1/a76/ports/${locationId}?${params.toString()}`,
|
||||
{
|
||||
location_description: data.location_description
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function deleteLocation(
|
||||
locationId: number,
|
||||
companyId: number
|
||||
): Promise<void> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
return api.delete(`/v1/a76/ports/${locationId}?${params.toString()}`);
|
||||
}
|
||||
@@ -1,61 +1,78 @@
|
||||
import { api } from '$lib/api';
|
||||
import type { ApiResponse } from '$lib/api';
|
||||
import type { PaginatedResponse } from '$lib/types';
|
||||
|
||||
export interface MultiCurrencyType {
|
||||
id: number;
|
||||
key: string;
|
||||
description?: string;
|
||||
tenant_id: string;
|
||||
company_id?: string;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
currency_type_code: string;
|
||||
country_key: string | null;
|
||||
conversion_factor: number | null;
|
||||
publication_date: number;
|
||||
company_id: number;
|
||||
tenant_id: number;
|
||||
}
|
||||
|
||||
export interface MultiCurrencyTypeCreate {
|
||||
key: string;
|
||||
description?: string;
|
||||
currency_type_code: string;
|
||||
country_key?: string | null;
|
||||
conversion_factor?: number | null;
|
||||
publication_date: number;
|
||||
}
|
||||
|
||||
export interface MultiCurrencyTypeUpdate extends Partial<MultiCurrencyTypeCreate> {}
|
||||
export interface MultiCurrencyTypeUpdate {
|
||||
currency_type_code?: string;
|
||||
country_key?: string | null;
|
||||
conversion_factor?: number | null;
|
||||
publication_date?: number;
|
||||
}
|
||||
|
||||
export interface MultiCurrencyTypeListResponse {
|
||||
export interface MultiCurrencyTypeListResponse extends PaginatedResponse {
|
||||
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
|
||||
});
|
||||
companyId: number,
|
||||
page?: number,
|
||||
pageSize?: number
|
||||
): Promise<MultiCurrencyTypeListResponse> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
if (page) params.append('page', page.toString());
|
||||
if (pageSize) params.append('page_size', pageSize.toString());
|
||||
|
||||
const response = await api.get(`/a76/multi_currency_types?${params.toString()}`);
|
||||
return response.data;
|
||||
return api.get<MultiCurrencyTypeListResponse>(`/v1/a76/multi-currency-types/?${params.toString()}`);
|
||||
}
|
||||
|
||||
export async function getMultiCurrencyType(id: number): Promise<MultiCurrencyType> {
|
||||
const response = await api.get(`/a76/multi_currency_types/${id}`);
|
||||
return response.data;
|
||||
export async function getMultiCurrencyType(
|
||||
multiCurrencyTypeId: number,
|
||||
companyId: number
|
||||
): Promise<MultiCurrencyType> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
return api.get<MultiCurrencyType>(`/v1/a76/multi-currency-types/${multiCurrencyTypeId}?${params.toString()}`);
|
||||
}
|
||||
|
||||
export async function createMultiCurrencyType(data: MultiCurrencyTypeCreate): Promise<MultiCurrencyType> {
|
||||
const response = await api.post('/a76/multi_currency_types', data);
|
||||
return response.data;
|
||||
export async function createMultiCurrencyType(
|
||||
data: MultiCurrencyTypeCreate,
|
||||
companyId: number
|
||||
): Promise<MultiCurrencyType> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
return api.post<MultiCurrencyType>(`/v1/a76/multi-currency-types/?${params.toString()}`, 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 updateMultiCurrencyType(
|
||||
multiCurrencyTypeId: number,
|
||||
data: MultiCurrencyTypeUpdate,
|
||||
companyId: number
|
||||
): Promise<MultiCurrencyType> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
return api.put<MultiCurrencyType>(
|
||||
`/v1/a76/multi-currency-types/${multiCurrencyTypeId}?${params.toString()}`,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
export async function deleteMultiCurrencyType(id: number): Promise<void> {
|
||||
await api.delete(`/a76/multi_currency_types/${id}`);
|
||||
}
|
||||
export async function deleteMultiCurrencyType(
|
||||
multiCurrencyTypeId: number,
|
||||
companyId: number
|
||||
): Promise<void> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
return api.delete(`/v1/a76/multi-currency-types/${multiCurrencyTypeId}?${params.toString()}`);
|
||||
}
|
||||
@@ -4,17 +4,17 @@ 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;
|
||||
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;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface PackageCreate {
|
||||
@@ -26,19 +26,9 @@ export interface PackageCreate {
|
||||
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;
|
||||
}
|
||||
export interface PackageUpdate extends Partial<PackageCreate> {}
|
||||
|
||||
export interface PackageListResponse {
|
||||
items: Package[];
|
||||
@@ -48,31 +38,44 @@ export interface PackageListResponse {
|
||||
pages: number;
|
||||
}
|
||||
|
||||
|
||||
export async function getPackages(
|
||||
page = 1,
|
||||
pageSize = 50,
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
companyId: number, // Obligatorio por el Mixin
|
||||
filters: Record<string, any> = {}
|
||||
): Promise<ApiResponse<PackageListResponse>> {
|
||||
const queryParams = new URLSearchParams({
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId.toString(),
|
||||
...filters
|
||||
});
|
||||
return await api.get(`/a76/packages?${queryParams.toString()}`);
|
||||
|
||||
return await api.get(`/v1/a76/packages?${params.toString()}`);
|
||||
}
|
||||
|
||||
export async function getPackage(id: number): Promise<ApiResponse<Package>> {
|
||||
return await api.get(`/a76/packages/${id}`);
|
||||
|
||||
export async function getPackage(id: number, companyId: number): Promise<ApiResponse<Package>> {
|
||||
return await api.get(`/v1/a76/packages/${id}?company_id=${companyId}`);
|
||||
}
|
||||
|
||||
export async function createPackage(data: PackageCreate): Promise<ApiResponse<Package>> {
|
||||
return await api.post(`/a76/packages`, data);
|
||||
export async function createPackage(
|
||||
data: PackageCreate,
|
||||
companyId: number
|
||||
): Promise<ApiResponse<Package>> {
|
||||
return await api.post(`/v1/a76/packages?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function updatePackage(id: number, data: PackageUpdate): Promise<ApiResponse<Package>> {
|
||||
return await api.put(`/a76/packages/${id}`, data);
|
||||
|
||||
export async function updatePackage(
|
||||
id: number,
|
||||
data: PackageUpdate,
|
||||
companyId: number
|
||||
): Promise<ApiResponse<Package>> {
|
||||
return await api.put(`/v1/a76/packages/${id}?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function deletePackage(id: number): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/a76/packages/${id}`);
|
||||
}
|
||||
export async function deletePackage(id: number, companyId: number): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/v1/a76/packages/${id}?company_id=${companyId}`);
|
||||
}
|
||||
@@ -45,24 +45,30 @@ export interface PortListResponse {
|
||||
export async function getPorts(
|
||||
page = 1,
|
||||
pageSize = 50,
|
||||
filters: Record<string, any> = {}
|
||||
filters: Record<string, any> = {},
|
||||
companyId?: number
|
||||
): Promise<ApiResponse<PortListResponse>> {
|
||||
const queryParams = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
...filters
|
||||
});
|
||||
return await api.get(`/a76/ports?${queryParams.toString()}`);
|
||||
|
||||
if (companyId) {
|
||||
queryParams.append('company_id', companyId.toString());
|
||||
}
|
||||
|
||||
return await api.get(`/v1/a76/ports?${queryParams.toString()}`);
|
||||
}
|
||||
|
||||
export async function createPort(data: PortCreate): Promise<ApiResponse<Port>> {
|
||||
return await api.post('/a76/ports', data);
|
||||
export async function createPort(data: PortCreate, companyId: number): Promise<ApiResponse<Port>> {
|
||||
return await api.post(`/v1/a76/ports?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function updatePort(id: number, data: PortUpdate): Promise<ApiResponse<Port>> {
|
||||
return await api.put(`/a76/ports/${id}`, data);
|
||||
export async function updatePort(id: number, data: PortUpdate, companyId: number): Promise<ApiResponse<Port>> {
|
||||
return await api.put(`/v1/a76/ports/${id}?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function deletePort(id: number): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/a76/ports/${id}`);
|
||||
export async function deletePort(id: number, companyId: number): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/v1/a76/ports/${id}?company_id=${companyId}`);
|
||||
}
|
||||
|
||||
@@ -2,60 +2,73 @@ 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;
|
||||
id: number;
|
||||
code: string;
|
||||
description?: string;
|
||||
customs_prevalidator?: string;
|
||||
patent_prevalidator?: string;
|
||||
|
||||
tenant_id: number;
|
||||
company_id: number;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface PrevalidatorCreate {
|
||||
code: string;
|
||||
description?: string;
|
||||
code: string;
|
||||
description?: string;
|
||||
customs_prevalidator?: string;
|
||||
patent_prevalidator?: string;
|
||||
}
|
||||
|
||||
export interface PrevalidatorUpdate extends Partial<PrevalidatorCreate> {}
|
||||
|
||||
export interface PrevalidatorListResponse {
|
||||
items: Prevalidator[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
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> = {}
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
filters: Record<string, any> = {},
|
||||
companyId?: number
|
||||
): Promise<ApiResponse<PrevalidatorListResponse>> {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
...filters
|
||||
});
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
...filters
|
||||
});
|
||||
if (companyId) {
|
||||
params.append('company_id', companyId.toString());
|
||||
}
|
||||
|
||||
const response = await api.get(`/a76/prevalidators?${params.toString()}`);
|
||||
return response.data;
|
||||
const response = await api.get(`/v1/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 getPrevalidator(id: number, companyId?: number): Promise<Prevalidator> {
|
||||
const params = new URLSearchParams();
|
||||
if (companyId) {
|
||||
params.append('company_id', companyId.toString());
|
||||
}
|
||||
const response = await api.get(`/v1/a76/prevalidators/${id}?${params.toString()}`);
|
||||
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 createPrevalidator(data: PrevalidatorCreate, companyId: number): Promise<Prevalidator> {
|
||||
const response = await api.post(`/v1/a76/prevalidators/?company_id=${companyId}`, 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 updatePrevalidator(id: number, data: PrevalidatorUpdate, companyId: number): Promise<Prevalidator> {
|
||||
const response = await api.put(`/v1/a76/prevalidators/${id}?company_id=${companyId}`, data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function deletePrevalidator(id: number): Promise<void> {
|
||||
await api.delete(`/a76/prevalidators/${id}`);
|
||||
export async function deletePrevalidator(id: number, companyId: number): Promise<void> {
|
||||
await api.delete(`/v1/a76/prevalidators/${id}?company_id=${companyId}`);
|
||||
}
|
||||
|
||||
@@ -2,62 +2,83 @@ 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;
|
||||
id: number;
|
||||
|
||||
code: string;
|
||||
signature: string | null;
|
||||
photo_path: string | null;
|
||||
|
||||
|
||||
tenant_id: number;
|
||||
company_id: number;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface SignatureCreate {
|
||||
name: string;
|
||||
position?: string;
|
||||
certificate?: string;
|
||||
code: string;
|
||||
signature?: string | null;
|
||||
photo_path?: string | null;
|
||||
|
||||
}
|
||||
|
||||
export interface SignatureUpdate extends Partial<SignatureCreate> {}
|
||||
|
||||
export interface SignatureListResponse {
|
||||
items: Signature[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
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> = {}
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
companyId: number,
|
||||
filters: Record<string, any> = {}
|
||||
): Promise<ApiResponse<SignatureListResponse>> {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
...filters
|
||||
});
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId.toString(),
|
||||
...filters
|
||||
});
|
||||
|
||||
const response = await api.get(`/a76/signatures?${params.toString()}`);
|
||||
return response.data;
|
||||
|
||||
const response = await api.get(`/v1/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 getSignature(id: number, companyId: number): Promise<Signature> {
|
||||
const response = await api.get(`/v1/a76/signatures/${id}/?company_id=${companyId}`);
|
||||
if (response.error) throw new Error(response.error);
|
||||
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 createSignature(
|
||||
data: SignatureCreate,
|
||||
companyId: number
|
||||
): Promise<Signature> {
|
||||
const response = await api.post(`/v1/a76/signatures/?company_id=${companyId}`, data);
|
||||
if (response.error) throw new Error(response.error);
|
||||
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 updateSignature(
|
||||
id: number,
|
||||
data: SignatureUpdate,
|
||||
companyId: number
|
||||
): Promise<Signature> {
|
||||
const response = await api.put(`/v1/a76/signatures/${id}/?company_id=${companyId}`, data);
|
||||
if (response.error) throw new Error(response.error);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function deleteSignature(id: number): Promise<void> {
|
||||
await api.delete(`/a76/signatures/${id}`);
|
||||
}
|
||||
export async function deleteSignature(id: number, companyId: number): Promise<void> {
|
||||
const response = await api.delete(`/v1/a76/signatures/${id}/?company_id=${companyId}`);
|
||||
if (response.error) throw new Error(response.error);
|
||||
}
|
||||
@@ -2,62 +2,80 @@ 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;
|
||||
id: number;
|
||||
from_unit_code: string;
|
||||
to_unit_code: string;
|
||||
conversion_factor: number;
|
||||
tenant_id: number; // number
|
||||
company_id: number; // number
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface UnitConversionCreate {
|
||||
from_unit_id: number;
|
||||
to_unit_id: number;
|
||||
conversion_factor: number;
|
||||
from_unit_code: string; // Ej: "KGM"
|
||||
to_unit_code: string; // Ej: "LBR"
|
||||
conversion_factor: number;
|
||||
// company_id va en la URL
|
||||
}
|
||||
|
||||
export interface UnitConversionUpdate extends Partial<UnitConversionCreate> {}
|
||||
|
||||
export interface UnitConversionListResponse {
|
||||
items: UnitConversion[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
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> = {}
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
companyId: number, // 👈 Obligatorio
|
||||
filters: Record<string, any> = {}
|
||||
): Promise<ApiResponse<UnitConversionListResponse>> {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
...filters
|
||||
});
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId.toString(),
|
||||
...filters
|
||||
});
|
||||
|
||||
const response = await api.get(`/a76/unit_conversions?${params.toString()}`);
|
||||
return response.data;
|
||||
// Agregamos /v1 y prefijo.
|
||||
// NOTA: Revisa si en tu router definiste "unit_conversions" o "unit-conversions"
|
||||
const response = await api.get(`/v1/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 getUnitConversion(id: number, companyId: number): Promise<UnitConversion> {
|
||||
const response = await api.get(`/v1/a76/unit-conversions/${id}/?company_id=${companyId}`);
|
||||
if (response.error) throw new Error(response.error);
|
||||
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 createUnitConversion(
|
||||
data: UnitConversionCreate,
|
||||
companyId: number
|
||||
): Promise<UnitConversion> {
|
||||
const response = await api.post(`/v1/a76/unit-conversions/?company_id=${companyId}`, data);
|
||||
if (response.error) throw new Error(response.error);
|
||||
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 updateUnitConversion(
|
||||
id: number,
|
||||
data: UnitConversionUpdate,
|
||||
companyId: number
|
||||
): Promise<UnitConversion> {
|
||||
const response = await api.put(`/v1/a76/unit-conversions/${id}/?company_id=${companyId}`, data);
|
||||
if (response.error) throw new Error(response.error);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function deleteUnitConversion(id: number): Promise<void> {
|
||||
await api.delete(`/a76/unit_conversions/${id}`);
|
||||
}
|
||||
export async function deleteUnitConversion(id: number, companyId: number): Promise<void> {
|
||||
const response = await api.delete(`/v1/a76/unit-conversions/${id}/?company_id=${companyId}`);
|
||||
if (response.error) throw new Error(response.error);
|
||||
}
|
||||
@@ -31,26 +31,28 @@ export interface UnitOfMeasureACEListResponse {
|
||||
export async function getUnitsOfMeasureACE(
|
||||
page = 1,
|
||||
pageSize = 50,
|
||||
companyId: number,
|
||||
filters: Record<string, any> = {}
|
||||
): Promise<ApiResponse<UnitOfMeasureACEListResponse>> {
|
||||
const queryParams = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId.toString(),
|
||||
...filters
|
||||
});
|
||||
return await api.get(`/a76/units-of-measure/ace?${queryParams.toString()}`);
|
||||
return await api.get(`/v1/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 createUnitOfMeasureACE(data: UnitOfMeasureACECreate, companyId: number): Promise<ApiResponse<UnitOfMeasureACE>> {
|
||||
return await api.post(`/v1/a76/units-of-measure/ace/?company_id=${companyId}`, 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 updateUnitOfMeasureACE(id: number, data: UnitOfMeasureACEUpdate, companyId: number): Promise<ApiResponse<UnitOfMeasureACE>> {
|
||||
return await api.put(`/v1/a76/units-of-measure/ace/${id}/?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function deleteUnitOfMeasureACE(id: number): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/a76/units-of-measure/ace/${id}`);
|
||||
export async function deleteUnitOfMeasureACE(id: number, companyId: number): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/v1/a76/units-of-measure/ace/${id}/?company_id=${companyId}`);
|
||||
}
|
||||
|
||||
// --- OMA ---
|
||||
@@ -81,28 +83,30 @@ export interface UnitOfMeasureOMAListResponse {
|
||||
}
|
||||
|
||||
export async function getUnitsOfMeasureOMA(
|
||||
page = 1,
|
||||
pageSize = 50,
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
companyId: number,
|
||||
filters: Record<string, any> = {}
|
||||
): Promise<ApiResponse<UnitOfMeasureOMAListResponse>> {
|
||||
const queryParams = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId.toString(),
|
||||
...filters
|
||||
});
|
||||
return await api.get(`/a76/units-of-measure/oma?${queryParams.toString()}`);
|
||||
return await api.get(`/v1/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 createUnitOfMeasureOMA(data: UnitOfMeasureOMACreate, companyId: number): Promise<ApiResponse<UnitOfMeasureOMA>> {
|
||||
return await api.post(`/v1/a76/units-of-measure/oma/?company_id=${companyId}`, 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 updateUnitOfMeasureOMA(id: number, data: UnitOfMeasureOMAUpdate, companyId: number): Promise<ApiResponse<UnitOfMeasureOMA>> {
|
||||
return await api.put(`/v1/a76/units-of-measure/oma/${id}/?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function deleteUnitOfMeasureOMA(id: number): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/a76/units-of-measure/oma/${id}`);
|
||||
export async function deleteUnitOfMeasureOMA(id: number, companyId: number): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/v1/a76/units-of-measure/oma/${id}/?company_id=${companyId}`);
|
||||
}
|
||||
|
||||
// --- American ---
|
||||
@@ -133,26 +137,139 @@ export interface UnitOfMeasureAmericanListResponse {
|
||||
}
|
||||
|
||||
export async function getUnitsOfMeasureAmerican(
|
||||
page = 1,
|
||||
pageSize = 50,
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
companyId: number,
|
||||
filters: Record<string, any> = {}
|
||||
): Promise<ApiResponse<UnitOfMeasureAmericanListResponse>> {
|
||||
const queryParams = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId.toString(),
|
||||
...filters
|
||||
});
|
||||
return await api.get(`/a76/units-of-measure/american?${queryParams.toString()}`);
|
||||
return await api.get(`/v1/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 createUnitOfMeasureAmerican(data: UnitOfMeasureAmericanCreate, companyId: number): Promise<ApiResponse<UnitOfMeasureAmerican>> {
|
||||
return await api.post(`/v1/a76/units-of-measure/american/?company_id=${companyId}`, 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 updateUnitOfMeasureAmerican(id: number, data: UnitOfMeasureAmericanUpdate, companyId: number): Promise<ApiResponse<UnitOfMeasureAmerican>> {
|
||||
return await api.put(`/v1/a76/units-of-measure/american/${id}/?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function deleteUnitOfMeasureAmerican(id: number): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/a76/units-of-measure/american/${id}`);
|
||||
export async function deleteUnitOfMeasureAmerican(id: number, companyId: number): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/v1/a76/units-of-measure/american/${id}/?company_id=${companyId}`);
|
||||
}
|
||||
|
||||
// --- General ---
|
||||
export interface UnitOfMeasureGeneral {
|
||||
id: number;
|
||||
code: string;
|
||||
description: string | null;
|
||||
created_at: string | null;
|
||||
updated_at: string | null;
|
||||
}
|
||||
|
||||
export interface UnitOfMeasureGeneralCreate {
|
||||
code: string;
|
||||
description?: string | null;
|
||||
}
|
||||
|
||||
export interface UnitOfMeasureGeneralUpdate {
|
||||
code?: string;
|
||||
description?: string | null;
|
||||
}
|
||||
|
||||
export interface UnitOfMeasureGeneralListResponse {
|
||||
items: UnitOfMeasureGeneral[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
}
|
||||
|
||||
export async function getUnitsOfMeasureGeneral(
|
||||
page = 1,
|
||||
pageSize = 50,
|
||||
companyId: number,
|
||||
filters: Record<string, any> = {}
|
||||
): Promise<ApiResponse<UnitOfMeasureGeneralListResponse>> {
|
||||
const queryParams = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId.toString(),
|
||||
...filters
|
||||
});
|
||||
return await api.get(`/v1/a76/units-of-measure/general/?${queryParams.toString()}`);
|
||||
}
|
||||
|
||||
export async function createUnitOfMeasureGeneral(data: UnitOfMeasureGeneralCreate, companyId: number): Promise<ApiResponse<UnitOfMeasureGeneral>> {
|
||||
return await api.post(`/v1/a76/units-of-measure/general/?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function updateUnitOfMeasureGeneral(id: number, data: UnitOfMeasureGeneralUpdate, companyId: number): Promise<ApiResponse<UnitOfMeasureGeneral>> {
|
||||
return await api.put(`/v1/a76/units-of-measure/general/${id}/?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function deleteUnitOfMeasureGeneral(id: number, companyId: number): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/v1/a76/units-of-measure/general/${id}/?company_id=${companyId}`);
|
||||
}
|
||||
|
||||
// --- Customs ---
|
||||
export interface UnitOfMeasureCustoms {
|
||||
id: number;
|
||||
code: string;
|
||||
description: string | null;
|
||||
scaii_unit_code: string | null;
|
||||
created_at: string | null;
|
||||
updated_at: string | null;
|
||||
}
|
||||
|
||||
export interface UnitOfMeasureCustomsCreate {
|
||||
code: string;
|
||||
description?: string | null;
|
||||
scaii_unit_code?: string | null;
|
||||
}
|
||||
|
||||
export interface UnitOfMeasureCustomsUpdate {
|
||||
code?: string;
|
||||
description?: string | null;
|
||||
scaii_unit_code?: string | null;
|
||||
}
|
||||
|
||||
export interface UnitOfMeasureCustomsListResponse {
|
||||
items: UnitOfMeasureCustoms[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
}
|
||||
|
||||
export async function getUnitsOfMeasureCustoms(
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
companyId: number,
|
||||
filters: Record<string, any> = {}
|
||||
): Promise<ApiResponse<UnitOfMeasureCustomsListResponse>> {
|
||||
const queryParams = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId.toString(),
|
||||
...filters
|
||||
});
|
||||
return await api.get(`/v1/a76/units-of-measure/customs/?${queryParams.toString()}`);
|
||||
}
|
||||
|
||||
export async function createUnitOfMeasureCustoms(data: UnitOfMeasureCustomsCreate, companyId: number): Promise<ApiResponse<UnitOfMeasureCustoms>> {
|
||||
return await api.post(`/v1/a76/units-of-measure/customs/?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function updateUnitOfMeasureCustoms(id: number, data: UnitOfMeasureCustomsUpdate, companyId: number): Promise<ApiResponse<UnitOfMeasureCustoms>> {
|
||||
return await api.put(`/v1/a76/units-of-measure/customs/${id}/?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function deleteUnitOfMeasureCustoms(id: number, companyId: number): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/v1/a76/units-of-measure/customs/${id}/?company_id=${companyId}`);
|
||||
}
|
||||
|
||||
318
frontend/src/lib/api/dashboard/a76/invoices.ts
Normal file
318
frontend/src/lib/api/dashboard/a76/invoices.ts
Normal file
@@ -0,0 +1,318 @@
|
||||
/**
|
||||
* API Client para Facturas (Invoices)
|
||||
* Gestiona las operaciones CRUD para facturas y sus relaciones
|
||||
*/
|
||||
import { api } from '$lib/api';
|
||||
|
||||
export type OperationType = 'imp' | 'exp';
|
||||
export type TransportType = 'none' | 'transport' | 'box' | 'licence plates' | 'truck' | 'vessel' | 'rail barge' | 'container' | 'airplane' | 'gondola' | 'flatbed';
|
||||
|
||||
// --- Interfaces ---
|
||||
|
||||
export interface InvoiceComplianceMx {
|
||||
invoice_id?: number;
|
||||
pedimento?: string | null;
|
||||
pedimento_code?: string | null;
|
||||
remesa?: number | null;
|
||||
aduana?: string | null;
|
||||
provider_header?: string | null;
|
||||
provider_id?: string | null;
|
||||
sold_to_header?: string | null;
|
||||
sold_to_id?: string | null;
|
||||
shipped_to_header?: string | null;
|
||||
shipped_to_id?: string | null;
|
||||
shipped_by_header?: string | null;
|
||||
shipped_by_id?: string | null;
|
||||
customs_broker_id?: string | null;
|
||||
is_mixed?: boolean | null;
|
||||
waste_type?: string | null;
|
||||
appendix_17?: number | null;
|
||||
edocument?: string | null;
|
||||
electronic_signature?: string | null;
|
||||
sem_id?: number | null;
|
||||
}
|
||||
|
||||
export interface InvoiceFinancials {
|
||||
id?: number;
|
||||
invoice_id?: number;
|
||||
currency?: string | null;
|
||||
currency_type?: string | null;
|
||||
exchange_rate?: number | null;
|
||||
value_mn?: number | null;
|
||||
value_me?: number | null;
|
||||
customs_value_mn?: number | null;
|
||||
freight?: number | null;
|
||||
insurance?: number | null;
|
||||
iva_mn?: number | null;
|
||||
iva_factor?: number | null;
|
||||
total_quantity?: number | null;
|
||||
gross_weight?: number | null;
|
||||
net_weight?: number | null;
|
||||
bundle_count?: number | null;
|
||||
}
|
||||
|
||||
export interface InvoiceLogistics {
|
||||
id?: number;
|
||||
invoice_id?: number;
|
||||
carrier_id?: string | null;
|
||||
transport_type?: TransportType | null;
|
||||
transport_mode?: string | null;
|
||||
driver_name?: string | null;
|
||||
is_rail?: string | null;
|
||||
rail_id?: string | null;
|
||||
vehicle_num?: string | null;
|
||||
license_plate?: string | null;
|
||||
seal_number?: string | null;
|
||||
guide_number?: string | null;
|
||||
entry_exit_date?: string | null;
|
||||
}
|
||||
|
||||
export interface InvoiceSalesDetails {
|
||||
id?: number;
|
||||
invoice_id?: number;
|
||||
line_number: number;
|
||||
sales_order?: string | null;
|
||||
colors_description?: string | null;
|
||||
square_color_code?: string | null;
|
||||
line_bundles?: number | null;
|
||||
}
|
||||
|
||||
export interface InvoiceCollections {
|
||||
id?: number;
|
||||
invoice_id?: number;
|
||||
concept?: string | null;
|
||||
is_collected?: number | null;
|
||||
collection_date?: string | null;
|
||||
amount?: number | null;
|
||||
collector_user?: string | null;
|
||||
}
|
||||
|
||||
export interface Invoice {
|
||||
id: number;
|
||||
tenant_id: number;
|
||||
company_id: number;
|
||||
operation_type?: OperationType | null;
|
||||
invoice_type?: string | null;
|
||||
invoice_number?: string | null;
|
||||
project_number?: string | null;
|
||||
purchase_order?: string | null;
|
||||
related_doc_id?: number | null;
|
||||
invoice_date?: string | null;
|
||||
capture_date: string;
|
||||
is_updated?: boolean | null;
|
||||
updated_date?: string | null;
|
||||
who_updated?: string | null;
|
||||
traffic_light_status?: string | null;
|
||||
process_log?: string | null;
|
||||
observation_es?: string | null;
|
||||
observation_en?: string | null;
|
||||
comments_status?: string | null;
|
||||
cfdi_uuid?: string | null;
|
||||
path_pdf?: string | null;
|
||||
path_xml?: string | null;
|
||||
compliance_mx?: InvoiceComplianceMx | null;
|
||||
financials?: InvoiceFinancials | null;
|
||||
logistics?: InvoiceLogistics[];
|
||||
details?: InvoiceSalesDetails[];
|
||||
collections?: InvoiceCollections[];
|
||||
}
|
||||
|
||||
export interface InvoiceListResponse {
|
||||
items: Invoice[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
}
|
||||
|
||||
export interface InvoiceData {
|
||||
operation_type?: OperationType | null;
|
||||
invoice_type?: string | null;
|
||||
invoice_number?: string | null;
|
||||
project_number?: string | null;
|
||||
purchase_order?: string | null;
|
||||
related_doc_id?: number | null;
|
||||
invoice_date?: string | null;
|
||||
traffic_light_status?: string | null;
|
||||
process_log?: string | null;
|
||||
observation_es?: string | null;
|
||||
observation_en?: string | null;
|
||||
comments_status?: string | null;
|
||||
cfdi_uuid?: string | null;
|
||||
path_pdf?: string | null;
|
||||
path_xml?: string | null;
|
||||
compliance_mx?: Omit<InvoiceComplianceMx, 'invoice_id'> | null;
|
||||
financials?: Omit<InvoiceFinancials, 'id' | 'invoice_id'> | null;
|
||||
logistics?: Omit<InvoiceLogistics, 'id' | 'invoice_id'>[] | null;
|
||||
details?: Omit<InvoiceSalesDetails, 'id' | 'invoice_id'>[] | null;
|
||||
collections?: Omit<InvoiceCollections, 'id' | 'invoice_id'>[] | null;
|
||||
}
|
||||
|
||||
export interface UpdateInvoiceData {
|
||||
operation_type?: OperationType | null;
|
||||
invoice_type?: string | null;
|
||||
invoice_number?: string | null;
|
||||
project_number?: string | null;
|
||||
purchase_order?: string | null;
|
||||
related_doc_id?: number | null;
|
||||
invoice_date?: string | null;
|
||||
traffic_light_status?: string | null;
|
||||
process_log?: string | null;
|
||||
observation_es?: string | null;
|
||||
observation_en?: string | null;
|
||||
comments_status?: string | null;
|
||||
cfdi_uuid?: string | null;
|
||||
path_pdf?: string | null;
|
||||
path_xml?: string | null;
|
||||
compliance_mx?: Partial<InvoiceComplianceMx> | null;
|
||||
financials?: Partial<InvoiceFinancials> | null;
|
||||
logistics?: Partial<InvoiceLogistics>[] | null;
|
||||
details?: Partial<InvoiceSalesDetails>[] | null;
|
||||
collections?: Partial<InvoiceCollections>[] | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* API para Facturas
|
||||
*/
|
||||
export const invoicesApi = {
|
||||
/**
|
||||
* Lista todas las facturas con paginación
|
||||
*/
|
||||
list: (companyId: number, page = 1, pageSize = 50, filters?: Record<string, any>) => {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString(),
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString()
|
||||
});
|
||||
|
||||
// Agregar filtros si existen
|
||||
if (filters) {
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
if (value !== null && value !== undefined && value !== '') {
|
||||
params.append(key, String(value));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return api.get<InvoiceListResponse>(`/v1/a76/invoices?${params.toString()}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Obtiene una factura por ID
|
||||
*/
|
||||
get: (invoiceId: number, companyId: number) => {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.get<Invoice>(`/v1/a76/invoices/${invoiceId}?${params.toString()}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Crea una nueva factura
|
||||
*/
|
||||
create: (companyId: number, data: CreateInvoiceData) => {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.post<Invoice>(`/v1/a76/invoices?${params.toString()}`, data);
|
||||
},
|
||||
|
||||
/**
|
||||
* Actualiza una factura existente
|
||||
*/
|
||||
update: (invoiceId: number, companyId: number, data: UpdateInvoiceData) => {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.put<Invoice>(`/v1/a76/invoices/${invoiceId}?${params.toString()}`, data);
|
||||
},
|
||||
|
||||
/**
|
||||
* Elimina una factura
|
||||
*/
|
||||
delete: (invoiceId: number, companyId: number) => {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.delete(`/v1/a76/invoices/${invoiceId}?${params.toString()}`);
|
||||
},
|
||||
|
||||
// --- Nested Resources ---
|
||||
|
||||
/**
|
||||
* Logística de factura
|
||||
*/
|
||||
logistics: {
|
||||
list: (invoiceId: number, companyId: number) => {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.get<InvoiceLogistics[]>(`/v1/a76/invoices/${invoiceId}/logistics?${params.toString()}`);
|
||||
},
|
||||
|
||||
create: (invoiceId: number, companyId: number, data: Omit<InvoiceLogistics, 'id' | 'invoice_id'>) => {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.post<InvoiceLogistics>(`/v1/a76/invoices/${invoiceId}/logistics?${params.toString()}`, data);
|
||||
},
|
||||
|
||||
delete: (invoiceId: number, logisticsId: number, companyId: number) => {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.delete(`/v1/a76/invoices/${invoiceId}/logistics/${logisticsId}?${params.toString()}`);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Detalles de venta de factura
|
||||
*/
|
||||
details: {
|
||||
list: (invoiceId: number, companyId: number) => {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.get<InvoiceSalesDetails[]>(`/v1/a76/invoices/${invoiceId}/details?${params.toString()}`);
|
||||
},
|
||||
|
||||
create: (invoiceId: number, companyId: number, data: Omit<InvoiceSalesDetails, 'id' | 'invoice_id'>) => {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.post<InvoiceSalesDetails>(`/v1/a76/invoices/${invoiceId}/details?${params.toString()}`, data);
|
||||
},
|
||||
|
||||
delete: (invoiceId: number, detailId: number, companyId: number) => {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.delete(`/v1/a76/invoices/${invoiceId}/details/${detailId}?${params.toString()}`);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Cobranzas de factura
|
||||
*/
|
||||
collections: {
|
||||
list: (invoiceId: number, companyId: number) => {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.get<InvoiceCollections[]>(`/v1/a76/invoices/${invoiceId}/collections?${params.toString()}`);
|
||||
},
|
||||
|
||||
create: (invoiceId: number, companyId: number, data: Omit<InvoiceCollections, 'id' | 'invoice_id'>) => {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.post<InvoiceCollections>(`/v1/a76/invoices/${invoiceId}/collections?${params.toString()}`, data);
|
||||
},
|
||||
|
||||
delete: (invoiceId: number, collectionId: number, companyId: number) => {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.delete(`/v1/a76/invoices/${invoiceId}/collections/${collectionId}?${params.toString()}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -9,6 +9,7 @@ export interface InvoiceType {
|
||||
description: string;
|
||||
note?: string;
|
||||
type?: string;
|
||||
operation?: string;
|
||||
}
|
||||
|
||||
export interface InvoiceTypeListResponse {
|
||||
@@ -40,11 +41,20 @@ export const invoiceTypesApi = {
|
||||
* Lista todos los tipos de factura con paginación
|
||||
* @param page - Número de página (por defecto 1)
|
||||
* @param pageSize - Tamaño de página (por defecto 50)
|
||||
* @param operation - Filtrar por tipo de operación (imp, exp)
|
||||
*/
|
||||
list: (page = 1, pageSize = 50) =>
|
||||
api.get<InvoiceTypeListResponse>(
|
||||
`/v1/public/refrence_data/invoice-types?page=${page}&page_size=${pageSize}`
|
||||
),
|
||||
list: (page = 1, pageSize = 50, operation?: string) => {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString()
|
||||
});
|
||||
if (operation) {
|
||||
params.append('operation', operation);
|
||||
}
|
||||
return api.get<InvoiceTypeListResponse>(
|
||||
`/v1/public/refrence_data/invoice-types?${params.toString()}`
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Obtiene un tipo de factura por key
|
||||
|
||||
@@ -1 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="techGradient" x1="16" y1="16" x2="48" y2="48" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#00F2FE" /> <stop offset="100%" stop-color="#4FACFE" /> </linearGradient>
|
||||
</defs>
|
||||
|
||||
<rect width="64" height="64" rx="18" fill="#0F172A"/>
|
||||
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M32 14L46 26V40L32 52L18 40V26L32 14ZM32 20.5L23 28.2V35.8L32 43.5L41 35.8V28.2L32 20.5Z" fill="url(#techGradient)"/>
|
||||
|
||||
<path d="M32 20.5V30M32 34V43.5" stroke="#0F172A" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M23 35.8L32 30M41 35.8L32 30" stroke="#0F172A" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 781 B |
@@ -1,127 +0,0 @@
|
||||
<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/general_catalogs/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>
|
||||
@@ -1,381 +1,221 @@
|
||||
<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 { customsBrokersApi, type CreateCustomsBrokerData } from "$lib/api/dashboard/a76/customs-brokers";
|
||||
import { companyStore } from "$lib/stores/company.svelte";
|
||||
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 { Separator } from "$lib/components/ui/separator";
|
||||
import { Loader2 } from "lucide-svelte";
|
||||
import type { CreateCustomsBrokerData, CustomsBroker } from "$lib/api/dashboard/a76/customs-brokers"; // Ajusta la ruta
|
||||
import { toast } from "svelte-sonner";
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
onSuccess
|
||||
}: {
|
||||
open: boolean;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
// --- Props ---
|
||||
export let open = false;
|
||||
export let mode: "create" | "edit" = "create";
|
||||
export let initialData: CustomsBroker | null = null;
|
||||
export let companyId: string; // Necesario según tu API
|
||||
|
||||
let formData = $state({
|
||||
broker_key: "",
|
||||
name: "",
|
||||
type: "",
|
||||
address: "",
|
||||
postal_code: "",
|
||||
city: "",
|
||||
state: "",
|
||||
phone: "",
|
||||
fax: "",
|
||||
email: "",
|
||||
country: "",
|
||||
tax_id: "",
|
||||
personal_id: "",
|
||||
position: "",
|
||||
license: "",
|
||||
company: "",
|
||||
contact: ""
|
||||
});
|
||||
// La función onSave ahora devuelve una promesa para manejar el loading aquí
|
||||
export let onSave: (data: CreateCustomsBrokerData) => Promise<void>;
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
// --- Estado ---
|
||||
let loading = false;
|
||||
|
||||
async function handleSubmit(e: Event) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!companyStore.activeCompany) {
|
||||
error = "No hay compañía seleccionada";
|
||||
return;
|
||||
}
|
||||
// Estado del formulario
|
||||
let formData: CreateCustomsBrokerData = {
|
||||
broker_key: "",
|
||||
license: "",
|
||||
name: "",
|
||||
tax_id: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
fax: "",
|
||||
contact: "",
|
||||
address: "",
|
||||
postal_code: "",
|
||||
city: "",
|
||||
state: "",
|
||||
country: "",
|
||||
tenant_id: "", // Se llenará en el submit o por defecto
|
||||
company_id: ""
|
||||
};
|
||||
|
||||
loading = true;
|
||||
error = null;
|
||||
// --- Reactividad ---
|
||||
$: if (open) {
|
||||
if (mode === "edit" && initialData) {
|
||||
// Cargar datos existentes
|
||||
formData = {
|
||||
...initialData,
|
||||
// Aseguramos que no sean null/undefined para los inputs
|
||||
name: initialData.name || "",
|
||||
tax_id: initialData.tax_id || "",
|
||||
email: initialData.email || "",
|
||||
phone: initialData.phone || "",
|
||||
fax: initialData.fax || "",
|
||||
contact: initialData.contact || "",
|
||||
address: initialData.address || "",
|
||||
postal_code: initialData.postal_code || "",
|
||||
city: initialData.city || "",
|
||||
state: initialData.state || "",
|
||||
country: initialData.country || "",
|
||||
license: initialData.license || ""
|
||||
};
|
||||
} else {
|
||||
// Reset para crear
|
||||
formData = {
|
||||
broker_key: "",
|
||||
license: "",
|
||||
name: "",
|
||||
tax_id: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
fax: "",
|
||||
contact: "",
|
||||
address: "",
|
||||
postal_code: "",
|
||||
city: "",
|
||||
state: "",
|
||||
country: "MEX", // Valor por defecto sugerido
|
||||
tenant_id: "default", // Ajustar según lógica de tu app
|
||||
company_id: companyId
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const payload: CreateCustomsBrokerData = {
|
||||
broker_key: formData.broker_key,
|
||||
name: formData.name || null,
|
||||
type: formData.type || null,
|
||||
address: formData.address || null,
|
||||
postal_code: formData.postal_code || null,
|
||||
city: formData.city || null,
|
||||
state: formData.state || null,
|
||||
phone: formData.phone || null,
|
||||
fax: formData.fax || null,
|
||||
email: formData.email || null,
|
||||
country: formData.country || null,
|
||||
tax_id: formData.tax_id || null,
|
||||
personal_id: formData.personal_id || null,
|
||||
position: formData.position || null,
|
||||
license: formData.license || null,
|
||||
company: formData.company || null,
|
||||
contact: formData.contact || null,
|
||||
tenant_id: "1", // TODO: Get from user context
|
||||
company_id: companyStore.activeCompany.id.toString()
|
||||
};
|
||||
// --- Handlers ---
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
loading = true;
|
||||
|
||||
const response = await customsBrokersApi.create(payload);
|
||||
// Validaciones básicas
|
||||
if (!formData.broker_key) {
|
||||
toast.error("La Clave del Agente es obligatoria");
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
if (!formData.license) {
|
||||
toast.error("La Patente es obligatoria");
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.error) {
|
||||
if (response.status === 401) {
|
||||
error = 'Sesión expirada. Recargando página...';
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1500);
|
||||
} else {
|
||||
error = response.error;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Inyectar company_id si no viene
|
||||
const payload = { ...formData, company_id: companyId };
|
||||
|
||||
// Éxito
|
||||
open = false;
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : "Error al guardar";
|
||||
console.error("Error saving:", e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleOpenChange(newOpen: boolean) {
|
||||
if (!newOpen) {
|
||||
// Limpiar form al cerrar
|
||||
formData = {
|
||||
broker_key: "",
|
||||
name: "",
|
||||
type: "",
|
||||
address: "",
|
||||
postal_code: "",
|
||||
city: "",
|
||||
state: "",
|
||||
phone: "",
|
||||
fax: "",
|
||||
email: "",
|
||||
country: "",
|
||||
tax_id: "",
|
||||
personal_id: "",
|
||||
position: "",
|
||||
license: "",
|
||||
company: "",
|
||||
contact: ""
|
||||
};
|
||||
error = null;
|
||||
}
|
||||
open = newOpen;
|
||||
}
|
||||
await onSave(payload);
|
||||
open = false;
|
||||
toast.success(mode === 'create' ? "Agente creado correctamente" : "Agente actualizado correctamente");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error("Error al guardar el agente aduanal");
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open onOpenChange={handleOpenChange}>
|
||||
<Dialog.Content class="sm:max-w-[700px] max-h-[90vh] overflow-y-auto">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Nuevo Agente Aduanal</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Completa los datos para crear un nuevo agente aduanal.
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="sm:max-w-[700px] max-h-[90vh] overflow-y-auto">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>
|
||||
{mode === "create" ? "Nuevo Agente Aduanal" : "Editar Agente Aduanal"}
|
||||
</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Ingresa los datos generales del agente. La configuración de VU y Personal se gestiona aparte.
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<form onsubmit={handleSubmit} class="space-y-6">
|
||||
{#if error}
|
||||
<div class="rounded-lg border border-destructive bg-destructive/10 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="grid gap-6 py-4">
|
||||
|
||||
<div class="space-y-4">
|
||||
<h4 class="text-sm font-medium leading-none text-muted-foreground">Identificación</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="broker_key">Clave Agente *</Label>
|
||||
<Input id="broker_key" bind:value={formData.broker_key} placeholder="Ej. 550" disabled={mode === 'edit' || loading} />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="license">Patente *</Label>
|
||||
<Input id="license" bind:value={formData.license} placeholder="Ej. 3421" disabled={loading} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2 col-span-2">
|
||||
<Label for="name">Nombre / Razón Social</Label>
|
||||
<Input id="name" bind:value={formData.name} placeholder="Nombre del Agente o Agencia" disabled={loading} />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="tax_id">RFC</Label>
|
||||
<Input id="tax_id" bind:value={formData.tax_id} placeholder="RFC de la agencia" disabled={loading} />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="contact">Nombre Contacto</Label>
|
||||
<Input id="contact" bind:value={formData.contact} placeholder="Persona de contacto" disabled={loading} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Información básica -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-sm font-semibold">Información Básica</h3>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="broker_key">Clave *</Label>
|
||||
<Input
|
||||
id="broker_key"
|
||||
bind:value={formData.broker_key}
|
||||
placeholder="Ej: 12345"
|
||||
maxlength={5}
|
||||
required
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
<Separator />
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="type">Tipo</Label>
|
||||
<Input
|
||||
id="type"
|
||||
bind:value={formData.type}
|
||||
placeholder="Tipo de agente"
|
||||
maxlength={9}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<h4 class="text-sm font-medium leading-none text-muted-foreground">Contacto</h4>
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div class="space-y-2 col-span-1">
|
||||
<Label for="phone">Teléfono</Label>
|
||||
<Input id="phone" bind:value={formData.phone} disabled={loading} />
|
||||
</div>
|
||||
<div class="space-y-2 col-span-2">
|
||||
<Label for="email">Correo Electrónico</Label>
|
||||
<Input id="email" type="email" bind:value={formData.email} disabled={loading} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="name">Nombre</Label>
|
||||
<Input
|
||||
id="name"
|
||||
bind:value={formData.name}
|
||||
placeholder="Nombre del agente aduanal"
|
||||
maxlength={80}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
<Separator />
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="license">Patente</Label>
|
||||
<Input
|
||||
id="license"
|
||||
bind:value={formData.license}
|
||||
placeholder="Número de patente"
|
||||
maxlength={4}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<h4 class="text-sm font-medium leading-none text-muted-foreground">Dirección Fiscal</h4>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="address">Calle y Número</Label>
|
||||
<Input id="address" bind:value={formData.address} disabled={loading} />
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="company">Empresa</Label>
|
||||
<Input
|
||||
id="company"
|
||||
bind:value={formData.company}
|
||||
placeholder="Empresa del agente"
|
||||
maxlength={200}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="postal_code">C.P.</Label>
|
||||
<Input id="postal_code" bind:value={formData.postal_code} disabled={loading} />
|
||||
</div>
|
||||
<div class="space-y-2 col-span-2">
|
||||
<Label for="city">Ciudad</Label>
|
||||
<Input id="city" bind:value={formData.city} disabled={loading} />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="state">Estado</Label>
|
||||
<Input id="state" bind:value={formData.state} disabled={loading} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="country">País</Label>
|
||||
<Input id="country" bind:value={formData.country} disabled={loading} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Información de contacto -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-sm font-semibold">Información de Contacto</h3>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="phone">Teléfono</Label>
|
||||
<Input
|
||||
id="phone"
|
||||
bind:value={formData.phone}
|
||||
placeholder="Número telefónico"
|
||||
maxlength={30}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="fax">Fax</Label>
|
||||
<Input
|
||||
id="fax"
|
||||
bind:value={formData.fax}
|
||||
placeholder="Número de fax"
|
||||
maxlength={30}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
bind:value={formData.email}
|
||||
placeholder="correo@ejemplo.com"
|
||||
maxlength={100}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="contact">Contacto</Label>
|
||||
<Input
|
||||
id="contact"
|
||||
bind:value={formData.contact}
|
||||
placeholder="Nombre del contacto"
|
||||
maxlength={80}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dirección -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-sm font-semibold">Dirección</h3>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="address">Dirección</Label>
|
||||
<Input
|
||||
id="address"
|
||||
bind:value={formData.address}
|
||||
placeholder="Calle y número"
|
||||
maxlength={1500}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="postal_code">Código Postal</Label>
|
||||
<Input
|
||||
id="postal_code"
|
||||
bind:value={formData.postal_code}
|
||||
placeholder="C.P."
|
||||
maxlength={15}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="city">Ciudad</Label>
|
||||
<Input
|
||||
id="city"
|
||||
bind:value={formData.city}
|
||||
placeholder="Ciudad"
|
||||
maxlength={30}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="state">Estado</Label>
|
||||
<Input
|
||||
id="state"
|
||||
bind:value={formData.state}
|
||||
placeholder="Estado"
|
||||
maxlength={30}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="country">País</Label>
|
||||
<Input
|
||||
id="country"
|
||||
bind:value={formData.country}
|
||||
placeholder="País"
|
||||
maxlength={3}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Información fiscal -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-sm font-semibold">Información Fiscal</h3>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="tax_id">RFC</Label>
|
||||
<Input
|
||||
id="tax_id"
|
||||
bind:value={formData.tax_id}
|
||||
placeholder="RFC"
|
||||
maxlength={30}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="personal_id">CURP</Label>
|
||||
<Input
|
||||
id="personal_id"
|
||||
bind:value={formData.personal_id}
|
||||
placeholder="CURP"
|
||||
maxlength={20}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="position">Posición</Label>
|
||||
<Input
|
||||
id="position"
|
||||
bind:value={formData.position}
|
||||
placeholder="Cargo o posición"
|
||||
maxlength={30}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button type="button" variant="outline" onclick={() => (open = false)} disabled={loading}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{#if loading}
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-4 w-4 animate-spin rounded-full border-2 border-primary-foreground border-t-transparent"></div>
|
||||
Guardando...
|
||||
</div>
|
||||
{:else}
|
||||
Crear Agente Aduanal
|
||||
{/if}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
<Dialog.Footer>
|
||||
<Button variant="outline" on:click={() => (open = false)} disabled={loading}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button on:click={handleSubmit} disabled={loading}>
|
||||
{#if loading}
|
||||
<Loader2 class="mr-2 h-4 w-4 animate-spin" />
|
||||
Guardando
|
||||
{:else}
|
||||
Guardar Agente
|
||||
{/if}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -5,44 +5,44 @@ import DataTableActions from './data-table-actions.svelte';
|
||||
|
||||
export function createColumns(onSuccess?: () => void): ColumnDef<ExchangeRate>[] {
|
||||
return [
|
||||
{
|
||||
accessorKey: 'date',
|
||||
header: 'Fecha',
|
||||
cell: ({ row }) => {
|
||||
const dateStr = row.original.date;
|
||||
if (!dateStr) return 'N/A';
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleDateString('es-MX');
|
||||
{
|
||||
accessorKey: 'date',
|
||||
header: 'Fecha',
|
||||
cell: ({ row }) => {
|
||||
const dateStr = row.original.date;
|
||||
if (!dateStr) return 'N/A';
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleDateString('es-MX');
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'value',
|
||||
header: 'Valor',
|
||||
cell: ({ row }) => {
|
||||
const value = row.original.value;
|
||||
if (value === null || value === undefined) return 'N/A';
|
||||
return value.toFixed(6);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'local_currency',
|
||||
header: 'Moneda Local',
|
||||
cell: ({ row }) => row.original.local_currency ?? 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'foreign_currency',
|
||||
header: 'Moneda Extranjera',
|
||||
cell: ({ row }) => row.original.foreign_currency ?? 'N/A'
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: 'Acciones',
|
||||
cell: ({ row }) => {
|
||||
return renderComponent(DataTableActions, {
|
||||
item: row.original,
|
||||
onSuccess
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'value',
|
||||
header: 'Tipo de Cambio',
|
||||
cell: ({ row }) => {
|
||||
const value = row.original.value;
|
||||
if (value === null || value === undefined) return 'N/A';
|
||||
return value.toFixed(6);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'local_currency',
|
||||
header: 'Moneda Local',
|
||||
cell: ({ row }) => row.original.local_currency ?? 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'foreign_currency',
|
||||
header: 'Moneda Extranjera',
|
||||
cell: ({ row }) => row.original.foreign_currency ?? 'N/A'
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: 'Acciones',
|
||||
cell: ({ row }) => {
|
||||
return renderComponent(DataTableActions, {
|
||||
item: row.original,
|
||||
onSuccess
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -1,164 +1,151 @@
|
||||
<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 { LoaderCircle } from 'lucide-svelte';
|
||||
import type {
|
||||
ExchangeRate,
|
||||
ExchangeRateCreate,
|
||||
ExchangeRateUpdate
|
||||
} from '$lib/api/dashboard/a76/general_catalogs/exchange-rate';
|
||||
import { createExchangeRate, updateExchangeRate } from '$lib/api/dashboard/a76/general_catalogs/exchange-rate';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
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 {
|
||||
createExchangeRate,
|
||||
updateExchangeRate,
|
||||
type ExchangeRate
|
||||
} from "$lib/api/dashboard/a76/general_catalogs/exchange-rate";
|
||||
import { companyStore } from "$lib/stores/company.svelte";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
item?: ExchangeRate | null;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSuccess?: (item: ExchangeRate) => void;
|
||||
}
|
||||
let {
|
||||
open = $bindable(false),
|
||||
item = null,
|
||||
onSuccess
|
||||
}: {
|
||||
open: boolean;
|
||||
item?: ExchangeRate | null;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
let { open = $bindable(false), item = null, onOpenChange, onSuccess }: Props = $props();
|
||||
const isEdit = $derived(!!item);
|
||||
const title = $derived(isEdit ? "Editar Tipo de Cambio" : "Nuevo Tipo de Cambio");
|
||||
|
||||
let formData = $state<ExchangeRateCreate | ExchangeRateUpdate>({
|
||||
date: '',
|
||||
value: null,
|
||||
local_currency: null,
|
||||
foreign_currency: null
|
||||
});
|
||||
let formData = $state({
|
||||
date: '',
|
||||
value: null as number | null,
|
||||
local_currency: '',
|
||||
foreign_currency: ''
|
||||
});
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
let isEdit = $derived(!!item);
|
||||
// Cargar datos al abrir
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
if (item && item.date) {
|
||||
const formattedDate = item.date.includes('T') ? item.date.split('T')[0] : item.date;
|
||||
formData = {
|
||||
date: formattedDate,
|
||||
value: item.value,
|
||||
local_currency: item.local_currency || '',
|
||||
foreign_currency: item.foreign_currency || ''
|
||||
};
|
||||
} else {
|
||||
formData = {
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
value: null,
|
||||
local_currency: 'MXN',
|
||||
foreign_currency: 'USD'
|
||||
};
|
||||
}
|
||||
error = null;
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (item) {
|
||||
const date = new Date(item.date);
|
||||
const dateStr = date.toISOString().split('T')[0];
|
||||
|
||||
formData = {
|
||||
date: dateStr,
|
||||
value: item.value,
|
||||
local_currency: item.local_currency,
|
||||
foreign_currency: item.foreign_currency
|
||||
};
|
||||
} else {
|
||||
const today = new Date();
|
||||
const dateStr = today.toISOString().split('T')[0];
|
||||
formData = {
|
||||
date: dateStr,
|
||||
value: null,
|
||||
local_currency: null,
|
||||
foreign_currency: null
|
||||
};
|
||||
}
|
||||
error = null;
|
||||
});
|
||||
async function handleSubmit() {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) throw new Error('No hay una compañía seleccionada');
|
||||
|
||||
async function handleSubmit(e: Event) {
|
||||
e.preventDefault();
|
||||
loading = true;
|
||||
error = null;
|
||||
if (!formData.date) throw new Error('La fecha es requerida');
|
||||
if (formData.value === null) throw new Error('El valor es requerido');
|
||||
|
||||
const dataToSend = {
|
||||
date: formData.date,
|
||||
value: Number(formData.value),
|
||||
local_currency: formData.local_currency?.trim().toUpperCase() || null,
|
||||
foreign_currency: formData.foreign_currency?.trim().toUpperCase() || null
|
||||
};
|
||||
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
error = 'No hay una empresa seleccionada';
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
if (isEdit && item) {
|
||||
await updateExchangeRate(item.id, dataToSend, companyId);
|
||||
alert(`✅ Tipo de cambio actualizado correctamente`);
|
||||
} else {
|
||||
await createExchangeRate(dataToSend, companyId);
|
||||
alert(`✅ Tipo de cambio creado correctamente`);
|
||||
}
|
||||
|
||||
try {
|
||||
let result: ExchangeRate;
|
||||
if (isEdit && item) {
|
||||
result = await updateExchangeRate(item.id, formData as ExchangeRateUpdate, companyId);
|
||||
} else {
|
||||
result = await createExchangeRate(formData as ExchangeRateCreate, companyId);
|
||||
}
|
||||
|
||||
if (onSuccess) {
|
||||
onSuccess(result);
|
||||
}
|
||||
onOpenChange(false);
|
||||
} catch (err: any) {
|
||||
error = err.message || `Error al ${isEdit ? 'actualizar' : 'crear'} el tipo de cambio`;
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
open = false;
|
||||
if (onSuccess) onSuccess();
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Error al guardar';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root {open} onOpenChange={onOpenChange}>
|
||||
<Dialog.Content class="sm:max-w-[500px]">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{isEdit ? 'Editar' : 'Crear'} Tipo de Cambio</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay class="fixed inset-0 z-[9999] bg-black/80 backdrop-blur-sm" />
|
||||
|
||||
<Dialog.Content class="fixed left-[50%] top-[50%] z-[10000] w-full max-w-[500px] translate-x-[-50%] translate-y-[-50%] border bg-background p-6 shadow-lg sm:rounded-lg">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<form onsubmit={handleSubmit} class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="date">Fecha *</Label>
|
||||
<Input
|
||||
id="date"
|
||||
type="date"
|
||||
bind:value={formData.date}
|
||||
required
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="space-y-4 py-4">
|
||||
{#if error}
|
||||
<div class="rounded-md bg-destructive/15 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="value">Tipo de Cambio *</Label>
|
||||
<Input
|
||||
id="value"
|
||||
type="number"
|
||||
step="0.000001"
|
||||
bind:value={formData.value}
|
||||
placeholder="0.000000"
|
||||
required
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-4">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="date" class="text-right">Fecha *</Label>
|
||||
<div class="col-span-3">
|
||||
<Input id="date" type="date" bind:value={formData.date} disabled={loading} required />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="local_currency">Moneda Local</Label>
|
||||
<Input
|
||||
id="local_currency"
|
||||
type="text"
|
||||
maxlength="7"
|
||||
bind:value={formData.local_currency}
|
||||
placeholder="MXN"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="value" class="text-right">Valor *</Label>
|
||||
<div class="col-span-3">
|
||||
<Input id="value" type="number" step="0.000001" bind:value={formData.value} disabled={loading} required />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="foreign_currency">Moneda Extranjera</Label>
|
||||
<Input
|
||||
id="foreign_currency"
|
||||
type="text"
|
||||
maxlength="7"
|
||||
bind:value={formData.foreign_currency}
|
||||
placeholder="USD"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="local_currency" class="text-right">Local</Label>
|
||||
<div class="col-span-3">
|
||||
<Input id="local_currency" bind:value={formData.local_currency} maxlength={3} disabled={loading} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<p class="text-sm text-destructive">{error}</p>
|
||||
{/if}
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="foreign_currency" class="text-right">Extranjera</Label>
|
||||
<div class="col-span-3">
|
||||
<Input id="foreign_currency" bind:value={formData.foreign_currency} maxlength={3} disabled={loading} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button type="button" variant="outline" onclick={() => onOpenChange(false)} disabled={loading}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{#if loading}
|
||||
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
{isEdit ? 'Actualizar' : 'Crear'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
<Dialog.Footer>
|
||||
<Button type="button" variant="outline" onclick={() => open = false} disabled={loading}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? 'Guardando...' : isEdit ? 'Actualizar' : 'Crear'}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
@@ -27,23 +27,21 @@
|
||||
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
alert('No hay compañía seleccionada');
|
||||
alert('❌ Error: No hay compañía seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
await deleteExchangeRate(item.id, companyId);
|
||||
|
||||
// Éxito
|
||||
alert(`✅ Tipo de cambio del ${new Date(item.date).toLocaleDateString('es-MX')} eliminado correctamente`);
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
} catch (err: any) {
|
||||
error = err.message || 'Error al eliminar el tipo de cambio';
|
||||
alert(`Error: ${error}`);
|
||||
alert(`❌ Error: ${error}`);
|
||||
console.error('Error deleting:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
|
||||
@@ -1,123 +1,106 @@
|
||||
<script lang="ts" generics="TData, TValue">
|
||||
import { onMount } from 'svelte';
|
||||
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 {
|
||||
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[];
|
||||
loading: boolean;
|
||||
hasMore: boolean;
|
||||
loadMore: () => void;
|
||||
};
|
||||
type DataTableProps<TData, TValue> = {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
data: TData[];
|
||||
pageCount: number;
|
||||
totalItems: number;
|
||||
};
|
||||
|
||||
let {
|
||||
data,
|
||||
columns,
|
||||
loading,
|
||||
hasMore,
|
||||
loadMore
|
||||
}: DataTableProps<TData, TValue> = $props();
|
||||
let {
|
||||
data,
|
||||
columns,
|
||||
pageCount,
|
||||
totalItems
|
||||
}: DataTableProps<TData, TValue> = $props();
|
||||
|
||||
const table = createSvelteTable({
|
||||
get data() {
|
||||
return data;
|
||||
},
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel()
|
||||
});
|
||||
const table = createSvelteTable({
|
||||
get data() { return data; },
|
||||
get columns() { return columns; },
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
get pageCount() { return pageCount; },
|
||||
});
|
||||
|
||||
let scrollContainer = $state<HTMLDivElement>();
|
||||
let loadingTrigger = $state<HTMLDivElement>();
|
||||
function handlePageChange(newPage: number) {
|
||||
const url = new URL($page.url);
|
||||
url.searchParams.set('page', newPage.toString());
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
}
|
||||
|
||||
// Intersection Observer para detectar cuando el usuario llega al final
|
||||
onMount(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const [entry] = entries;
|
||||
if (entry.isIntersecting && hasMore && !loading) {
|
||||
loadMore();
|
||||
}
|
||||
},
|
||||
{
|
||||
root: scrollContainer,
|
||||
threshold: 0.1
|
||||
}
|
||||
);
|
||||
|
||||
if (loadingTrigger) {
|
||||
observer.observe(loadingTrigger);
|
||||
}
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
});
|
||||
const currentPage = $derived(Number($page.url.searchParams.get('page') || 1));
|
||||
</script>
|
||||
|
||||
<div class="w-full">
|
||||
<div class="rounded-md border max-h-[600px] overflow-y-auto" bind:this={scrollContainer}>
|
||||
<Table.Root>
|
||||
<Table.Header class="sticky top-0 bg-background z-10">
|
||||
{#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}
|
||||
|
||||
<!-- Loading Trigger - Se activa cuando es visible -->
|
||||
{#if hasMore}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={columns.length} class="h-20 text-center">
|
||||
<div bind:this={loadingTrigger}>
|
||||
{#if loading}
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
<div class="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
|
||||
<span class="text-muted-foreground text-sm">Cargando más...</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-muted-foreground text-sm">
|
||||
Desplázate para cargar más
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/if}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</div>
|
||||
<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 px-2">
|
||||
<div class="flex-1 text-sm text-muted-foreground">
|
||||
Total: {totalItems} registros
|
||||
</div>
|
||||
<div class="space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => handlePageChange(currentPage - 1)}
|
||||
disabled={currentPage <= 1}
|
||||
>
|
||||
Anterior
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => handlePageChange(currentPage + 1)}
|
||||
disabled={currentPage >= pageCount}
|
||||
>
|
||||
Siguiente
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
<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";
|
||||
// 👇 1. Importamos la API correcta
|
||||
import {
|
||||
updateClassificationConcept,
|
||||
createClassificationConcept,
|
||||
type ClassificationConcept
|
||||
} from "$lib/api/dashboard/a76/general_catalogs/classification-concepts";
|
||||
// 👇 2. Importamos el Store para el ID de la empresa
|
||||
import { companyStore } from "$lib/stores/company.svelte";
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
item = null,
|
||||
onSuccess
|
||||
}: {
|
||||
open: boolean;
|
||||
item?: ClassificationConcept | null;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
const isEdit = $derived(!!item);
|
||||
const title = $derived(isEdit ? "Editar Clasificación" : "Nueva Clasificación");
|
||||
|
||||
// 👇 3. Estado limpio: Solo lo que existe en la BD
|
||||
let formData = $state({
|
||||
classification: ''
|
||||
});
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// Cargar datos al abrir
|
||||
$effect(() => {
|
||||
if (item) {
|
||||
formData = {
|
||||
classification: item.classification || ''
|
||||
};
|
||||
} else {
|
||||
formData = {
|
||||
classification: ''
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
error = null;
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
// 👇 4. Validar Company ID
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) throw new Error('No hay una compañía seleccionada');
|
||||
|
||||
// Validar campos
|
||||
if (!formData.classification.trim()) throw new Error('El nombre de la clasificación es requerido');
|
||||
|
||||
const dataToSend = {
|
||||
classification: formData.classification.trim()
|
||||
};
|
||||
|
||||
// 👇 5. Llamar a la API pasando el companyId
|
||||
if (isEdit && item) {
|
||||
await updateClassificationConcept(item.id, dataToSend, companyId);
|
||||
} else {
|
||||
await createClassificationConcept(dataToSend, companyId);
|
||||
}
|
||||
|
||||
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="p-3 text-sm text-destructive bg-destructive/10 rounded-md">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="grid gap-4 py-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="classification">Clasificación <span class="text-destructive">*</span></Label>
|
||||
<Input
|
||||
id="classification"
|
||||
bind:value={formData.classification}
|
||||
placeholder="Ej: GENERAL"
|
||||
maxlength={30}
|
||||
/>
|
||||
</div>
|
||||
</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>
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { ClassificationConcept } from '$lib/api/dashboard/a76/general_catalogs/classification-concepts';
|
||||
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<ClassificationConcept>[] {
|
||||
return [
|
||||
{
|
||||
accessorKey: 'classification',
|
||||
header: 'Clasificación',
|
||||
cell: ({ row }) => row.original.classification || '-'
|
||||
},
|
||||
{
|
||||
accessorKey: 'description',
|
||||
header: 'Descripción',
|
||||
cell: ({ row }) => row.original.description || '-'
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: 'Acciones',
|
||||
cell: ({ row }) => {
|
||||
return renderComponent(DataTableActions, {
|
||||
item: row.original,
|
||||
onSuccess
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
|
||||
import { deleteClassificationConcept, type ClassificationConcept } from "$lib/api/dashboard/a76/general_catalogs/classification-concepts";
|
||||
import { companyStore } from "$lib/stores/company.svelte";
|
||||
import { EllipsisVertical, Pencil, LoaderCircle, Trash2 } from 'lucide-svelte';
|
||||
import CreateEditDialog from "$lib/components/dashboard/general_catalogs/classification/create-edit-dialog.svelte";
|
||||
|
||||
let {
|
||||
item,
|
||||
onSuccess
|
||||
}: {
|
||||
item: ClassificationConcept;
|
||||
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 clasificación "${item.classification}"?\n\nNota: No se puede eliminar si tiene registros relacionados.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!companyStore.activeCompany) {
|
||||
alert('❌ Error: No hay una compañía seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const response = await deleteClassificationConcept(item.id, companyStore.activeCompany.id);
|
||||
|
||||
if (response.error) {
|
||||
alert(`❌ Error al eliminar:\n\n${response.error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Éxito
|
||||
if (response.status === 204 || response.status === 200 || !response.error) {
|
||||
alert(`✅ Clasificación "${item.classification}" eliminada correctamente`);
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
const errorMsg = e instanceof Error ? e.message : 'Error desconocido al eliminar el registro';
|
||||
alert(`❌ Error al eliminar:\n\n${errorMsg}`);
|
||||
} 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}
|
||||
/>
|
||||
@@ -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; },
|
||||
get columns() { return columns; },
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
get pageCount() { return pageCount; },
|
||||
});
|
||||
|
||||
function handlePageChange(newPage: number) {
|
||||
const url = new URL($page.url);
|
||||
url.searchParams.set('page', newPage.toString());
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
}
|
||||
|
||||
const currentPage = $derived(Number($page.url.searchParams.get('page') || 1));
|
||||
</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 px-2">
|
||||
<div class="flex-1 text-sm text-muted-foreground">
|
||||
Total: {totalItems} registros
|
||||
</div>
|
||||
<div class="space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => handlePageChange(currentPage - 1)}
|
||||
disabled={currentPage <= 1}
|
||||
>
|
||||
Anterior
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => handlePageChange(currentPage + 1)}
|
||||
disabled={currentPage >= pageCount}
|
||||
>
|
||||
Siguiente
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,296 @@
|
||||
<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 Tabs from "$lib/components/ui/tabs";
|
||||
import { Switch } from "$lib/components/ui/switch";
|
||||
import { createCompany, updateCompany, type Company } from "$lib/api/dashboard/a76/general_catalogs/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");
|
||||
|
||||
// Inicialización completa con todos los campos del modelo
|
||||
let formData = $state({
|
||||
// General
|
||||
name: '',
|
||||
rfc: '',
|
||||
curp: '',
|
||||
main_activity: '',
|
||||
|
||||
// Programas
|
||||
program: '',
|
||||
program_number: '',
|
||||
prosec: 0,
|
||||
prosec_authorization: '',
|
||||
|
||||
// Responsable
|
||||
responsible_name: '',
|
||||
responsible_last_name: '',
|
||||
responsible_mother_last_name: '',
|
||||
responsible_rfc: '',
|
||||
position: '',
|
||||
|
||||
// Configuración / Operativo
|
||||
manufacturer_id: '',
|
||||
has_express_line: false,
|
||||
is_service_company: false,
|
||||
order_format_type: '',
|
||||
|
||||
// Certificaciones
|
||||
ctpat_svi: '',
|
||||
trusted_exporter_number: ''
|
||||
});
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// Cargar datos al abrir para editar
|
||||
$effect(() => {
|
||||
if (item) {
|
||||
formData = {
|
||||
name: item.name || '',
|
||||
rfc: item.rfc || '',
|
||||
curp: item.curp || '',
|
||||
main_activity: item.main_activity || '',
|
||||
|
||||
program: item.program || '',
|
||||
program_number: item.program_number || '',
|
||||
prosec: item.prosec || 0,
|
||||
prosec_authorization: item.prosec_authorization || '',
|
||||
|
||||
responsible_name: item.responsible_name || '',
|
||||
responsible_last_name: item.responsible_last_name || '',
|
||||
responsible_mother_last_name: item.responsible_mother_last_name || '',
|
||||
responsible_rfc: item.responsible_rfc || '',
|
||||
position: item.position || '',
|
||||
|
||||
manufacturer_id: item.manufacturer_id || '',
|
||||
has_express_line: item.has_express_line || false,
|
||||
is_service_company: item.is_service_company || false,
|
||||
order_format_type: item.order_format_type || '',
|
||||
|
||||
ctpat_svi: item.ctpat_svi || '',
|
||||
trusted_exporter_number: item.trusted_exporter_number || ''
|
||||
};
|
||||
} else {
|
||||
// Reset manual si es nuevo
|
||||
formData = {
|
||||
name: '', rfc: '', curp: '', main_activity: '',
|
||||
program: '', program_number: '', prosec: 0, prosec_authorization: '',
|
||||
responsible_name: '', responsible_last_name: '', responsible_mother_last_name: '', responsible_rfc: '', position: '',
|
||||
manufacturer_id: '', has_express_line: false, is_service_company: false, order_format_type: '',
|
||||
ctpat_svi: '', trusted_exporter_number: ''
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
error = null;
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
if (!formData.name.trim()) throw new Error('El nombre es requerido');
|
||||
if (!formData.rfc.trim()) throw new Error('El RFC es requerido');
|
||||
|
||||
// Construir payload limpiando strings vacíos a null
|
||||
const dataToSend = {
|
||||
name: formData.name.trim(),
|
||||
rfc: formData.rfc.trim(),
|
||||
curp: formData.curp.trim() || null,
|
||||
main_activity: formData.main_activity.trim() || null,
|
||||
|
||||
program: formData.program.trim() || null,
|
||||
program_number: formData.program_number.trim() || null,
|
||||
prosec: formData.prosec || null,
|
||||
prosec_authorization: formData.prosec_authorization.trim() || null,
|
||||
|
||||
// Concatenar nombre completo del responsable si se desea guardar en 'responsible' también
|
||||
responsible: `${formData.responsible_name} ${formData.responsible_last_name}`.trim() || null,
|
||||
responsible_name: formData.responsible_name.trim() || null,
|
||||
responsible_last_name: formData.responsible_last_name.trim() || null,
|
||||
responsible_mother_last_name: formData.responsible_mother_last_name.trim() || null,
|
||||
responsible_rfc: formData.responsible_rfc.trim() || null,
|
||||
position: formData.position.trim() || null,
|
||||
|
||||
manufacturer_id: formData.manufacturer_id.trim() || null,
|
||||
has_express_line: formData.has_express_line,
|
||||
is_service_company: formData.is_service_company,
|
||||
order_format_type: formData.order_format_type.trim() || null,
|
||||
|
||||
ctpat_svi: formData.ctpat_svi.trim() || null,
|
||||
trusted_exporter_number: formData.trusted_exporter_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-2xl max-h-[90vh] overflow-y-auto">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="space-y-4">
|
||||
{#if error}
|
||||
<div class="p-3 text-sm text-red-600 bg-red-50 rounded-md border border-red-200">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Tabs.Root value="general" class="w-full">
|
||||
<Tabs.List class="grid w-full grid-cols-4">
|
||||
<Tabs.Trigger value="general">General</Tabs.Trigger>
|
||||
<Tabs.Trigger value="programas">Programas</Tabs.Trigger>
|
||||
<Tabs.Trigger value="responsable">Responsable</Tabs.Trigger>
|
||||
<Tabs.Trigger value="config">Config</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
|
||||
<Tabs.Content value="general" class="space-y-4 py-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="name">Razón Social <span class="text-destructive">*</span></Label>
|
||||
<Input id="name" bind:value={formData.name} placeholder="Nombre de la empresa" />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="rfc">RFC <span class="text-destructive">*</span></Label>
|
||||
<Input id="rfc" bind:value={formData.rfc} maxlength={13} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="curp">CURP</Label>
|
||||
<Input id="curp" bind:value={formData.curp} maxlength={18} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="main_activity">Actividad Principal</Label>
|
||||
<Input id="main_activity" bind:value={formData.main_activity} />
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="programas" class="space-y-4 py-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="program">Programa (IMMEX)</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>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="prosec">Sector PROSEC (ID)</Label>
|
||||
<Input id="prosec" type="number" bind:value={formData.prosec} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="prosec_auth">Autorización PROSEC</Label>
|
||||
<Input id="prosec_auth" bind:value={formData.prosec_authorization} />
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="responsable" class="space-y-4 py-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="resp_name">Nombre</Label>
|
||||
<Input id="resp_name" bind:value={formData.responsible_name} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="resp_last">Apellido Paterno</Label>
|
||||
<Input id="resp_last" bind:value={formData.responsible_last_name} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="resp_mother">Apellido Materno</Label>
|
||||
<Input id="resp_mother" bind:value={formData.responsible_mother_last_name} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="resp_rfc">RFC Responsable</Label>
|
||||
<Input id="resp_rfc" bind:value={formData.responsible_rfc} maxlength={13} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="position">Puesto / Cargo</Label>
|
||||
<Input id="position" bind:value={formData.position} />
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="config" class="space-y-4 py-4">
|
||||
<div class="grid grid-cols-2 gap-6">
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch id="express" bind:checked={formData.has_express_line} />
|
||||
<Label for="express">Carril Exprés (OEA/NEEC)</Label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch id="service" bind:checked={formData.is_service_company} />
|
||||
<Label for="service">Es Empresa de Servicios</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t my-2"></div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="man_id">Manufacturer ID (MID)</Label>
|
||||
<Input id="man_id" bind:value={formData.manufacturer_id} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="order_format">Formato de Pedido</Label>
|
||||
<Input id="order_format" bind:value={formData.order_format_type} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="ctpat">C-TPAT / SVI</Label>
|
||||
<Input id="ctpat" bind:value={formData.ctpat_svi} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="exporter">No. Exportador Confiable</Label>
|
||||
<Input id="exporter" bind:value={formData.trusted_exporter_number} />
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button type="button" variant="outline" onclick={() => open = false}>Cancelar</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? 'Guardando...' : 'Guardar Empresa'}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -18,7 +18,7 @@
|
||||
let dialogOpen = $state(false);
|
||||
|
||||
async function handleDelete() {
|
||||
if (!confirm(`¿Estás seguro de eliminar la empresa "${item.name}"?`)) {
|
||||
if (!confirm(`¿Estás seguro de eliminar la empresa "${item.name}"?\n\nNota: No se puede eliminar si tiene registros relacionados.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -28,16 +28,23 @@
|
||||
try {
|
||||
const response = await deleteCompany(item.id);
|
||||
|
||||
// Si hay error en la respuesta
|
||||
if (response.error) {
|
||||
alert(`Error al eliminar: ${response.error}`);
|
||||
alert(`❌ Error al eliminar:\n\n${response.error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
// Éxito (status 204 o 200)
|
||||
if (response.status === 204 || response.status === 200 || !response.error) {
|
||||
alert(`✅ Empresa "${item.name}" eliminada correctamente`);
|
||||
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Error al eliminar el registro');
|
||||
const errorMsg = e instanceof Error ? e.message : 'Error desconocido al eliminar el registro';
|
||||
alert(`❌ Error al eliminar:\n\n${errorMsg}`);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
@@ -55,7 +62,7 @@
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end">
|
||||
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
|
||||
<DropdownMenu.Item onclick={() => dialogOpen = true}>
|
||||
<DropdownMenu.Item onclick={() => dialogOpen = true}>
|
||||
<Pencil class="mr-2 h-4 w-4" />
|
||||
Editar
|
||||
</DropdownMenu.Item>
|
||||
@@ -0,0 +1,109 @@
|
||||
<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";
|
||||
|
||||
// Props exactos que manda tu página de Companies
|
||||
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; },
|
||||
get columns() { return columns; },
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
get pageCount() { return pageCount; },
|
||||
});
|
||||
|
||||
// Función para navegar cambiando la URL ?page=X
|
||||
function handlePageChange(newPage: number) {
|
||||
const url = new URL($page.url);
|
||||
url.searchParams.set('page', newPage.toString());
|
||||
goto(url, { keepFocus: true, noScroll: true }); // Truco: noScroll evita saltos feos
|
||||
}
|
||||
|
||||
// Helper para saber la página actual
|
||||
const currentPage = $derived(Number($page.url.searchParams.get('page') || 1));
|
||||
</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 px-2">
|
||||
<div class="flex-1 text-sm text-muted-foreground">
|
||||
Total: {totalItems} registros
|
||||
</div>
|
||||
<div class="space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => handlePageChange(currentPage - 1)}
|
||||
disabled={currentPage <= 1}
|
||||
>
|
||||
Anterior
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => handlePageChange(currentPage + 1)}
|
||||
disabled={currentPage >= pageCount}
|
||||
>
|
||||
Siguiente
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { Concept } from '$lib/api/dashboard/a76/general_catalogs/concepts';
|
||||
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<Concept>[] {
|
||||
return [
|
||||
{
|
||||
accessorKey: 'code',
|
||||
header: 'Código',
|
||||
cell: ({ row }) => row.original.code || '-'
|
||||
},
|
||||
{
|
||||
accessorKey: 'description',
|
||||
header: 'Descripción',
|
||||
cell: ({ row }) => row.original.description || '-'
|
||||
},
|
||||
{
|
||||
accessorKey: 'type',
|
||||
header: 'Tipo',
|
||||
cell: ({ row }) => row.original.type || '-'
|
||||
},
|
||||
{
|
||||
accessorKey: 'section',
|
||||
header: 'Sección',
|
||||
cell: ({ row }) => row.original.section?.toString() || '-'
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: 'Acciones',
|
||||
cell: ({ row }) => {
|
||||
return renderComponent(DataTableActions, {
|
||||
item: row.original,
|
||||
onSuccess
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
<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 Tabs from "$lib/components/ui/tabs";
|
||||
import { Switch } from "$lib/components/ui/switch";
|
||||
import { createConcept, updateConcept, type Concept } from "$lib/api/dashboard/a76/general_catalogs/concepts";
|
||||
import { companyStore } from "$lib/stores/company.svelte";
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
item = null,
|
||||
onSuccess
|
||||
}: {
|
||||
open: boolean;
|
||||
item?: Concept | null;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
const isEdit = $derived(!!item);
|
||||
const title = $derived(isEdit ? "Editar Concepto" : "Nuevo Concepto");
|
||||
|
||||
let formData = $state({
|
||||
code: '',
|
||||
description: '',
|
||||
description_en: '',
|
||||
detailed_description: '',
|
||||
priority: '',
|
||||
priority_ame: '',
|
||||
first_total: '',
|
||||
type: '',
|
||||
is_printed: false,
|
||||
section: '',
|
||||
classification: '',
|
||||
});
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// Cargar datos al abrir
|
||||
$effect(() => {
|
||||
if (item) {
|
||||
formData = {
|
||||
code: item.code || '',
|
||||
description: item.description || '',
|
||||
description_en: item.description_en || '',
|
||||
detailed_description: item.detailed_description || '',
|
||||
priority: item.priority ? String(item.priority) : '', // Asegurar string
|
||||
priority_ame: item.priority_ame ? String(item.priority_ame) : '',
|
||||
first_total: item.first_total ? String(item.first_total) : '',
|
||||
type: item.type || '',
|
||||
is_printed: item.is_printed || false,
|
||||
section: item.section || '',
|
||||
classification: item.classification || '',
|
||||
};
|
||||
} else {
|
||||
// Limpiar formulario
|
||||
formData = {
|
||||
code: '',
|
||||
description: '',
|
||||
description_en: '',
|
||||
detailed_description: '',
|
||||
priority: '',
|
||||
priority_ame: '',
|
||||
first_total: '',
|
||||
type: '',
|
||||
is_printed: false,
|
||||
section: '',
|
||||
classification: ''
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
error = null;
|
||||
loading = true;
|
||||
|
||||
|
||||
if (!companyStore.activeCompany) {
|
||||
error = "No hay una empresa seleccionada. Recarga la página.";
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
const companyId = companyStore.activeCompany.id;
|
||||
|
||||
try {
|
||||
if (!formData.code.trim()) throw new Error('El código es requerido');
|
||||
|
||||
const dataToSend = {
|
||||
code: formData.code.trim(),
|
||||
company_id: companyId,
|
||||
description: formData.description.trim(),
|
||||
description_en: formData.description_en.trim() || null,
|
||||
detailed_description: formData.detailed_description.trim() || null,
|
||||
priority: formData.priority ? parseInt(formData.priority) : null,
|
||||
priority_ame: formData.priority_ame ? parseInt(formData.priority_ame) : null,
|
||||
section: formData.section ? parseInt(formData.section) : null,
|
||||
first_total: !!formData.first_total,
|
||||
type: formData.type.trim() || null,
|
||||
is_printed: Boolean(formData.is_printed),
|
||||
classification: formData.classification.trim() || null,
|
||||
};
|
||||
|
||||
let response;
|
||||
|
||||
if (isEdit && item) {
|
||||
|
||||
response = await updateConcept(item.id, dataToSend, companyId);
|
||||
} else {
|
||||
|
||||
response = await createConcept(dataToSend, companyId);
|
||||
}
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(response.error); // Axios a veces devuelve error en el body
|
||||
}
|
||||
|
||||
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-2xl max-h-[90vh] overflow-y-auto">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="space-y-4">
|
||||
{#if error}
|
||||
<div class="p-3 text-sm text-red-600 bg-red-50 rounded-md border border-red-200">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Tabs.Root value="general" class="w-full">
|
||||
<Tabs.Content value="general" class="space-y-4 py-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="code">Código <span class="text-destructive">*</span></Label>
|
||||
<Input id="code" bind:value={formData.code} maxlength={10} />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="priority">Prioridad</Label>
|
||||
<Input id="priority" bind:value={formData.priority} type="number" />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="priority_ame">Prioridad AME</Label>
|
||||
<Input id="priority_ame" bind:value={formData.priority_ame} type="number"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="description">Descripción <span class="text-destructive">*</span></Label>
|
||||
<Input id="description" bind:value={formData.description} maxlength={100} />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="description_en">Descripción (Inglés)</Label>
|
||||
<Input id="description_en" bind:value={formData.description_en} maxlength={100} />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="detailed_description">Descripción Detallada</Label>
|
||||
<Input id="detailed_description" bind:value={formData.detailed_description} />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="first_total">Primer Total</Label>
|
||||
<Input id="first_total" bind:value={formData.first_total} type="number" />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="type">Tipo</Label>
|
||||
<Input id="type" bind:value={formData.type} maxlength={5} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="section">Sección</Label>
|
||||
<Input id="section" bind:value={formData.section} maxlength={10} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="classification">Clasificación</Label>
|
||||
<Input id="classification" bind:value={formData.classification} maxlength={10} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2 pt-2">
|
||||
<Switch id="is_printed" bind:checked={formData.is_printed} />
|
||||
<Label for="is_printed">¿Se Imprime?</Label>
|
||||
</div>
|
||||
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button type="button" variant="outline" onclick={() => open = false}>Cancelar</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? 'Guardando...' : 'Guardar Concepto'}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -0,0 +1,89 @@
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
|
||||
import { deleteConcept, type Concept } from "$lib/api/dashboard/a76/general_catalogs/concepts";
|
||||
import { companyStore } from "$lib/stores/company.svelte";
|
||||
import { EllipsisVertical, Pencil, LoaderCircle, Trash2 } from 'lucide-svelte';
|
||||
import CreateEditDialog from "./create-edit-dialog.svelte";
|
||||
|
||||
let {
|
||||
item,
|
||||
onSuccess
|
||||
}: {
|
||||
item: Concept;
|
||||
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 concepto "${item.code}"?\n\nNota: No se puede eliminar si tiene registros relacionados.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!companyStore.activeCompany) {
|
||||
alert('❌ Error: No hay una compañía seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const response = await deleteConcept(item.id, companyStore.activeCompany.id);
|
||||
|
||||
if (response.error) {
|
||||
alert(`❌ Error al eliminar:\n\n${response.error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Éxito
|
||||
if (response.status === 204 || response.status === 200 || !response.error) {
|
||||
alert(`✅ Concepto "${item.code}" eliminado correctamente`);
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
const errorMsg = e instanceof Error ? e.message : 'Error desconocido al eliminar el registro';
|
||||
alert(`❌ Error al eliminar:\n\n${errorMsg}`);
|
||||
} 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}
|
||||
/>
|
||||
@@ -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; },
|
||||
get columns() { return columns; },
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
get pageCount() { return pageCount; },
|
||||
});
|
||||
|
||||
function handlePageChange(newPage: number) {
|
||||
const url = new URL($page.url);
|
||||
url.searchParams.set('page', newPage.toString());
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
}
|
||||
|
||||
const currentPage = $derived(Number($page.url.searchParams.get('page') || 1));
|
||||
</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 px-2">
|
||||
<div class="flex-1 text-sm text-muted-foreground">
|
||||
Total: {totalItems} registros
|
||||
</div>
|
||||
<div class="space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => handlePageChange(currentPage - 1)}
|
||||
disabled={currentPage <= 1}
|
||||
>
|
||||
Anterior
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => handlePageChange(currentPage + 1)}
|
||||
disabled={currentPage >= pageCount}
|
||||
>
|
||||
Siguiente
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { CustomsBrokerConcept } from '$lib/api/dashboard/a76/general_catalogs/customs-broker-concepts';
|
||||
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<CustomsBrokerConcept>[] {
|
||||
return [
|
||||
{
|
||||
accessorKey: 'code',
|
||||
header: 'Código',
|
||||
cell: ({ row }) => row.original.code || '-'
|
||||
},
|
||||
{
|
||||
accessorKey: 'description',
|
||||
header: 'Descripción',
|
||||
cell: ({ row }) => row.original.description || '-'
|
||||
},
|
||||
{
|
||||
accessorKey: 'type',
|
||||
header: 'Tipo',
|
||||
cell: ({ row }) => row.original.type || '-'
|
||||
},
|
||||
{
|
||||
accessorKey: 'section',
|
||||
header: 'Sección',
|
||||
cell: ({ row }) => row.original.section?.toString() || '-'
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: 'Acciones',
|
||||
cell: ({ row }) => {
|
||||
return renderComponent(DataTableActions, {
|
||||
item: row.original,
|
||||
onSuccess
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
<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 {
|
||||
createCustomsBrokerConcept,
|
||||
updateCustomsBrokerConcept,
|
||||
type CustomsBrokerConcept
|
||||
} from "$lib/api/dashboard/a76/general_catalogs/customs-broker-concepts";
|
||||
import { companyStore } from "$lib/stores/company.svelte";
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
item = null,
|
||||
onSuccess
|
||||
}: {
|
||||
open: boolean;
|
||||
item?: CustomsBrokerConcept | null;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
const isEdit = $derived(!!item);
|
||||
const title = $derived(isEdit ? "Editar Concepto AA" : "Nuevo Concepto AA");
|
||||
|
||||
// 3. Estado alineado al modelo de BD
|
||||
let formData = $state({
|
||||
broker_key: '',
|
||||
concept: '',
|
||||
amount: null as number | null,
|
||||
priority: null as number | null
|
||||
});
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// 4. Cargar datos al editar
|
||||
$effect(() => {
|
||||
if (item) {
|
||||
formData = {
|
||||
broker_key: item.broker_key || '',
|
||||
concept: item.concept || '',
|
||||
amount: item.amount || null,
|
||||
priority: item.priority || null
|
||||
};
|
||||
} else {
|
||||
formData = {
|
||||
broker_key: '',
|
||||
concept: '',
|
||||
amount: null,
|
||||
priority: null
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
error = null;
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) throw new Error('No hay una compañía seleccionada');
|
||||
|
||||
// Validaciones
|
||||
if (!formData.broker_key.trim()) throw new Error('La Clave AA es requerida');
|
||||
if (!formData.concept.trim()) throw new Error('El Concepto es requerido');
|
||||
|
||||
// 5. Preparar datos con los tipos correctos (Números)
|
||||
const dataToSend = {
|
||||
broker_key: formData.broker_key.trim(),
|
||||
concept: formData.concept.trim(),
|
||||
amount: formData.amount ? Number(formData.amount) : undefined,
|
||||
priority: formData.priority ? Number(formData.priority) : undefined
|
||||
};
|
||||
|
||||
// 6. Corregida la sintaxis de llamada a la API
|
||||
if (isEdit && item) {
|
||||
// UPDATE: (id, data, companyId)
|
||||
await updateCustomsBrokerConcept(item.id, dataToSend, companyId);
|
||||
} else {
|
||||
// CREATE: (data, companyId)
|
||||
await createCustomsBrokerConcept(dataToSend, companyId);
|
||||
}
|
||||
|
||||
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 max-h-[90vh] overflow-y-auto">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
{isEdit ? 'Modifica el concepto del agente aduanal' : 'Crea un nuevo concepto'}
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="space-y-4">
|
||||
{#if error}
|
||||
<div class="rounded-md bg-destructive/15 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="grid gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="broker_key">Clave AA <span class="text-destructive">*</span></Label>
|
||||
<Input
|
||||
id="broker_key"
|
||||
bind:value={formData.broker_key}
|
||||
placeholder="Ej: 550"
|
||||
maxlength={5}
|
||||
disabled={isEdit}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="concept">Concepto <span class="text-destructive">*</span></Label>
|
||||
<Input
|
||||
id="concept"
|
||||
bind:value={formData.concept}
|
||||
placeholder="Ej: FLETE"
|
||||
maxlength={15}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="amount">Importe</Label>
|
||||
<Input
|
||||
id="amount"
|
||||
type="number"
|
||||
step="0.01"
|
||||
bind:value={formData.amount}
|
||||
placeholder="0.00"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="priority">Prioridad</Label>
|
||||
<Input
|
||||
id="priority"
|
||||
type="number"
|
||||
bind:value={formData.priority}
|
||||
placeholder="Ej: 1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button type="button" variant="outline" onclick={() => open = false} disabled={loading}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? 'Guardando...' : isEdit ? 'Actualizar' : 'Crear'}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -0,0 +1,89 @@
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
|
||||
import { deleteCustomsBrokerConcept, type CustomsBrokerConcept } from "$lib/api/dashboard/a76/general_catalogs/customs-broker-concepts";
|
||||
import { companyStore } from "$lib/stores/company.svelte";
|
||||
import { EllipsisVertical, Pencil, LoaderCircle, Trash2 } from 'lucide-svelte';
|
||||
import CreateEditDialog from "./create-edite-dialoge.svelte";
|
||||
|
||||
let {
|
||||
item,
|
||||
onSuccess
|
||||
}: {
|
||||
item: CustomsBrokerConcept;
|
||||
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 concepto "${item.code}"?\n\nNota: No se puede eliminar si tiene registros relacionados.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!companyStore.activeCompany) {
|
||||
alert('❌ Error: No hay una compañía seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const response = await deleteCustomsBrokerConcept(item.id, companyStore.activeCompany.id);
|
||||
|
||||
if (response.error) {
|
||||
alert(`❌ Error al eliminar:\n\n${response.error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Éxito
|
||||
if (response.status === 204 || response.status === 200 || !response.error) {
|
||||
alert(`✅ Concepto "${item.code}" eliminado correctamente`);
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
const errorMsg = e instanceof Error ? e.message : 'Error desconocido al eliminar el registro';
|
||||
alert(`❌ Error al eliminar:\n\n${errorMsg}`);
|
||||
} 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}
|
||||
/>
|
||||
@@ -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; },
|
||||
get columns() { return columns; },
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
get pageCount() { return pageCount; },
|
||||
});
|
||||
|
||||
function handlePageChange(newPage: number) {
|
||||
const url = new URL($page.url);
|
||||
url.searchParams.set('page', newPage.toString());
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
}
|
||||
|
||||
const currentPage = $derived(Number($page.url.searchParams.get('page') || 1));
|
||||
</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 px-2">
|
||||
<div class="flex-1 text-sm text-muted-foreground">
|
||||
Total: {totalItems} registros
|
||||
</div>
|
||||
<div class="space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => handlePageChange(currentPage - 1)}
|
||||
disabled={currentPage <= 1}
|
||||
>
|
||||
Anterior
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => handlePageChange(currentPage + 1)}
|
||||
disabled={currentPage >= pageCount}
|
||||
>
|
||||
Siguiente
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { ColumnDef } from '@tanstack/table-core';
|
||||
import type { Doda } from '$lib/api/dashboard/a76/general_catalogs/doda';
|
||||
import { renderComponent } from '$lib/components/ui/data-table';
|
||||
import DataTableActions from './data-table-actions.svelte';
|
||||
|
||||
export function createColumns(onSuccess?: () => void): ColumnDef<Doda>[] {
|
||||
return [
|
||||
{
|
||||
accessorKey: 'integration_number',
|
||||
header: 'No. Integración',
|
||||
cell: ({ row }) => row.original.integration_number || 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'patent',
|
||||
header: 'Patente',
|
||||
cell: ({ row }) => row.original.patent || 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'pedimentos',
|
||||
header: 'Pedimentos',
|
||||
cell: ({ row }) => row.original.pedimentos || 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'doda_date',
|
||||
header: 'Fecha',
|
||||
cell: ({ row }) => row.original.doda_date || 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'status',
|
||||
header: 'Estatus',
|
||||
cell: ({ row }) => row.original.status || 'N/A'
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
cell: ({ row }) => {
|
||||
return renderComponent(DataTableActions, {
|
||||
item: row.original,
|
||||
onSuccess
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
<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 { Switch } from "$lib/components/ui/switch";
|
||||
import * as Tabs from "$lib/components/ui/tabs";
|
||||
import { companyStore } from "$lib/stores/company.svelte";
|
||||
import {
|
||||
createDoda,
|
||||
updateDoda,
|
||||
type Doda
|
||||
} from '$lib/api/dashboard/a76/general_catalogs/doda';
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
item = null,
|
||||
onSuccess
|
||||
}: {
|
||||
open: boolean;
|
||||
item?: Doda | null;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
const isEdit = $derived(!!item);
|
||||
const title = $derived(isEdit ? `Editar DODA ${item?.integration_number || ''}` : "Nuevo DODA");
|
||||
|
||||
// Estado del formulario
|
||||
let formData = $state({
|
||||
integration_number: '',
|
||||
doda_date: undefined as number | undefined,
|
||||
doda_time: undefined as number | undefined,
|
||||
dispatch_customs: '',
|
||||
customs_sections: '',
|
||||
patent: '',
|
||||
pedimentos: '',
|
||||
caat: '',
|
||||
transport_identification: '',
|
||||
fast_id: '',
|
||||
operation_type: '',
|
||||
selected: false,
|
||||
user_selected: '',
|
||||
last_user: '',
|
||||
responsible: '',
|
||||
carrier: '',
|
||||
shipments: '',
|
||||
pedimento_type: '',
|
||||
original_chain: '',
|
||||
serial_number: '',
|
||||
electronic_signature: '',
|
||||
transaction_number: '',
|
||||
status: '',
|
||||
linq_sat_qr: '',
|
||||
sat_certificate: '',
|
||||
sat_digital_seal: '',
|
||||
xml_doda_sent_path: '',
|
||||
xml_doda_response_path: '',
|
||||
sat_original_chain: '',
|
||||
customs_clearance: undefined as number | undefined,
|
||||
unique_badge_number: ''
|
||||
});
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// Cargar datos
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
if (item) {
|
||||
formData = {
|
||||
integration_number: item.integration_number || '',
|
||||
doda_date: item.doda_date,
|
||||
doda_time: item.doda_time,
|
||||
dispatch_customs: item.dispatch_customs || '',
|
||||
customs_sections: item.customs_sections || '',
|
||||
patent: item.patent || '',
|
||||
pedimentos: item.pedimentos || '',
|
||||
caat: item.caat || '',
|
||||
transport_identification: item.transport_identification || '',
|
||||
fast_id: item.fast_id || '',
|
||||
operation_type: item.operation_type || '',
|
||||
selected: item.selected || false,
|
||||
user_selected: item.user_selected || '',
|
||||
last_user: item.last_user || '',
|
||||
responsible: item.responsible || '',
|
||||
carrier: item.carrier || '',
|
||||
shipments: item.shipments || '',
|
||||
pedimento_type: item.pedimento_type || '',
|
||||
original_chain: item.original_chain || '',
|
||||
serial_number: item.serial_number || '',
|
||||
electronic_signature: item.electronic_signature || '',
|
||||
transaction_number: item.transaction_number || '',
|
||||
status: item.status || '',
|
||||
linq_sat_qr: item.linq_sat_qr || '',
|
||||
sat_certificate: item.sat_certificate || '',
|
||||
sat_digital_seal: item.sat_digital_seal || '',
|
||||
xml_doda_sent_path: item.xml_doda_sent_path || '',
|
||||
xml_doda_response_path: item.xml_doda_response_path || '',
|
||||
sat_original_chain: item.sat_original_chain || '',
|
||||
customs_clearance: item.customs_clearance,
|
||||
unique_badge_number: item.unique_badge_number || ''
|
||||
};
|
||||
} else {
|
||||
// Reset
|
||||
formData = {
|
||||
integration_number: '',
|
||||
doda_date: undefined,
|
||||
doda_time: undefined,
|
||||
dispatch_customs: '',
|
||||
customs_sections: '',
|
||||
patent: '',
|
||||
pedimentos: '',
|
||||
caat: '',
|
||||
transport_identification: '',
|
||||
fast_id: '',
|
||||
operation_type: '',
|
||||
selected: false,
|
||||
user_selected: '',
|
||||
last_user: '',
|
||||
responsible: '',
|
||||
carrier: '',
|
||||
shipments: '',
|
||||
pedimento_type: '',
|
||||
original_chain: '',
|
||||
serial_number: '',
|
||||
electronic_signature: '',
|
||||
transaction_number: '',
|
||||
status: '',
|
||||
linq_sat_qr: '',
|
||||
sat_certificate: '',
|
||||
sat_digital_seal: '',
|
||||
xml_doda_sent_path: '',
|
||||
xml_doda_response_path: '',
|
||||
sat_original_chain: '',
|
||||
customs_clearance: undefined,
|
||||
unique_badge_number: ''
|
||||
};
|
||||
}
|
||||
error = null;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
error = 'No hay compañía seleccionada';
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const idToUpdate = item?.id;
|
||||
|
||||
if (isEdit && idToUpdate) {
|
||||
await updateDoda(idToUpdate, formData, companyId);
|
||||
} else {
|
||||
await createDoda(formData, companyId);
|
||||
}
|
||||
|
||||
open = false;
|
||||
if (onSuccess) onSuccess();
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
error = e instanceof Error ? e.message : 'Error al guardar DODA';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="sm:max-w-[900px] max-h-[90vh] overflow-y-auto">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="py-4">
|
||||
{#if error}
|
||||
<div class="mb-4 rounded-md bg-destructive/15 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Tabs.Root value="general" class="w-full">
|
||||
<Tabs.List class="grid w-full grid-cols-4">
|
||||
<Tabs.Trigger value="general">General</Tabs.Trigger>
|
||||
<Tabs.Trigger value="transport">Aduana/Transp.</Tabs.Trigger>
|
||||
<Tabs.Trigger value="sat">SAT / Digital</Tabs.Trigger>
|
||||
<Tabs.Trigger value="other">Otros</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
|
||||
<!-- TAB: GENERAL -->
|
||||
<Tabs.Content value="general" class="space-y-4 py-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="integration_number">No. Integración</Label>
|
||||
<Input id="integration_number" bind:value={formData.integration_number} maxlength={30} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="status">Estatus</Label>
|
||||
<Input id="status" bind:value={formData.status} maxlength={30} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="doda_date">Fecha (YYYYMMDD)</Label>
|
||||
<Input type="number" id="doda_date" bind:value={formData.doda_date} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="doda_time">Hora (HHMMSS)</Label>
|
||||
<Input type="number" id="doda_time" bind:value={formData.doda_time} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="operation_type">Tipo Operación</Label>
|
||||
<Input id="operation_type" bind:value={formData.operation_type} maxlength={1} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="pedimentos">Pedimentos</Label>
|
||||
<Input id="pedimentos" bind:value={formData.pedimentos} maxlength={80} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="pedimento_type">Tipo Pedimento</Label>
|
||||
<Input id="pedimento_type" bind:value={formData.pedimento_type} maxlength={30} />
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<!-- TAB: ADUANA / TRANSPORTE -->
|
||||
<Tabs.Content value="transport" class="space-y-4 py-4">
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="patent">Patente</Label>
|
||||
<Input id="patent" bind:value={formData.patent} maxlength={4} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="dispatch_customs">Aduana Despacho</Label>
|
||||
<Input id="dispatch_customs" bind:value={formData.dispatch_customs} maxlength={3} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="customs_sections">Sección Aduanera</Label>
|
||||
<Input id="customs_sections" bind:value={formData.customs_sections} maxlength={3} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="caat">CAAT</Label>
|
||||
<Input id="caat" bind:value={formData.caat} maxlength={10} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="carrier">Transportista (Carrier)</Label>
|
||||
<Input id="carrier" bind:value={formData.carrier} maxlength={8} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="transport_identification">Ident. Transporte</Label>
|
||||
<Input id="transport_identification" bind:value={formData.transport_identification} maxlength={20} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="fast_id">FAST ID</Label>
|
||||
<Input id="fast_id" bind:value={formData.fast_id} maxlength={20} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="shipments">Embarques (Shipments)</Label>
|
||||
<Input id="shipments" bind:value={formData.shipments} maxlength={80} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="customs_clearance">Despacho Aduanero (ID)</Label>
|
||||
<Input type="number" id="customs_clearance" bind:value={formData.customs_clearance} />
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<!-- TAB: SAT / DIGITAL -->
|
||||
<Tabs.Content value="sat" class="space-y-4 py-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="serial_number">Número de Serie</Label>
|
||||
<Input id="serial_number" bind:value={formData.serial_number} maxlength={21} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="transaction_number">No. Transacción</Label>
|
||||
<Input id="transaction_number" bind:value={formData.transaction_number} maxlength={30} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="unique_badge_number">Número Único de Gafete</Label>
|
||||
<Input id="unique_badge_number" bind:value={formData.unique_badge_number} maxlength={250} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="original_chain">Cadena Original</Label>
|
||||
<Textarea id="original_chain" bind:value={formData.original_chain} class="h-20" />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="electronic_signature">Firma Electrónica</Label>
|
||||
<Textarea id="electronic_signature" bind:value={formData.electronic_signature} class="h-20" />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="sat_digital_seal">Sello Digital SAT</Label>
|
||||
<Textarea id="sat_digital_seal" bind:value={formData.sat_digital_seal} class="h-20" />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="sat_original_chain">Cadena Original SAT</Label>
|
||||
<Textarea id="sat_original_chain" bind:value={formData.sat_original_chain} class="h-20" />
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="xml_doda_sent_path">Ruta XML Enviado</Label>
|
||||
<Input id="xml_doda_sent_path" bind:value={formData.xml_doda_sent_path} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="xml_doda_response_path">Ruta XML Respuesta</Label>
|
||||
<Input id="xml_doda_response_path" bind:value={formData.xml_doda_response_path} />
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<!-- TAB: OTROS -->
|
||||
<Tabs.Content value="other" class="space-y-4 py-4">
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch id="selected" bind:checked={formData.selected} />
|
||||
<Label for="selected">Seleccionado</Label>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="user_selected">Usuario Selección</Label>
|
||||
<Input id="user_selected" bind:value={formData.user_selected} maxlength={30} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="last_user">Último Usuario</Label>
|
||||
<Input id="last_user" bind:value={formData.last_user} maxlength={30} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="responsible">Responsable</Label>
|
||||
<Input id="responsible" bind:value={formData.responsible} maxlength={14} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="linq_sat_qr">LINQ SAT QR</Label>
|
||||
<Input id="linq_sat_qr" bind:value={formData.linq_sat_qr} maxlength={1000} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="sat_certificate">Certificado SAT</Label>
|
||||
<Input id="sat_certificate" bind:value={formData.sat_certificate} maxlength={2001} />
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
|
||||
<Dialog.Footer class="mt-6">
|
||||
<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>
|
||||
@@ -0,0 +1,85 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import { EllipsisVertical, Pencil, Trash2, LoaderCircle } from 'lucide-svelte';
|
||||
import type { Doda } from '$lib/api/dashboard/a76/general_catalogs/doda';
|
||||
import { deleteDoda } from '$lib/api/dashboard/a76/general_catalogs/doda';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import CreateEditDialog from './create-edit-dialog.svelte';
|
||||
|
||||
let {
|
||||
item,
|
||||
onSuccess
|
||||
}: {
|
||||
item: Doda;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
let dialogOpen = $state(false);
|
||||
let selectedItem = $state<Doda | null>(null);
|
||||
|
||||
async function handleDelete() {
|
||||
if (!confirm('¿Está seguro de eliminar este registro DODA?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
alert('❌ Error: No hay compañía seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
await deleteDoda(item.id, companyId);
|
||||
alert('✅ Registro eliminado correctamente');
|
||||
if (onSuccess) onSuccess();
|
||||
} catch (err: any) {
|
||||
error = err.message || 'Error al eliminar el registro';
|
||||
alert(`❌ Error: ${error}`);
|
||||
console.error('Error deleting doda:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleEdit() {
|
||||
selectedItem = item;
|
||||
dialogOpen = true;
|
||||
}
|
||||
</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={handleEdit}>
|
||||
<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}
|
||||
item={selectedItem}
|
||||
onSuccess={onSuccess}
|
||||
/>
|
||||
@@ -0,0 +1,102 @@
|
||||
<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">
|
||||
<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>
|
||||
<div class="text-sm text-muted-foreground">
|
||||
Página {Number($page.url.searchParams.get('page') || 1)} de {pageCount}
|
||||
</div>
|
||||
<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>
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { ColumnDef } from '@tanstack/table-core';
|
||||
import type { ElectronicNotice } from '$lib/api/dashboard/a76/general_catalogs/electronic-notices';
|
||||
import { renderComponent } from '$lib/components/ui/data-table';
|
||||
import DataTableActions from './data-table-actions.svelte';
|
||||
|
||||
export function createColumns(onSuccess?: () => void): ColumnDef<ElectronicNotice>[] {
|
||||
return [
|
||||
{
|
||||
accessorKey: 'notice_number',
|
||||
header: 'No. Aviso',
|
||||
cell: ({ row }) => row.original.notice_number || 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'year',
|
||||
header: 'Año',
|
||||
cell: ({ row }) => row.original.year || 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'patent',
|
||||
header: 'Patente',
|
||||
cell: ({ row }) => row.original.patent || 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'pedimento',
|
||||
header: 'Pedimento',
|
||||
cell: ({ row }) => row.original.pedimento || 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'status',
|
||||
header: 'Estatus',
|
||||
cell: ({ row }) => row.original.status || 'N/A'
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
cell: ({ row }) => {
|
||||
return renderComponent(DataTableActions, {
|
||||
item: row.original,
|
||||
onSuccess
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
<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 { companyStore } from "$lib/stores/company.svelte";
|
||||
import {
|
||||
createElectronicNotice,
|
||||
updateElectronicNotice,
|
||||
type ElectronicNotice
|
||||
} from '$lib/api/dashboard/a76/general_catalogs/electronic-notices';
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
item = null,
|
||||
onSuccess
|
||||
}: {
|
||||
open: boolean;
|
||||
item?: ElectronicNotice | null;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
const isEdit = $derived(!!item);
|
||||
const title = $derived(isEdit ? `Editar Aviso ${item?.notice_number || ''}` : "Nuevo Aviso Electrónico");
|
||||
|
||||
// Estado del formulario
|
||||
let formData = $state({
|
||||
notice_number: '',
|
||||
year: '',
|
||||
patent: '',
|
||||
pedimento: '',
|
||||
invoice: '',
|
||||
status: '',
|
||||
validation_acknowledgment: '',
|
||||
certificate_number: '',
|
||||
file_sent: '',
|
||||
file_response: '',
|
||||
fea: ''
|
||||
});
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// Cargar datos
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
if (item) {
|
||||
formData = {
|
||||
notice_number: item.notice_number || '',
|
||||
year: item.year || '',
|
||||
patent: item.patent || '',
|
||||
pedimento: item.pedimento || '',
|
||||
invoice: item.invoice || '',
|
||||
status: item.status || '',
|
||||
validation_acknowledgment: item.validation_acknowledgment || '',
|
||||
certificate_number: item.certificate_number || '',
|
||||
file_sent: item.file_sent || '',
|
||||
file_response: item.file_response || '',
|
||||
fea: item.fea || ''
|
||||
};
|
||||
} else {
|
||||
// Reset
|
||||
formData = {
|
||||
notice_number: '',
|
||||
year: '',
|
||||
patent: '',
|
||||
pedimento: '',
|
||||
invoice: '',
|
||||
status: '',
|
||||
validation_acknowledgment: '',
|
||||
certificate_number: '',
|
||||
file_sent: '',
|
||||
file_response: '',
|
||||
fea: ''
|
||||
};
|
||||
}
|
||||
error = null;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
error = 'No hay compañía seleccionada';
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
if (isEdit && item) {
|
||||
await updateElectronicNotice(item.id, formData, companyId);
|
||||
} else {
|
||||
await createElectronicNotice(formData, companyId);
|
||||
}
|
||||
|
||||
open = false;
|
||||
if (onSuccess) onSuccess();
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
error = e instanceof Error ? e.message : 'Error al guardar aviso electrónico';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="sm:max-w-[600px] max-h-[90vh] overflow-y-auto">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="grid gap-4 py-4">
|
||||
{#if error}
|
||||
<div class="rounded-md bg-destructive/15 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="notice_number">No. Aviso</Label>
|
||||
<Input id="notice_number" bind:value={formData.notice_number} placeholder="Ej. 12345" maxlength={500} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="year">Año</Label>
|
||||
<Input id="year" bind:value={formData.year} placeholder="Ej. 2024" maxlength={20} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="patent">Patente</Label>
|
||||
<Input id="patent" bind:value={formData.patent} placeholder="Ej. 1234" maxlength={4} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="pedimento">Pedimento</Label>
|
||||
<Input id="pedimento" bind:value={formData.pedimento} placeholder="Ej. 1234567" maxlength={15} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="invoice">Factura</Label>
|
||||
<Input id="invoice" bind:value={formData.invoice} placeholder="Ej. F-123" maxlength={50} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="status">Estatus</Label>
|
||||
<Input id="status" bind:value={formData.status} placeholder="Ej. Validado" maxlength={100} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="validation_acknowledgment">Acuse Validación</Label>
|
||||
<Input id="validation_acknowledgment" bind:value={formData.validation_acknowledgment} placeholder="Ej. AC-123" maxlength={20} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="certificate_number">No. Certificado</Label>
|
||||
<Input id="certificate_number" bind:value={formData.certificate_number} placeholder="Ej. CERT-123" maxlength={50} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="fea">FEA</Label>
|
||||
<Input id="fea" bind:value={formData.fea} placeholder="Firma Electrónica Avanzada" maxlength={1000} />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="file_sent">Archivo Enviado</Label>
|
||||
<Input id="file_sent" bind:value={formData.file_sent} placeholder="Nombre del archivo enviado" maxlength={1000} />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="file_response">Archivo Respuesta</Label>
|
||||
<Input id="file_response" bind:value={formData.file_response} placeholder="Nombre del archivo respuesta" maxlength={1000} />
|
||||
</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>
|
||||
@@ -0,0 +1,85 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import { EllipsisVertical, Pencil, Trash2, LoaderCircle } from 'lucide-svelte';
|
||||
import type { ElectronicNotice } from '$lib/api/dashboard/a76/general_catalogs/electronic-notices';
|
||||
import { deleteElectronicNotice } from '$lib/api/dashboard/a76/general_catalogs/electronic-notices';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import CreateEditDialog from './create-edit-dialog.svelte';
|
||||
|
||||
let {
|
||||
item,
|
||||
onSuccess
|
||||
}: {
|
||||
item: ElectronicNotice;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
let dialogOpen = $state(false);
|
||||
let selectedItem = $state<ElectronicNotice | null>(null);
|
||||
|
||||
async function handleDelete() {
|
||||
if (!confirm('¿Está seguro de eliminar este aviso electrónico?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
alert('❌ Error: No hay compañía seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
await deleteElectronicNotice(item.id, companyId);
|
||||
alert('✅ Aviso electrónico eliminado correctamente');
|
||||
if (onSuccess) onSuccess();
|
||||
} catch (err: any) {
|
||||
error = err.message || 'Error al eliminar el aviso electrónico';
|
||||
alert(`❌ Error: ${error}`);
|
||||
console.error('Error deleting electronic notice:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleEdit() {
|
||||
selectedItem = item;
|
||||
dialogOpen = true;
|
||||
}
|
||||
</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={handleEdit}>
|
||||
<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}
|
||||
item={selectedItem}
|
||||
onSuccess={onSuccess}
|
||||
/>
|
||||
@@ -0,0 +1,104 @@
|
||||
<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} registros
|
||||
</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>
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { ColumnDef } from '@tanstack/table-core';
|
||||
import type { Equivalency } from '$lib/api/dashboard/a76/general_catalogs/equivalencies';
|
||||
import { renderComponent } from '$lib/components/ui/data-table';
|
||||
import DataTableActions from './data-table-actions.svelte';
|
||||
|
||||
export function createColumns(onSuccess?: () => void): ColumnDef<Equivalency>[] {
|
||||
return [
|
||||
{
|
||||
accessorKey: 'fraccion_mex',
|
||||
header: 'Fracción MX',
|
||||
cell: ({ row }) => row.original.fraccion_mex || 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'fraccion_us',
|
||||
header: 'Fracción US',
|
||||
cell: ({ row }) => row.original.fraccion_us || 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'description',
|
||||
header: 'Descripción',
|
||||
cell: ({ row }) => row.original.description || 'N/A'
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
cell: ({ row }) => {
|
||||
return renderComponent(DataTableActions, {
|
||||
item: row.original,
|
||||
onSuccess
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -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 { companyStore } from "$lib/stores/company.svelte";
|
||||
import {
|
||||
createEquivalency,
|
||||
updateEquivalency,
|
||||
type Equivalency
|
||||
} from '$lib/api/dashboard/a76/general_catalogs/equivalencies';
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
item = null,
|
||||
onSuccess
|
||||
}: {
|
||||
open: boolean;
|
||||
item?: Equivalency | null;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
const isEdit = $derived(!!item);
|
||||
const title = $derived(isEdit ? `Editar Equivalencia ${item?.fraccion_mex || ''}` : "Nueva Equivalencia");
|
||||
|
||||
// Estado del formulario
|
||||
let formData = $state({
|
||||
fraccion_mex: '',
|
||||
fraccion_us: '',
|
||||
description: ''
|
||||
});
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// Cargar datos
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
if (item) {
|
||||
formData = {
|
||||
fraccion_mex: item.fraccion_mex || '',
|
||||
fraccion_us: item.fraccion_us || '',
|
||||
description: item.description || ''
|
||||
};
|
||||
} else {
|
||||
// Reset
|
||||
formData = {
|
||||
fraccion_mex: '',
|
||||
fraccion_us: '',
|
||||
description: ''
|
||||
};
|
||||
}
|
||||
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 updateEquivalency(item.id, formData, companyId);
|
||||
} else {
|
||||
response = await createEquivalency(formData, companyId);
|
||||
}
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
open = false;
|
||||
if (onSuccess) onSuccess();
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
error = e instanceof Error ? e.message : 'Error al guardar equivalencia';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="sm:max-w-[500px]">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="grid gap-4 py-4">
|
||||
{#if error}
|
||||
<div class="rounded-md bg-destructive/15 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="fraccion_mex" class="text-right">Fracción MX</Label>
|
||||
<Input id="fraccion_mex" bind:value={formData.fraccion_mex} class="col-span-3" maxlength={10} required placeholder="Ej. 8544.11.01" />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="fraccion_us" class="text-right">Fracción US</Label>
|
||||
<Input id="fraccion_us" bind:value={formData.fraccion_us} class="col-span-3" maxlength={100} required placeholder="Ej. 8544.11.00" />
|
||||
</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" maxlength={200} />
|
||||
</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>
|
||||
@@ -0,0 +1,90 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import { EllipsisVertical, Pencil, Trash2, LoaderCircle } from 'lucide-svelte';
|
||||
import type { Equivalency } from '$lib/api/dashboard/a76/general_catalogs/equivalencies';
|
||||
import { deleteEquivalency } from '$lib/api/dashboard/a76/general_catalogs/equivalencies';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import CreateEditDialog from './create-edit-dialog.svelte';
|
||||
|
||||
let {
|
||||
item,
|
||||
onSuccess
|
||||
}: {
|
||||
item: Equivalency;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
let dialogOpen = $state(false);
|
||||
let selectedItem = $state<Equivalency | null>(null);
|
||||
|
||||
async function handleDelete() {
|
||||
if (!confirm('¿Está seguro de eliminar esta equivalencia?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
alert('❌ Error: No hay compañía seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
const response = await deleteEquivalency(item.id, companyId);
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
alert('✅ Equivalencia eliminada correctamente');
|
||||
if (onSuccess) onSuccess();
|
||||
} catch (err: any) {
|
||||
error = err.message || 'Error al eliminar la equivalencia';
|
||||
alert(`❌ Error: ${error}`);
|
||||
console.error('Error deleting equivalency:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleEdit() {
|
||||
selectedItem = item;
|
||||
dialogOpen = true;
|
||||
}
|
||||
</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={handleEdit}>
|
||||
<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}
|
||||
item={selectedItem}
|
||||
onSuccess={onSuccess}
|
||||
/>
|
||||
@@ -0,0 +1,104 @@
|
||||
<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>
|
||||
{#if table.getRowModel().rows.length}
|
||||
{#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>
|
||||
{/each}
|
||||
{:else}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={columns.length} class="h-24 text-center">
|
||||
No hay resultados.
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/if}
|
||||
</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} registros
|
||||
</div>
|
||||
<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>
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { ColumnDef } from '@tanstack/table-core';
|
||||
import type { ErrorCatalog } from '$lib/api/dashboard/a76/general_catalogs/error-catalogs';
|
||||
import { renderComponent } from '$lib/components/ui/data-table';
|
||||
import DataTableActions from './data-table-actions.svelte';
|
||||
|
||||
export function createColumns(onSuccess?: () => void): ColumnDef<ErrorCatalog>[] {
|
||||
return [
|
||||
{
|
||||
accessorKey: 'code',
|
||||
header: 'Código'
|
||||
},
|
||||
{
|
||||
accessorKey: 'description',
|
||||
header: 'Descripción',
|
||||
cell: ({ row }) => row.original.description ?? '—'
|
||||
},
|
||||
{
|
||||
accessorKey: 'classification_id',
|
||||
header: 'Clasificación',
|
||||
cell: ({ row }) => row.original.classification_id ?? '—'
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: 'Acciones',
|
||||
cell: ({ row }) => renderComponent(DataTableActions, {
|
||||
item: row.original,
|
||||
onSuccess
|
||||
})
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
<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 {
|
||||
createErrorCatalog,
|
||||
updateErrorCatalog,
|
||||
getErrorClassifications,
|
||||
type ErrorCatalog,
|
||||
type ErrorClassification
|
||||
} from "$lib/api/dashboard/a76/general_catalogs/error-catalogs";
|
||||
import { companyStore } from "$lib/stores/company.svelte";
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
item = null,
|
||||
onSuccess
|
||||
}: {
|
||||
open: boolean;
|
||||
item?: ErrorCatalog | null;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
const isEdit = $derived(!!item);
|
||||
const title = $derived(isEdit ? "Editar Error" : "Nuevo Error");
|
||||
|
||||
let formData = $state({
|
||||
code: "",
|
||||
description: "",
|
||||
classification_id: ""
|
||||
});
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
let classifications = $state<ErrorClassification[]>([]);
|
||||
let loadingClassifications = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
if (item) {
|
||||
formData = {
|
||||
code: item.code || "",
|
||||
description: item.description || "",
|
||||
classification_id: item.classification_id ? String(item.classification_id) : ""
|
||||
};
|
||||
} else {
|
||||
formData = { code: "", description: "", classification_id: "" };
|
||||
}
|
||||
error = null;
|
||||
loadClassifications();
|
||||
}
|
||||
});
|
||||
|
||||
async function loadClassifications() {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) return;
|
||||
loadingClassifications = true;
|
||||
try {
|
||||
const response = await getErrorClassifications(companyId, 1, 100);
|
||||
classifications = response.items || [];
|
||||
} catch (err) {
|
||||
console.error("Error loading classifications", err);
|
||||
} finally {
|
||||
loadingClassifications = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) throw new Error("No hay una compañía seleccionada");
|
||||
|
||||
if (!formData.code.trim()) throw new Error("El código es requerido");
|
||||
|
||||
const basePayload = {
|
||||
description: formData.description?.trim() || null,
|
||||
classification_id: formData.classification_id ? Number(formData.classification_id) : null
|
||||
};
|
||||
|
||||
if (isEdit && item) {
|
||||
await updateErrorCatalog(item.id, basePayload, companyId);
|
||||
alert("✅ Error actualizado correctamente");
|
||||
} else {
|
||||
const createPayload = {
|
||||
code: formData.code.trim(),
|
||||
...basePayload
|
||||
};
|
||||
await createErrorCatalog(createPayload, companyId);
|
||||
alert("✅ Error creado correctamente");
|
||||
}
|
||||
|
||||
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.Portal>
|
||||
<Dialog.Overlay class="fixed inset-0 z-[9999] bg-black/80 backdrop-blur-sm" />
|
||||
|
||||
<Dialog.Content class="fixed left-[50%] top-[50%] z-[10000] w-full max-w-[520px] translate-x-[-50%] translate-y-[-50%] border bg-background p-6 shadow-lg sm:rounded-lg">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="space-y-4 py-4">
|
||||
{#if error}
|
||||
<div class="rounded-md bg-destructive/15 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="grid gap-4">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="code" class="text-right">Código *</Label>
|
||||
<div class="col-span-3">
|
||||
<Input id="code" bind:value={formData.code} maxlength={15} disabled={loading || isEdit} required />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="description" class="text-right">Descripción</Label>
|
||||
<div class="col-span-3">
|
||||
<Input id="description" bind:value={formData.description} maxlength={255} disabled={loading} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="classification" class="text-right">Clasificación</Label>
|
||||
<div class="col-span-3">
|
||||
<Select.Root bind:value={formData.classification_id} disabled={loading || loadingClassifications}>
|
||||
<Select.Trigger>
|
||||
{#if formData.classification_id}
|
||||
{#each classifications as classification (classification.id)}
|
||||
{#if String(classification.id) === formData.classification_id}
|
||||
{classification.code}{#if classification.level} - {classification.level}{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
{:else}
|
||||
<span class="text-muted-foreground">
|
||||
{loadingClassifications ? "Cargando..." : "Seleccione una clasificación"}
|
||||
</span>
|
||||
{/if}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Item value="">Sin clasificación</Select.Item>
|
||||
{#each classifications as classification (classification.id)}
|
||||
<Select.Item value={String(classification.id)}>
|
||||
{classification.code}{#if classification.level} - {classification.level}{/if}
|
||||
</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button type="button" variant="outline" onclick={() => open = false} disabled={loading}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? 'Guardando...' : isEdit ? 'Actualizar' : 'Crear'}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
@@ -0,0 +1,97 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import { EllipsisVertical, Pencil, Trash2, LoaderCircle } from 'lucide-svelte';
|
||||
import type { ErrorCatalog } from '$lib/api/dashboard/a76/general_catalogs/error-catalogs';
|
||||
import { deleteErrorCatalog } from '$lib/api/dashboard/a76/general_catalogs/error-catalogs';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import CreateEditDialog from './create-edit-dialog.svelte';
|
||||
|
||||
let {
|
||||
item,
|
||||
onSuccess
|
||||
}: {
|
||||
item: ErrorCatalog;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
let dialogOpen = $state(false);
|
||||
let selectedItem = $state<ErrorCatalog | null>(null);
|
||||
|
||||
async function handleDelete() {
|
||||
if (!confirm('¿Está seguro de que desea eliminar este error?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
alert('❌ Error: No hay compañía seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
await deleteErrorCatalog(item.id, companyId);
|
||||
alert('✅ Error eliminado correctamente');
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
} catch (err: any) {
|
||||
error = err.message || 'Error al eliminar el error';
|
||||
alert(`❌ Error: ${error}`);
|
||||
console.error('Error deleting:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleEdit() {
|
||||
selectedItem = item;
|
||||
dialogOpen = true;
|
||||
}
|
||||
|
||||
function handleDialogSuccess() {
|
||||
dialogOpen = false;
|
||||
selectedItem = null;
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<Button {...props} variant="ghost" size="icon" class="relative size-8 p-0">
|
||||
<span class="sr-only">Abrir menú</span>
|
||||
<EllipsisVertical size={16} />
|
||||
</Button>
|
||||
{/snippet}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end" class="w-[160px]">
|
||||
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item onclick={handleEdit}>
|
||||
<Pencil size={16} class="mr-2" />
|
||||
Editar
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item onclick={handleDelete} class="text-destructive" disabled={loading}>
|
||||
{#if loading}
|
||||
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
|
||||
{:else}
|
||||
<Trash2 size={16} class="mr-2" />
|
||||
{/if}
|
||||
Eliminar
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
|
||||
<CreateEditDialog
|
||||
bind:open={dialogOpen}
|
||||
item={selectedItem}
|
||||
onSuccess={handleDialogSuccess}
|
||||
/>
|
||||
@@ -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; },
|
||||
get columns() { return columns; },
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
get pageCount() { return pageCount; },
|
||||
});
|
||||
|
||||
function handlePageChange(newPage: number) {
|
||||
const url = new URL($page.url);
|
||||
url.searchParams.set('page', newPage.toString());
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
}
|
||||
|
||||
const currentPage = $derived(Number($page.url.searchParams.get('page') || 1));
|
||||
</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 px-2">
|
||||
<div class="flex-1 text-sm text-muted-foreground">
|
||||
Total: {totalItems} registros
|
||||
</div>
|
||||
<div class="space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => handlePageChange(currentPage - 1)}
|
||||
disabled={currentPage <= 1}
|
||||
>
|
||||
Anterior
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => handlePageChange(currentPage + 1)}
|
||||
disabled={currentPage >= pageCount}
|
||||
>
|
||||
Siguiente
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { ColumnDef } from '@tanstack/table-core';
|
||||
import type { Identifier } from '$lib/api/dashboard/a76/general_catalogs/identifiers';
|
||||
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',
|
||||
cell: ({ row }) => row.original.code || 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'description',
|
||||
header: 'Descripción',
|
||||
cell: ({ row }) => row.original.description || 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'level',
|
||||
header: 'Nivel',
|
||||
cell: ({ row }) => row.original.level || 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'complement',
|
||||
header: 'Complemento',
|
||||
cell: ({ row }) => row.original.complement || 'N/A'
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
cell: ({ row }) => {
|
||||
return renderComponent(DataTableActions, {
|
||||
item: row.original,
|
||||
onSuccess
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<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 { companyStore } from "$lib/stores/company.svelte";
|
||||
import {
|
||||
createIdentifier,
|
||||
updateIdentifier,
|
||||
type Identifier
|
||||
} from '$lib/api/dashboard/a76/general_catalogs/identifiers';
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
item = null,
|
||||
onSuccess
|
||||
}: {
|
||||
open: boolean;
|
||||
item?: Identifier | null;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
const isEdit = $derived(!!item);
|
||||
const title = $derived(isEdit ? `Editar Identificador ${item?.code || ''}` : "Nuevo Identificador");
|
||||
|
||||
// Estado del formulario
|
||||
let formData = $state({
|
||||
code: '',
|
||||
description: '',
|
||||
level: '',
|
||||
complement: ''
|
||||
});
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// Cargar datos
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
if (item) {
|
||||
formData = {
|
||||
code: item.code || '',
|
||||
description: item.description || '',
|
||||
level: item.level || '',
|
||||
complement: item.complement || ''
|
||||
};
|
||||
} else {
|
||||
// Reset
|
||||
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 {
|
||||
if (isEdit && item) {
|
||||
await updateIdentifier(item.id, formData, companyId);
|
||||
} else {
|
||||
await createIdentifier(formData, companyId);
|
||||
}
|
||||
|
||||
open = false;
|
||||
if (onSuccess) onSuccess();
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
error = e instanceof Error ? e.message : 'Error al guardar identificador';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="sm:max-w-[500px]">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="grid gap-4 py-4">
|
||||
{#if error}
|
||||
<div class="rounded-md bg-destructive/15 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="code">Clave</Label>
|
||||
<Input id="code" bind:value={formData.code} placeholder="Ej. AI" maxlength={2} required />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="description">Descripción</Label>
|
||||
<Input id="description" bind:value={formData.description} placeholder="Descripción del identificador" maxlength={1000} />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="level">Nivel</Label>
|
||||
<Input id="level" bind:value={formData.level} placeholder="Ej. G" maxlength={1} />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="complement">Complemento</Label>
|
||||
<Input id="complement" bind:value={formData.complement} placeholder="Información complementaria" maxlength={5000} />
|
||||
</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>
|
||||
@@ -0,0 +1,85 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import { EllipsisVertical, Pencil, Trash2, LoaderCircle } from 'lucide-svelte';
|
||||
import type { Identifier } from '$lib/api/dashboard/a76/general_catalogs/identifiers';
|
||||
import { deleteIdentifier } from '$lib/api/dashboard/a76/general_catalogs/identifiers';
|
||||
import { companyStore } from '$lib/stores/company.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);
|
||||
let selectedItem = $state<Identifier | null>(null);
|
||||
|
||||
async function handleDelete() {
|
||||
if (!confirm('¿Está seguro de eliminar este identificador?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
alert('❌ Error: No hay compañía seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
await deleteIdentifier(item.id, companyId);
|
||||
alert('✅ Identificador eliminado correctamente');
|
||||
if (onSuccess) onSuccess();
|
||||
} catch (err: any) {
|
||||
error = err.message || 'Error al eliminar el identificador';
|
||||
alert(`❌ Error: ${error}`);
|
||||
console.error('Error deleting identifier:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleEdit() {
|
||||
selectedItem = item;
|
||||
dialogOpen = true;
|
||||
}
|
||||
</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={handleEdit}>
|
||||
<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}
|
||||
item={selectedItem}
|
||||
onSuccess={onSuccess}
|
||||
/>
|
||||
@@ -0,0 +1,104 @@
|
||||
<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} registros
|
||||
</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>
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { ColumnDef } from '@tanstack/table-core';
|
||||
import type { INPC } from '$lib/api/dashboard/a76/general_catalogs/inpc';
|
||||
import { renderComponent } from '$lib/components/ui/data-table';
|
||||
import DataTableActions from './data-table-actions.svelte';
|
||||
|
||||
export function createColumns(onSuccess?: () => void): ColumnDef<INPC>[] {
|
||||
return [
|
||||
{
|
||||
accessorKey: 'year',
|
||||
header: 'Año',
|
||||
cell: ({ row }) => row.original.year || 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'month',
|
||||
header: 'Mes',
|
||||
cell: ({ row }) => row.original.month || 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'value',
|
||||
header: 'Valor',
|
||||
cell: ({ row }) => row.original.value?.toString() || 'N/A'
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
cell: ({ row }) => {
|
||||
return renderComponent(DataTableActions, {
|
||||
item: row.original,
|
||||
onSuccess
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
<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 { companyStore } from "$lib/stores/company.svelte";
|
||||
import {
|
||||
createINPC,
|
||||
updateINPC,
|
||||
type INPC
|
||||
} from '$lib/api/dashboard/a76/general_catalogs/inpc';
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
item = null,
|
||||
onSuccess
|
||||
}: {
|
||||
open: boolean;
|
||||
item?: INPC | null;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
const isEdit = $derived(!!item);
|
||||
const title = $derived(isEdit ? `Editar INPC ${item?.year}-${item?.month}` : "Nuevo INPC");
|
||||
|
||||
// Estado del formulario
|
||||
let formData = $state({
|
||||
year: '',
|
||||
month: '',
|
||||
value: ''
|
||||
});
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// Cargar datos
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
if (item) {
|
||||
formData = {
|
||||
year: item.year || '',
|
||||
month: item.month || '',
|
||||
value: item.value?.toString() || ''
|
||||
};
|
||||
} else {
|
||||
// Reset
|
||||
formData = {
|
||||
year: '',
|
||||
month: '',
|
||||
value: ''
|
||||
};
|
||||
}
|
||||
error = null;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
error = 'No hay compañía seleccionada';
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
year: formData.year,
|
||||
month: formData.month,
|
||||
value: formData.value ? parseFloat(formData.value) : undefined
|
||||
};
|
||||
|
||||
if (isEdit && item) {
|
||||
await updateINPC(item.id, payload, companyId);
|
||||
} else {
|
||||
await createINPC(payload, companyId);
|
||||
}
|
||||
|
||||
open = false;
|
||||
if (onSuccess) onSuccess();
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
error = e instanceof Error ? e.message : 'Error al guardar INPC';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="sm:max-w-[500px]">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="grid gap-4 py-4">
|
||||
{#if error}
|
||||
<div class="rounded-md bg-destructive/15 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="year" class="text-right">Año</Label>
|
||||
<Input id="year" bind:value={formData.year} class="col-span-3" maxlength={4} required />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="month" class="text-right">Mes</Label>
|
||||
<Input id="month" bind:value={formData.month} class="col-span-3" maxlength={2} required placeholder="01-12" />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="value" class="text-right">Valor</Label>
|
||||
<Input id="value" type="number" step="0.00000001" bind:value={formData.value} class="col-span-3" />
|
||||
</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>
|
||||
@@ -0,0 +1,165 @@
|
||||
<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 {
|
||||
createINPC,
|
||||
updateINPC,
|
||||
type INPC
|
||||
} from "$lib/api/dashboard/a76/general_catalogs/inpc";
|
||||
import { companyStore } from "$lib/stores/company.svelte";
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
item = null,
|
||||
onSuccess
|
||||
}: {
|
||||
open: boolean;
|
||||
item?: INPC | null;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
const isEdit = $derived(!!item);
|
||||
const title = $derived(isEdit ? "Editar INPC" : "Nuevo INPC");
|
||||
|
||||
let formData = $state({
|
||||
year: '',
|
||||
month: '',
|
||||
value: null as number | null
|
||||
});
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// Cargar datos al abrir
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
if (item) {
|
||||
formData = {
|
||||
year: item.year,
|
||||
month: item.month,
|
||||
value: item.value || null
|
||||
};
|
||||
} else {
|
||||
formData = {
|
||||
year: '',
|
||||
month: '',
|
||||
value: null
|
||||
};
|
||||
}
|
||||
error = null;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) throw new Error('No hay una compañía seleccionada');
|
||||
|
||||
// Validaciones
|
||||
if (!formData.year.trim()) throw new Error('El año es requerido');
|
||||
if (formData.year.length !== 4) throw new Error('El año debe tener 4 dígitos');
|
||||
if (!formData.month.trim()) throw new Error('El mes es requerido');
|
||||
|
||||
// Preparar datos (limpios)
|
||||
const dataToSend = {
|
||||
year: formData.year.trim(),
|
||||
month: formData.month.trim(),
|
||||
value: formData.value // Ya es número o null
|
||||
};
|
||||
|
||||
let response;
|
||||
|
||||
|
||||
if (isEdit && item) {
|
||||
response = await updateINPC(item.id, dataToSend, companyId);
|
||||
} else {
|
||||
|
||||
response = await createINPC(dataToSend, companyId);
|
||||
}
|
||||
|
||||
open = false;
|
||||
if (onSuccess) onSuccess();
|
||||
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Error al guardar el INPC';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="sm:max-w-[400px]">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="space-y-4 py-4">
|
||||
{#if error}
|
||||
<div class="rounded-md bg-destructive/15 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="grid gap-4">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="year" class="text-right">Año <span class="text-destructive">*</span></Label>
|
||||
<div class="col-span-3">
|
||||
<Input
|
||||
id="year"
|
||||
bind:value={formData.year}
|
||||
placeholder="Ej: 2025"
|
||||
maxlength={4}
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="month" class="text-right">Mes <span class="text-destructive">*</span></Label>
|
||||
<div class="col-span-3">
|
||||
<Input
|
||||
id="month"
|
||||
bind:value={formData.month}
|
||||
placeholder="Ej: 01"
|
||||
maxlength={2}
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
<p class="text-[10px] text-muted-foreground mt-1">Formato MM (Ej: 01, 12)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="value" class="text-right">Valor</Label>
|
||||
<div class="col-span-3">
|
||||
<Input
|
||||
id="value"
|
||||
type="number"
|
||||
step="0.00000001"
|
||||
bind:value={formData.value}
|
||||
placeholder="0.0000"
|
||||
disabled={loading}
|
||||
/>
|
||||
<p class="text-[10px] text-muted-foreground mt-1">Hasta 8 decimales</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button type="button" variant="outline" onclick={() => open = false} disabled={loading}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? 'Guardando...' : isEdit ? 'Actualizar' : 'Crear'}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -0,0 +1,85 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import { EllipsisVertical, Pencil, Trash2, LoaderCircle } from 'lucide-svelte';
|
||||
import type { INPC } from '$lib/api/dashboard/a76/general_catalogs/inpc';
|
||||
import { deleteINPC } from '$lib/api/dashboard/a76/general_catalogs/inpc';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import CreateEditDialog from './create-edit-dialog.svelte';
|
||||
|
||||
let {
|
||||
item,
|
||||
onSuccess
|
||||
}: {
|
||||
item: INPC;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
let dialogOpen = $state(false);
|
||||
let selectedItem = $state<INPC | null>(null);
|
||||
|
||||
async function handleDelete() {
|
||||
if (!confirm('¿Está seguro de eliminar este registro de INPC?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
alert('❌ Error: No hay compañía seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
await deleteINPC(item.id, companyId);
|
||||
alert('✅ Registro eliminado correctamente');
|
||||
if (onSuccess) onSuccess();
|
||||
} catch (err: any) {
|
||||
error = err.message || 'Error al eliminar el registro';
|
||||
alert(`❌ Error: ${error}`);
|
||||
console.error('Error deleting INPC:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleEdit() {
|
||||
selectedItem = item;
|
||||
dialogOpen = true;
|
||||
}
|
||||
</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={handleEdit}>
|
||||
<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}
|
||||
item={selectedItem}
|
||||
onSuccess={onSuccess}
|
||||
/>
|
||||
@@ -0,0 +1,104 @@
|
||||
<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>
|
||||
{#if table.getRowModel().rows.length}
|
||||
{#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>
|
||||
{/each}
|
||||
{:else}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={columns.length} class="h-24 text-center">
|
||||
No hay resultados.
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/if}
|
||||
</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} registros
|
||||
</div>
|
||||
<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>
|
||||
@@ -0,0 +1,153 @@
|
||||
<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 {
|
||||
createLegend,
|
||||
updateLegend,
|
||||
type Legend
|
||||
} from "$lib/api/dashboard/a76/general_catalogs/legends";
|
||||
import { companyStore } from "$lib/stores/company.svelte";
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
item = null,
|
||||
onSuccess
|
||||
}: {
|
||||
open: boolean;
|
||||
item?: Legend | null;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
const isEdit = $derived(!!item);
|
||||
const title = $derived(isEdit ? "Editar Leyenda" : "Nueva Leyenda");
|
||||
|
||||
// Estado del formulario
|
||||
// code es number | null para manejar el input type="number"
|
||||
let formData = $state({
|
||||
code: null as number | null,
|
||||
description: ''
|
||||
});
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// Cargar datos al abrir
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
if (item) {
|
||||
formData = {
|
||||
code: item.code, // Es number
|
||||
description: item.description || ''
|
||||
};
|
||||
} else {
|
||||
formData = {
|
||||
code: null,
|
||||
description: ''
|
||||
};
|
||||
}
|
||||
error = null;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) throw new Error('No hay una compañía seleccionada');
|
||||
|
||||
// Validaciones
|
||||
if (formData.code === null) throw new Error('La Clave (Código) es requerida');
|
||||
|
||||
// Preparar datos
|
||||
const dataToSend = {
|
||||
code: Number(formData.code), // Aseguramos que sea número
|
||||
description: formData.description.trim() || undefined
|
||||
};
|
||||
|
||||
let response;
|
||||
|
||||
if (isEdit && item) {
|
||||
response = await updateLegend(item.id, dataToSend, companyId);
|
||||
} else {
|
||||
response = await createLegend(dataToSend, companyId);
|
||||
}
|
||||
|
||||
// Verificar si hubo error
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
open = false;
|
||||
if (onSuccess) onSuccess();
|
||||
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Error al guardar la leyenda';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="sm:max-w-[500px]">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="space-y-4 py-4">
|
||||
{#if error}
|
||||
<div class="rounded-md bg-destructive/15 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="grid gap-4">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="code" class="text-right">Clave <span class="text-destructive">*</span></Label>
|
||||
<div class="col-span-3">
|
||||
<Input
|
||||
id="code"
|
||||
type="number"
|
||||
bind:value={formData.code}
|
||||
placeholder="Ej: 10"
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
<p class="text-[10px] text-muted-foreground mt-1">Debe ser un número entero.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="description" class="text-right">Descripción</Label>
|
||||
<div class="col-span-3">
|
||||
<Textarea
|
||||
id="description"
|
||||
bind:value={formData.description}
|
||||
placeholder="Ej: Esta leyenda aplica para..."
|
||||
maxlength={2000}
|
||||
disabled={loading}
|
||||
class="min-h-[100px]"
|
||||
/>
|
||||
<p class="text-[10px] text-muted-foreground mt-1 text-right">
|
||||
{formData.description.length}/2000
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button type="button" variant="outline" onclick={() => open = false} disabled={loading}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? 'Guardando...' : isEdit ? 'Actualizar' : 'Crear'}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { Legend } from '$lib/api/dashboard/a76/general_catalogs/legends';
|
||||
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<Legend>[] {
|
||||
return [
|
||||
{
|
||||
accessorKey: 'code',
|
||||
header: 'Código',
|
||||
cell: ({ row }) => row.original.code?.toString() || '-'
|
||||
},
|
||||
{
|
||||
accessorKey: 'description',
|
||||
header: 'Descripción',
|
||||
cell: ({ row }) => row.original.description || '-'
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: 'Acciones',
|
||||
cell: ({ row }) => {
|
||||
return renderComponent(DataTableActions, {
|
||||
item: row.original,
|
||||
onSuccess
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
|
||||
import { deleteLegend, type Legend } from "$lib/api/dashboard/a76/general_catalogs/legends";
|
||||
import { companyStore } from "$lib/stores/company.svelte";
|
||||
import { EllipsisVertical, Pencil, LoaderCircle, Trash2 } from 'lucide-svelte';
|
||||
import CreateDialog from '$lib/components/dashboard/general_catalogs/legend/create-edite-dialoge.svelte';
|
||||
|
||||
|
||||
let {
|
||||
item,
|
||||
onSuccess
|
||||
}: {
|
||||
item: Legend;
|
||||
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 leyenda "${item.code}"?\n\nNota: No se puede eliminar si tiene registros relacionados.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!companyStore.activeCompany) {
|
||||
alert('❌ Error: No hay una compañía seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const response = await deleteLegend(item.id, companyStore.activeCompany.id);
|
||||
|
||||
if (response.error) {
|
||||
alert(`❌ Error al eliminar:\n\n${response.error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Éxito
|
||||
if (response.status === 204 || response.status === 200 || !response.error) {
|
||||
alert(`✅ Leyenda "${item.code}" eliminada correctamente`);
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
const errorMsg = e instanceof Error ? e.message : 'Error desconocido al eliminar el registro';
|
||||
alert(`❌ Error al eliminar:\n\n${errorMsg}`);
|
||||
} 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>
|
||||
|
||||
<CreateDialog
|
||||
bind:open={dialogOpen}
|
||||
item={item}
|
||||
onSuccess={onSuccess}
|
||||
/>
|
||||
@@ -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; },
|
||||
get columns() { return columns; },
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
get pageCount() { return pageCount; },
|
||||
});
|
||||
|
||||
function handlePageChange(newPage: number) {
|
||||
const url = new URL($page.url);
|
||||
url.searchParams.set('page', newPage.toString());
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
}
|
||||
|
||||
const currentPage = $derived(Number($page.url.searchParams.get('page') || 1));
|
||||
</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 px-2">
|
||||
<div class="flex-1 text-sm text-muted-foreground">
|
||||
Total: {totalItems} registros
|
||||
</div>
|
||||
<div class="space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => handlePageChange(currentPage - 1)}
|
||||
disabled={currentPage <= 1}
|
||||
>
|
||||
Anterior
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => handlePageChange(currentPage + 1)}
|
||||
disabled={currentPage >= pageCount}
|
||||
>
|
||||
Siguiente
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,126 @@
|
||||
<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 {
|
||||
createLocation,
|
||||
updateLocation,
|
||||
type Location
|
||||
} from "$lib/api/dashboard/a76/general_catalogs/locations";
|
||||
import { companyStore } from "$lib/stores/company.svelte";
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
item = null,
|
||||
onSuccess
|
||||
}: {
|
||||
open: boolean;
|
||||
item?: Location | null;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
const isEdit = $derived(!!item);
|
||||
const title = $derived(isEdit ? "Editar Ubicación" : "Nueva Ubicación");
|
||||
|
||||
let formData = $state({
|
||||
location_code: "",
|
||||
location_description: ""
|
||||
});
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
if (item) {
|
||||
formData = {
|
||||
location_code: item.location_code || "",
|
||||
location_description: item.location_description || ""
|
||||
};
|
||||
} else {
|
||||
formData = { location_code: "", location_description: "" };
|
||||
}
|
||||
error = null;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) throw new Error("No hay una compañía seleccionada");
|
||||
|
||||
if (!formData.location_code.trim()) throw new Error("El código es requerido");
|
||||
|
||||
const basePayload = {
|
||||
location_description: formData.location_description?.trim() || null
|
||||
};
|
||||
|
||||
if (isEdit && item) {
|
||||
await updateLocation(item.id, basePayload, companyId);
|
||||
alert("✅ Ubicación actualizada correctamente");
|
||||
} else {
|
||||
const createPayload = {
|
||||
location_code: formData.location_code.trim(),
|
||||
...basePayload
|
||||
};
|
||||
await createLocation(createPayload, companyId);
|
||||
alert("✅ Ubicación creada correctamente");
|
||||
}
|
||||
|
||||
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.Portal>
|
||||
<Dialog.Overlay class="fixed inset-0 z-[9999] bg-black/80 backdrop-blur-sm" />
|
||||
|
||||
<Dialog.Content class="fixed left-[50%] top-[50%] z-[10000] w-full max-w-[520px] translate-x-[-50%] translate-y-[-50%] border bg-background p-6 shadow-lg sm:rounded-lg">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="space-y-4 py-4">
|
||||
{#if error}
|
||||
<div class="rounded-md bg-destructive/15 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="grid gap-4">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="location_code" class="text-right">Código *</Label>
|
||||
<div class="col-span-3">
|
||||
<Input id="location_code" bind:value={formData.location_code} maxlength={4} disabled={loading || isEdit} required />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="location_description" class="text-right">Descripción</Label>
|
||||
<div class="col-span-3">
|
||||
<Input id="location_description" bind:value={formData.location_description} maxlength={20} disabled={loading} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button type="button" variant="outline" onclick={() => open = false} disabled={loading}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? 'Guardando...' : isEdit ? 'Actualizar' : 'Crear'}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
@@ -0,0 +1,195 @@
|
||||
<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";
|
||||
// 👇 Verifica la ruta de tu archivo TS
|
||||
import {
|
||||
createMultiCurrencyType,
|
||||
updateMultiCurrencyType,
|
||||
type MultiCurrencyType
|
||||
} from "$lib/api/dashboard/a76/general_catalogs/multi-currency-types";
|
||||
import { companyStore } from "$lib/stores/company.svelte";
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
item = null,
|
||||
onSuccess
|
||||
}: {
|
||||
open: boolean;
|
||||
item?: MultiCurrencyType | null;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
const isEdit = $derived(!!item);
|
||||
const title = $derived(isEdit ? "Editar Tipo de Cambio Múltiple" : "Nuevo Tipo de Cambio Múltiple");
|
||||
|
||||
// Estado del formulario
|
||||
let formData = $state({
|
||||
currency_type_code: '',
|
||||
country_key: '',
|
||||
conversion_factor: null as number | null,
|
||||
date_str: '' // Usamos un string temporal para el input type="date"
|
||||
});
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// Cargar datos al abrir
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
if (item) {
|
||||
// Truco: Convertir Entero (20251231) -> String ("2025-12-31")
|
||||
let dateFormatted = '';
|
||||
if (item.publication_date) {
|
||||
const s = item.publication_date.toString();
|
||||
if (s.length === 8) {
|
||||
dateFormatted = `${s.slice(0, 4)}-${s.slice(4, 6)}-${s.slice(6, 8)}`;
|
||||
}
|
||||
}
|
||||
|
||||
formData = {
|
||||
currency_type_code: item.currency_type_code,
|
||||
country_key: item.country_key || '',
|
||||
conversion_factor: item.conversion_factor,
|
||||
date_str: dateFormatted
|
||||
};
|
||||
} else {
|
||||
// Default: Fecha de hoy
|
||||
formData = {
|
||||
currency_type_code: '',
|
||||
country_key: '',
|
||||
conversion_factor: null,
|
||||
date_str: new Date().toISOString().split('T')[0]
|
||||
};
|
||||
}
|
||||
error = null;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) throw new Error('No hay una compañía seleccionada');
|
||||
|
||||
// Validaciones
|
||||
if (!formData.currency_type_code.trim()) throw new Error('El código de moneda es requerido');
|
||||
if (!formData.date_str) throw new Error('La fecha de publicación es requerida');
|
||||
|
||||
// Truco: Convertir String ("2025-12-31") -> Entero (20251231)
|
||||
// Quitamos los guiones y parseamos a int
|
||||
const dateInt = parseInt(formData.date_str.replaceAll('-', ''), 10);
|
||||
|
||||
// Preparar datos
|
||||
const dataToSend = {
|
||||
currency_type_code: formData.currency_type_code.trim().toUpperCase(),
|
||||
country_key: formData.country_key.trim().toUpperCase() || null,
|
||||
conversion_factor: formData.conversion_factor ? Number(formData.conversion_factor) : null,
|
||||
publication_date: dateInt // Mandamos el INT que espera Python
|
||||
};
|
||||
|
||||
if (isEdit && item) {
|
||||
await updateMultiCurrencyType(item.id, dataToSend, companyId);
|
||||
alert(`✅ Tipo de moneda múltiple actualizado correctamente`);
|
||||
} else {
|
||||
await createMultiCurrencyType(dataToSend, companyId);
|
||||
alert(`✅ Tipo de moneda múltiple creado correctamente`);
|
||||
}
|
||||
|
||||
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="sm:max-w-[500px]">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="space-y-4 py-4">
|
||||
{#if error}
|
||||
<div class="rounded-md bg-destructive/15 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="grid gap-4">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="currency_code" class="text-right">Moneda <span class="text-destructive">*</span></Label>
|
||||
<div class="col-span-3">
|
||||
<Input
|
||||
id="currency_code"
|
||||
bind:value={formData.currency_type_code}
|
||||
placeholder="Ej: USD"
|
||||
maxlength={3}
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
<p class="text-[10px] text-muted-foreground mt-1">Código de moneda (FK).</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="country_key" class="text-right">País</Label>
|
||||
<div class="col-span-3">
|
||||
<Input
|
||||
id="country_key"
|
||||
bind:value={formData.country_key}
|
||||
placeholder="Ej: MEX"
|
||||
maxlength={3}
|
||||
disabled={loading}
|
||||
/>
|
||||
<p class="text-[10px] text-muted-foreground mt-1">Clave M3 del país (FK).</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="pub_date" class="text-right">Fecha <span class="text-destructive">*</span></Label>
|
||||
<div class="col-span-3">
|
||||
<Input
|
||||
id="pub_date"
|
||||
type="date"
|
||||
bind:value={formData.date_str}
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
<p class="text-[10px] text-muted-foreground mt-1">Se guarda como entero (YYYYMMDD).</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="factor" class="text-right">Factor</Label>
|
||||
<div class="col-span-3">
|
||||
<Input
|
||||
id="factor"
|
||||
type="number"
|
||||
step="0.000001"
|
||||
bind:value={formData.conversion_factor}
|
||||
placeholder="0.000000"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button type="button" variant="outline" onclick={() => open = false} disabled={loading}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? 'Guardando...' : isEdit ? 'Actualizar' : 'Crear'}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { ColumnDef } from '@tanstack/table-core';
|
||||
import type { Prevalidator } from '$lib/api/dashboard/a76/general_catalogs/prevalidators';
|
||||
import { renderComponent } from '$lib/components/ui/data-table';
|
||||
import DataTableActions from './data-table-actions.svelte';
|
||||
|
||||
export function createColumns(onSuccess?: () => void): ColumnDef<Prevalidator>[] {
|
||||
return [
|
||||
{
|
||||
accessorKey: 'code',
|
||||
header: 'Código',
|
||||
cell: ({ row }) => row.original.code || 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'description',
|
||||
header: 'Descripción',
|
||||
cell: ({ row }) => row.original.description || 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'customs_prevalidator',
|
||||
header: 'Aduana',
|
||||
cell: ({ row }) => row.original.customs_prevalidator || 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'patent_prevalidator',
|
||||
header: 'Patente',
|
||||
cell: ({ row }) => row.original.patent_prevalidator || 'N/A'
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
cell: ({ row }) => {
|
||||
return renderComponent(DataTableActions, {
|
||||
item: row.original,
|
||||
onSuccess
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
<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 { companyStore } from "$lib/stores/company.svelte";
|
||||
import {
|
||||
createPrevalidator,
|
||||
updatePrevalidator,
|
||||
type Prevalidator
|
||||
} from '$lib/api/dashboard/a76/general_catalogs/prevalidators';
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
item = null,
|
||||
onSuccess
|
||||
}: {
|
||||
open: boolean;
|
||||
item?: Prevalidator | null;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
const isEdit = $derived(!!item);
|
||||
const title = $derived(isEdit ? `Editar Prevalidador ${item?.code || ''}` : "Nuevo Prevalidador");
|
||||
|
||||
// Estado del formulario
|
||||
let formData = $state({
|
||||
code: '',
|
||||
description: '',
|
||||
customs_prevalidator: '',
|
||||
patent_prevalidator: ''
|
||||
});
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// Cargar datos
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
if (item) {
|
||||
formData = {
|
||||
code: item.code || '',
|
||||
description: item.description || '',
|
||||
customs_prevalidator: item.customs_prevalidator || '',
|
||||
patent_prevalidator: item.patent_prevalidator || ''
|
||||
};
|
||||
} else {
|
||||
// Reset
|
||||
formData = {
|
||||
code: '',
|
||||
description: '',
|
||||
customs_prevalidator: '',
|
||||
patent_prevalidator: ''
|
||||
};
|
||||
}
|
||||
error = null;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
error = 'No hay compañía seleccionada';
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
if (isEdit && item) {
|
||||
await updatePrevalidator(item.id, formData, companyId);
|
||||
} else {
|
||||
await createPrevalidator(formData, companyId);
|
||||
}
|
||||
|
||||
open = false;
|
||||
if (onSuccess) onSuccess();
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
error = e instanceof Error ? e.message : 'Error al guardar prevalidador';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="sm:max-w-[500px]">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="grid gap-4 py-4">
|
||||
{#if error}
|
||||
<div class="rounded-md bg-destructive/15 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="code">Código</Label>
|
||||
<Input id="code" bind:value={formData.code} placeholder="Ej. 123" maxlength={20} />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="description">Descripción</Label>
|
||||
<Input id="description" bind:value={formData.description} placeholder="Descripción del prevalidador" maxlength={50} />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="customs_prevalidator">Aduana</Label>
|
||||
<Input id="customs_prevalidator" bind:value={formData.customs_prevalidator} placeholder="Ej. 123" maxlength={20} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="patent_prevalidator">Patente</Label>
|
||||
<Input id="patent_prevalidator" bind:value={formData.patent_prevalidator} placeholder="Ej. 1234" maxlength={20} />
|
||||
</div>
|
||||
</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>
|
||||
@@ -0,0 +1,85 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import { EllipsisVertical, Pencil, Trash2, LoaderCircle } from 'lucide-svelte';
|
||||
import type { Prevalidator } from '$lib/api/dashboard/a76/general_catalogs/prevalidators';
|
||||
import { deletePrevalidator } from '$lib/api/dashboard/a76/general_catalogs/prevalidators';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import CreateEditDialog from './create-edit-dialog.svelte';
|
||||
|
||||
let {
|
||||
item,
|
||||
onSuccess
|
||||
}: {
|
||||
item: Prevalidator;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
let dialogOpen = $state(false);
|
||||
let selectedItem = $state<Prevalidator | null>(null);
|
||||
|
||||
async function handleDelete() {
|
||||
if (!confirm('¿Está seguro de eliminar este prevalidador?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
alert('❌ Error: No hay compañía seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
await deletePrevalidator(item.id, companyId);
|
||||
alert('✅ Prevalidador eliminado correctamente');
|
||||
if (onSuccess) onSuccess();
|
||||
} catch (err: any) {
|
||||
error = err.message || 'Error al eliminar el prevalidador';
|
||||
alert(`❌ Error: ${error}`);
|
||||
console.error('Error deleting prevalidator:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleEdit() {
|
||||
selectedItem = item;
|
||||
dialogOpen = true;
|
||||
}
|
||||
</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={handleEdit}>
|
||||
<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}
|
||||
item={selectedItem}
|
||||
onSuccess={onSuccess}
|
||||
/>
|
||||
@@ -0,0 +1,102 @@
|
||||
<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">
|
||||
<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>
|
||||
<div class="text-sm text-muted-foreground">
|
||||
Página {Number($page.url.searchParams.get('page') || 1)} de {pageCount}
|
||||
</div>
|
||||
<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>
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { ColumnDef } from '@tanstack/table-core';
|
||||
import type { Signature } from '$lib/api/dashboard/a76/general_catalogs/signatures';
|
||||
import { renderComponent } from '$lib/components/ui/data-table';
|
||||
import DataTableActions from './data-table-actions.svelte';
|
||||
|
||||
export function createColumns(onSuccess?: () => void): ColumnDef<Signature>[] {
|
||||
return [
|
||||
{
|
||||
accessorKey: 'code',
|
||||
header: 'Código',
|
||||
cell: ({ row }) => row.original.code || 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'signature',
|
||||
header: 'Firma',
|
||||
cell: ({ row }) => row.original.signature ?? 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'photo_path',
|
||||
header: 'Ruta Foto',
|
||||
cell: ({ row }) => row.original.photo_path ?? 'N/A'
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: 'Acciones',
|
||||
cell: ({ row }) => renderComponent(DataTableActions, {
|
||||
item: row.original,
|
||||
onSuccess
|
||||
})
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
<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"; // Para que quepa más texto en la firma
|
||||
// 👇 Verifica tu ruta de importación
|
||||
import {
|
||||
createSignature,
|
||||
updateSignature,
|
||||
type Signature
|
||||
} from "$lib/api/dashboard/a76/general_catalogs/signatures";
|
||||
import { companyStore } from "$lib/stores/company.svelte";
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
item = null,
|
||||
onSuccess
|
||||
}: {
|
||||
open: boolean;
|
||||
item?: Signature | null;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
const isEdit = $derived(!!item);
|
||||
const title = $derived(isEdit ? "Editar Firma" : "Nueva Firma");
|
||||
|
||||
// Estado del formulario
|
||||
let formData = $state({
|
||||
code: '',
|
||||
signature: '',
|
||||
photo_path: ''
|
||||
});
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// Cargar datos al abrir
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
if (item) {
|
||||
formData = {
|
||||
code: item.code,
|
||||
signature: item.signature || '',
|
||||
photo_path: item.photo_path || ''
|
||||
};
|
||||
} else {
|
||||
formData = {
|
||||
code: '',
|
||||
signature: '',
|
||||
photo_path: ''
|
||||
};
|
||||
}
|
||||
error = null;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) throw new Error('No hay una compañía seleccionada');
|
||||
|
||||
// Validaciones
|
||||
if (!formData.code.trim()) throw new Error('El código es requerido');
|
||||
|
||||
// Preparar datos
|
||||
const dataToSend = {
|
||||
code: formData.code.trim(),
|
||||
signature: formData.signature.trim() || null,
|
||||
photo_path: formData.photo_path.trim() || null
|
||||
};
|
||||
|
||||
let response;
|
||||
|
||||
// 👇 companyId por fuera (Argumento separado)
|
||||
if (isEdit && item) {
|
||||
response = await updateSignature(item.id, dataToSend, companyId);
|
||||
} else {
|
||||
response = await createSignature(dataToSend, companyId);
|
||||
}
|
||||
|
||||
open = false;
|
||||
if (onSuccess) onSuccess();
|
||||
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Error al guardar la firma';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="sm:max-w-[500px]">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="space-y-4 py-4">
|
||||
{#if error}
|
||||
<div class="rounded-md bg-destructive/15 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="grid gap-4">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="code" class="text-right">Código <span class="text-destructive">*</span></Label>
|
||||
<div class="col-span-3">
|
||||
<Input
|
||||
id="code"
|
||||
bind:value={formData.code}
|
||||
placeholder="Ej: REP_LEGAL"
|
||||
maxlength={10}
|
||||
disabled={loading || isEdit}
|
||||
required
|
||||
/>
|
||||
<p class="text-[10px] text-muted-foreground mt-1">Máximo 10 caracteres.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="signature" class="text-right">Firma / Nombre</Label>
|
||||
<div class="col-span-3">
|
||||
<Textarea
|
||||
id="signature"
|
||||
bind:value={formData.signature}
|
||||
placeholder="Ej: Juan Pérez - Representante Legal"
|
||||
maxlength={1000}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="photo_path" class="text-right">Ruta Foto</Label>
|
||||
<div class="col-span-3">
|
||||
<Input
|
||||
id="photo_path"
|
||||
bind:value={formData.photo_path}
|
||||
placeholder="Ej: /uploads/firmas/juan.png"
|
||||
maxlength={1000}
|
||||
disabled={loading}
|
||||
/>
|
||||
<p class="text-[10px] text-muted-foreground mt-1">Ruta del archivo (Texto).</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button type="button" variant="outline" onclick={() => open = false} disabled={loading}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? 'Guardando...' : isEdit ? 'Actualizar' : 'Crear'}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -0,0 +1,93 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import { EllipsisVertical, Pencil, Trash2, LoaderCircle } from 'lucide-svelte';
|
||||
import type { Signature } from '$lib/api/dashboard/a76/general_catalogs/signatures';
|
||||
import { deleteSignature } from '$lib/api/dashboard/a76/general_catalogs/signatures';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import CreateEditDialog from './create-edit-dialog.svelte';
|
||||
|
||||
let {
|
||||
item,
|
||||
onSuccess
|
||||
}: {
|
||||
item: Signature;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
let dialogOpen = $state(false);
|
||||
let selectedItem = $state<Signature | null>(null);
|
||||
|
||||
async function handleDelete() {
|
||||
if (!confirm('¿Está seguro de eliminar esta firma electrónica?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
alert('❌ Error: No hay compañía seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
await deleteSignature(item.id, companyId);
|
||||
alert('✅ Firma eliminada correctamente');
|
||||
if (onSuccess) onSuccess();
|
||||
} catch (err: any) {
|
||||
error = err.message || 'Error al eliminar la firma';
|
||||
alert(`❌ Error: ${error}`);
|
||||
console.error('Error deleting signature:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleEdit() {
|
||||
selectedItem = item;
|
||||
dialogOpen = true;
|
||||
}
|
||||
|
||||
function handleDialogSuccess() {
|
||||
dialogOpen = false;
|
||||
selectedItem = null;
|
||||
if (onSuccess) onSuccess();
|
||||
}
|
||||
</script>
|
||||
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<Button {...props} variant="ghost" size="icon" class="relative size-8 p-0">
|
||||
<span class="sr-only">Abrir menú</span>
|
||||
<EllipsisVertical size={16} />
|
||||
</Button>
|
||||
{/snippet}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end" class="w-[160px]">
|
||||
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item onclick={handleEdit}>
|
||||
<Pencil size={16} class="mr-2" />
|
||||
Editar
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item onclick={handleDelete} class="text-destructive" disabled={loading}>
|
||||
{#if loading}
|
||||
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
|
||||
{:else}
|
||||
<Trash2 size={16} class="mr-2" />
|
||||
{/if}
|
||||
Eliminar
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
|
||||
<CreateEditDialog
|
||||
bind:open={dialogOpen}
|
||||
item={selectedItem}
|
||||
onSuccess={handleDialogSuccess}
|
||||
/>
|
||||
@@ -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; },
|
||||
get columns() { return columns; },
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
get pageCount() { return pageCount; },
|
||||
});
|
||||
|
||||
function handlePageChange(newPage: number) {
|
||||
const url = new URL($page.url);
|
||||
url.searchParams.set('page', newPage.toString());
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
}
|
||||
|
||||
const currentPage = $derived(Number($page.url.searchParams.get('page') || 1));
|
||||
</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 px-2">
|
||||
<div class="flex-1 text-sm text-muted-foreground">
|
||||
Total: {totalItems} registros
|
||||
</div>
|
||||
<div class="space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => handlePageChange(currentPage - 1)}
|
||||
disabled={currentPage <= 1}
|
||||
>
|
||||
Anterior
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => handlePageChange(currentPage + 1)}
|
||||
disabled={currentPage >= pageCount}
|
||||
>
|
||||
Siguiente
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,164 @@
|
||||
<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 {
|
||||
createUnitConversion,
|
||||
updateUnitConversion,
|
||||
type UnitConversion
|
||||
} from "$lib/api/dashboard/a76/general_catalogs/unit-conversions"; // 👈 Asegúrate que la ruta del archivo ts coincida
|
||||
import { companyStore } from "$lib/stores/company.svelte";
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
item = null,
|
||||
onSuccess
|
||||
}: {
|
||||
open: boolean;
|
||||
item?: UnitConversion | null;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
const isEdit = $derived(!!item);
|
||||
const title = $derived(isEdit ? "Editar Conversión" : "Nueva Conversión");
|
||||
|
||||
// Estado del formulario
|
||||
let formData = $state({
|
||||
from_unit_code: '',
|
||||
to_unit_code: '',
|
||||
conversion_factor: null as number | null
|
||||
});
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// Cargar datos al abrir
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
if (item) {
|
||||
formData = {
|
||||
from_unit_code: item.from_unit_code,
|
||||
to_unit_code: item.to_unit_code,
|
||||
conversion_factor: item.conversion_factor
|
||||
};
|
||||
} else {
|
||||
formData = {
|
||||
from_unit_code: '',
|
||||
to_unit_code: '',
|
||||
conversion_factor: null
|
||||
};
|
||||
}
|
||||
error = null;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) throw new Error('No hay una compañía seleccionada');
|
||||
|
||||
// Validaciones
|
||||
if (!formData.from_unit_code.trim()) throw new Error('La unidad origen es requerida');
|
||||
if (!formData.to_unit_code.trim()) throw new Error('La unidad destino es requerida');
|
||||
if (formData.conversion_factor === null || formData.conversion_factor === undefined) throw new Error('El factor de conversión es requerido');
|
||||
|
||||
// Preparar datos
|
||||
const dataToSend = {
|
||||
from_unit_code: formData.from_unit_code.trim().toUpperCase(), // Normalizamos a mayúsculas
|
||||
to_unit_code: formData.to_unit_code.trim().toUpperCase(),
|
||||
conversion_factor: Number(formData.conversion_factor)
|
||||
};
|
||||
|
||||
let response;
|
||||
|
||||
// 👇 companyId por fuera (Argumento separado)
|
||||
if (isEdit && item) {
|
||||
response = await updateUnitConversion(item.id, dataToSend, companyId);
|
||||
} else {
|
||||
response = await createUnitConversion(dataToSend, companyId);
|
||||
}
|
||||
|
||||
open = false;
|
||||
if (onSuccess) onSuccess();
|
||||
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Error al guardar la conversión';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="sm:max-w-[500px]">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="space-y-4 py-4">
|
||||
{#if error}
|
||||
<div class="rounded-md bg-destructive/15 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="grid gap-4">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="from_code" class="text-right">De Unidad <span class="text-destructive">*</span></Label>
|
||||
<div class="col-span-3">
|
||||
<Input
|
||||
id="from_code"
|
||||
bind:value={formData.from_unit_code}
|
||||
placeholder="Ej: KGM"
|
||||
maxlength={5}
|
||||
disabled={loading}
|
||||
/>
|
||||
<p class="text-[10px] text-muted-foreground mt-1">Código de la unidad origen.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="to_code" class="text-right">A Unidad <span class="text-destructive">*</span></Label>
|
||||
<div class="col-span-3">
|
||||
<Input
|
||||
id="to_code"
|
||||
bind:value={formData.to_unit_code}
|
||||
placeholder="Ej: LBR"
|
||||
maxlength={5}
|
||||
disabled={loading}
|
||||
/>
|
||||
<p class="text-[10px] text-muted-foreground mt-1">Código de la unidad destino.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="factor" class="text-right">Factor <span class="text-destructive">*</span></Label>
|
||||
<div class="col-span-3">
|
||||
<Input
|
||||
id="factor"
|
||||
type="number"
|
||||
step="0.000001"
|
||||
bind:value={formData.conversion_factor}
|
||||
placeholder="Ej: 2.20462"
|
||||
disabled={loading}
|
||||
/>
|
||||
<p class="text-[10px] text-muted-foreground mt-1">Numeric(13, 6).</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button type="button" variant="outline" onclick={() => open = false} disabled={loading}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? 'Guardando...' : isEdit ? 'Actualizar' : 'Crear'}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { ColumnDef } from '@tanstack/table-core';
|
||||
import type { UnitConversion } from '$lib/api/dashboard/a76/general_catalogs/unit-conversions';
|
||||
import { renderComponent } from '$lib/components/ui/data-table';
|
||||
import DataTableActions from './data-table-actions.svelte';
|
||||
|
||||
export function createColumns(onSuccess?: () => void): ColumnDef<UnitConversion>[] {
|
||||
return [
|
||||
{
|
||||
accessorKey: 'from_unit_code',
|
||||
header: 'Desde código'
|
||||
},
|
||||
{
|
||||
accessorKey: 'to_unit_code',
|
||||
header: 'Hacia código'
|
||||
},
|
||||
{
|
||||
accessorKey: 'conversion_factor',
|
||||
header: 'Factor de conversión'
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
cell: ({ row }) =>
|
||||
renderComponent(DataTableActions, {
|
||||
conversion: row.original,
|
||||
onSuccess
|
||||
})
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
<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 { createUnitConversion, updateUnitConversion, type UnitConversion } from "$lib/api/dashboard/a76/general_catalogs/unit-conversions";
|
||||
import { companyStore } from "$lib/stores/company.svelte";
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
conversion = null,
|
||||
mode = 'create',
|
||||
onSuccess
|
||||
}: {
|
||||
open: boolean;
|
||||
conversion?: UnitConversion | null;
|
||||
mode?: 'create' | 'edit';
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
const isEdit = $derived(mode === 'edit');
|
||||
const title = $derived(isEdit ? "Editar Conversión" : "Nueva Conversión");
|
||||
|
||||
let formData = $state({
|
||||
from_unit_code: '',
|
||||
to_unit_code: '',
|
||||
conversion_factor: ''
|
||||
});
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
$effect(() => {
|
||||
if (conversion) {
|
||||
formData = {
|
||||
from_unit_code: conversion.from_unit_code || '',
|
||||
to_unit_code: conversion.to_unit_code || '',
|
||||
conversion_factor: conversion.conversion_factor.toString() || ''
|
||||
};
|
||||
} else {
|
||||
formData = {
|
||||
from_unit_code: '',
|
||||
to_unit_code: '',
|
||||
conversion_factor: ''
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) throw new Error('No hay una compañía seleccionada');
|
||||
|
||||
if (!formData.from_unit_code.trim()) throw new Error('El código origen es requerido');
|
||||
if (!formData.to_unit_code.trim()) throw new Error('El código destino es requerido');
|
||||
if (!formData.conversion_factor || formData.conversion_factor === '') throw new Error('El factor de conversión es requerido');
|
||||
|
||||
const dataToSend = {
|
||||
from_unit_code: formData.from_unit_code.trim(),
|
||||
to_unit_code: formData.to_unit_code.trim(),
|
||||
conversion_factor: parseFloat(formData.conversion_factor)
|
||||
};
|
||||
|
||||
if (isNaN(dataToSend.conversion_factor)) {
|
||||
throw new Error('El factor de conversión debe ser un número válido');
|
||||
}
|
||||
|
||||
if (isEdit && conversion) {
|
||||
await updateUnitConversion(conversion.id, dataToSend, companyId);
|
||||
alert(`✅ Conversión "${dataToSend.from_unit_code} → ${dataToSend.to_unit_code}" actualizada correctamente`);
|
||||
} else {
|
||||
await createUnitConversion(dataToSend, companyId);
|
||||
alert(`✅ Conversión "${dataToSend.from_unit_code} → ${dataToSend.to_unit_code}" creada correctamente`);
|
||||
}
|
||||
|
||||
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="p-3 text-sm text-destructive bg-destructive/10 rounded-md">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="grid gap-4 py-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="from_unit_code">Código origen <span class="text-destructive">*</span></Label>
|
||||
<Input
|
||||
id="from_unit_code"
|
||||
bind:value={formData.from_unit_code}
|
||||
maxlength={5}
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="to_unit_code">Código destino <span class="text-destructive">*</span></Label>
|
||||
<Input
|
||||
id="to_unit_code"
|
||||
bind:value={formData.to_unit_code}
|
||||
maxlength={5}
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="conversion_factor">Factor de conversión <span class="text-destructive">*</span></Label>
|
||||
<Input
|
||||
id="conversion_factor"
|
||||
type="number"
|
||||
step="0.000001"
|
||||
bind:value={formData.conversion_factor}
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button type="button" variant="outline" onclick={() => open = false} disabled={loading}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? 'Guardando...' : isEdit ? 'Actualizar' : 'Crear'}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -0,0 +1,79 @@
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
|
||||
import { deleteUnitConversion, type UnitConversion } from "$lib/api/dashboard/a76/general_catalogs/unit-conversions";
|
||||
import { companyStore } from "$lib/stores/company.svelte";
|
||||
import { EllipsisVertical, Pencil, LoaderCircle, Trash2 } from 'lucide-svelte';
|
||||
import CreateEditDialog from "./create-edit-dialog.svelte";
|
||||
|
||||
let {
|
||||
conversion,
|
||||
onSuccess
|
||||
}: {
|
||||
conversion: UnitConversion;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
let loading = $state(false);
|
||||
let dialogOpen = $state(false);
|
||||
|
||||
async function handleDelete() {
|
||||
if (!confirm(`¿Estás seguro de eliminar la conversión "${conversion.from_unit_code} → ${conversion.to_unit_code}"?\n\nNota: No se puede eliminar si tiene registros relacionados.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!companyStore.activeCompany) {
|
||||
alert('❌ Error: No hay una compañía seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
await deleteUnitConversion(conversion.id, companyStore.activeCompany.id);
|
||||
alert(`✅ Conversión "${conversion.from_unit_code} → ${conversion.to_unit_code}" eliminada correctamente`);
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
} catch (e) {
|
||||
const errorMsg = e instanceof Error ? e.message : 'Error desconocido al eliminar el registro';
|
||||
alert(`❌ Error al eliminar:\n\n${errorMsg}`);
|
||||
} 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}
|
||||
conversion={conversion}
|
||||
mode="edit"
|
||||
{onSuccess}
|
||||
/>
|
||||
@@ -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; },
|
||||
get columns() { return columns; },
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
get pageCount() { return pageCount; },
|
||||
});
|
||||
|
||||
function handlePageChange(newPage: number) {
|
||||
const url = new URL($page.url);
|
||||
url.searchParams.set('page', newPage.toString());
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
}
|
||||
|
||||
const currentPage = $derived(Number($page.url.searchParams.get('page') || 1));
|
||||
</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 px-2">
|
||||
<div class="flex-1 text-sm text-muted-foreground">
|
||||
Total: {totalItems} registros
|
||||
</div>
|
||||
<div class="space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => handlePageChange(currentPage - 1)}
|
||||
disabled={currentPage <= 1}
|
||||
>
|
||||
Anterior
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => handlePageChange(currentPage + 1)}
|
||||
disabled={currentPage >= pageCount}
|
||||
>
|
||||
Siguiente
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { UnitOfMeasureACE } from '$lib/api/dashboard/a76/general_catalogs/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',
|
||||
cell: ({ row }) => row.original.code || '-'
|
||||
},
|
||||
{
|
||||
accessorKey: 'description',
|
||||
header: 'Descripción',
|
||||
cell: ({ row }) => row.original.description || '-'
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: 'Acciones',
|
||||
cell: ({ row }) => {
|
||||
return renderComponent(DataTableActions, {
|
||||
item: row.original,
|
||||
onSuccess
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
<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/general_catalogs/units-of-measure";
|
||||
import { companyStore } from "$lib/stores/company.svelte";
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
item = null,
|
||||
onSuccess
|
||||
}: {
|
||||
open: boolean;
|
||||
item?: UnitOfMeasureACE | null;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
const isEdit = $derived(!!item);
|
||||
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 (item) {
|
||||
formData = {
|
||||
code: item.code || '',
|
||||
description: item.description || ''
|
||||
};
|
||||
} else {
|
||||
formData = {
|
||||
code: '',
|
||||
description: ''
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) throw new Error('No hay una compañía seleccionada');
|
||||
|
||||
if (!formData.code.trim()) throw new Error('El código es requerido');
|
||||
|
||||
const dataToSend = {
|
||||
code: formData.code.trim(),
|
||||
description: formData.description.trim() || null
|
||||
};
|
||||
|
||||
let response;
|
||||
if (isEdit && item) {
|
||||
response = await updateUnitOfMeasureACE(item.id, dataToSend, companyId);
|
||||
} else {
|
||||
response = await createUnitOfMeasureACE(dataToSend, companyId);
|
||||
}
|
||||
|
||||
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="p-3 text-sm text-destructive bg-destructive/10 rounded-md">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="grid gap-4 py-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="code">Código <span class="text-destructive">*</span></Label>
|
||||
<Input
|
||||
id="code"
|
||||
bind:value={formData.code}
|
||||
maxlength={4}
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="description">Descripción</Label>
|
||||
<Input
|
||||
id="description"
|
||||
bind:value={formData.description}
|
||||
maxlength={49}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button type="button" variant="outline" onclick={() => open = false} disabled={loading}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? 'Guardando...' : isEdit ? 'Actualizar' : 'Crear'}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -0,0 +1,86 @@
|
||||
<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/general_catalogs/units-of-measure";
|
||||
import { companyStore } from "$lib/stores/company.svelte";
|
||||
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 dialogOpen = $state(false);
|
||||
|
||||
async function handleDelete() {
|
||||
if (!confirm(`¿Estás seguro de eliminar la unidad ACE "${item.code}"?\n\nNota: No se puede eliminar si tiene registros relacionados.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!companyStore.activeCompany) {
|
||||
alert('❌ Error: No hay una compañía seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
const response = await deleteUnitOfMeasureACE(item.id, companyStore.activeCompany.id);
|
||||
|
||||
if (response.error) {
|
||||
alert(`❌ Error al eliminar:\n\n${response.error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.status === 204 || response.status === 200 || !response.error) {
|
||||
alert(`✅ Unidad ACE "${item.code}" eliminada correctamente`);
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
const errorMsg = e instanceof Error ? e.message : 'Error desconocido al eliminar el registro';
|
||||
alert(`❌ Error al eliminar:\n\n${errorMsg}`);
|
||||
} 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}
|
||||
/>
|
||||
@@ -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; },
|
||||
get columns() { return columns; },
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
get pageCount() { return pageCount; },
|
||||
});
|
||||
|
||||
function handlePageChange(newPage: number) {
|
||||
const url = new URL($page.url);
|
||||
url.searchParams.set('page', newPage.toString());
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
}
|
||||
|
||||
const currentPage = $derived(Number($page.url.searchParams.get('page') || 1));
|
||||
</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 px-2">
|
||||
<div class="flex-1 text-sm text-muted-foreground">
|
||||
Total: {totalItems} registros
|
||||
</div>
|
||||
<div class="space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => handlePageChange(currentPage - 1)}
|
||||
disabled={currentPage <= 1}
|
||||
>
|
||||
Anterior
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => handlePageChange(currentPage + 1)}
|
||||
disabled={currentPage >= pageCount}
|
||||
>
|
||||
Siguiente
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { ColumnDef } from "@tanstack/table-core";
|
||||
import type { UnitOfMeasureAmerican } from "$lib/api/dashboard/a76/general_catalogs/units-of-measure";
|
||||
import { renderComponent } from "$lib/components/ui/data-table/index.js";
|
||||
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",
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
cell: ({ row }) =>
|
||||
renderComponent(DataTableActions, {
|
||||
unit: row.original,
|
||||
onSuccess
|
||||
}),
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<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 type {
|
||||
UnitOfMeasureAmerican,
|
||||
UnitOfMeasureAmericanCreate,
|
||||
UnitOfMeasureAmericanUpdate
|
||||
} from '$lib/api/dashboard/a76/general_catalogs/units-of-measure';
|
||||
import {
|
||||
createUnitOfMeasureAmerican,
|
||||
updateUnitOfMeasureAmerican
|
||||
} from '$lib/api/dashboard/a76/general_catalogs/units-of-measure';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
unit?: UnitOfMeasureAmerican;
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
let { open = $bindable(), unit, onSuccess }: Props = $props();
|
||||
|
||||
let formData = $state({
|
||||
code: '',
|
||||
description: ''
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
if (unit) {
|
||||
formData = {
|
||||
code: unit.code,
|
||||
description: unit.description || ''
|
||||
};
|
||||
} else {
|
||||
formData = { code: '', description: '' };
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit(e: Event) {
|
||||
e.preventDefault();
|
||||
|
||||
const activeCompanyId = companyStore.activeCompany?.id;
|
||||
if (!activeCompanyId) {
|
||||
alert('No hay una compañía activa seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
const data: UnitOfMeasureAmericanCreate | UnitOfMeasureAmericanUpdate = {
|
||||
code: formData.code,
|
||||
description: formData.description || null
|
||||
};
|
||||
|
||||
const response = unit
|
||||
? await updateUnitOfMeasureAmerican(unit.id, data, activeCompanyId)
|
||||
: await createUnitOfMeasureAmerican(data, activeCompanyId);
|
||||
|
||||
if (response.error) {
|
||||
alert(response.error.detail || 'Error al guardar');
|
||||
} else {
|
||||
alert(unit ? 'Unidad actualizada' : 'Unidad creada');
|
||||
open = false;
|
||||
onSuccess?.();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="sm:max-w-[425px]">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{unit ? 'Editar' : 'Crear'} Unidad Americana</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
<form onsubmit={handleSubmit} class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="code">Código * (máx. 3 caracteres)</Label>
|
||||
<Input id="code" bind:value={formData.code} required maxlength="3" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="description">Descripción (máx. 40 caracteres)</Label>
|
||||
<Input id="description" bind:value={formData.description} maxlength="40" />
|
||||
</div>
|
||||
<Dialog.Footer>
|
||||
<Button type="button" variant="outline" onclick={() => (open = false)}>Cancelar</Button>
|
||||
<Button type="submit">Guardar</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -0,0 +1,60 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import { MoreHorizontal, Pencil, Trash2 } from 'lucide-svelte';
|
||||
import CreateEditDialog from './create-edit-dialog.svelte';
|
||||
import type { UnitOfMeasureAmerican } from '$lib/api/dashboard/a76/general_catalogs/units-of-measure';
|
||||
import { deleteUnitOfMeasureAmerican } from '$lib/api/dashboard/a76/general_catalogs/units-of-measure';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
|
||||
interface Props {
|
||||
unit: UnitOfMeasureAmerican;
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
let { unit, onSuccess }: Props = $props();
|
||||
let dialogOpen = $state(false);
|
||||
|
||||
async function handleDelete() {
|
||||
if (!confirm('¿Estás seguro de eliminar esta unidad?')) return;
|
||||
|
||||
const activeCompanyId = companyStore.activeCompany?.id;
|
||||
if (!activeCompanyId) {
|
||||
alert('No hay una compañía activa seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await deleteUnitOfMeasureAmerican(unit.id, activeCompanyId);
|
||||
if (response.error) {
|
||||
alert(response.error.detail || 'Error al eliminar');
|
||||
} else if (response.status === 204 || response.status === 200) {
|
||||
alert('Unidad eliminada');
|
||||
onSuccess?.();
|
||||
}
|
||||
}
|
||||
</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>
|
||||
<MoreHorizontal 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 onclick={handleDelete}>
|
||||
<Trash2 class="mr-2 h-4 w-4" />
|
||||
Eliminar
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
|
||||
<CreateEditDialog bind:open={dialogOpen} {unit} {onSuccess} />
|
||||
@@ -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; },
|
||||
get columns() { return columns; },
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
get pageCount() { return pageCount; },
|
||||
});
|
||||
|
||||
function handlePageChange(newPage: number) {
|
||||
const url = new URL($page.url);
|
||||
url.searchParams.set('page', newPage.toString());
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
}
|
||||
|
||||
const currentPage = $derived(Number($page.url.searchParams.get('page') || 1));
|
||||
</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 px-2">
|
||||
<div class="flex-1 text-sm text-muted-foreground">
|
||||
Total: {totalItems} registros
|
||||
</div>
|
||||
<div class="space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => handlePageChange(currentPage - 1)}
|
||||
disabled={currentPage <= 1}
|
||||
>
|
||||
Anterior
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onclick={() => handlePageChange(currentPage + 1)}
|
||||
disabled={currentPage >= pageCount}
|
||||
>
|
||||
Siguiente
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { ColumnDef } from "@tanstack/table-core";
|
||||
import type { UnitOfMeasureCustoms } from "$lib/api/dashboard/a76/general_catalogs/units-of-measure";
|
||||
import { renderComponent } from "$lib/components/ui/data-table/index.js";
|
||||
import DataTableActions from "./data-table-actions.svelte";
|
||||
|
||||
export function createColumns(onSuccess?: () => void): ColumnDef<UnitOfMeasureCustoms>[] {
|
||||
return [
|
||||
{
|
||||
accessorKey: "code",
|
||||
header: "Código",
|
||||
},
|
||||
{
|
||||
accessorKey: "description",
|
||||
header: "Descripción",
|
||||
},
|
||||
{
|
||||
accessorKey: "scaii_unit_code",
|
||||
header: "Código SCAII",
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
cell: ({ row }) =>
|
||||
renderComponent(DataTableActions, {
|
||||
unit: row.original,
|
||||
onSuccess
|
||||
}),
|
||||
},
|
||||
];
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user