feat: update company management components and services

- Upgrade vue-tsc to version 3.2.6 in package.json.
- Refactor Companies.vue to replace address column with domains_count and update company service references.
- Modify CompaniesForm.vue to include new fields for email, primary domain, and certificate files, and adjust validation logic.
- Revamp companies.service.ts to implement new API endpoints for tenant management and improve error handling.
- Introduce companies.mapper.ts for payload transformation between form data and API requirements.
- Update companies.types.ts to reflect changes in data structure and types for better type safety.
This commit is contained in:
edgar.mendez 2026-03-21 18:04:08 -06:00
parent 6fe7c82c6d
commit 93a2527e60
9 changed files with 559 additions and 603 deletions

1
.gitignore vendored
View File

@ -30,3 +30,4 @@ dist-ssr
# Docker # Docker
docker-compose.override.yml docker-compose.override.yml
.agents

4
components.d.ts vendored
View File

@ -25,19 +25,19 @@ declare module 'vue' {
IconField: typeof import('primevue/iconfield')['default'] IconField: typeof import('primevue/iconfield')['default']
InputIcon: typeof import('primevue/inputicon')['default'] InputIcon: typeof import('primevue/inputicon')['default']
InputNumber: typeof import('primevue/inputnumber')['default'] InputNumber: typeof import('primevue/inputnumber')['default']
InputSwitch: typeof import('primevue/inputswitch')['default']
InputText: typeof import('primevue/inputtext')['default'] InputText: typeof import('primevue/inputtext')['default']
KpiCard: typeof import('./src/components/shared/KpiCard.vue')['default'] KpiCard: typeof import('./src/components/shared/KpiCard.vue')['default']
Menu: typeof import('primevue/menu')['default'] Menu: typeof import('primevue/menu')['default']
Message: typeof import('primevue/message')['default'] Message: typeof import('primevue/message')['default']
Paginator: typeof import('primevue/paginator')['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'] ProgressSpinner: typeof import('primevue/progressspinner')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
Sidebar: typeof import('./src/components/layout/Sidebar.vue')['default'] Sidebar: typeof import('./src/components/layout/Sidebar.vue')['default']
Tag: typeof import('primevue/tag')['default'] Tag: typeof import('primevue/tag')['default']
Toast: typeof import('primevue/toast')['default'] Toast: typeof import('primevue/toast')['default']
Toolbar: typeof import('primevue/toolbar')['default']
TopBar: typeof import('./src/components/layout/TopBar.vue')['default'] TopBar: typeof import('./src/components/layout/TopBar.vue')['default']
} }
export interface GlobalDirectives { export interface GlobalDirectives {

56
package-lock.json generated
View File

@ -27,7 +27,7 @@
"@vue/tsconfig": "^0.8.1", "@vue/tsconfig": "^0.8.1",
"typescript": "~5.9.3", "typescript": "~5.9.3",
"vite": "^7.1.7", "vite": "^7.1.7",
"vue-tsc": "^3.1.0" "vue-tsc": "^3.2.6"
} }
}, },
"node_modules/@babel/helper-string-parser": { "node_modules/@babel/helper-string-parser": {
@ -1216,30 +1216,30 @@
} }
}, },
"node_modules/@volar/language-core": { "node_modules/@volar/language-core": {
"version": "2.4.23", "version": "2.4.28",
"resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.23.tgz", "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz",
"integrity": "sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==", "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@volar/source-map": "2.4.23" "@volar/source-map": "2.4.28"
} }
}, },
"node_modules/@volar/source-map": { "node_modules/@volar/source-map": {
"version": "2.4.23", "version": "2.4.28",
"resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.23.tgz", "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz",
"integrity": "sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q==", "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@volar/typescript": { "node_modules/@volar/typescript": {
"version": "2.4.23", "version": "2.4.28",
"resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.23.tgz", "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz",
"integrity": "sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag==", "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@volar/language-core": "2.4.23", "@volar/language-core": "2.4.28",
"path-browserify": "^1.0.1", "path-browserify": "^1.0.1",
"vscode-uri": "^3.0.8" "vscode-uri": "^3.0.8"
} }
@ -1325,27 +1325,19 @@
} }
}, },
"node_modules/@vue/language-core": { "node_modules/@vue/language-core": {
"version": "3.1.3", "version": "3.2.6",
"resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.1.3.tgz", "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.2.6.tgz",
"integrity": "sha512-KpR1F/eGAG9D1RZ0/T6zWJs6dh/pRLfY5WupecyYKJ1fjVmDMgTPw9wXmKv2rBjo4zCJiOSiyB8BDP1OUwpMEA==", "integrity": "sha512-xYYYX3/aVup576tP/23sEUpgiEnujrENaoNRbaozC1/MA9I6EGFQRJb4xrt/MmUCAGlxTKL2RmT8JLTPqagCkg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@volar/language-core": "2.4.23", "@volar/language-core": "2.4.28",
"@vue/compiler-dom": "^3.5.0", "@vue/compiler-dom": "^3.5.0",
"@vue/shared": "^3.5.0", "@vue/shared": "^3.5.0",
"alien-signals": "^3.0.0", "alien-signals": "^3.0.0",
"muggle-string": "^0.4.1", "muggle-string": "^0.4.1",
"path-browserify": "^1.0.1", "path-browserify": "^1.0.1",
"picomatch": "^4.0.2" "picomatch": "^4.0.2"
},
"peerDependencies": {
"typescript": "*"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
} }
}, },
"node_modules/@vue/reactivity": { "node_modules/@vue/reactivity": {
@ -1468,9 +1460,9 @@
} }
}, },
"node_modules/alien-signals": { "node_modules/alien-signals": {
"version": "3.0.6", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.0.6.tgz", "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.1.2.tgz",
"integrity": "sha512-gCs0YqC1mkYGC6IRXsSrA62ShOSv1FlVN5tRp/Cs2vRWLK/BAeluWIdfsl253pFQPznKEvRmHhfep7crWfyfWQ==", "integrity": "sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@ -2802,14 +2794,14 @@
} }
}, },
"node_modules/vue-tsc": { "node_modules/vue-tsc": {
"version": "3.1.3", "version": "3.2.6",
"resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.1.3.tgz", "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.2.6.tgz",
"integrity": "sha512-StMNfZHwPIXQgY3KxPKM0Jsoc8b46mDV3Fn2UlHCBIwRJApjqrSwqeMYgWf0zpN+g857y74pv7GWuBm+UqQe1w==", "integrity": "sha512-gYW/kWI0XrwGzd0PKc7tVB/qpdeAkIZLNZb10/InizkQjHjnT8weZ/vBarZoj4kHKbUTZT/bAVgoOr8x4NsQ/Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@volar/typescript": "2.4.23", "@volar/typescript": "2.4.28",
"@vue/language-core": "3.1.3" "@vue/language-core": "3.2.6"
}, },
"bin": { "bin": {
"vue-tsc": "bin/vue-tsc.js" "vue-tsc": "bin/vue-tsc.js"

View File

@ -28,6 +28,6 @@
"@vue/tsconfig": "^0.8.1", "@vue/tsconfig": "^0.8.1",
"typescript": "~5.9.3", "typescript": "~5.9.3",
"vite": "^7.1.7", "vite": "^7.1.7",
"vue-tsc": "^3.1.0" "vue-tsc": "^3.2.6"
} }
} }

