mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 19:03:17 +00:00
203 lines
7.6 KiB
Vue
203 lines
7.6 KiB
Vue
<template>
|
||
<div class="list-page">
|
||
<DeskTableBar v-model="tableQ" placeholder="Buscar periodo o proyecto">
|
||
<Chip :label="`${listedPeriods.length} periodos`" icon="pi pi-calendar" />
|
||
<Chip :label="`${openPeriods} abiertos`" icon="pi pi-unlock" />
|
||
<Chip :label="`${closedPeriods} cerrados`" icon="pi pi-lock" />
|
||
</DeskTableBar>
|
||
|
||
<Toolbar>
|
||
<template #start>
|
||
<DatePicker v-model="weekStart" date-format="yy-mm-dd" placeholder="Inicio semana" show-icon />
|
||
<DatePicker v-model="weekEnd" date-format="yy-mm-dd" placeholder="Fin semana" show-icon />
|
||
</template>
|
||
</Toolbar>
|
||
|
||
<Toolbar>
|
||
<template #start>
|
||
<Select v-model="attWorker" :options="workers" option-label="label" option-value="id" placeholder="Persona" />
|
||
<DatePicker v-model="attDate" date-format="yy-mm-dd" placeholder="Día" show-icon />
|
||
<ToggleSwitch v-model="attPresent" />
|
||
<Tag :value="attPresent ? 'Presente' : 'Falta'" :severity="attPresent ? 'success' : 'danger'" />
|
||
<Button label="Registrar" icon="pi pi-check" @click="saveAtt" />
|
||
</template>
|
||
</Toolbar>
|
||
<Message severity="secondary" :closable="false">
|
||
El neto es días presentes × jornal, menos descuentos y abono de préstamo. Solo personal activo del proyecto.
|
||
</Message>
|
||
|
||
<DataTable :value="listedPeriods" paginator :rows="12" :rows-per-page-options="[12, 25, 50]" scrollable scroll-height="flex" empty-message="Sin periodos" @row-click="(e: { data: { id: number } }) => open(e.data.id)">
|
||
<Column field="id" header="#" />
|
||
<Column field="project_name" header="Proyecto" />
|
||
<Column field="week_start" header="Desde" />
|
||
<Column field="week_end" header="Hasta" />
|
||
<Column header="Estado">
|
||
<template #body="{ data }">
|
||
<Tag :value="String(data.status)" :severity="data.status === 'closed' ? 'success' : 'warn'" />
|
||
</template>
|
||
</Column>
|
||
</DataTable>
|
||
|
||
<Dialog v-model:visible="show" header="Detalle de nómina" modal :style="{ width: '920px' }">
|
||
<DeskTableBar v-model="linesQ" placeholder="Buscar persona">
|
||
<Chip :label="`${listedLines.length} líneas`" icon="pi pi-users" />
|
||
</DeskTableBar>
|
||
<DataTable :value="listedLines" class="mt-3">
|
||
<Column field="last_name_p" header="Apellido" />
|
||
<Column field="first_name" header="Nombre" />
|
||
<Column field="days" header="Días" />
|
||
<Column field="daily_wage" header="Jornal" />
|
||
<Column field="gross" header="Bruto" />
|
||
<Column field="discounts" header="Desc." />
|
||
<Column field="loan_payment" header="Préstamo" />
|
||
<Column field="net" header="Neto" />
|
||
</DataTable>
|
||
<template #footer>
|
||
<Button label="CSV" icon="pi pi-download" outlined @click="csv" />
|
||
<Button label="Cerrar periodo" icon="pi pi-lock" @click="closeP" />
|
||
</template>
|
||
</Dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
const { api, download } = useApi();
|
||
const desk = useDesktop();
|
||
const toast = useToast();
|
||
const projects = ref<{ id: number; name: string }[]>([]);
|
||
const projectId = ref<number | null>(null);
|
||
const weekStart = ref<Date | null>(null);
|
||
const weekEnd = ref<Date | null>(null);
|
||
const periods = ref<Record<string, unknown>[]>([]);
|
||
const workers = ref<{ id: number; label: string }[]>([]);
|
||
const attWorker = ref<number | null>(null);
|
||
const attDate = ref<Date | null>(null);
|
||
const attPresent = ref(true);
|
||
const show = ref(false);
|
||
const lines = ref<Record<string, unknown>[]>([]);
|
||
const periodId = ref(0);
|
||
const { tableQ, matches } = useTableSearch();
|
||
const linesQ = ref("");
|
||
const listedPeriods = computed(() =>
|
||
periods.value.filter((period) => matches(period.id, period.project_name, period.week_start, period.week_end, period.status)),
|
||
);
|
||
const openPeriods = computed(() => listedPeriods.value.filter((period) => period.status !== "closed").length);
|
||
const closedPeriods = computed(() => listedPeriods.value.filter((period) => period.status === "closed").length);
|
||
const listedLines = computed(() => {
|
||
const q = linesQ.value.trim().toLowerCase();
|
||
if (!q) return lines.value;
|
||
return lines.value.filter((row) =>
|
||
[row.last_name_p, row.first_name].some((part) => String(part ?? "").toLowerCase().includes(q)),
|
||
);
|
||
});
|
||
|
||
function iso(d: Date | null) {
|
||
if (!d) return "";
|
||
return d.toISOString().slice(0, 10);
|
||
}
|
||
|
||
async function load() {
|
||
const p = await api<{ projects: { id: number; name: string }[] }>("/v1/projects?status=activo,pausado");
|
||
projects.value = p.projects;
|
||
if (!projectId.value && p.projects[0]) projectId.value = p.projects[0].id;
|
||
const pe = await api<{ periods: Record<string, unknown>[] }>(
|
||
`/v1/payroll/periods${projectId.value ? `?project_id=${projectId.value}` : ""}`,
|
||
);
|
||
periods.value = pe.periods;
|
||
const w = await api<{ workers: { id: number; first_name: string; last_name_p: string }[] }>(
|
||
`/v1/workers?status=activo${projectId.value ? `&project_id=${projectId.value}` : ""}`,
|
||
);
|
||
workers.value = w.workers.map((x) => ({ id: x.id, label: `${x.last_name_p} ${x.first_name}` }));
|
||
syncChrome();
|
||
}
|
||
|
||
function syncChrome() {
|
||
const messages = [];
|
||
if (openPeriods.value > 0) {
|
||
messages.push({
|
||
key: "nom_abiertos",
|
||
severity: "info" as const,
|
||
title: `${openPeriods.value} periodos de nómina abiertos`,
|
||
detail: "Genera CSV o cierra el periodo cuando esté listo.",
|
||
route: "/nomina",
|
||
});
|
||
}
|
||
desk.setStatusMessages(messages);
|
||
desk.configure({
|
||
tab: "Nómina",
|
||
treeTitle: "Proyectos",
|
||
showTree: true,
|
||
showDetail: false,
|
||
treeKey: projectId.value ? `p:${projectId.value}` : "",
|
||
countLabel: `${periods.value.length} periodos`,
|
||
onTreeSelect: (key) => {
|
||
if (!key.startsWith("p:")) return;
|
||
projectId.value = Number(key.slice(2));
|
||
load();
|
||
},
|
||
tree: projects.value.map((p) => ({
|
||
key: `p:${p.id}`,
|
||
label: p.name,
|
||
icon: "pi pi-folder",
|
||
})),
|
||
ribbon: [
|
||
{
|
||
key: "acciones",
|
||
label: "Nómina",
|
||
items: [{ key: "gen", label: "Periodo", icon: "pi pi-calculator", command: createPeriod }],
|
||
},
|
||
{
|
||
key: "vista",
|
||
label: "Vista",
|
||
items: [{ key: "refresh", label: "Actualizar", icon: "pi pi-refresh", command: load }],
|
||
},
|
||
],
|
||
});
|
||
}
|
||
|
||
async function saveAtt() {
|
||
if (!projectId.value || !attWorker.value || !attDate.value) return;
|
||
await api("/v1/attendance", {
|
||
method: "PUT",
|
||
json: {
|
||
project_id: projectId.value,
|
||
worker_id: attWorker.value,
|
||
work_date: iso(attDate.value),
|
||
present: attPresent.value,
|
||
},
|
||
});
|
||
toast.add({ severity: "success", summary: "Asistencia", life: 1500 });
|
||
}
|
||
|
||
async function createPeriod() {
|
||
if (!projectId.value || !weekStart.value || !weekEnd.value) return;
|
||
await api("/v1/payroll/periods", {
|
||
method: "POST",
|
||
json: { project_id: projectId.value, week_start: iso(weekStart.value), week_end: iso(weekEnd.value) },
|
||
});
|
||
toast.add({ severity: "success", summary: "Periodo generado", life: 2000 });
|
||
await load();
|
||
}
|
||
|
||
async function open(id: number) {
|
||
periodId.value = id;
|
||
const r = await api<{ lines: Record<string, unknown>[] }>(`/v1/payroll/periods/${id}`);
|
||
lines.value = r.lines;
|
||
show.value = true;
|
||
}
|
||
|
||
function csv() {
|
||
download(`/v1/payroll/periods/${periodId.value}/csv`, `nomina-${periodId.value}.csv`).then(() => {
|
||
toast.add({ severity: "success", summary: "CSV descargado", life: 2500 });
|
||
});
|
||
}
|
||
|
||
async function closeP() {
|
||
await api(`/v1/payroll/periods/${periodId.value}`, { method: "PATCH", json: { status: "closed" } });
|
||
show.value = false;
|
||
await load();
|
||
}
|
||
|
||
onMounted(load);
|
||
onBeforeUnmount(() => desk.clearStatusMessages());
|
||
</script>
|