mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 16:53:17 +00:00
La vigencia de 3 meses para alta o reingreso cuenta desde la fecha de carga. Quien recibe el expediente revisa si el documento es válido. Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
592 lines
21 KiB
Vue
592 lines
21 KiB
Vue
<template>
|
|
<div class="dash">
|
|
<div class="kpi-grid">
|
|
<Card
|
|
v-for="s in stats"
|
|
:key="s.label"
|
|
class="dash-kpi"
|
|
:class="{ clickable: Boolean(s.to) }"
|
|
@click="s.to && navigateTo(s.to)"
|
|
>
|
|
<template #content>
|
|
<div class="dash-kpi-head">
|
|
<Avatar :icon="s.icon" shape="circle" :style="{ background: s.bg, color: '#fff' }" />
|
|
<Tag :value="s.hint" :severity="s.severity" />
|
|
</div>
|
|
<div class="kpi-value mt-3">{{ s.value }}</div>
|
|
<div class="muted">{{ s.label }}</div>
|
|
</template>
|
|
</Card>
|
|
</div>
|
|
|
|
<div class="dash-charts">
|
|
<Card>
|
|
<template #content>
|
|
<div class="dash-chart-label">Expediente</div>
|
|
<div class="dash-chart">
|
|
<Chart v-if="hasValues(pipelineChart)" type="doughnut" :data="pipelineChart" :options="pieOptions" :key="`pipe-${themeKey}`" />
|
|
<p v-else class="muted dash-empty">Sin personal cargado</p>
|
|
</div>
|
|
</template>
|
|
</Card>
|
|
<Card>
|
|
<template #content>
|
|
<div class="dash-chart-label">IMSS</div>
|
|
<div class="dash-chart">
|
|
<Chart v-if="hasValues(imssChart)" type="doughnut" :data="imssChart" :options="pieOptions" :key="`imss-${themeKey}`" />
|
|
<p v-else class="muted dash-empty">Sin activos</p>
|
|
</div>
|
|
</template>
|
|
</Card>
|
|
<Card>
|
|
<template #content>
|
|
<div class="dash-chart-label">Riesgo de gafete</div>
|
|
<div class="dash-chart">
|
|
<Chart v-if="hasValues(riskChart)" type="doughnut" :data="riskChart" :options="pieOptions" :key="`risk-${themeKey}`" />
|
|
<p v-else class="muted dash-empty">Sin activos</p>
|
|
</div>
|
|
</template>
|
|
</Card>
|
|
<Card class="dash-span-2">
|
|
<template #content>
|
|
<div class="dash-chart-label">Personal por obra</div>
|
|
<div class="dash-chart dash-chart--bar">
|
|
<Chart v-if="hasValues(projectChart)" type="bar" :data="projectChart" :options="barOptions" :key="`proy-${themeKey}`" />
|
|
<p v-else class="muted dash-empty">Nadie asignado a obra</p>
|
|
</div>
|
|
</template>
|
|
</Card>
|
|
<Card>
|
|
<template #content>
|
|
<div class="dash-chart-label">Tipo de trabajo</div>
|
|
<div class="dash-chart">
|
|
<Chart v-if="hasValues(workChart)" type="doughnut" :data="workChart" :options="pieOptions" :key="`work-${themeKey}`" />
|
|
<p v-else class="muted dash-empty">Sin activos</p>
|
|
</div>
|
|
</template>
|
|
</Card>
|
|
<Card class="dash-span-2">
|
|
<template #content>
|
|
<div class="dash-chart-label">{{ payrollTitle }}</div>
|
|
<div class="dash-chart dash-chart--bar">
|
|
<Chart v-if="hasValues(payrollChart)" type="bar" :data="payrollChart" :options="moneyBarOptions" :key="`pay-${themeKey}`" />
|
|
<p v-else class="muted dash-empty">No hay semana de nómina abierta</p>
|
|
</div>
|
|
</template>
|
|
</Card>
|
|
<Card>
|
|
<template #content>
|
|
<div class="dash-chart-label">Atajos</div>
|
|
<div class="dash-shortcuts">
|
|
<Button label="Proyectos" icon="pi pi-building" outlined @click="navigateTo('/proyectos')" />
|
|
<Button label="Padrón" icon="pi pi-users" outlined @click="navigateTo('/padron')" />
|
|
<Button label="Por proyecto" icon="pi pi-th-large" outlined @click="navigateTo('/kanban')" />
|
|
<Button label="Nómina" icon="pi pi-wallet" outlined @click="navigateTo('/nomina')" />
|
|
<Button label="Gafetes" icon="pi pi-id-card" outlined @click="navigateTo('/gafetes')" />
|
|
</div>
|
|
</template>
|
|
</Card>
|
|
</div>
|
|
|
|
<div class="home-split">
|
|
<div class="list-page">
|
|
<DeskTableBar v-model="tableQ" placeholder="Buscar persona o cargo">
|
|
<Chip :label="`${listedReady.length} listos para gafete`" icon="pi pi-id-card" />
|
|
</DeskTableBar>
|
|
<DataTable :value="listedReady" paginator :rows="6" :rows-per-page-options="[6, 12, 25]" empty-message="Nadie en listo gafete">
|
|
<Column header="Persona">
|
|
<template #body="{ data }">
|
|
<div class="person-cell">
|
|
<Avatar :label="initials(data.first_name, data.last_name_p)" shape="circle" size="small" />
|
|
<span>{{ workerFullName(data) }}</span>
|
|
</div>
|
|
</template>
|
|
</Column>
|
|
<Column field="position" header="Cargo" />
|
|
<Column header="Riesgo">
|
|
<template #body="{ data }">
|
|
<Tag :value="riskLevelLabel(data.risk_code)" :severity="riskLevelSeverity(data.risk_code)" />
|
|
</template>
|
|
</Column>
|
|
</DataTable>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { ChartData, ChartOptions } from "chart.js";
|
|
import type { DeskStatusMessage } from "~/composables/useDesktop";
|
|
import { workerFullName } from "~/types/padron";
|
|
|
|
const { initials, riskLevelLabel, riskLevelSeverity, riskLevel, pipelineLabel } = useUi();
|
|
const { isDark } = useTheme();
|
|
const desk = useDesktop();
|
|
const { api } = useApi();
|
|
const { tableQ, matches } = useTableSearch();
|
|
|
|
type Worker = {
|
|
id?: number;
|
|
first_name: string;
|
|
middle_name?: string | null;
|
|
last_name_p: string;
|
|
last_name_m?: string | null;
|
|
status: string;
|
|
pipeline_status: string;
|
|
position: string;
|
|
risk_code: string;
|
|
risk_color: string;
|
|
work_type?: string;
|
|
imss_status?: string;
|
|
imss_ready?: boolean;
|
|
in_project_without_imss?: boolean;
|
|
freshness_required?: boolean;
|
|
loan_balance?: number;
|
|
project_ids?: string | null;
|
|
missing_docs?: number;
|
|
};
|
|
|
|
type ProjectRow = {
|
|
id?: number;
|
|
name?: string;
|
|
code?: string;
|
|
status: string;
|
|
active_count?: number;
|
|
missing_docs?: number;
|
|
};
|
|
|
|
type JobRow = { delivered?: number | boolean };
|
|
type WeekRow = {
|
|
status?: string;
|
|
week_start?: string;
|
|
week_end?: string;
|
|
required_net?: number;
|
|
payable_net?: number;
|
|
loan_recovery?: number;
|
|
};
|
|
|
|
const workers = ref<Worker[]>([]);
|
|
const projects = ref<ProjectRow[]>([]);
|
|
const openWeek = ref<WeekRow | null>(null);
|
|
const stats = ref([
|
|
{ label: "Activos", value: "—", icon: "pi pi-users", bg: "#0f2744", hint: "Alta", severity: "success" as const, to: "/padron" },
|
|
{ label: "Obras", value: "—", icon: "pi pi-building", bg: "#1c4a7a", hint: "Proyectos", severity: "info" as const, to: "/proyectos" },
|
|
{ label: "A pagar (semana)", value: "—", icon: "pi pi-wallet", bg: "#194ca8", hint: "Nómina", severity: "info" as const, to: "/nomina" },
|
|
{ label: "Saldo préstamos", value: "—", icon: "pi pi-replay", bg: "#008514", hint: "Por cobrar", severity: "success" as const, to: "/nomina" },
|
|
{ label: "Listos gafete", value: "—", icon: "pi pi-id-card", bg: "#c9a227", hint: "Imprimir", severity: "warn" as const, to: "/gafetes" },
|
|
{ label: "En obra sin IMSS", value: "—", icon: "pi pi-exclamation-triangle", bg: "#a20000", hint: "Prioridad", severity: "danger" as const, to: "/padron" },
|
|
]);
|
|
|
|
const listedReady = computed(() =>
|
|
(workers.value ?? [])
|
|
.filter((w) => w.pipeline_status === "listo_gafete")
|
|
.filter((w) => matches(w.first_name, w.last_name_p, w.last_name_m, w.middle_name, w.position, w.risk_code)),
|
|
);
|
|
|
|
const activos = computed(() =>
|
|
(workers.value ?? []).filter((w) => w.status === "activo" && w.pipeline_status !== "baja"),
|
|
);
|
|
|
|
const themeKey = computed(() => (isDark.value ? "dark" : "light"));
|
|
const ink = computed(() => (isDark.value ? "#e2e8f0" : "#0f2744"));
|
|
const grid = computed(() => (isDark.value ? "rgba(148,163,184,0.16)" : "rgba(15,39,68,0.08)"));
|
|
|
|
function money(n: number) {
|
|
return (Number(n) || 0).toLocaleString("es-MX", { style: "currency", currency: "MXN", maximumFractionDigits: 0 });
|
|
}
|
|
|
|
function hasProject(worker: Worker) {
|
|
return String(worker.project_ids || "")
|
|
.split(",")
|
|
.map((part) => Number(part.trim()))
|
|
.some((id) => id > 0);
|
|
}
|
|
|
|
function hasValues(chart: { datasets?: { data?: unknown[] }[] }) {
|
|
return (chart.datasets ?? []).some((set) => (set.data ?? []).some((n) => Number(n) > 0));
|
|
}
|
|
|
|
function doughnut(labels: string[], data: number[], colors: string[]): ChartData<"doughnut"> {
|
|
const packed = labels
|
|
.map((label, i) => ({ label, value: Number(data[i] || 0), color: colors[i] }))
|
|
.filter((row) => row.value > 0);
|
|
return {
|
|
labels: packed.map((row) => row.label),
|
|
datasets: [{
|
|
data: packed.map((row) => row.value),
|
|
backgroundColor: packed.map((row) => row.color),
|
|
borderWidth: 0,
|
|
hoverOffset: 4,
|
|
}],
|
|
};
|
|
}
|
|
|
|
const pipelineChart = computed(() => {
|
|
const order = ["incompleto", "listo_gafete", "impreso", "activo", "baja"];
|
|
const colors = ["#6b7785", "#1c4a7a", "#c9a227", "#008514", "#a20000"];
|
|
const counts = order.map((key) => (workers.value ?? []).filter((w) => w.pipeline_status === key).length);
|
|
return doughnut(order.map((key) => pipelineLabel(key)), counts, colors);
|
|
});
|
|
|
|
const imssChart = computed(() => {
|
|
const list = activos.value;
|
|
const alta = list.filter((w) => w.imss_status === "alta").length;
|
|
const obra = list.filter((w) => w.in_project_without_imss).length;
|
|
const resto = Math.max(0, list.length - alta - obra);
|
|
return doughnut(
|
|
["Alta IMSS", "En obra sin alta", "Sin alta"],
|
|
[alta, obra, resto],
|
|
["#008514", "#a20000", "#c9a227"],
|
|
);
|
|
});
|
|
|
|
const riskChart = computed(() => {
|
|
const list = activos.value;
|
|
return doughnut(
|
|
["Alto", "Medio", "Bajo"],
|
|
[
|
|
list.filter((w) => riskLevel(w.risk_code) === "alto").length,
|
|
list.filter((w) => riskLevel(w.risk_code) === "medio").length,
|
|
list.filter((w) => riskLevel(w.risk_code) === "bajo").length,
|
|
],
|
|
["#a20000", "#c9a227", "#008514"],
|
|
);
|
|
});
|
|
|
|
const workChart = computed(() => {
|
|
const list = activos.value;
|
|
return doughnut(
|
|
["Jornal", "Destajo"],
|
|
[list.filter((w) => w.work_type !== "D").length, list.filter((w) => w.work_type === "D").length],
|
|
["#1c4a7a", "#2f6fe4"],
|
|
);
|
|
});
|
|
|
|
const projectChart = computed((): ChartData<"bar"> => {
|
|
const rows = [...projects.value]
|
|
.filter((p) => p.status === "activo" || Number(p.active_count) > 0)
|
|
.sort((a, b) => Number(b.active_count || 0) - Number(a.active_count || 0))
|
|
.slice(0, 8);
|
|
const unassigned = activos.value.filter((w) => !hasProject(w)).length;
|
|
const labels = [...rows.map((p) => String(p.name || p.code || "Obra")), ...(unassigned ? ["Sin asignar"] : [])];
|
|
const data = [...rows.map((p) => Number(p.active_count || 0)), ...(unassigned ? [unassigned] : [])];
|
|
const colors = [...rows.map(() => "#2f6fe4"), ...(unassigned ? ["#c9a227"] : [])];
|
|
return {
|
|
labels,
|
|
datasets: [{ label: "Personas", data, backgroundColor: colors, borderRadius: 6, maxBarThickness: 28 }],
|
|
};
|
|
});
|
|
|
|
const payrollTitle = computed(() => {
|
|
const week = openWeek.value;
|
|
if (!week?.week_start) return "Nómina de la semana";
|
|
return `Nómina ${week.week_start} → ${week.week_end || ""}`.trim();
|
|
});
|
|
|
|
const payrollChart = computed((): ChartData<"bar"> => {
|
|
const week = openWeek.value;
|
|
return {
|
|
labels: ["Requerida", "A pagar", "Préstamos"],
|
|
datasets: [{
|
|
label: "MXN",
|
|
data: [
|
|
Number(week?.required_net || 0),
|
|
Number(week?.payable_net || 0),
|
|
Number(week?.loan_recovery || 0),
|
|
],
|
|
backgroundColor: ["#0f2744", "#2f6fe4", "#008514"],
|
|
borderRadius: 6,
|
|
maxBarThickness: 48,
|
|
}],
|
|
};
|
|
});
|
|
|
|
const pieOptions = computed((): ChartOptions<"doughnut"> => ({
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
cutout: "62%",
|
|
plugins: {
|
|
legend: {
|
|
position: "bottom",
|
|
labels: { color: ink.value, boxWidth: 10, padding: 12, font: { size: 11 } },
|
|
},
|
|
},
|
|
}));
|
|
|
|
const barOptions = computed((): ChartOptions<"bar"> => ({
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
indexAxis: "y",
|
|
plugins: { legend: { display: false } },
|
|
scales: {
|
|
x: {
|
|
beginAtZero: true,
|
|
ticks: { color: ink.value, precision: 0 },
|
|
grid: { color: grid.value },
|
|
},
|
|
y: {
|
|
ticks: { color: ink.value },
|
|
grid: { display: false },
|
|
},
|
|
},
|
|
}));
|
|
|
|
const moneyBarOptions = computed((): ChartOptions<"bar"> => ({
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: { display: false },
|
|
tooltip: {
|
|
callbacks: {
|
|
label: (ctx) => money(Number(ctx.parsed.y ?? 0)),
|
|
},
|
|
},
|
|
},
|
|
scales: {
|
|
x: {
|
|
ticks: { color: ink.value },
|
|
grid: { display: false },
|
|
},
|
|
y: {
|
|
beginAtZero: true,
|
|
ticks: {
|
|
color: ink.value,
|
|
callback: (value) => money(Number(value)),
|
|
},
|
|
grid: { color: grid.value },
|
|
},
|
|
},
|
|
}));
|
|
|
|
function buildHomeAlerts(input: {
|
|
workers: Worker[];
|
|
projects: ProjectRow[];
|
|
jobs: JobRow[];
|
|
weeks: WeekRow[];
|
|
imssReady: number;
|
|
}): DeskStatusMessage[] {
|
|
const active = input.workers.filter((w) => w.status === "activo" && w.pipeline_status !== "baja");
|
|
const sinImssObra = active.filter((w) => w.in_project_without_imss).length;
|
|
const incompletos = active.filter((w) => w.pipeline_status === "incompleto" || Number(w.missing_docs) > 0).length;
|
|
const freshness = active.filter((w) => w.freshness_required).length;
|
|
const sinAsignar = active.filter((w) => !hasProject(w)).length;
|
|
const listos = active.filter((w) => w.pipeline_status === "listo_gafete").length;
|
|
const sinImss = active.filter((w) => w.imss_status !== "alta").length;
|
|
const prestamos = active.filter((w) => Number(w.loan_balance) > 0).length;
|
|
const riesgoAlto = active.filter((w) => riskLevel(w.risk_code) === "alto").length;
|
|
const proyDocs = input.projects.filter((p) => Number(p.missing_docs) > 0).length;
|
|
const proyPausados = input.projects.filter((p) => p.status === "pausado").length;
|
|
const jobsPendientes = input.jobs.filter((j) => !j.delivered).length;
|
|
const periodosAbiertos = input.weeks.filter((p) => p.status !== "paid").length;
|
|
|
|
const messages: DeskStatusMessage[] = [];
|
|
|
|
if (sinImssObra > 0) {
|
|
messages.push({
|
|
key: "home_imss_obra",
|
|
severity: "danger",
|
|
title: `${sinImssObra} en obra sin alta IMSS`,
|
|
detail: "Personal en proyecto sin movimiento de alta. Prioridad operativa.",
|
|
route: "/padron",
|
|
filter: "in_project_without_imss",
|
|
});
|
|
}
|
|
if (incompletos > 0) {
|
|
messages.push({
|
|
key: "home_expediente",
|
|
severity: "warn",
|
|
title: `${incompletos} con expediente incompleto`,
|
|
detail: "Documentos obligatorios faltantes o inválidos en el padrón.",
|
|
route: "/padron",
|
|
filter: "incomplete",
|
|
});
|
|
}
|
|
if (freshness > 0) {
|
|
messages.push({
|
|
key: "home_frescos",
|
|
severity: "warn",
|
|
title: `${freshness} con documentos por refrescar`,
|
|
detail: "Reingreso o alta pendiente: constancias subidas hace más de 3 meses.",
|
|
route: "/padron",
|
|
filter: "freshness",
|
|
});
|
|
}
|
|
if (proyDocs > 0) {
|
|
messages.push({
|
|
key: "home_proy_docs",
|
|
severity: "warn",
|
|
title: `${proyDocs} obras con expediente pendiente`,
|
|
detail: "Proyectos activos o pausados sin documentos de obra completos.",
|
|
route: "/proyectos",
|
|
treeKey: "activo",
|
|
});
|
|
}
|
|
if (riesgoAlto > 0) {
|
|
messages.push({
|
|
key: "home_riesgo",
|
|
severity: "warn",
|
|
title: `${riesgoAlto} con riesgo alto`,
|
|
detail: "Personal con barra de gafete de riesgo alto.",
|
|
route: "/kanban",
|
|
});
|
|
}
|
|
if (listos > 0) {
|
|
messages.push({
|
|
key: "home_gafete",
|
|
severity: "info",
|
|
title: `${listos} listos para gafete`,
|
|
detail: "Pueden pasar a impresión en el módulo de gafetes.",
|
|
route: "/gafetes",
|
|
});
|
|
}
|
|
if (input.imssReady > 0) {
|
|
messages.push({
|
|
key: "home_imss_ready",
|
|
severity: "info",
|
|
title: `${input.imssReady} listos para alta IMSS`,
|
|
detail: "Expediente completo; falta registrar el alta con el patrón.",
|
|
route: "/padron",
|
|
filter: "imss_ready",
|
|
});
|
|
}
|
|
if (sinAsignar > 0) {
|
|
messages.push({
|
|
key: "home_sin_asignar",
|
|
severity: "info",
|
|
title: `${sinAsignar} sin asignar a obra`,
|
|
detail: "Activos en padrón sin proyecto. Revisa kanban o padrón.",
|
|
route: "/padron",
|
|
filter: "unassigned",
|
|
treeKey: "unassigned",
|
|
});
|
|
}
|
|
if (sinImss > 0 && sinImssObra === 0) {
|
|
messages.push({
|
|
key: "home_sin_imss",
|
|
severity: "info",
|
|
title: `${sinImss} sin alta IMSS`,
|
|
detail: "Activos que aún no tienen patrón / alta registrada.",
|
|
route: "/padron",
|
|
filter: "without_imss",
|
|
});
|
|
}
|
|
if (jobsPendientes > 0) {
|
|
messages.push({
|
|
key: "home_jobs",
|
|
severity: "info",
|
|
title: `${jobsPendientes} gafetes PDF por entregar`,
|
|
detail: "Trabajos generados pendientes de marcar entrega en obra.",
|
|
route: "/gafetes",
|
|
});
|
|
}
|
|
if (proyPausados > 0) {
|
|
messages.push({
|
|
key: "home_pausados",
|
|
severity: "info",
|
|
title: `${proyPausados} proyectos pausados`,
|
|
detail: "Obras en pausa; confirma si reactivan o concluyen.",
|
|
route: "/proyectos",
|
|
treeKey: "pausado",
|
|
});
|
|
}
|
|
if (prestamos > 0) {
|
|
messages.push({
|
|
key: "home_prestamos",
|
|
severity: "info",
|
|
title: `${prestamos} con préstamo activo`,
|
|
detail: "Hay saldos por descontar; revisa ficha o nómina.",
|
|
route: "/kanban",
|
|
});
|
|
}
|
|
if (periodosAbiertos > 0) {
|
|
messages.push({
|
|
key: "home_nomina",
|
|
severity: "info",
|
|
title: `${periodosAbiertos} semanas de nómina abiertas`,
|
|
detail: "Hay una semana sin pagar. Ármala el jueves y págala el sábado.",
|
|
route: "/nomina",
|
|
});
|
|
}
|
|
|
|
return messages;
|
|
}
|
|
|
|
function mondayOf(d: Date) {
|
|
const iso = d.toLocaleDateString("en-CA", { timeZone: "America/Cancun" });
|
|
const [y, m, day] = iso.split("-").map(Number);
|
|
const utc = new Date(Date.UTC(y, m - 1, day));
|
|
const mon0 = (utc.getUTCDay() + 6) % 7;
|
|
utc.setUTCDate(utc.getUTCDate() - mon0);
|
|
return utc.toISOString().slice(0, 10);
|
|
}
|
|
|
|
onMounted(async () => {
|
|
desk.configure({
|
|
tab: "Inicio",
|
|
treeTitle: "Inicio",
|
|
showTree: false,
|
|
showDetail: false,
|
|
ribbon: [
|
|
{
|
|
key: "acciones",
|
|
label: "Acciones",
|
|
items: [
|
|
{ key: "alta", label: "Alta", icon: "pi pi-plus", command: () => navigateTo("/padron?nuevo=1") },
|
|
{ key: "print", label: "Gafetes", icon: "pi pi-print", command: () => navigateTo("/gafetes") },
|
|
],
|
|
},
|
|
],
|
|
});
|
|
try {
|
|
const [w, p, j, pe] = await Promise.all([
|
|
api<{ workers?: Worker[]; imss_stats?: { imss_ready_count?: number } }>("/v1/workers"),
|
|
api<{ projects?: ProjectRow[] }>("/v1/projects?status=activo,pausado"),
|
|
api<{ jobs?: JobRow[] }>("/v1/badge-jobs"),
|
|
api<{ weeks?: WeekRow[] }>("/v1/payroll/weeks/open"),
|
|
]);
|
|
const workerRows = w.workers ?? [];
|
|
const projectRows = p.projects ?? [];
|
|
const jobRows = j.jobs ?? [];
|
|
const weekRows = pe.weeks ?? [];
|
|
workers.value = workerRows;
|
|
projects.value = projectRows;
|
|
const currentStart = mondayOf(new Date());
|
|
openWeek.value = weekRows.find((week) => week.week_start === currentStart) || weekRows[0] || null;
|
|
const activeList = workerRows.filter((x) => x.status === "activo" && x.pipeline_status !== "baja");
|
|
const activosN = activeList.length;
|
|
const listos = workerRows.filter((x) => x.pipeline_status === "listo_gafete").length;
|
|
const activosProyectos = projectRows.filter((x) => x.status === "activo").length;
|
|
const loanSaldo = activeList.reduce((sum, row) => sum + Number(row.loan_balance || 0), 0);
|
|
const sinImssObra = activeList.filter((x) => x.in_project_without_imss).length;
|
|
stats.value = [
|
|
{ label: "Activos", value: String(activosN), icon: "pi pi-users", bg: "#0f2744", hint: "Alta", severity: "success", to: "/padron" },
|
|
{ label: "Obras", value: String(activosProyectos), icon: "pi pi-building", bg: "#1c4a7a", hint: "Proyectos", severity: "info", to: "/proyectos" },
|
|
{ label: "A pagar (semana)", value: openWeek.value ? money(Number(openWeek.value.payable_net || 0)) : "—", icon: "pi pi-wallet", bg: "#194ca8", hint: "Nómina", severity: "info", to: "/nomina" },
|
|
{ label: "Saldo préstamos", value: money(loanSaldo), icon: "pi pi-replay", bg: "#008514", hint: "Por cobrar", severity: "success", to: "/nomina" },
|
|
{ label: "Listos gafete", value: String(listos), icon: "pi pi-id-card", bg: "#c9a227", hint: "Imprimir", severity: "warn", to: "/gafetes" },
|
|
{ label: "En obra sin IMSS", value: String(sinImssObra), icon: "pi pi-exclamation-triangle", bg: "#a20000", hint: "Prioridad", severity: "danger", to: "/padron" },
|
|
];
|
|
desk.setStatusMessages(
|
|
buildHomeAlerts({
|
|
workers: workerRows,
|
|
projects: projectRows,
|
|
jobs: jobRows,
|
|
weeks: weekRows,
|
|
imssReady: w.imss_stats?.imss_ready_count || 0,
|
|
}),
|
|
);
|
|
} catch (e) {
|
|
desk.setStatusMessages([{
|
|
key: "home_load_error",
|
|
severity: "danger",
|
|
title: "No se pudo cargar el tablero",
|
|
detail: e instanceof Error ? e.message : "Revise la conexión con el servidor.",
|
|
route: "/",
|
|
}]);
|
|
}
|
|
});
|
|
|
|
onBeforeUnmount(() => {
|
|
desk.clearStatusMessages();
|
|
});
|
|
</script>
|