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:
@@ -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 },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user