diff --git a/src/services/attendance.service.ts b/src/services/attendance.service.ts index 11a4bba..c9d7cb0 100644 --- a/src/services/attendance.service.ts +++ b/src/services/attendance.service.ts @@ -529,43 +529,120 @@ export async function getPrintData(monthStr: string, filterUserId: number | null const records = await prisma.attendance.findMany({ where, - include: { users: { select: { id: true, first_name: true, last_name: true } } }, - orderBy: [{ user_id: 'asc' }, { shift_date: 'asc' }], + include: { + 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 = {}; + 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 = {}; + 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> = {}; for (const rec of records) { const uid = String(rec.user_id); if (!userTotals[uid]) { - const u = rec.users; - userTotals[uid] = { name: u ? `${u.first_name} ${u.last_name}`.trim() : `User #${uid}`, worked: 0, vacation: 0, sick: 0, holiday: 0, unpaid: 0 }; + const u = (rec as any).users; + 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'; if (lt !== 'work') { const hrs = Number(rec.leave_hours) || 8; - if (lt === 'vacation') userTotals[uid].vacation += hrs; - else if (lt === 'sick') userTotals[uid].sick += hrs; - else if (lt === 'holiday') userTotals[uid].holiday += hrs; - else if (lt === 'unpaid') userTotals[uid].unpaid += hrs; + if (lt === 'vacation') (userTotals[uid].vacation_hours as number) += hrs; + else if (lt === 'sick') (userTotals[uid].sick_hours as number) += hrs; + else if (lt === 'holiday') (userTotals[uid].holiday_hours as number) += hrs; + else if (lt === 'unpaid') (userTotals[uid].unpaid_hours as number) += hrs; } 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)) { - 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> = {}; + 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 { user_totals: userTotals, + leave_balances: leaveBalances, users: users.map(u => ({ id: u.id, name: `${u.first_name} ${u.last_name}`.trim() })), month: monthStr, month_name: `${MONTH_NAMES[mo - 1]} ${yr}`, selected_user: filterUserId, + selected_user_name: selectedUserName, year: yr, - fund: { business_days: bizDays, hours: bizDays * 8 }, + fund: { business_days: countWorkingDays(yr, mo - 1), hours: fundHours }, }; }