diff --git a/.gitignore b/.gitignore index 378ffdb..a45c388 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,4 @@ dist-ssr # Docker docker-compose.override.yml +.agents \ No newline at end of file diff --git a/components.d.ts b/components.d.ts index 75d40af..b38179b 100644 --- a/components.d.ts +++ b/components.d.ts @@ -25,19 +25,19 @@ declare module 'vue' { IconField: typeof import('primevue/iconfield')['default'] InputIcon: typeof import('primevue/inputicon')['default'] InputNumber: typeof import('primevue/inputnumber')['default'] + InputSwitch: typeof import('primevue/inputswitch')['default'] InputText: typeof import('primevue/inputtext')['default'] KpiCard: typeof import('./src/components/shared/KpiCard.vue')['default'] Menu: typeof import('primevue/menu')['default'] Message: typeof import('primevue/message')['default'] Paginator: typeof import('primevue/paginator')['default'] - Panel: typeof import('primevue/panel')['default'] + Password: typeof import('primevue/password')['default'] ProgressSpinner: typeof import('primevue/progressspinner')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] Sidebar: typeof import('./src/components/layout/Sidebar.vue')['default'] Tag: typeof import('primevue/tag')['default'] Toast: typeof import('primevue/toast')['default'] - Toolbar: typeof import('primevue/toolbar')['default'] TopBar: typeof import('./src/components/layout/TopBar.vue')['default'] } export interface GlobalDirectives { diff --git a/package-lock.json b/package-lock.json index af3fa98..70d2372 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,7 @@ "@vue/tsconfig": "^0.8.1", "typescript": "~5.9.3", "vite": "^7.1.7", - "vue-tsc": "^3.1.0" + "vue-tsc": "^3.2.6" } }, "node_modules/@babel/helper-string-parser": { @@ -1216,30 +1216,30 @@ } }, "node_modules/@volar/language-core": { - "version": "2.4.23", - "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.23.tgz", - "integrity": "sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==", + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz", + "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==", "dev": true, "license": "MIT", "dependencies": { - "@volar/source-map": "2.4.23" + "@volar/source-map": "2.4.28" } }, "node_modules/@volar/source-map": { - "version": "2.4.23", - "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.23.tgz", - "integrity": "sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q==", + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz", + "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==", "dev": true, "license": "MIT" }, "node_modules/@volar/typescript": { - "version": "2.4.23", - "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.23.tgz", - "integrity": "sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag==", + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz", + "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==", "dev": true, "license": "MIT", "dependencies": { - "@volar/language-core": "2.4.23", + "@volar/language-core": "2.4.28", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } @@ -1325,27 +1325,19 @@ } }, "node_modules/@vue/language-core": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.1.3.tgz", - "integrity": "sha512-KpR1F/eGAG9D1RZ0/T6zWJs6dh/pRLfY5WupecyYKJ1fjVmDMgTPw9wXmKv2rBjo4zCJiOSiyB8BDP1OUwpMEA==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.2.6.tgz", + "integrity": "sha512-xYYYX3/aVup576tP/23sEUpgiEnujrENaoNRbaozC1/MA9I6EGFQRJb4xrt/MmUCAGlxTKL2RmT8JLTPqagCkg==", "dev": true, "license": "MIT", "dependencies": { - "@volar/language-core": "2.4.23", + "@volar/language-core": "2.4.28", "@vue/compiler-dom": "^3.5.0", "@vue/shared": "^3.5.0", "alien-signals": "^3.0.0", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1", "picomatch": "^4.0.2" - }, - "peerDependencies": { - "typescript": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } } }, "node_modules/@vue/reactivity": { @@ -1468,9 +1460,9 @@ } }, "node_modules/alien-signals": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.0.6.tgz", - "integrity": "sha512-gCs0YqC1mkYGC6IRXsSrA62ShOSv1FlVN5tRp/Cs2vRWLK/BAeluWIdfsl253pFQPznKEvRmHhfep7crWfyfWQ==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.1.2.tgz", + "integrity": "sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==", "dev": true, "license": "MIT" }, @@ -2802,14 +2794,14 @@ } }, "node_modules/vue-tsc": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.1.3.tgz", - "integrity": "sha512-StMNfZHwPIXQgY3KxPKM0Jsoc8b46mDV3Fn2UlHCBIwRJApjqrSwqeMYgWf0zpN+g857y74pv7GWuBm+UqQe1w==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.2.6.tgz", + "integrity": "sha512-gYW/kWI0XrwGzd0PKc7tVB/qpdeAkIZLNZb10/InizkQjHjnT8weZ/vBarZoj4kHKbUTZT/bAVgoOr8x4NsQ/Q==", "dev": true, "license": "MIT", "dependencies": { - "@volar/typescript": "2.4.23", - "@vue/language-core": "3.1.3" + "@volar/typescript": "2.4.28", + "@vue/language-core": "3.2.6" }, "bin": { "vue-tsc": "bin/vue-tsc.js" diff --git a/package.json b/package.json index 1fe36b3..07071b0 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,6 @@ "@vue/tsconfig": "^0.8.1", "typescript": "~5.9.3", "vite": "^7.1.7", - "vue-tsc": "^3.1.0" + "vue-tsc": "^3.2.6" } } diff --git a/src/components/layout/Sidebar.vue b/src/components/layout/Sidebar.vue index dc22bfd..8929e0c 100644 --- a/src/components/layout/Sidebar.vue +++ b/src/components/layout/Sidebar.vue @@ -1,15 +1,18 @@ @@ -341,4 +362,4 @@ onMounted(() => { .space-y-6 > * + * { margin-top: 1.5rem; } - \ No newline at end of file + diff --git a/src/modules/catalog/components/companies/CompaniesForm.vue b/src/modules/catalog/components/companies/CompaniesForm.vue index 8cf067a..344db37 100644 --- a/src/modules/catalog/components/companies/CompaniesForm.vue +++ b/src/modules/catalog/components/companies/CompaniesForm.vue @@ -1,107 +1,158 @@ @@ -426,4 +366,4 @@ const handleClose = () => { .space-y-6 > * + * { margin-top: 1.5rem; } - \ No newline at end of file + diff --git a/src/modules/catalog/components/companies/companies.mapper.ts b/src/modules/catalog/components/companies/companies.mapper.ts new file mode 100644 index 0000000..5ec4cc5 --- /dev/null +++ b/src/modules/catalog/components/companies/companies.mapper.ts @@ -0,0 +1,114 @@ +import type { CreateTenantPayload, TenantFormData, UpdateTenantPayload } from './companies.types'; + +const appendIfNotEmpty = (formData: FormData, key: string, value: string | number | null | undefined) => { + if (value === null || value === undefined) { + return; + } + + const normalized = String(value).trim(); + if (normalized.length > 0) { + formData.append(key, normalized); + } +}; + +const appendNumericIfDefined = (formData: FormData, key: string, value: number | null | undefined) => { + if (value === null || value === undefined || Number.isNaN(value)) { + return; + } + + formData.append(key, String(value)); +}; + +const appendFileIfPresent = (formData: FormData, key: string, value: File | null | undefined) => { + if (value instanceof File) { + formData.append(key, value); + } +}; + +export const buildCreateTenantPayload = (data: TenantFormData): CreateTenantPayload => ({ + company_name: data.company_name.trim(), + rfc: data.rfc.trim().toUpperCase(), + email: data.email.trim(), + primary_domain: data.primary_domain.trim(), + curp: data.curp.trim().toUpperCase(), + phone: data.phone.trim(), + legal_representative: data.legal_representative.trim(), + tax_regime: data.tax_regime.trim(), + is_active: data.is_active, + vat_rate: data.vat_rate, + isr_withholding: data.isr_withholding, + vat_withholding: data.vat_withholding, + additional_tax: data.additional_tax, + certificate_password: data.certificate_password, + certificate_cer_file: data.certificate_cer_file, + certificate_key_file: data.certificate_key_file, +}); + +export const buildUpdateTenantPayload = (data: TenantFormData): UpdateTenantPayload => ({ + company_name: data.company_name.trim(), + rfc: data.rfc.trim().toUpperCase(), + email: data.email.trim(), + primary_domain: data.primary_domain.trim() || undefined, + curp: data.curp.trim().toUpperCase(), + phone: data.phone.trim(), + legal_representative: data.legal_representative.trim(), + tax_regime: data.tax_regime.trim(), + is_active: data.is_active, + vat_rate: data.vat_rate, + isr_withholding: data.isr_withholding, + vat_withholding: data.vat_withholding, + additional_tax: data.additional_tax, + certificate_password: data.certificate_password, + certificate_cer_file: data.certificate_cer_file, + certificate_key_file: data.certificate_key_file, +}); + +export const toTenantFormData = (data?: Partial): TenantFormData => ({ + id: data?.id, + company_name: data?.company_name ?? '', + rfc: data?.rfc ?? '', + email: data?.email ?? '', + primary_domain: data?.primary_domain ?? '', + curp: data?.curp ?? '', + phone: data?.phone ?? '', + legal_representative: data?.legal_representative ?? '', + tax_regime: data?.tax_regime ?? '', + is_active: data?.is_active ?? true, + vat_rate: data?.vat_rate ?? 0.0, + isr_withholding: data?.isr_withholding ?? 0.0, + vat_withholding: data?.vat_withholding ?? 0.0, + additional_tax: data?.additional_tax ?? 0.0, + certificate_password: data?.certificate_password ?? '', + certificate_cer_file: null, + certificate_key_file: null, +}); + +export const tenantPayloadToFormData = ( + data: CreateTenantPayload | UpdateTenantPayload, + options?: { forcePut?: boolean } +): FormData => { + const formData = new FormData(); + + if (options?.forcePut) { + formData.append('_method', 'PUT'); + } + + appendIfNotEmpty(formData, 'company_name', data.company_name); + appendIfNotEmpty(formData, 'rfc', data.rfc); + appendIfNotEmpty(formData, 'email', data.email); + appendIfNotEmpty(formData, 'primary_domain', data.primary_domain); + appendIfNotEmpty(formData, 'curp', data.curp); + appendIfNotEmpty(formData, 'phone', data.phone); + appendIfNotEmpty(formData, 'legal_representative', data.legal_representative); + appendIfNotEmpty(formData, 'tax_regime', data.tax_regime); + appendNumericIfDefined(formData, 'vat_rate', data.vat_rate); + appendNumericIfDefined(formData, 'isr_withholding', data.isr_withholding); + appendNumericIfDefined(formData, 'vat_withholding', data.vat_withholding); + appendNumericIfDefined(formData, 'additional_tax', data.additional_tax); + appendIfNotEmpty(formData, 'certificate_password', data.certificate_password); + formData.append('is_active', data.is_active ? '1' : '0'); + appendFileIfPresent(formData, 'certificate_cer_file', data.certificate_cer_file); + appendFileIfPresent(formData, 'certificate_key_file', data.certificate_key_file); + + return formData; +}; diff --git a/src/modules/catalog/components/companies/companies.service.ts b/src/modules/catalog/components/companies/companies.service.ts index 5b81e6d..ab99294 100644 --- a/src/modules/catalog/components/companies/companies.service.ts +++ b/src/modules/catalog/components/companies/companies.service.ts @@ -1,123 +1,79 @@ import api from '@/services/api'; -import type { - Company, - CompanyCreateResponse, - CompanyFormData, - CompanyQueryParams, - PaginatedResponse -} from './companies.types'; - - -export class CompanyService { +import { + buildCreateTenantPayload, + buildUpdateTenantPayload, + tenantPayloadToFormData, +} from './companies.mapper'; +import type { CompaniesPagination, TenantFormData } from './companies.types'; +/** + * Servicio para obtener todas las empresas (tenants) con paginación. + */ +export class CompaniesService { /** - * Obtener todas las empresas con paginación y filtros opcionales + * Obtiene todas las empresas del sistema. + * @returns Promesa con la respuesta paginada de empresas */ - async getAll(params?: CompanyQueryParams): Promise | Company[]> { + public async getAll(page = 1): Promise { try { - const response = await api.get('api/catalogs/companies', { params }); - - // Si la respuesta tiene paginación, devolver el objeto completo - if (response.data.current_page !== undefined) { - return response.data as PaginatedResponse; - } - - // Si no tiene paginación, devolver solo el array de data - return response.data.data as Company[]; - } catch (error: any) { + const response = await api.get('/api/admin/admin/tenants', { + params: { page }, + }); + return response.data; + } catch (error) { console.error('Error al obtener empresas:', error); throw error; } } /** - * Obtener una empresa por ID + * Crea una empresa (tenant) usando multipart/form-data. */ - async getById(id: number): Promise { + public async create(data: TenantFormData): Promise { try { - const response = await api.get(`api/catalogs/companies/${id}`); - return response.data; - } catch (error: any) { - console.error(`Error al obtener empresa con ID ${id}:`, error); - throw error; - } - } - - /** - * Crear una nueva empresa - */ - async create(data: CompanyFormData): Promise { - try { - const response = await api.post('api/catalogs/companies', data); - return response.data; - } catch (error: any) { - console.error('Error al crear empresa:', error); - // Mejorar el mensaje de error si viene del backend - if (error.response?.data) { - throw { - ...error, - message: error.response.data.message || error.response.data.error || 'Error al crear la empresa' - }; - } - throw error; - } - } - - /** - * Actualizar una empresa existente - */ - async update(id: number, data: Partial): Promise { - try { - const response = await api.put(`api/catalogs/companies/${id}`, data); - return response.data; - } catch (error: any) { - console.error(`Error al actualizar empresa con ID ${id}:`, error); - // Mejorar el mensaje de error si viene del backend - if (error.response?.data) { - throw { - ...error, - message: error.response.data.message || error.response.data.error || 'Error al actualizar la empresa' - }; - } - throw error; - } - } - - /** - * Eliminar una empresa - */ - async delete(id: number): Promise { - try { - await api.delete(`api/catalogs/companies/${id}`); - } catch (error: any) { - console.error(`Error al eliminar empresa con ID ${id}:`, error); - // Mejorar el mensaje de error si viene del backend - if (error.response?.data) { - throw { - ...error, - message: error.response.data.message || error.response.data.error || 'Error al eliminar la empresa' - }; - } - throw error; - } - } - - /** - * Buscar empresas por múltiples criterios - */ - async search(params: CompanyQueryParams): Promise { - try { - const response = await api.get('api/catalogs/companies', { - params: { ...params, paginate: false } + const payload = buildCreateTenantPayload(data); + const formData = tenantPayloadToFormData(payload); + await api.post('/api/admin/admin/tenants', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, }); - return response.data.data; - } catch (error: any) { - console.error('Error al buscar empresas:', error); + } catch (error) { + console.error('Error al crear empresa:', error); throw error; } } -}; + /** + * Actualiza una empresa (tenant) usando POST + _method=PUT para multipart. + */ + public async update(id: number, data: TenantFormData): Promise { + try { + const payload = buildUpdateTenantPayload(data); + const formData = tenantPayloadToFormData(payload, { forcePut: true }); + await api.post(`/api/admin/admin/tenants/${id}`, formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + } catch (error) { + console.error('Error al actualizar empresa:', error); + throw error; + } + } -// Exportar una instancia única del servicio -export const companyService = new CompanyService(); + /** + * Elimina una empresa por su ID. + * @param id ID de la empresa + */ + public async delete(id: number): Promise { + try { + await api.delete(`/api/admin/admin/tenants/${id}`); + } catch (error) { + console.error('Error al eliminar empresa:', error); + throw error; + } + } +} + +export const companiesService = new CompaniesService(); diff --git a/src/modules/catalog/components/companies/companies.types.ts b/src/modules/catalog/components/companies/companies.types.ts index 3351a50..392a172 100644 --- a/src/modules/catalog/components/companies/companies.types.ts +++ b/src/modules/catalog/components/companies/companies.types.ts @@ -1,72 +1,71 @@ -export interface CompanyAddress { - country: string; - postal_code: string; - state: string; - municipality: string; - city: string; - street: string; - num_ext: string; - num_int?: string; +export interface Company { + id: number; + rfc: string; + curp?: string | null; + vat_rate?: number | null; + isr_withholding?: number | null; + vat_withholding?: number | null; + additional_tax?: number | null; + company_name: string; + email?: string; + primary_domain?: string; + phone?: string | null; + legal_representative?: string | null; + tax_regime?: string | null; + created_at: string; + updated_at: string; + deleted_at?: string | null; + data?: Record | null; + is_active: boolean; + trial_ends_at?: string | null; + subscribed_until?: string | null; + domains_count: number; + domains: CompanyDomain[]; } -export interface Company { +export interface CompanyDomain { + id?: number; + domain: string; +} + +export interface TenantFormData { id?: number; - rfc: string; - curp?: string; company_name: string; + rfc: string; + email: string; + primary_domain: string; + curp: string; + phone: string; + legal_representative: string; + tax_regime: string; + is_active: boolean; vat_rate: number; isr_withholding: number; vat_withholding: number; additional_tax: number; - address: CompanyAddress; - created_at?: string; - updated_at?: string; + certificate_password: string; + certificate_cer_file: File | null; + certificate_key_file: File | null; } -export interface CompanyCreateResponse{ - data: Company; -} +export type CreateTenantPayload = Omit; -export interface CompanyFormData extends Omit {} +export type UpdateTenantPayload = Omit & { + primary_domain?: string; +}; -export interface CompanyStats { - total: number; - active: number; - pending: number; - monthlyIncrease: number; - activePercentage: number; -} - -export interface PaginationLink { - url: string | null; - label: string; - active: boolean; -} - -export interface PaginatedResponse { +export interface CompaniesPagination { current_page: number; - data: T[]; + data: Company[]; first_page_url: string; from: number; last_page: number; last_page_url: string; - links: PaginationLink[]; - next_page_url: string | null; + links: Array<{ url: string | null; label: string; active: boolean }>; + next_page_url?: string | null; path: string; per_page: number; - prev_page_url: string | null; + prev_page_url?: string | null; to: number; total: number; } - -export interface CompanyListResponse { - data: Company[]; -} - -export interface CompanyQueryParams { - paginate?: boolean; - rfc?: string; - curp?: string; - company_name?: string; - page?: number; -} diff --git a/src/modules/catalog/components/units/Units.vue b/src/modules/catalog/components/units/Units.vue index e6e4568..c18fa5d 100644 --- a/src/modules/catalog/components/units/Units.vue +++ b/src/modules/catalog/components/units/Units.vue @@ -1,5 +1,5 @@