fix: attendance print - return proper data structure with records, leave balances, and fund stats

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-03-23 09:36:52 +01:00
parent 8c1fd07293
commit 23ae832eeb

View File

@@ -529,43 +529,120 @@ export async function getPrintData(monthStr: string, filterUserId: number | null
const records = await prisma.attendance.findMany({ const records = await prisma.attendance.findMany({
where, where,
include: { users: { select: { id: true, first_name: true, last_name: true } } }, include: {
orderBy: [{ user_id: 'asc' }, { shift_date: 'asc' }], users: { select: { id: true, first_name: true, last_name: true } },
attendance_project_logs: {
orderBy: { started_at: 'asc' },
},
},
orderBy: [{ users: { last_name: 'asc' } }, { shift_date: 'asc' }],
}); });
const userTotals: Record<string, { name: string; worked: number; vacation: number; sick: number; holiday: number; unpaid: number }> = {}; const fundHours = countWorkingDays(yr, mo - 1) * 8;
// Load project names for enrichment
const projectIds = [...new Set(records.flatMap(r => (r as any).attendance_project_logs?.map((l: any) => l.project_id) || []).filter(Boolean))];
const projectMap: Record<number, string> = {};
if (projectIds.length > 0) {
const projects = await prisma.projects.findMany({
where: { id: { in: projectIds } },
select: { id: true, name: true, project_number: true },
});
for (const p of projects) {
projectMap[p.id] = p.project_number ? `${p.project_number} ${p.name}` : (p.name || `#${p.id}`);
}
}
// Group records by user and calculate totals
const userTotals: Record<string, Record<string, unknown>> = {};
for (const rec of records) { for (const rec of records) {
const uid = String(rec.user_id); const uid = String(rec.user_id);
if (!userTotals[uid]) { if (!userTotals[uid]) {
const u = rec.users; const u = (rec as any).users;
userTotals[uid] = { name: u ? `${u.first_name} ${u.last_name}`.trim() : `User #${uid}`, worked: 0, vacation: 0, sick: 0, holiday: 0, unpaid: 0 }; userTotals[uid] = {
name: u ? `${u.first_name} ${u.last_name}`.trim() : `User #${uid}`,
minutes: 0,
records: [],
vacation_hours: 0,
sick_hours: 0,
holiday_hours: 0,
unpaid_hours: 0,
fund: fundHours,
worked_hours: 0,
covered: 0,
missing: 0,
overtime: 0,
};
} }
// Build record with project_logs for frontend
const projectLogs = (rec as any).attendance_project_logs?.map((log: any) => ({
project_id: log.project_id,
project_name: projectMap[log.project_id] || null,
hours: log.hours,
minutes: log.minutes,
started_at: log.started_at,
ended_at: log.ended_at,
})) || [];
(userTotals[uid].records as unknown[]).push({
...rec,
project_logs: projectLogs,
project_name: projectLogs.length > 0 ? projectLogs[0].project_name : null,
});
const lt = (rec.leave_type as string) || 'work'; const lt = (rec.leave_type as string) || 'work';
if (lt !== 'work') { if (lt !== 'work') {
const hrs = Number(rec.leave_hours) || 8; const hrs = Number(rec.leave_hours) || 8;
if (lt === 'vacation') userTotals[uid].vacation += hrs; if (lt === 'vacation') (userTotals[uid].vacation_hours as number) += hrs;
else if (lt === 'sick') userTotals[uid].sick += hrs; else if (lt === 'sick') (userTotals[uid].sick_hours as number) += hrs;
else if (lt === 'holiday') userTotals[uid].holiday += hrs; else if (lt === 'holiday') (userTotals[uid].holiday_hours as number) += hrs;
else if (lt === 'unpaid') userTotals[uid].unpaid += hrs; else if (lt === 'unpaid') (userTotals[uid].unpaid_hours as number) += hrs;
} else if (rec.arrival_time && rec.departure_time) { } else if (rec.arrival_time && rec.departure_time) {
userTotals[uid].worked += calcWorkedHours(rec.arrival_time, rec.departure_time, rec.break_start, rec.break_end); const mins = calcWorkedHours(rec.arrival_time, rec.departure_time, rec.break_start, rec.break_end) * 60;
(userTotals[uid].minutes as number) += Math.round(mins);
} }
} }
// Calculate fund coverage per user
for (const uid of Object.keys(userTotals)) { for (const uid of Object.keys(userTotals)) {
userTotals[uid].worked = Math.round(userTotals[uid].worked * 10) / 10; const ut = userTotals[uid];
const workedH = Math.round((ut.minutes as number) / 60 * 10) / 10;
ut.worked_hours = workedH;
const covered = workedH + (ut.vacation_hours as number) + (ut.sick_hours as number) + (ut.holiday_hours as number);
ut.covered = Math.round(covered * 10) / 10;
ut.missing = Math.max(0, Math.round(((ut.fund as number) - covered) * 10) / 10);
ut.overtime = Math.max(0, Math.round((covered - (ut.fund as number)) * 10) / 10);
} }
const bizDays = countWorkingDays(yr, mo - 1); // Leave balances
const leaveBalances: Record<string, Record<string, number>> = {};
const balanceRecords = await prisma.leave_balances.findMany({ where: { year: yr } });
for (const bal of balanceRecords) {
const uid = String(bal.user_id);
leaveBalances[uid] = {
vacation_total: Number(bal.vacation_total) || 160,
vacation_remaining: (Number(bal.vacation_total) || 160) - (Number(bal.vacation_used) || 0),
};
}
// Selected user name
let selectedUserName = '';
if (filterUserId) {
const u = users.find(u => u.id === filterUserId);
if (u) selectedUserName = `${u.first_name} ${u.last_name}`.trim();
}
return { return {
user_totals: userTotals, user_totals: userTotals,
leave_balances: leaveBalances,
users: users.map(u => ({ id: u.id, name: `${u.first_name} ${u.last_name}`.trim() })), users: users.map(u => ({ id: u.id, name: `${u.first_name} ${u.last_name}`.trim() })),
month: monthStr, month: monthStr,
month_name: `${MONTH_NAMES[mo - 1]} ${yr}`, month_name: `${MONTH_NAMES[mo - 1]} ${yr}`,
selected_user: filterUserId, selected_user: filterUserId,
selected_user_name: selectedUserName,
year: yr, year: yr,
fund: { business_days: bizDays, hours: bizDays * 8 }, fund: { business_days: countWorkingDays(yr, mo - 1), hours: fundHours },
}; };
} }