panels-origin/web-panel/pages/configuracion.vue
Alberto Martinez 90bcd48f95 Facturas, nómina, dispersión e IMSS (#28)
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
## Resumen

Implementa las observaciones de facturas, nómina e IMSS sobre el catálogo de proveedores y la nómina semanal que ya existían.

## Qué queda disponible

- Proveedores: régimen fiscal, código postal, estatus activo/inactivo y filtro por nombre o RFC.
- Facturas recibidas y emitidas: Excel, UUID, revisión si el RFC no está en el catálogo, clasificación a obra/concepto y gasto `source = invoice` solo para la recibida clasificada.
- Nómina: bono, descuento con observación, horas y día festivo dentro del armado semanal. La semana pagada no acepta cambios.
- Préstamos: edición (libre sin pagos; con pagos solo nota y cuota) y estado de cuenta con saldo = entregado + comisión − pagos.
- Finiquitos y liquidaciones: conceptos, neto descontando descuentos vigentes y saldo de préstamo, y gasto al pagar.
- Dispersión: corte semanal, corrección antes de definitiva y comprobante.
- Historial de alta, baja y reingreso en la ficha, más movimientos IMSS.
- Reporte por periodo en Excel y PDF.
- Jornal IMSS distinto del jornal real y consulta de cuotas exportable a Excel.

## Verificación

`deno check` de la API pasó. En este entorno no hay Postgres, así que `api/pendientes_test.ts` queda listo para correr cuando la base tenga las migraciones 042–050 y el permiso IAM 007.

<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a href="https://cursor.com/agents/bc-5e23dc3b-921e-4091-9650-7091d3a52bbf?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a href="https://cursor.com/background-agent?bcId=bc-5e23dc3b-921e-4091-9650-7091d3a52bbf&cursor_ref=pr_footer&cursor_cta=open_in_cursor"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img alt="Open in Cursor" width="131" height="28" src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a>&nbsp;</div>
2026-09-25 03:26:30 +00:00

651 lines
26 KiB
Vue

<template>
<div class="list-page">
<DeskTableBar v-model="tableQ" :placeholder="searchPlaceholder">
<Chip v-if="tab === 'empresas'" :label="`${listedCompanies.length} empresas`" icon="pi pi-briefcase" />
<Chip v-if="tab === 'empresas'" :label="`${subCompanies} subempresas`" icon="pi pi-th-large" />
<Chip v-if="tab === 'nomina'" :label="`${units.length} unidades`" icon="pi pi-percentage" />
<Chip v-if="tab === 'proveedores'" :label="`${listedSuppliers.length} proveedores`" icon="pi pi-truck" />
<Chip v-if="tab === 'unidades'" :label="`${listedMeasureUnits.length} unidades`" icon="pi pi-box" />
<Chip v-if="tab === 'riesgos'" :label="`${listedRisks.length} riesgos`" icon="pi pi-exclamation-triangle" />
<Chip v-if="tab === 'temas'" :label="`${listedThemes.length} temas`" icon="pi pi-palette" />
</DeskTableBar>
<Tabs v-model:value="tab">
<TabList>
<Tab value="empresas">Empresas</Tab>
<Tab value="proveedores">Proveedores</Tab>
<Tab value="unidades">Unidades</Tab>
<Tab value="nomina">Nómina</Tab>
<Tab value="riesgos">Riesgos</Tab>
<Tab value="temas">Temas de gafete</Tab>
</TabList>
<TabPanels>
<TabPanel value="empresas">
<Message severity="info" :closable="false">
Primero la empresa (razón social, RFC, régimen y registro patronal), luego las obras y al final el padrón.
La cuenta pertenece a <strong>{{ principal?.name || principal?.code || "la empresa principal" }}</strong>.
Use <strong>Nueva subempresa</strong> en la cinta para dar de alta otra razón o patrón IMSS.
</Message>
<DataTable :value="listedCompanies" class="mt-3" data-key="id" paginator :rows="15" empty-message="No hay empresas registradas.">
<Column field="code" header="Código" />
<Column field="nombre_comercial" header="Nombre comercial">
<template #body="{ data }">{{ data.nombre_comercial || data.name }}</template>
</Column>
<Column field="razon_social" header="Razón social">
<template #body="{ data }">{{ data.razon_social || "—" }}</template>
</Column>
<Column field="rfc" header="RFC">
<template #body="{ data }"><span class="identifier">{{ data.rfc || "—" }}</span></template>
</Column>
<Column field="registro_patronal" header="Reg. patronal">
<template #body="{ data }"><span class="identifier">{{ data.registro_patronal || "—" }}</span></template>
</Column>
<Column field="regimen_fiscal" header="Régimen">
<template #body="{ data }">{{ regimenLabel(data.regimen_fiscal) }}</template>
</Column>
<Column header="Tipo">
<template #body="{ data }">
<Tag :value="data.kind === 'principal' ? 'Principal' : 'Subempresa'" :severity="data.kind === 'principal' ? 'info' : 'secondary'" />
</template>
</Column>
<Column header="Estatus">
<template #body="{ data }">
<Tag :value="data.status === 'activo' ? 'Activa' : 'Inactiva'" :severity="data.status === 'activo' ? 'success' : 'secondary'" />
</template>
</Column>
<Column header="" header-style="width: 8rem">
<template #body="{ data }">
<Button icon="pi pi-pencil" text rounded size="small" aria-label="Editar" @click="openEdit(data)" />
</template>
</Column>
</DataTable>
</TabPanel>
<TabPanel value="nomina">
<Message severity="info" :closable="false">
Comisión de préstamos de la empresa. Al prestar se pueden cambiar; lo aceptado se congela en ese préstamo.
</Message>
<div class="company-form-grid mt-3" style="max-width: 36rem">
<FormField label="Comisión activa">
<ToggleSwitch v-model="payroll.loan_commission_enabled" />
</FormField>
<FormField label="Porcentaje">
<InputNumber v-model="payroll.loan_commission_pct" suffix=" %" :min="0" :max="100" :max-fraction-digits="2" class="w-full" />
</FormField>
<FormField label="Tope préstamo chico (entregado)">
<InputNumber v-model="payroll.loan_small_max" mode="currency" currency="MXN" locale="es-MX" class="w-full" />
</FormField>
<div class="span-2">
<Button label="Guardar nómina" icon="pi pi-check" :loading="savingPayroll" @click="savePayroll" />
</div>
</div>
<h3 class="mt-4">Unidades de destajo</h3>
<p class="muted">Solo para destajo de nómina. Las unidades de gastos y presupuesto están en <strong>Unidades</strong>.</p>
<DataTable :value="units" class="mt-2" empty-message="Sin unidades">
<Column field="code" header="Código" />
<Column field="label" header="Nombre" />
</DataTable>
<div class="company-form-grid mt-3" style="max-width: 36rem">
<FormField label="Código">
<InputText v-model="unitForm.code" class="w-full" placeholder="ml" />
</FormField>
<FormField label="Nombre">
<InputText v-model="unitForm.label" class="w-full" placeholder="Metro lineal" />
</FormField>
<div>
<Button label="Agregar unidad" icon="pi pi-plus" outlined @click="addUnit" />
</div>
</div>
</TabPanel>
<TabPanel value="proveedores">
<Message severity="info" :closable="false">
Catálogo de proveedores para capturar gastos. En Gastos solo se selecciona; aquí se guardan nombre, RFC y datos generales.
</Message>
<DataTable :value="listedSuppliers" class="mt-3" data-key="id" paginator :rows="15" empty-message="No hay proveedores. Use Nuevo proveedor.">
<Column field="name" header="Nombre" />
<Column field="legal_name" header="Razón social">
<template #body="{ data }">{{ data.legal_name || "—" }}</template>
</Column>
<Column field="rfc" header="RFC">
<template #body="{ data }"><span class="identifier">{{ data.rfc || "—" }}</span></template>
</Column>
<Column field="regimen_fiscal" header="Régimen">
<template #body="{ data }">{{ data.regimen_fiscal || "—" }}</template>
</Column>
<Column header="Estatus">
<template #body="{ data }">
<Tag :value="data.status === 'inactivo' ? 'Inactivo' : 'Activo'" :severity="data.status === 'inactivo' ? 'secondary' : 'success'" />
</template>
</Column>
<Column field="phone" header="Teléfono">
<template #body="{ data }">{{ data.phone || "—" }}</template>
</Column>
<Column field="email" header="Correo">
<template #body="{ data }">{{ data.email || "—" }}</template>
</Column>
<Column header="" header-style="width: 5rem">
<template #body="{ data }">
<Button icon="pi pi-pencil" text rounded size="small" aria-label="Editar" @click="openSupplier(data)" />
</template>
</Column>
</DataTable>
</TabPanel>
<TabPanel value="unidades">
<Message severity="info" :closable="false">
Unidades de medida para gastos (pieza, m², bulto…). Distintas de las unidades de destajo de nómina.
</Message>
<DataTable :value="listedMeasureUnits" class="mt-3" data-key="code" paginator :rows="15" empty-message="Sin unidades">
<Column field="code" header="Código" />
<Column field="label" header="Nombre" />
</DataTable>
<div class="company-form-grid mt-3" style="max-width: 36rem">
<FormField label="Código">
<InputText v-model="measureUnitForm.code" class="w-full" placeholder="bulto" />
</FormField>
<FormField label="Nombre">
<InputText v-model="measureUnitForm.label" class="w-full" placeholder="Bulto" />
</FormField>
<div>
<Button label="Agregar unidad" icon="pi pi-plus" outlined @click="addMeasureUnit" />
</div>
</div>
</TabPanel>
<TabPanel value="riesgos">
<DataTable :value="listedRisks" paginator :rows="15">
<Column field="code" header="Barra gafete" />
<Column field="label" header="Color" />
<Column header="Nivel">
<template #body="{ data }">
<Tag :value="riskLevelLabel(data.code)" :severity="riskLevelSeverity(data.code)" />
</template>
</Column>
<Column header="Muestra">
<template #body="{ data }">
<Tag :value="data.color" :severity="riskSeverity(data.code)" />
</template>
</Column>
</DataTable>
</TabPanel>
<TabPanel value="temas">
<DataTable :value="listedThemes" paginator :rows="15">
<Column field="id" header="ID" />
<Column field="name" header="Nombre" />
<Column header="Layout">
<template #body="{ data }">
<Chip :label="data.layout" icon="pi pi-id-card" />
</template>
</Column>
</DataTable>
</TabPanel>
</TabPanels>
</Tabs>
<Dialog
v-model:visible="showForm"
:header="editing ? 'Editar empresa' : 'Nueva subempresa'"
modal
class="company-form-dialog"
:style="{ width: 'min(820px, 96vw)' }"
>
<Tabs v-if="editing" v-model:value="formTab" class="mb-3">
<TabList>
<Tab value="datos">Datos</Tab>
<Tab value="docs">Expediente</Tab>
</TabList>
</Tabs>
<Message v-if="formTab === 'datos'" severity="secondary" :closable="false" class="mb-3">
Datos fiscales y patronales de la empresa. El nombre corto se usa en listados; la razón social y el RFC quedan para IMSS y facturación.
</Message>
<div v-show="formTab === 'datos'" class="company-form-grid">
<FormField label="Nombre comercial" required>
<InputText v-model="form.nombre_comercial" class="w-full" placeholder="Constructora Maya" />
</FormField>
<FormField label="Razón social" required>
<InputText v-model="form.razon_social" class="w-full" placeholder="Constructora Maya S.A. de C.V." />
</FormField>
<FormField label="Código {{ editing ? '' : '(opcional)' }}">
<InputText v-model="form.code" class="w-full identifier-input" placeholder="Se genera del nombre si lo deja vacío" maxlength="16" />
</FormField>
<FormField label="RFC">
<InputText v-model="form.rfc" class="w-full identifier-input" maxlength="13" placeholder="XAXX010101000" />
</FormField>
<FormField label="Régimen fiscal">
<Select
v-model="form.regimen_fiscal"
:options="regimenOpts"
option-label="label"
option-value="value"
placeholder="Seleccione régimen"
show-clear
class="w-full"
/>
</FormField>
<FormField label="Registro patronal IMSS">
<InputText v-model="form.registro_patronal" class="w-full identifier-input" placeholder="Número patronal" />
</FormField>
<FormField label="Clase de riesgo IMSS">
<Select
v-model="form.clase_riesgo"
:options="claseRiesgoOpts"
option-label="label"
option-value="value"
placeholder="I a V"
show-clear
class="w-full"
/>
</FormField>
<FormField label="Giro / actividad">
<InputText v-model="form.giro" class="w-full" placeholder="Construcción, servicios…" />
</FormField>
<FormField label="Domicilio fiscal" class="span-2">
<InputText v-model="form.domicilio_fiscal" class="w-full" placeholder="Calle, número, colonia" />
</FormField>
<FormField label="C.P.">
<InputText v-model="form.codigo_postal" class="w-full" maxlength="5" placeholder="77500" />
</FormField>
<FormField label="Ciudad">
<InputText v-model="form.ciudad" class="w-full" />
</FormField>
<FormField label="Estado">
<InputText v-model="form.estado" class="w-full" />
</FormField>
<FormField label="Teléfono">
<InputText v-model="form.telefono" class="w-full" />
</FormField>
<FormField label="Correo">
<InputText v-model="form.email" class="w-full no-caps" />
</FormField>
<FormField label="Representante legal" class="span-2">
<InputText v-model="form.representante_legal" class="w-full" />
</FormField>
<FormField v-if="editing && editing.kind !== 'principal'" label="Estatus">
<Select v-model="form.status" :options="statusOpts" option-label="label" option-value="value" class="w-full" />
</FormField>
</div>
<CompanyDocuments
v-if="editing && formTab === 'docs'"
:company-id="editing.id"
:documents="companyDocs.documents"
:checklist="companyDocs.checklist"
@changed="loadCompanyDocs"
/>
<Message v-if="formError" severity="error" class="mt-3" :closable="false">{{ formError }}</Message>
<template #footer>
<Button label="Cerrar" severity="secondary" text @click="showForm = false" />
<Button v-if="formTab === 'datos'" :label="editing ? 'Guardar' : 'Crear'" icon="pi pi-check" :loading="saving" @click="save" />
</template>
</Dialog>
<SupplierFormDialog v-model="showSupplierForm" :supplier="editingSupplier" @saved="onSupplierSaved" />
</div>
</template>
<script setup lang="ts">
import FormField from "~/components/padron/FormField.vue";
import CompanyDocuments from "~/components/CompanyDocuments.vue";
import SupplierFormDialog, { type Supplier } from "~/components/SupplierFormDialog.vue";
import type { CompanyOption } from "~/types/padron";
const emptyForm = () => ({
nombre_comercial: "",
razon_social: "",
code: "",
status: "activo",
registro_patronal: "",
rfc: "",
regimen_fiscal: "",
clase_riesgo: "",
domicilio_fiscal: "",
codigo_postal: "",
ciudad: "",
estado: "",
telefono: "",
email: "",
representante_legal: "",
giro: "",
});
const { riskSeverity, riskLevelLabel, riskLevelSeverity } = useUi();
const desk = useDesktop();
const { api } = useApi();
const toast = useToast();
const route = useRoute();
const tab = ref("empresas");
const risks = ref<Record<string, string>[]>([]);
const themes = ref<Record<string, string>[]>([]);
const companies = ref<CompanyOption[]>([]);
const payroll = reactive({
loan_commission_enabled: true,
loan_commission_pct: 10,
loan_small_max: 500,
});
const units = ref<{ code: string; label: string }[]>([]);
const unitForm = reactive({ code: "", label: "" });
const suppliers = ref<Supplier[]>([]);
const measureUnits = ref<{ code: string; label: string }[]>([]);
const measureUnitForm = reactive({ code: "", label: "" });
const showSupplierForm = ref(false);
const editingSupplier = ref<Supplier | null>(null);
const savingPayroll = ref(false);
const showForm = ref(false);
const saving = ref(false);
const formError = ref("");
const editing = ref<CompanyOption | null>(null);
const formTab = ref("datos");
const companyDocs = reactive<{ documents: Array<{ id: number; type_code: string; original_name: string; uploaded_at: string; is_current: number; mime?: string }>; checklist: Array<{ code: string; label: string; required: boolean; present: boolean; category?: string }> }>({
documents: [],
checklist: [],
});
const form = reactive(emptyForm());
const statusOpts = [
{ label: "Activa", value: "activo" },
{ label: "Inactiva", value: "inactivo" },
];
const regimenOpts = [
{ value: "601", label: "601 · General de Ley Personas Morales" },
{ value: "603", label: "603 · Personas Morales con Fines no Lucrativos" },
{ value: "605", label: "605 · Sueldos y Salarios" },
{ value: "606", label: "606 · Arrendamiento" },
{ value: "612", label: "612 · Actividades Empresariales y Profesionales" },
{ value: "621", label: "621 · Incorporación Fiscal" },
{ value: "625", label: "625 · Plataformas Tecnológicas" },
{ value: "626", label: "626 · Régimen Simplificado de Confianza" },
];
const claseRiesgoOpts = [
{ value: "I", label: "Clase I" },
{ value: "II", label: "Clase II" },
{ value: "III", label: "Clase III" },
{ value: "IV", label: "Clase IV" },
{ value: "V", label: "Clase V" },
];
const principal = computed(() => companies.value.find((company) => company.kind === "principal"));
const { tableQ, matches } = useTableSearch();
const searchPlaceholder = computed(() =>
tab.value === "riesgos"
? "Buscar riesgo o color"
: tab.value === "temas"
? "Buscar tema"
: tab.value === "nomina"
? "Nómina y destajo"
: tab.value === "proveedores"
? "Buscar proveedor o RFC"
: tab.value === "unidades"
? "Buscar unidad"
: "Buscar empresa, RFC o código",
);
const listedCompanies = computed(() =>
companies.value.filter((company) =>
matches(
company.code,
company.name,
company.nombre_comercial,
company.razon_social,
company.rfc,
company.registro_patronal,
company.regimen_fiscal,
company.kind,
company.parent_name,
company.parent_code,
company.status,
),
),
);
const subCompanies = computed(() => listedCompanies.value.filter((company) => company.kind !== "principal").length);
const listedRisks = computed(() => risks.value.filter((risk) => matches(risk.code, risk.label, risk.color)));
const listedThemes = computed(() => themes.value.filter((theme) => matches(theme.id, theme.name, theme.layout)));
const listedSuppliers = computed(() =>
suppliers.value.filter((s) => matches(s.name, s.legal_name, s.rfc, s.email, s.phone, s.city)),
);
const listedMeasureUnits = computed(() =>
measureUnits.value.filter((u) => matches(u.code, u.label)),
);
function regimenLabel(code?: string | null) {
if (!code) return "—";
return regimenOpts.find((item) => item.value === code)?.label || code;
}
function resetForm() {
Object.assign(form, emptyForm());
}
async function load() {
const [c, pay] = await Promise.all([
api<{
risks: Record<string, string>[];
themes: Record<string, string>[];
companies: CompanyOption[];
}>("/v1/catalogs"),
api<{
settings: { loan_commission_enabled: number; loan_commission_pct: number; loan_small_max: number };
units: { code: string; label: string }[];
}>("/v1/payroll/settings"),
]);
risks.value = c.risks;
themes.value = c.themes;
companies.value = c.companies || [];
payroll.loan_commission_enabled = Boolean(pay.settings.loan_commission_enabled);
payroll.loan_commission_pct = Number(pay.settings.loan_commission_pct);
payroll.loan_small_max = Number(pay.settings.loan_small_max);
units.value = pay.units || [];
try {
const sup = await api<{ suppliers?: Supplier[]; data?: { suppliers?: Supplier[] } }>("/v1/suppliers");
suppliers.value = sup.suppliers ?? sup.data?.suppliers ?? [];
} catch {
suppliers.value = [];
}
try {
const mu = await api<{ units?: { code: string; label: string }[]; data?: { units?: { code: string; label: string }[] } }>("/v1/measure-units");
measureUnits.value = mu.units ?? mu.data?.units ?? [];
} catch {
measureUnits.value = [];
}
syncChrome();
}
async function savePayroll() {
savingPayroll.value = true;
try {
await api("/v1/payroll/settings", { method: "PATCH", json: payroll });
toast.add({ severity: "success", summary: "Nómina actualizada", life: 1800 });
await load();
} finally {
savingPayroll.value = false;
}
}
async function addUnit() {
if (!unitForm.code.trim()) return;
await api("/v1/destajo/units", { method: "POST", json: { code: unitForm.code, label: unitForm.label } });
unitForm.code = "";
unitForm.label = "";
toast.add({ severity: "success", summary: "Unidad agregada", life: 1600 });
await load();
}
async function addMeasureUnit() {
if (!measureUnitForm.code.trim()) return;
await api("/v1/measure-units", { method: "POST", json: { code: measureUnitForm.code, label: measureUnitForm.label } });
measureUnitForm.code = "";
measureUnitForm.label = "";
toast.add({ severity: "success", summary: "Unidad de medida agregada", life: 1600 });
await load();
}
function openNewSupplier() {
editingSupplier.value = null;
showSupplierForm.value = true;
}
function openSupplier(row: Supplier) {
editingSupplier.value = row;
showSupplierForm.value = true;
}
async function onSupplierSaved() {
toast.add({ severity: "success", summary: "Proveedor guardado", life: 1600 });
await load();
}
function openNew() {
editing.value = null;
formTab.value = "datos";
resetForm();
formError.value = "";
showForm.value = true;
}
async function loadCompanyDocs() {
if (!editing.value) {
companyDocs.documents = [];
companyDocs.checklist = [];
return;
}
const detail = await api<{
documents: typeof companyDocs.documents;
checklist: typeof companyDocs.checklist;
}>(`/v1/companies/${editing.value.id}`);
companyDocs.documents = detail.documents || [];
companyDocs.checklist = detail.checklist || [];
}
async function openEdit(company: CompanyOption) {
editing.value = company;
resetForm();
form.nombre_comercial = company.nombre_comercial || company.name || "";
form.razon_social = company.razon_social || company.name || "";
form.code = company.code;
form.status = company.status;
form.registro_patronal = company.registro_patronal || "";
form.rfc = company.rfc || "";
form.regimen_fiscal = company.regimen_fiscal || "";
form.clase_riesgo = company.clase_riesgo || "";
form.domicilio_fiscal = company.domicilio_fiscal || "";
form.codigo_postal = company.codigo_postal || "";
form.ciudad = company.ciudad || "";
form.estado = company.estado || "";
form.telefono = company.telefono || "";
form.email = company.email || "";
form.representante_legal = company.representante_legal || "";
form.giro = company.giro || "";
formError.value = "";
formTab.value = "datos";
showForm.value = true;
await loadCompanyDocs();
}
function payload() {
return {
name: form.nombre_comercial || form.razon_social,
nombre_comercial: form.nombre_comercial,
razon_social: form.razon_social,
code: form.code || undefined,
registro_patronal: form.registro_patronal,
rfc: form.rfc,
regimen_fiscal: form.regimen_fiscal,
clase_riesgo: form.clase_riesgo,
domicilio_fiscal: form.domicilio_fiscal,
codigo_postal: form.codigo_postal,
ciudad: form.ciudad,
estado: form.estado,
telefono: form.telefono,
email: form.email,
representante_legal: form.representante_legal,
giro: form.giro,
};
}
async function save() {
if (!form.nombre_comercial.trim() && !form.razon_social.trim()) {
formError.value = "Indique nombre comercial o razón social";
return;
}
saving.value = true;
formError.value = "";
try {
if (editing.value) {
await api(`/v1/companies/${editing.value.id}`, {
method: "PATCH",
json: {
...payload(),
status: editing.value.kind === "principal" ? "activo" : form.status,
},
});
toast.add({ severity: "success", summary: "Empresa actualizada", life: 2200 });
} else {
await api("/v1/companies", { method: "POST", json: payload() });
toast.add({ severity: "success", summary: "Subempresa creada", life: 2200 });
}
showForm.value = false;
await load();
} catch (error) {
const detail = error as { data?: { error?: string } };
formError.value = detail.data?.error || (error instanceof Error ? error.message : "No se pudo guardar");
} finally {
saving.value = false;
}
}
function syncChrome() {
desk.configure({
tab: "Configuración",
treeTitle: "Catálogos",
showTree: true,
showDetail: false,
treeKey: tab.value,
onTreeSelect: (key) => {
if (["empresas", "proveedores", "unidades", "riesgos", "temas", "nomina"].includes(key)) tab.value = key;
},
tree: [
{ key: "empresas", label: "Empresas", icon: "pi pi-briefcase", data: { count: companies.value.length } },
{ key: "proveedores", label: "Proveedores", icon: "pi pi-truck", data: { count: suppliers.value.length } },
{ key: "unidades", label: "Unidades", icon: "pi pi-box", data: { count: measureUnits.value.length } },
{ key: "nomina", label: "Nómina", icon: "pi pi-wallet" },
{ key: "riesgos", label: "Riesgos", icon: "pi pi-exclamation-triangle", data: { count: risks.value.length } },
{ key: "temas", label: "Temas de gafete", icon: "pi pi-palette", data: { count: themes.value.length } },
],
ribbon: [
{
key: "acciones",
label: "Acciones",
items: [
{ key: "alta", label: "Nueva subempresa", icon: "pi pi-plus", disabled: tab.value !== "empresas", command: openNew },
{ key: "proveedor", label: "Nuevo proveedor", icon: "pi pi-plus", disabled: tab.value !== "proveedores", command: openNewSupplier },
],
},
{
key: "vista",
label: "Vista",
items: [{ key: "refresh", label: "Actualizar", icon: "pi pi-refresh", command: load }],
},
],
});
}
onMounted(load);
watch(
() => String(route.query.tab || ""),
(key) => {
if (["empresas", "proveedores", "unidades", "riesgos", "temas", "nomina"].includes(key)) tab.value = key;
},
{ immediate: true },
);
watch(tab, (v) => {
desk.treeKey.value = v;
syncChrome();
});
</script>
<style scoped>
.company-form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px 12px;
}
.company-form-grid :deep(.form-field.span-2),
.company-form-grid :deep(.span-2) {
grid-column: 1 / -1;
}
@media (max-width: 700px) {
.company-form-grid {
grid-template-columns: 1fr;
}
}
</style>