mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 15:33:17 +00:00
Pre-computed merge for change #19
This commit is contained in:
commit
e188fe34e0
3 changed files with 170 additions and 6 deletions
|
|
@ -217,14 +217,109 @@ export async function patchDestajoCut(db: Db, weekId: number, cutId: number, pat
|
||||||
if (!env.ok) throwFromRpc(env);
|
if (!env.ok) throwFromRpc(env);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AttendanceRow = {
|
||||||
|
worker_id: number;
|
||||||
|
project_id: number;
|
||||||
|
work_date: string;
|
||||||
|
present: boolean | number;
|
||||||
|
project_name?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
function isoDay(value: unknown): string {
|
||||||
|
return String(value ?? "").slice(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function weekBoard(week: Record<string, unknown> | null | undefined) {
|
||||||
|
const required = Number(week?.required_net || 0);
|
||||||
|
const payable = Number(week?.payable_net || 0);
|
||||||
|
const loanRecovery = Math.max(0, roundMoney(required - payable));
|
||||||
|
const faltante = Math.max(0, roundMoney(payable - required));
|
||||||
|
return {
|
||||||
|
required_net: required,
|
||||||
|
payable_net: payable,
|
||||||
|
loan_recovery: loanRecovery,
|
||||||
|
faltante,
|
||||||
|
delta: roundMoney(payable - required),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function attachJornalCells(
|
||||||
|
db: Db,
|
||||||
|
sheets: Array<Record<string, unknown>>,
|
||||||
|
days: string[],
|
||||||
|
tenantId: number,
|
||||||
|
): Promise<Array<Record<string, unknown>>> {
|
||||||
|
const from = days[0];
|
||||||
|
const to = days[days.length - 1];
|
||||||
|
if (!from || !to) return sheets;
|
||||||
|
const rows = await db.prepare(
|
||||||
|
`SELECT a.worker_id, a.project_id, a.work_date, a.present, p.name AS project_name
|
||||||
|
FROM attendance a
|
||||||
|
JOIN projects p ON p.id = a.project_id
|
||||||
|
WHERE a.work_date BETWEEN ? AND ?
|
||||||
|
AND p.tenant_id = ?`,
|
||||||
|
).all(from, to, tenantId) as AttendanceRow[];
|
||||||
|
|
||||||
|
return sheets.map((sheet) => {
|
||||||
|
if (sheet.kind !== "obra") return sheet;
|
||||||
|
const projectId = Number(sheet.project_id);
|
||||||
|
const lines = ((sheet.lines as Array<Record<string, unknown>>) ?? []).map((line) => {
|
||||||
|
const workerId = Number(line.worker_id);
|
||||||
|
const cells = days.map((date) => {
|
||||||
|
const here = rows.find((r) =>
|
||||||
|
Number(r.worker_id) === workerId &&
|
||||||
|
isoDay(r.work_date) === date &&
|
||||||
|
Number(r.project_id) === projectId
|
||||||
|
);
|
||||||
|
const other = rows.find((r) =>
|
||||||
|
Number(r.worker_id) === workerId &&
|
||||||
|
isoDay(r.work_date) === date &&
|
||||||
|
Number(r.project_id) !== projectId &&
|
||||||
|
Boolean(r.present)
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
date,
|
||||||
|
present: Boolean(here?.present),
|
||||||
|
other_project_id: other ? Number(other.project_id) : null,
|
||||||
|
other_project_name: other?.project_name ?? null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return { ...line, cells };
|
||||||
|
});
|
||||||
|
return { ...sheet, lines };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function getWeekBundle(db: Db, tenantId: number, weekStart: string) {
|
export async function getWeekBundle(db: Db, tenantId: number, weekStart: string) {
|
||||||
await ensureWeek(db, tenantId, weekStart);
|
await ensureWeek(db, tenantId, weekStart);
|
||||||
const env = await callCoreFn<Record<string, unknown>>(db, "core.fn_payroll_week_bundle", { tenant_id: tenantId, week_start: weekStart });
|
const env = await callCoreFn<Record<string, unknown>>(db, "core.fn_payroll_week_bundle", { tenant_id: tenantId, week_start: weekStart });
|
||||||
if (!env.ok) throwFromRpc(env);
|
if (!env.ok) throwFromRpc(env);
|
||||||
const data = env.data ?? {};
|
const data = env.data ?? {};
|
||||||
const today = await resolveToday(db, tenantId);
|
const today = await resolveToday(db, tenantId);
|
||||||
const start = String((data.week as Record<string, unknown>)?.week_start ?? weekStart);
|
const week = (data.week ?? null) as Record<string, unknown> | null;
|
||||||
return { ...data, can_assemble: canAssembleWeek(start, today), assemble_from: weekAssembleDate(start) };
|
const start = isoDay(week?.week_start) || weekStart;
|
||||||
|
const days = weekDays(start);
|
||||||
|
const sheets = await attachJornalCells(
|
||||||
|
db,
|
||||||
|
(data.sheets as Array<Record<string, unknown>>) ?? [],
|
||||||
|
days,
|
||||||
|
tenantId,
|
||||||
|
);
|
||||||
|
const bounds = destajoPeriodBounds(start);
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
week,
|
||||||
|
sheets,
|
||||||
|
days,
|
||||||
|
destajo_period: data.destajo_period ?? {
|
||||||
|
period_start: bounds.periodStart,
|
||||||
|
period_end: bounds.periodEnd,
|
||||||
|
},
|
||||||
|
board: data.board ?? weekBoard(week),
|
||||||
|
units: data.units ?? [...DEFAULT_UNITS],
|
||||||
|
can_assemble: canAssembleWeek(start, today),
|
||||||
|
assemble_from: weekAssembleDate(start),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function weekCsv(db: Db, weekId: number): Promise<string> {
|
export async function weekCsv(db: Db, weekId: number): Promise<string> {
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import {
|
||||||
thisSaturday,
|
thisSaturday,
|
||||||
weekAssembleDate,
|
weekAssembleDate,
|
||||||
weekContaining,
|
weekContaining,
|
||||||
|
weekDays,
|
||||||
} from "./payroll.ts";
|
} from "./payroll.ts";
|
||||||
|
|
||||||
// tenant_id fijo y solo-de-tests: cada test corre en su propia transacción
|
// tenant_id fijo y solo-de-tests: cada test corre en su propia transacción
|
||||||
|
|
@ -81,6 +82,14 @@ Deno.test("semana lun-sáb y corte destajo jue-jue", () => {
|
||||||
const w = weekContaining("2026-08-26");
|
const w = weekContaining("2026-08-26");
|
||||||
assertEquals(w.weekStart, "2026-08-24");
|
assertEquals(w.weekStart, "2026-08-24");
|
||||||
assertEquals(w.weekEnd, "2026-08-29");
|
assertEquals(w.weekEnd, "2026-08-29");
|
||||||
|
assertEquals(weekDays("2026-08-24"), [
|
||||||
|
"2026-08-24",
|
||||||
|
"2026-08-25",
|
||||||
|
"2026-08-26",
|
||||||
|
"2026-08-27",
|
||||||
|
"2026-08-28",
|
||||||
|
"2026-08-29",
|
||||||
|
]);
|
||||||
const d = destajoPeriodBounds("2026-08-24");
|
const d = destajoPeriodBounds("2026-08-24");
|
||||||
assertEquals(d.periodEnd, "2026-08-27");
|
assertEquals(d.periodEnd, "2026-08-27");
|
||||||
assertEquals(d.periodStart, "2026-08-20");
|
assertEquals(d.periodStart, "2026-08-20");
|
||||||
|
|
@ -265,6 +274,59 @@ Deno.test("un día no se marca en dos obras", async () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
type JornalCell = {
|
||||||
|
date: string;
|
||||||
|
present: boolean;
|
||||||
|
other_project_id: number | null;
|
||||||
|
other_project_name: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
Deno.test("paquete de semana incluye días L–S y celdas de asistencia", async () => {
|
||||||
|
await withTestDb(async (db) => {
|
||||||
|
const { companyId, projectId, workerId } = await seedFixture(db);
|
||||||
|
const project2Id = await addSecondProject(db, companyId);
|
||||||
|
await setAttendance(db, TENANT_ID, {
|
||||||
|
project_id: projectId,
|
||||||
|
worker_id: workerId,
|
||||||
|
work_date: "2026-08-24",
|
||||||
|
present: true,
|
||||||
|
});
|
||||||
|
await setAttendance(db, TENANT_ID, {
|
||||||
|
project_id: project2Id,
|
||||||
|
worker_id: workerId,
|
||||||
|
work_date: "2026-08-25",
|
||||||
|
present: true,
|
||||||
|
});
|
||||||
|
const bundle = await getWeekBundle(db, TENANT_ID, "2026-08-24");
|
||||||
|
assertEquals(bundle.days, [
|
||||||
|
"2026-08-24",
|
||||||
|
"2026-08-25",
|
||||||
|
"2026-08-26",
|
||||||
|
"2026-08-27",
|
||||||
|
"2026-08-28",
|
||||||
|
"2026-08-29",
|
||||||
|
]);
|
||||||
|
const obra = (bundle.sheets as Array<{
|
||||||
|
kind: string;
|
||||||
|
project_id: number;
|
||||||
|
lines: Array<{ worker_id: number; cells?: JornalCell[] }>;
|
||||||
|
}>).find((sheet) => sheet.kind === "obra" && Number(sheet.project_id) === projectId);
|
||||||
|
assertExists(obra);
|
||||||
|
const line = obra.lines.find((row) => Number(row.worker_id) === workerId);
|
||||||
|
assertExists(line);
|
||||||
|
assertEquals(line.cells?.length, 6);
|
||||||
|
assertEquals(line.cells?.[0], {
|
||||||
|
date: "2026-08-24",
|
||||||
|
present: true,
|
||||||
|
other_project_id: null,
|
||||||
|
other_project_name: null,
|
||||||
|
});
|
||||||
|
assertEquals(line.cells?.[1]?.present, false);
|
||||||
|
assertEquals(line.cells?.[1]?.other_project_id, project2Id);
|
||||||
|
assertEquals(line.cells?.[1]?.other_project_name, "Obra Sur");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
Deno.test("préstamo de personal: el día en otra obra no se prellena aquí", async () => {
|
Deno.test("préstamo de personal: el día en otra obra no se prellena aquí", async () => {
|
||||||
await withTestDb(async (db) => {
|
await withTestDb(async (db) => {
|
||||||
const { companyId, projectId, workerId } = await seedFixture(db);
|
const { companyId, projectId, workerId } = await seedFixture(db);
|
||||||
|
|
|
||||||
|
|
@ -46,9 +46,15 @@
|
||||||
<TabPanels>
|
<TabPanels>
|
||||||
<TabPanel value="obra">
|
<TabPanel value="obra">
|
||||||
<Message severity="secondary" :closable="false">
|
<Message severity="secondary" :closable="false">
|
||||||
Lun–sáb. Clic en la persona abre la ficha (consulta). El préstamo se registra con <strong>Préstamo</strong> en la barra o desde la ficha.
|
Marque L–S para asistencia y desmarque para falta. Clic en la persona abre la ficha (consulta). El préstamo se registra con <strong>Préstamo</strong> en la barra o desde la ficha.
|
||||||
</Message>
|
</Message>
|
||||||
<DataTable :value="obraLines" scrollable scroll-height="flex" empty-message="Sin personal de jornal en esta obra">
|
<DataTable
|
||||||
|
:value="obraLines"
|
||||||
|
scrollable
|
||||||
|
scroll-height="flex"
|
||||||
|
table-style="min-width: 52rem"
|
||||||
|
empty-message="Sin personal de jornal en esta obra"
|
||||||
|
>
|
||||||
<Column header="Persona" frozen>
|
<Column header="Persona" frozen>
|
||||||
<template #body="{ data }">
|
<template #body="{ data }">
|
||||||
<button type="button" class="person-link" @click="openPerson(data.worker_id)">
|
<button type="button" class="person-link" @click="openPerson(data.worker_id)">
|
||||||
|
|
@ -60,7 +66,7 @@
|
||||||
v-for="(day, idx) in days"
|
v-for="(day, idx) in days"
|
||||||
:key="day"
|
:key="day"
|
||||||
:header="dayHeaders[idx]"
|
:header="dayHeaders[idx]"
|
||||||
style="width: 5.5rem; text-align: center"
|
style="width: 4.2rem; min-width: 3.6rem; text-align: center"
|
||||||
>
|
>
|
||||||
<template #body="{ data }">
|
<template #body="{ data }">
|
||||||
<Tag
|
<Tag
|
||||||
|
|
@ -377,7 +383,8 @@ async function load() {
|
||||||
destajoPeriod.value = bundle.destajo_period;
|
destajoPeriod.value = bundle.destajo_period;
|
||||||
sheets.value = bundle.sheets;
|
sheets.value = bundle.sheets;
|
||||||
destajoCuts.value = bundle.destajo_cuts || [];
|
destajoCuts.value = bundle.destajo_cuts || [];
|
||||||
days.value = bundle.days || [];
|
const start = String(bundle.week?.week_start || weekStart.value);
|
||||||
|
days.value = bundle.days?.length ? bundle.days : [0, 1, 2, 3, 4, 5].map((n) => addDaysIso(start, n));
|
||||||
board.value = {
|
board.value = {
|
||||||
required_net: 0,
|
required_net: 0,
|
||||||
payable_net: 0,
|
payable_net: 0,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue