diff --git a/api/payroll.ts b/api/payroll.ts index 4698762..b2093a5 100644 --- a/api/payroll.ts +++ b/api/payroll.ts @@ -217,14 +217,102 @@ export async function patchDestajoCut(db: Db, weekId: number, cutId: number, pat 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 | 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>, + days: string[], +): Promise>> { + 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 ?`, + ).all(from, to) as AttendanceRow[]; + + return sheets.map((sheet) => { + if (sheet.kind !== "obra") return sheet; + const projectId = Number(sheet.project_id); + const lines = ((sheet.lines as Array>) ?? []).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) { await ensureWeek(db, tenantId, weekStart); const env = await callCoreFn>(db, "core.fn_payroll_week_bundle", { tenant_id: tenantId, week_start: weekStart }); if (!env.ok) throwFromRpc(env); const data = env.data ?? {}; const today = await resolveToday(db, tenantId); - const start = String((data.week as Record)?.week_start ?? weekStart); - return { ...data, can_assemble: canAssembleWeek(start, today), assemble_from: weekAssembleDate(start) }; + const week = (data.week ?? null) as Record | null; + const start = isoDay(week?.week_start) || weekStart; + const days = weekDays(start); + const sheets = await attachJornalCells(db, (data.sheets as Array>) ?? [], days); + 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 { diff --git a/api/payroll_test.ts b/api/payroll_test.ts index 981ea10..d2e1f84 100644 --- a/api/payroll_test.ts +++ b/api/payroll_test.ts @@ -21,6 +21,7 @@ import { thisSaturday, weekAssembleDate, weekContaining, + weekDays, } from "./payroll.ts"; // 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"); assertEquals(w.weekStart, "2026-08-24"); 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"); assertEquals(d.periodEnd, "2026-08-27"); 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 () => { await withTestDb(async (db) => { const { companyId, projectId, workerId } = await seedFixture(db); diff --git a/web-panel/pages/nomina.vue b/web-panel/pages/nomina.vue index e4650f1..b82ddea 100644 --- a/web-panel/pages/nomina.vue +++ b/web-panel/pages/nomina.vue @@ -46,9 +46,15 @@ - Lun–sáb. Clic en la persona abre la ficha (consulta). El préstamo se registra con Préstamo 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 Préstamo en la barra o desde la ficha. - +