const { currentDateString, parseDateOnlyString } = require("../utils/date"); function addDays(date, days) { const next = new Date(date); next.setUTCDate(next.getUTCDate() + days); return next; } async function buildEmployeeStats(prisma, options = {}) { const timeZone = options.timeZone ?? "Asia/Shanghai"; const todayString = currentDateString(timeZone); const today = parseDateOnlyString(todayString); const last30DateFrom = addDays(today, -29); const next30DateTo = addDays(today, 30); const activeEmployeeWhere = { status: "active" }; const [employmentStatusRows, departmentRows, departments, recentHires, recentDepartures, expiringContracts, validCertificateHolders, expiringCertificates] = await Promise.all([ prisma.employee.groupBy({ by: ["employment_status"], where: activeEmployeeWhere, _count: { _all: true }, }), prisma.employee.groupBy({ by: ["department_id"], where: activeEmployeeWhere, _count: { _all: true }, }), prisma.department.findMany({ where: { archived_at: null }, select: { id: true, name: true }, }), prisma.employee.count({ where: { hire_date: { gte: last30DateFrom, lte: new Date(`${todayString}T23:59:59.999Z`), }, }, }), prisma.employee.count({ where: { last_working_date: { gte: last30DateFrom, lte: new Date(`${todayString}T23:59:59.999Z`), }, }, }), prisma.employeeContract.count({ where: { terminated_date: null, end_date: { gte: today, lte: new Date(`${next30DateTo.toISOString().slice(0, 10)}T23:59:59.999Z`), }, employee: activeEmployeeWhere, }, }), prisma.employeeCertificate.groupBy({ by: ["employee_id"], where: { employee: activeEmployeeWhere, OR: [ { expiry_date: null }, { expiry_date: { gte: today, }, }, ], }, }), prisma.employeeCertificate.count({ where: { employee: activeEmployeeWhere, expiry_date: { gte: today, lte: new Date(`${next30DateTo.toISOString().slice(0, 10)}T23:59:59.999Z`), }, }, }), ]); const departmentMap = new Map(departments.map((item) => [item.id, item.name])); return { asOfDate: todayString, employmentStatusSummary: { total: employmentStatusRows.reduce((sum, item) => sum + item._count._all, 0), items: employmentStatusRows .map((item) => ({ status: item.employment_status, count: item._count._all, })) .sort((left, right) => right.count - left.count || left.status.localeCompare(right.status)), }, departmentHeadcount: departmentRows .map((item) => ({ departmentId: item.department_id, departmentName: departmentMap.get(item.department_id) ?? "未知部门", count: item._count._all, })) .sort((left, right) => right.count - left.count || left.departmentName.localeCompare(right.departmentName)), recentMovements: { dateFrom: last30DateFrom.toISOString().slice(0, 10), dateTo: todayString, hires: recentHires, departures: recentDepartures, }, expiringContracts: { dateFrom: todayString, dateTo: next30DateTo.toISOString().slice(0, 10), count: expiringContracts, }, certificateSummary: { dateFrom: todayString, dateTo: next30DateTo.toISOString().slice(0, 10), validHolderCount: validCertificateHolders.length, expiringSoonCount: expiringCertificates, }, }; } module.exports = { buildEmployeeStats, };