panels-origin/web-panel/pages/configuracion.vue
2026-08-21 16:55:01 -05:00

409 lines
15 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 === '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="riesgos">Riesgos</Tab>
<Tab value="temas">Temas de gafete</Tab>
</TabList>
<TabPanels>
<TabPanel value="empresas">
<Message severity="info" :closable="false">
La cuenta pertenece a <strong>{{ principal?.name || principal?.code || "la empresa principal" }}</strong>.
Capture razón social, RFC, régimen y registro patronal; se usarán en IMSS y trámites posteriores.
</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="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(760px, 96vw)' }"
>
<Message 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 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>
<Message v-if="formError" severity="error" class="mt-3" :closable="false">{{ formError }}</Message>
<template #footer>
<Button label="Cancelar" severity="secondary" text @click="showForm = false" />
<Button :label="editing ? 'Guardar' : 'Crear'" icon="pi pi-check" :loading="saving" @click="save" />
</template>
</Dialog>
</div>
</template>
<script setup lang="ts">
import FormField from "~/components/padron/FormField.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 tab = ref("empresas");
const risks = ref<Record<string, string>[]>([]);
const themes = ref<Record<string, string>[]>([]);
const companies = ref<CompanyOption[]>([]);
const showForm = ref(false);
const saving = ref(false);
const formError = ref("");
const editing = ref<CompanyOption | null>(null);
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" : "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)));
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 = await api<{
risks: Record<string, string>[];
themes: Record<string, string>[];
companies: CompanyOption[];
}>("/v1/catalogs");
risks.value = c.risks;
themes.value = c.themes;
companies.value = c.companies || [];
syncChrome();
}
function openNew() {
editing.value = null;
resetForm();
formError.value = "";
showForm.value = true;
}
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 = "";
showForm.value = true;
}
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 (key === "empresas" || key === "riesgos" || key === "temas") tab.value = key;
},
tree: [
{ key: "empresas", label: "Empresas", icon: "pi pi-briefcase", data: { count: companies.value.length } },
{ 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: "vista",
label: "Vista",
items: [{ key: "refresh", label: "Actualizar", icon: "pi pi-refresh", command: load }],
},
],
});
}
onMounted(load);
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>