mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 16:13:17 +00:00
409 lines
15 KiB
Vue
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>
|