View File

@ -60,15 +60,11 @@
</template> </template>
</Column> </Column>
<Column field="address" header="Ubicación" style="min-width: 200px"> <Column field="domains_count" header="Dominios" style="min-width: 120px">
<template #body="{ data }"> <template #body="{ data }">
<div v-if="data.address"> <span class="font-mono text-sm">{{ data.domains_count || 0 }}</span>
<p class="text-sm">{{ data.address.city }}</p>
<p class="text-xs text-slate-400">{{ data.address.state }}</p>
</div>
<span v-else class="text-slate-400">Sin dirección</span>
</template> </template>
</Column> </Column>
<Column header="Acciones" :exportable="false" style="min-width: 120px"> <Column header="Acciones" :exportable="false" style="min-width: 120px">
<template #body="{ data }"> <template #body="{ data }">
@ -132,8 +128,8 @@
import { ref, onMounted } from 'vue'; import { ref, onMounted } from 'vue';
import { useToast } from 'primevue/usetoast'; import { useToast } from 'primevue/usetoast';
import CompaniesForm from './CompaniesForm.vue'; import CompaniesForm from './CompaniesForm.vue';
import { companyService } from './companies.service'; import { companiesService } from './companies.service';
import type { Company, PaginatedResponse } from './companies.types'; import type { Company, TenantFormData } from './companies.types';
// Toast // Toast
const toast = useToast(); const toast = useToast();
@ -146,7 +142,7 @@ const companies = ref<Company[]>([]);
const loading = ref(false); const loading = ref(false);
const dialogVisible = ref(false); const dialogVisible = ref(false);
const deleteDialogVisible = ref(false); const deleteDialogVisible = ref(false);
const selectedCompany = ref<Company | null>(null); const selectedCompany = ref<Company | undefined>(undefined);
const companyToDelete = ref<Company | null>(null); const companyToDelete = ref<Company | null>(null);
// Pagination // Pagination
@ -155,37 +151,17 @@ const totalRecords = ref(0);
const perPage = ref(10); const perPage = ref(10);
// Search filters // Search filters
const searchFilters = ref({
rfc: '',
curp: '',
company_name: ''
});
// Methods // Methods
const loadCompanies = async () => { const loadCompanies = async (page = currentPage.value) => {
loading.value = true; loading.value = true;
try { try {
const params = { const response = await companiesService.getAll(page);
paginate: true, companies.value = response.data;
page: currentPage.value, currentPage.value = response.current_page;
...searchFilters.value totalRecords.value = response.total;
}; perPage.value = response.per_page;
const response = await companyService.getAll(params);
if ('current_page' in response) {
// Respuesta paginada
const paginatedData = response as PaginatedResponse<Company>;
companies.value = paginatedData.data;
currentPage.value = paginatedData.current_page;
totalRecords.value = paginatedData.total;
perPage.value = paginatedData.per_page;
} else {
// Respuesta sin paginación
companies.value = response as Company[];
totalRecords.value = companies.value.length;
}
} catch (error) { } catch (error) {
console.error('Error loading companies:', error); console.error('Error loading companies:', error);
companies.value = []; companies.value = [];
@ -196,25 +172,25 @@ const loadCompanies = async () => {
const onPageChange = (event: any) => { const onPageChange = (event: any) => {
currentPage.value = event.page + 1; // PrimeVue usa índice base 0 currentPage.value = event.page + 1; // PrimeVue usa índice base 0
loadCompanies(); loadCompanies(currentPage.value);
}; };
const openCreateDialog = () => { const openCreateDialog = () => {
selectedCompany.value = null; selectedCompany.value = undefined;
dialogVisible.value = true; dialogVisible.value = true;
}; };
const editCompany = (company: any) => { const editCompany = (company: Company) => {
selectedCompany.value = { ...company }; selectedCompany.value = { ...company };
dialogVisible.value = true; dialogVisible.value = true;
}; }; // No change needed assigns a valid Company
const viewCompany = (company: any) => { const viewCompany = (company: Company) => {
// TODO: Implementar vista de detalles // Aquí podrías abrir un modal de detalles o navegar a una vista
console.log('View company:', company); console.log('View company:', company);
}; };
const confirmDelete = (company: any) => { const confirmDelete = (company: Company) => {
companyToDelete.value = company; companyToDelete.value = company;
deleteDialogVisible.value = true; deleteDialogVisible.value = true;
}; };
@ -223,7 +199,7 @@ const deleteCompany = async () => {
if (!companyToDelete.value?.id) return; if (!companyToDelete.value?.id) return;
try { try {
await companyService.delete(companyToDelete.value.id); await companiesService.delete(companyToDelete.value.id);
toast.add({ toast.add({
severity: 'success', severity: 'success',
@ -248,76 +224,54 @@ const deleteCompany = async () => {
} }
}; };
const handleSave = async (companyData: any) => { const handleSave = async (companyData: TenantFormData) => {
try { try {
if (selectedCompany.value?.id) { const isEditMode = Boolean(selectedCompany.value?.id);
// Actualizar empresa existente
await companyService.update(selectedCompany.value.id, companyData);
if (isEditMode && selectedCompany.value?.id) {
await companiesService.update(selectedCompany.value.id, companyData);
toast.add({ toast.add({
severity: 'success', severity: 'success',
summary: 'Empresa actualizada', summary: 'Empresa actualizada',
detail: `La empresa "${companyData.company_name}" ha sido actualizada exitosamente`, detail: `La empresa "${companyData.company_name}" se actualizó correctamente`,
life: 3000 life: 3000,
}); });
} else { } else {
// Crear nueva empresa await companiesService.create(companyData);
await companyService.create(companyData);
toast.add({ toast.add({
severity: 'success', severity: 'success',
summary: 'Empresa creada', summary: 'Empresa creada',
detail: `La empresa "${companyData.company_name}" ha sido registrada exitosamente`, detail: `La empresa "${companyData.company_name}" se creó correctamente`,
life: 3000 life: 3000,
}); });
} }
// Recargar la lista después de guardar
await loadCompanies(); await loadCompanies();
dialogVisible.value = false; dialogVisible.value = false;
selectedCompany.value = null; selectedCompany.value = undefined;
companiesFormRef.value?.resetSubmitting();
return true; return true;
} catch (error: any) { } catch (error: any) {
console.error('Error saving company:', error); console.error('Error saving company:', error);
// Verificar si es un error de validación (422) if (error.response?.status === 422 && error.response?.data?.errors && companiesFormRef.value) {
if (error.response?.status === 422 && error.response?.data?.errors) { companiesFormRef.value.setValidationErrors(error.response.data.errors);
const backendErrors = error.response.data.errors;
// Setear los errores en el formulario
if (companiesFormRef.value) {
companiesFormRef.value.setValidationErrors(backendErrors);
}
// Mostrar toast con mensaje general
toast.add({
severity: 'warn',
summary: 'Errores de validación',
detail: error.response.data.message || 'Por favor, corrija los errores en el formulario',
life: 5000
});
} else {
// Otros errores
const errorMessage = error.response?.data?.message ||
error.response?.data?.error ||
error.message ||
'No se pudo guardar la empresa';
toast.add({ toast.add({
severity: 'error', severity: 'error',
summary: selectedCompany.value?.id ? 'Error al actualizar' : 'Error al crear', summary: 'Error de validación',
detail: errorMessage, detail: 'Revisa los campos marcados en rojo',
life: 5000 life: 4000,
}); });
return false;
// Resetear loading en el formulario para otros tipos de error
if (companiesFormRef.value) {
companiesFormRef.value.setValidationErrors({});
}
} }
companiesFormRef.value?.resetSubmitting();
toast.add({
severity: 'error',
summary: 'Error al guardar',
detail: error.response?.data?.message || 'No se pudo guardar la empresa',
life: 5000,
});
return false; return false;
} }
}; };

View File

@ -3,23 +3,35 @@
v-model:visible="isVisible" v-model:visible="isVisible"
:modal="true" :modal="true"
:closable="true" :closable="true"
:style="{ width: '900px' }" :style="{ width: '980px' }"
:header="formTitle" :header="formTitle"
@hide="handleClose" @hide="handleClose"
> >
<form @submit.prevent="handleSubmit" class="space-y-6"> <form class="space-y-6" @submit.prevent="handleSubmit">
<!-- Información Fiscal -->
<div class="border-b pb-4"> <div class="border-b pb-4">
<h3 class="text-lg font-semibold mb-4">Información Fiscal</h3> <h3 class="text-lg font-semibold mb-4">Información principal</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="flex flex-col gap-2 md:col-span-2">
<label for="company_name" class="font-medium text-sm">Razón social *</label>
<InputText
id="company_name"
v-model="form.company_name"
placeholder="Empresa Demo SA de CV"
:class="{ 'p-invalid': errors.company_name }"
/>
<small v-if="errors.company_name" class="p-error text-red-600">{{ errors.company_name }}</small>
</div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<label for="rfc" class="font-medium text-sm">RFC *</label> <label for="rfc" class="font-medium text-sm">RFC *</label>
<InputText <InputText
id="rfc" id="rfc"
v-model="form.rfc" v-model="form.rfc"
placeholder="ABC123456789" placeholder="DEMO010101ABC"
:class="{ 'p-invalid': errors.rfc }"
maxlength="13" maxlength="13"
class="font-mono"
:class="{ 'p-invalid': errors.rfc }"
@input="form.rfc = form.rfc.toUpperCase()"
/> />
<small v-if="errors.rfc" class="p-error text-red-600">{{ errors.rfc }}</small> <small v-if="errors.rfc" class="p-error text-red-600">{{ errors.rfc }}</small>
</div> </div>
@ -29,79 +41,118 @@
<InputText <InputText
id="curp" id="curp"
v-model="form.curp" v-model="form.curp"
placeholder="XYZ987654321" placeholder="XEXX010101HNEXXXA1"
:class="{ 'p-invalid': errors.curp }"
maxlength="18" maxlength="18"
class="font-mono"
:class="{ 'p-invalid': errors.curp }"
@input="form.curp = form.curp.toUpperCase()"
/> />
<small v-if="errors.curp" class="p-error text-red-600">{{ errors.curp }}</small> <small v-if="errors.curp" class="p-error text-red-600">{{ errors.curp }}</small>
</div> </div>
<div class="flex flex-col gap-2 md:col-span-2"> <div class="flex flex-col gap-2">
<label for="company_name" class="font-medium text-sm">Razón Social *</label> <label for="email" class="font-medium text-sm">Email *</label>
<InputText <InputText
id="company_name" id="email"
v-model="form.company_name" v-model="form.email"
placeholder="Corporativo Ejemplo S.A. de C.V." placeholder="admin@empresa-demo.com"
:class="{ 'p-invalid': errors.company_name }" :class="{ 'p-invalid': errors.email }"
/> />
<small v-if="errors.company_name" class="p-error text-red-600">{{ errors.company_name }}</small> <small v-if="errors.email" class="p-error text-red-600">{{ errors.email }}</small>
</div>
<div class="flex flex-col gap-2">
<label for="primary_domain" class="font-medium text-sm">
Dominio primario {{ isEditMode ? '(opcional en edición)' : '*' }}
</label>
<InputText
id="primary_domain"
v-model="form.primary_domain"
placeholder="empresa-demo.local"
:disabled="isEditMode"
:class="{ 'p-invalid': errors.primary_domain }"
/>
<small v-if="errors.primary_domain" class="p-error text-red-600">{{ errors.primary_domain }}</small>
</div>
<div class="flex flex-col gap-2">
<label for="phone" class="font-medium text-sm">Teléfono</label>
<InputText id="phone" v-model="form.phone" placeholder="5551234567" />
</div>
<div class="flex flex-col gap-2">
<label for="tax_regime" class="font-medium text-sm">Régimen fiscal</label>
<InputText id="tax_regime" v-model="form.tax_regime" placeholder="601" />
</div>
<div class="flex flex-col gap-2 md:col-span-2">
<label for="legal_representative" class="font-medium text-sm">Representante legal</label>
<InputText
id="legal_representative"
v-model="form.legal_representative"
placeholder="Juan Pérez"
/>
</div>
<div class="flex items-center gap-3 md:col-span-2">
<InputSwitch v-model="form.is_active" inputId="is_active" />
<label for="is_active" class="font-medium text-sm">
Tenant activo
</label>
</div> </div>
</div> </div>
</div> </div>
<!-- Tasas e Impuestos -->
<div class="border-b pb-4"> <div class="border-b pb-4">
<h3 class="text-lg font-semibold mb-4">Tasas e Impuestos (%)</h3> <h3 class="text-lg font-semibold mb-4">Tasas e impuestos (%)</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<label for="vat_rate" class="font-medium text-sm">Tasa de IVA *</label> <label for="vat_rate" class="font-medium text-sm">Tasa de IVA (opcional)</label>
<InputNumber <InputNumber
id="vat_rate" id="vat_rate"
v-model="form.vat_rate" v-model="form.vat_rate"
:min="0" :min="0"
:max="100" :max="100"
:minFractionDigits="2" :minFractionDigits="1"
:maxFractionDigits="2" :maxFractionDigits="2"
suffix="%" suffix="%"
:class="{ 'p-invalid': errors.vat_rate }"
/> />
<small v-if="errors.vat_rate" class="p-error text-red-600">{{ errors.vat_rate }}</small>
</div> </div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<label for="isr_withholding" class="font-medium text-sm">Retención ISR</label> <label for="isr_withholding" class="font-medium text-sm">Retención ISR (opcional)</label>
<InputNumber <InputNumber
id="isr_withholding" id="isr_withholding"
v-model="form.isr_withholding" v-model="form.isr_withholding"
:min="0" :min="0"
:max="100" :max="100"
:minFractionDigits="2" :minFractionDigits="1"
:maxFractionDigits="2" :maxFractionDigits="2"
suffix="%" suffix="%"
/> />
</div> </div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<label for="vat_withholding" class="font-medium text-sm">Retención IVA</label> <label for="vat_withholding" class="font-medium text-sm">Retención IVA (opcional)</label>
<InputNumber <InputNumber
id="vat_withholding" id="vat_withholding"
v-model="form.vat_withholding" v-model="form.vat_withholding"
:min="0" :min="0"
:max="100" :max="100"
:minFractionDigits="2" :minFractionDigits="1"
:maxFractionDigits="2" :maxFractionDigits="2"
suffix="%" suffix="%"
/> />
</div> </div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<label for="additional_tax" class="font-medium text-sm">Impuesto Adicional</label> <label for="additional_tax" class="font-medium text-sm">Impuesto adicional (opcional)</label>
<InputNumber <InputNumber
id="additional_tax" id="additional_tax"
v-model="form.additional_tax" v-model="form.additional_tax"
:min="0" :min="0"
:max="100" :max="100"
:minFractionDigits="2" :minFractionDigits="1"
:maxFractionDigits="2" :maxFractionDigits="2"
suffix="%" suffix="%"
/> />
@ -109,315 +160,204 @@
</div> </div>
</div> </div>
<!-- Dirección -->
<div> <div>
<h3 class="text-lg font-semibold mb-4">Dirección Fiscal</h3> <h3 class="text-lg font-semibold mb-4">Certificados SAT</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<label for="country" class="font-medium text-sm">País *</label> <label for="certificate_password" class="font-medium text-sm">Password del certificado</label>
<InputText <Password
id="country" id="certificate_password"
v-model="form.address.country" v-model="form.certificate_password"
placeholder="México" :feedback="false"
:class="{ 'p-invalid': errors.country }" toggleMask
placeholder="Password del certificado"
:class="{ 'p-invalid': errors.certificate_password }"
/> />
<small v-if="errors.country" class="p-error text-red-600">{{ errors.country }}</small> <small v-if="errors.certificate_password" class="p-error text-red-600">{{ errors.certificate_password }}</small>
</div> </div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<label for="postal_code" class="font-medium text-sm">Código Postal *</label> <label for="certificate_cer_file" class="font-medium text-sm">Archivo .cer</label>
<InputText <input
id="postal_code" id="certificate_cer_file"
v-model="form.address.postal_code" type="file"
placeholder="12345" accept=".cer"
:class="{ 'p-invalid': errors.postal_code }" class="p-2 border rounded-md border-surface-300 dark:border-surface-700"
maxlength="10" @change="onFileChange($event, 'certificate_cer_file')"
/> />
<small v-if="errors.postal_code" class="p-error text-red-600">{{ errors.postal_code }}</small> <small v-if="errors.certificate_cer_file" class="p-error text-red-600">{{ errors.certificate_cer_file }}</small>
</div> </div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<label for="state" class="font-medium text-sm">Estado *</label> <label for="certificate_key_file" class="font-medium text-sm">Archivo .key</label>
<InputText <input
id="state" id="certificate_key_file"
v-model="form.address.state" type="file"
placeholder="CDMX" accept=".key"
:class="{ 'p-invalid': errors.state }" class="p-2 border rounded-md border-surface-300 dark:border-surface-700"
/> @change="onFileChange($event, 'certificate_key_file')"
<small v-if="errors.state" class="p-error text-red-600">{{ errors.state }}</small>
</div>
<div class="flex flex-col gap-2">
<label for="municipality" class="font-medium text-sm">Municipio / Alcaldía *</label>
<InputText
id="municipality"
v-model="form.address.municipality"
placeholder="Benito Juárez"
:class="{ 'p-invalid': errors.municipality }"
/>
<small v-if="errors.municipality" class="p-error text-red-600">{{ errors.municipality }}</small>
</div>
<div class="flex flex-col gap-2">
<label for="city" class="font-medium text-sm">Ciudad *</label>
<InputText
id="city"
v-model="form.address.city"
placeholder="Ciudad de México"
:class="{ 'p-invalid': errors.city }"
/>
<small v-if="errors.city" class="p-error text-red-600">{{ errors.city }}</small>
</div>
<div class="flex flex-col gap-2">
<label for="street" class="font-medium text-sm">Calle *</label>
<InputText
id="street"
v-model="form.address.street"
placeholder="Av. Ejemplo"
:class="{ 'p-invalid': errors.street }"
/>
<small v-if="errors.street" class="p-error text-red-600">{{ errors.street }}</small>
</div>
<div class="flex flex-col gap-2">
<label for="num_ext" class="font-medium text-sm">Número Exterior *</label>
<InputText
id="num_ext"
v-model="form.address.num_ext"
placeholder="100"
:class="{ 'p-invalid': errors.num_ext }"
/>
<small v-if="errors.num_ext" class="p-error text-red-600">{{ errors.num_ext }}</small>
</div>
<div class="flex flex-col gap-2">
<label for="num_int" class="font-medium text-sm">Número Interior</label>
<InputText
id="num_int"
v-model="form.address.num_int"
placeholder="10"
/> />
<small v-if="errors.certificate_key_file" class="p-error text-red-600">{{ errors.certificate_key_file }}</small>
</div> </div>
</div> </div>
</div> </div>
<div class="text-xs text-slate-500"> <div class="text-xs text-slate-500">* Campos obligatorios</div>
* Campos obligatorios
</div>
</form> </form>
<template #footer> <template #footer>
<div class="flex justify-end gap-2"> <div class="flex justify-end gap-2">
<Button <Button label="Cancelar" icon="pi pi-times" @click="handleClose" class="p-button-text" />
label="Cancelar" <Button label="Guardar" icon="pi pi-check" @click="handleSubmit" :loading="loading" />
icon="pi pi-times"
@click="handleClose"
class="p-button-text"
/>
<Button
label="Guardar"
icon="pi pi-check"
@click="handleSubmit"
:loading="loading"
/>
</div> </div>
</template> </template>
</Dialog> </Dialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import type { Company, TenantFormData } from './companies.types';
import { toTenantFormData } from './companies.mapper';
// Props
const props = defineProps<{ const props = defineProps<{
visible: boolean; visible: boolean;
company?: any; company?: Company;
}>(); }>();
// Emits
const emit = defineEmits<{ const emit = defineEmits<{
close: []; close: [];
save: [data: any]; save: [data: TenantFormData];
}>(); }>();
// Métodos
// Método para setear errores de validación del backend
const setValidationErrors = (backendErrors: Record<string, string[]>) => {
loading.value = false;
errors.value = {};
// Mapear los errores del backend al formato del formulario
Object.keys(backendErrors).forEach(key => {
if (backendErrors[key] && backendErrors[key].length > 0) {
// Tomar el primer mensaje de error de cada campo
const firstError = backendErrors[key][0];
if (firstError) {
errors.value[key] = firstError;
}
}
});
};
// Expose method to set errors from parent
defineExpose({
setValidationErrors
});
// State
const loading = ref(false); const loading = ref(false);
const errors = ref<Record<string, string>>({});
const form = ref<TenantFormData>(toTenantFormData());
const isVisible = computed({ const isVisible = computed({
get: () => props.visible, get: () => props.visible,
set: (value) => { set: (value) => {
if (!value) emit('close'); if (!value) emit('close');
} },
}); });
const formTitle = computed(() => { const isEditMode = computed(() => Boolean(props.company?.id));
return props.company ? 'Editar Empresa' : 'Registrar Nueva Empresa'; const formTitle = computed(() => (isEditMode.value ? 'Editar Empresa' : 'Registrar Nueva Empresa'));
});
// Form data const setValidationErrors = (backendErrors: Record<string, string[]>) => {
const defaultForm = () => ({
rfc: '',
curp: '',
company_name: '',
vat_rate: 16.00,
isr_withholding: 0.00,
vat_withholding: 0.00,
additional_tax: 0.00,
address: {
country: 'México',
postal_code: '',
state: '',
municipality: '',
city: '',
street: '',
num_ext: '',
num_int: ''
}
});
const form = ref(defaultForm());
const errors = ref<Record<string, string>>({});
// Watch for company changes
watch(() => props.company, (newCompany) => {
if (newCompany) {
form.value = {
...defaultForm(),
...newCompany,
address: {
...defaultForm().address,
...(newCompany.address || {})
}
};
} else {
form.value = defaultForm();
}
errors.value = {};
loading.value = false; // Reset loading state
}, { immediate: true });
// Watch for dialog visibility to reset loading
watch(() => props.visible, (isVisible) => {
if (!isVisible) {
loading.value = false; loading.value = false;
}
});
// Methods
const validateForm = (): boolean => {
errors.value = {}; errors.value = {};
let isValid = true;
// RFC validation Object.keys(backendErrors).forEach((key) => {
if (!form.value.rfc || form.value.rfc.trim() === '') { const firstError = backendErrors[key]?.[0];
errors.value.rfc = 'El RFC es obligatorio'; if (firstError) {
isValid = false; errors.value[key] = firstError;
} else if (form.value.rfc.length < 12 || form.value.rfc.length > 13) {
errors.value.rfc = 'El RFC debe tener 12 o 13 caracteres';
isValid = false;
} }
});
// Company name validation
if (!form.value.company_name || form.value.company_name.trim() === '') {
errors.value.company_name = 'La razón social es obligatoria';
isValid = false;
}
// VAT rate validation
if (form.value.vat_rate === null || form.value.vat_rate === undefined) {
errors.value.vat_rate = 'La tasa de IVA es obligatoria';
isValid = false;
}
// Address validations
if (!form.value.address.country || form.value.address.country.trim() === '') {
errors.value.country = 'El país es obligatorio';
isValid = false;
}
if (!form.value.address.postal_code || form.value.address.postal_code.trim() === '') {
errors.value.postal_code = 'El código postal es obligatorio';
isValid = false;
}
if (!form.value.address.state || form.value.address.state.trim() === '') {
errors.value.state = 'El estado es obligatorio';
isValid = false;
}
if (!form.value.address.municipality || form.value.address.municipality.trim() === '') {
errors.value.municipality = 'El municipio es obligatorio';
isValid = false;
}
if (!form.value.address.city || form.value.address.city.trim() === '') {
errors.value.city = 'La ciudad es obligatoria';
isValid = false;
}
if (!form.value.address.street || form.value.address.street.trim() === '') {
errors.value.street = 'La calle es obligatoria';
isValid = false;
}
if (!form.value.address.num_ext || form.value.address.num_ext.trim() === '') {
errors.value.num_ext = 'El número exterior es obligatorio';
isValid = false;
}
return isValid;
}; };
const handleSubmit = async () => { const resetSubmitting = () => {
loading.value = false;
};
defineExpose({
setValidationErrors,
resetSubmitting,
});
watch(
() => props.company,
(newCompany) => {
const primaryDomain = newCompany?.primary_domain ?? newCompany?.domains?.[0]?.domain ?? '';
form.value = toTenantFormData({
id: newCompany?.id,
company_name: newCompany?.company_name,
rfc: newCompany?.rfc,
email: newCompany?.email,
primary_domain: primaryDomain,
curp: newCompany?.curp ?? '',
phone: newCompany?.phone ?? '',
legal_representative: newCompany?.legal_representative ?? '',
tax_regime: newCompany?.tax_regime ?? '',
is_active: newCompany?.is_active ?? true,
vat_rate: newCompany?.vat_rate ?? 0.0,
isr_withholding: newCompany?.isr_withholding ?? 0.0,
vat_withholding: newCompany?.vat_withholding ?? 0.0,
additional_tax: newCompany?.additional_tax ?? 0.0,
});
errors.value = {};
loading.value = false;
},
{ immediate: true }
);
watch(
() => props.visible,
(visible) => {
if (!visible) {
loading.value = false;
}
}
);
const isValidEmail = (email: string): boolean => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
const validateForm = (): boolean => {
errors.value = {};
let valid = true;
if (!form.value.company_name.trim()) {
errors.value.company_name = 'La razón social es obligatoria';
valid = false;
}
if (!form.value.rfc.trim()) {
errors.value.rfc = 'El RFC es obligatorio';
valid = false;
} else if (form.value.rfc.trim().length > 13) {
errors.value.rfc = 'El RFC no puede exceder 13 caracteres';
valid = false;
}
if (!form.value.email.trim()) {
errors.value.email = 'El email es obligatorio';
valid = false;
} else if (!isValidEmail(form.value.email.trim())) {
errors.value.email = 'Ingresa un email válido';
valid = false;
}
if (!isEditMode.value && !form.value.primary_domain.trim()) {
errors.value.primary_domain = 'El dominio primario es obligatorio';
valid = false;
}
return valid;
};
const onFileChange = (event: Event, field: 'certificate_cer_file' | 'certificate_key_file') => {
const target = event.target as HTMLInputElement;
const file = target.files?.[0] ?? null;
form.value[field] = file;
};
const handleSubmit = () => {
if (!validateForm()) { if (!validateForm()) {
return; return;
} }
loading.value = true; loading.value = true;
try { emit('save', {
// Convert string values to uppercase for RFC and CURP
const formData = {
...form.value, ...form.value,
rfc: form.value.rfc.toUpperCase(), rfc: form.value.rfc.trim().toUpperCase(),
curp: form.value.curp ? form.value.curp.toUpperCase() : null, curp: form.value.curp.trim().toUpperCase(),
}; });
emit('save', formData);
// El loading se mantendrá hasta que el padre cierre el diálogo
// o maneje el error
} catch (error) {
console.error('Error submitting form:', error);
loading.value = false;
}
}; };
const handleClose = () => { const handleClose = () => {
form.value = defaultForm(); form.value = toTenantFormData();
errors.value = {}; errors.value = {};
loading.value = false;
emit('close'); emit('close');
}; };
</script> </script>

View File

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

View File

@ -1,123 +1,79 @@
import api from '@/services/api'; import api from '@/services/api';
import type { import {
Company, buildCreateTenantPayload,
CompanyCreateResponse, buildUpdateTenantPayload,
CompanyFormData, tenantPayloadToFormData,
CompanyQueryParams, } from './companies.mapper';
PaginatedResponse import type { CompaniesPagination, TenantFormData } from './companies.types';
} from './companies.types';
/**
export class CompanyService { * Servicio para obtener todas las empresas (tenants) con paginación.
/**
* Obtener todas las empresas con paginación y filtros opcionales
*/ */
async getAll(params?: CompanyQueryParams): Promise<PaginatedResponse<Company> | Company[]> { export class CompaniesService {
/**
* Obtiene todas las empresas del sistema.
* @returns Promesa con la respuesta paginada de empresas
*/
public async getAll(page = 1): Promise<CompaniesPagination> {
try { try {
const response = await api.get('api/catalogs/companies', { params }); const response = await api.get('/api/admin/admin/tenants', {
params: { page },
// Si la respuesta tiene paginación, devolver el objeto completo });
if (response.data.current_page !== undefined) { return response.data;
return response.data as PaginatedResponse<Company>; } catch (error) {
}
// Si no tiene paginación, devolver solo el array de data
return response.data.data as Company[];
} catch (error: any) {
console.error('Error al obtener empresas:', error); console.error('Error al obtener empresas:', error);
throw error; throw error;
} }
} }
/** /**
* Obtener una empresa por ID * Crea una empresa (tenant) usando multipart/form-data.
*/ */
async getById(id: number): Promise<Company> { public async create(data: TenantFormData): Promise<void> {
try { try {
const response = await api.get(`api/catalogs/companies/${id}`); const payload = buildCreateTenantPayload(data);
return response.data; const formData = tenantPayloadToFormData(payload);
} catch (error: any) { await api.post('/api/admin/admin/tenants', formData, {
console.error(`Error al obtener empresa con ID ${id}:`, error); headers: {
throw error; 'Content-Type': 'multipart/form-data',
} },
}
/**
* Crear una nueva empresa
*/
async create(data: CompanyFormData): Promise<CompanyCreateResponse> {
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<CompanyFormData>): Promise<Company> {
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<void> {
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<Company[]> {
try {
const response = await api.get('api/catalogs/companies', {
params: { ...params, paginate: false }
}); });
return response.data.data; } catch (error) {
} catch (error: any) { console.error('Error al crear empresa:', error);
console.error('Error al buscar empresas:', error);
throw error; throw error;
} }
} }
}; /**
* Actualiza una empresa (tenant) usando POST + _method=PUT para multipart.
*/
public async update(id: number, data: TenantFormData): Promise<void> {
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<void> {
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();

View File

@ -1,72 +1,71 @@
export interface CompanyAddress { export interface Company {
country: string; id: number;
postal_code: string; rfc: string;
state: string; curp?: string | null;
municipality: string; vat_rate?: number | null;
city: string; isr_withholding?: number | null;
street: string; vat_withholding?: number | null;
num_ext: string; additional_tax?: number | null;
num_int?: string; 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<string, unknown> | 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; id?: number;
rfc: string;
curp?: string;
company_name: 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; vat_rate: number;
isr_withholding: number; isr_withholding: number;
vat_withholding: number; vat_withholding: number;
additional_tax: number; additional_tax: number;
address: CompanyAddress; certificate_password: string;
created_at?: string; certificate_cer_file: File | null;
updated_at?: string; certificate_key_file: File | null;
} }
export interface CompanyCreateResponse{ export type CreateTenantPayload = Omit<TenantFormData, 'id'>;
data: Company;
}
export interface CompanyFormData extends Omit<Company, 'id' | 'created_at' | 'updated_at'> {} export type UpdateTenantPayload = Omit<TenantFormData, 'id' | 'primary_domain'> & {
primary_domain?: string;
};
export interface CompanyStats { export interface CompaniesPagination {
total: number;
active: number;
pending: number;
monthlyIncrease: number;
activePercentage: number;
}
export interface PaginationLink {
url: string | null;
label: string;
active: boolean;
}
export interface PaginatedResponse<T> {
current_page: number; current_page: number;
data: T[]; data: Company[];
first_page_url: string; first_page_url: string;
from: number; from: number;
last_page: number; last_page: number;
last_page_url: string; last_page_url: string;
links: PaginationLink[]; links: Array<{ url: string | null; label: string; active: boolean }>;
next_page_url: string | null; next_page_url?: string | null;
path: string; path: string;
per_page: number; per_page: number;
prev_page_url: string | null; prev_page_url?: string | null;
to: number; to: number;
total: number; total: number;
} }
export interface CompanyListResponse {
data: Company[];
}
export interface CompanyQueryParams {
paginate?: boolean;
rfc?: string;
curp?: string;
company_name?: string;
page?: number;
}