Merge branch 'features/Creacion-formularios_catalogos_generales' into development

This commit is contained in:
AlexeerCT
2025-12-24 15:05:09 -06:00
279 changed files with 23252 additions and 3644 deletions

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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}`);
}

View File

@@ -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}`);
}

View File

@@ -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}`);
}

View File

@@ -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}`);
}

View File

@@ -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}`);
}

View File

@@ -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}`);
}

View File

@@ -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}`);
}

View File

@@ -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}`);
}

View File

@@ -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()}`);
}

View File

@@ -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}`);
}

View File

@@ -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}`);
}

View File

@@ -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}`);
}

View File

@@ -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()}`);
}

View File

@@ -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()}`);
}

View File

@@ -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}`);
}

View File

@@ -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}`);
}

View File

@@ -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}`);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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}`);
}

View 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()}`);
}
}
};

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
});
}
}
];
}

View File

@@ -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>

View File

@@ -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;

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
});
}
}
];
}

View File

@@ -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}
/>

View File

@@ -0,0 +1,106 @@
<script lang="ts" generics="TData, TValue">
import {
type ColumnDef,
getCoreRowModel,
} from "@tanstack/table-core";
import { createSvelteTable, FlexRender } from "$lib/components/ui/data-table/index.js";
import * as Table from "$lib/components/ui/table/index.js";
import { Button } from "$lib/components/ui/button";
import { goto } from "$app/navigation";
import { page } from "$app/stores";
type DataTableProps<TData, TValue> = {
columns: ColumnDef<TData, TValue>[];
data: TData[];
pageCount: number;
totalItems: number;
};
let {
data,
columns,
pageCount,
totalItems
}: DataTableProps<TData, TValue> = $props();
const table = createSvelteTable({
get data() { return data; },
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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
});
}
}
];
}

View File

@@ -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>

View File

@@ -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}
/>

View File

@@ -0,0 +1,106 @@
<script lang="ts" generics="TData, TValue">
import {
type ColumnDef,
getCoreRowModel,
} from "@tanstack/table-core";
import { createSvelteTable, FlexRender } from "$lib/components/ui/data-table/index.js";
import * as Table from "$lib/components/ui/table/index.js";
import { Button } from "$lib/components/ui/button";
import { goto } from "$app/navigation";
import { page } from "$app/stores";
type DataTableProps<TData, TValue> = {
columns: ColumnDef<TData, TValue>[];
data: TData[];
pageCount: number;
totalItems: number;
};
let {
data,
columns,
pageCount,
totalItems
}: DataTableProps<TData, TValue> = $props();
const table = createSvelteTable({
get data() { return data; },
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>

View File

@@ -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
});
}
}
];
}

View File

@@ -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>

View File

@@ -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}
/>

View File

@@ -0,0 +1,106 @@
<script lang="ts" generics="TData, TValue">
import {
type ColumnDef,
getCoreRowModel,
} from "@tanstack/table-core";
import { createSvelteTable, FlexRender } from "$lib/components/ui/data-table/index.js";
import * as Table from "$lib/components/ui/table/index.js";
import { Button } from "$lib/components/ui/button";
import { goto } from "$app/navigation";
import { page } from "$app/stores";
type DataTableProps<TData, TValue> = {
columns: ColumnDef<TData, TValue>[];
data: TData[];
pageCount: number;
totalItems: number;
};
let {
data,
columns,
pageCount,
totalItems
}: DataTableProps<TData, TValue> = $props();
const table = createSvelteTable({
get data() { return data; },
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>

View File

@@ -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
});
}
}
];
}

View File

@@ -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>

View File

@@ -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}
/>

View File

@@ -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>

View File

@@ -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
});
}
}
];
}

View File

@@ -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>

View File

@@ -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}
/>

View File

@@ -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>

View File

@@ -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
});
}
}
];
}

View File

@@ -0,0 +1,127 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import * as Dialog from "$lib/components/ui/dialog";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import { 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>

View File

@@ -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}
/>

View File

@@ -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>

View File

@@ -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
})
}
];
}

View File

@@ -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>

View File

@@ -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}
/>

View File

@@ -0,0 +1,106 @@
<script lang="ts" generics="TData, TValue">
import {
type ColumnDef,
getCoreRowModel,
} from "@tanstack/table-core";
import { createSvelteTable, FlexRender } from "$lib/components/ui/data-table/index.js";
import * as Table from "$lib/components/ui/table/index.js";
import { Button } from "$lib/components/ui/button";
import { goto } from "$app/navigation";
import { page } from "$app/stores";
type DataTableProps<TData, TValue> = {
columns: ColumnDef<TData, TValue>[];
data: TData[];
pageCount: number;
totalItems: number;
};
let {
data,
columns,
pageCount,
totalItems
}: DataTableProps<TData, TValue> = $props();
const table = createSvelteTable({
get data() { return data; },
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>

View File

@@ -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
});
}
}
];
}

View File

@@ -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>

View File

@@ -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}
/>

View File

@@ -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>

View File

@@ -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
});
}
}
];
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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}
/>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
});
}
}
];
}

View File

@@ -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}
/>

View File

@@ -0,0 +1,106 @@
<script lang="ts" generics="TData, TValue">
import {
type ColumnDef,
getCoreRowModel,
} from "@tanstack/table-core";
import { createSvelteTable, FlexRender } from "$lib/components/ui/data-table/index.js";
import * as Table from "$lib/components/ui/table/index.js";
import { Button } from "$lib/components/ui/button";
import { goto } from "$app/navigation";
import { page } from "$app/stores";
type DataTableProps<TData, TValue> = {
columns: ColumnDef<TData, TValue>[];
data: TData[];
pageCount: number;
totalItems: number;
};
let {
data,
columns,
pageCount,
totalItems
}: DataTableProps<TData, TValue> = $props();
const table = createSvelteTable({
get data() { return data; },
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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
});
}
}
];
}

View File

@@ -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>

View File

@@ -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}
/>

View File

@@ -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>

View File

@@ -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
})
}
];
}

View File

@@ -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>

View File

@@ -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}
/>

View File

@@ -0,0 +1,106 @@
<script lang="ts" generics="TData, TValue">
import {
type ColumnDef,
getCoreRowModel,
} from "@tanstack/table-core";
import { createSvelteTable, FlexRender } from "$lib/components/ui/data-table/index.js";
import * as Table from "$lib/components/ui/table/index.js";
import { Button } from "$lib/components/ui/button";
import { goto } from "$app/navigation";
import { page } from "$app/stores";
type DataTableProps<TData, TValue> = {
columns: ColumnDef<TData, TValue>[];
data: TData[];
pageCount: number;
totalItems: number;
};
let {
data,
columns,
pageCount,
totalItems
}: DataTableProps<TData, TValue> = $props();
const table = createSvelteTable({
get data() { return data; },
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>

View File

@@ -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>

View File

@@ -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
})
}
];
}

View File

@@ -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>

View File

@@ -0,0 +1,79 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
import { 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}
/>

View File

@@ -0,0 +1,106 @@
<script lang="ts" generics="TData, TValue">
import {
type ColumnDef,
getCoreRowModel,
} from "@tanstack/table-core";
import { createSvelteTable, FlexRender } from "$lib/components/ui/data-table/index.js";
import * as Table from "$lib/components/ui/table/index.js";
import { Button } from "$lib/components/ui/button";
import { goto } from "$app/navigation";
import { page } from "$app/stores";
type DataTableProps<TData, TValue> = {
columns: ColumnDef<TData, TValue>[];
data: TData[];
pageCount: number;
totalItems: number;
};
let {
data,
columns,
pageCount,
totalItems
}: DataTableProps<TData, TValue> = $props();
const table = createSvelteTable({
get data() { return data; },
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>

View File

@@ -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
});
}
}
];
}

View File

@@ -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>

View File

@@ -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}
/>

View File

@@ -0,0 +1,106 @@
<script lang="ts" generics="TData, TValue">
import {
type ColumnDef,
getCoreRowModel,
} from "@tanstack/table-core";
import { createSvelteTable, FlexRender } from "$lib/components/ui/data-table/index.js";
import * as Table from "$lib/components/ui/table/index.js";
import { Button } from "$lib/components/ui/button";
import { goto } from "$app/navigation";
import { page } from "$app/stores";
type DataTableProps<TData, TValue> = {
columns: ColumnDef<TData, TValue>[];
data: TData[];
pageCount: number;
totalItems: number;
};
let {
data,
columns,
pageCount,
totalItems
}: DataTableProps<TData, TValue> = $props();
const table = createSvelteTable({
get data() { return data; },
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>

View File

@@ -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
}),
},
];
}

View File

@@ -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>

View File

@@ -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} />

View File

@@ -0,0 +1,106 @@
<script lang="ts" generics="TData, TValue">
import {
type ColumnDef,
getCoreRowModel,
} from "@tanstack/table-core";
import { createSvelteTable, FlexRender } from "$lib/components/ui/data-table/index.js";
import * as Table from "$lib/components/ui/table/index.js";
import { Button } from "$lib/components/ui/button";
import { goto } from "$app/navigation";
import { page } from "$app/stores";
type DataTableProps<TData, TValue> = {
columns: ColumnDef<TData, TValue>[];
data: TData[];
pageCount: number;
totalItems: number;
};
let {
data,
columns,
pageCount,
totalItems
}: DataTableProps<TData, TValue> = $props();
const table = createSvelteTable({
get data() { return data; },
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>

View File

@@ -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