diff --git a/docs/swagger/swagger-spec.json b/docs/swagger/swagger-spec.json index 01572f2..97745e0 100644 --- a/docs/swagger/swagger-spec.json +++ b/docs/swagger/swagger-spec.json @@ -1134,6 +1134,45 @@ "SchedulePresets" ] } + }, + "/schedule-presets/apply-presets/{email}": { + "post": { + "operationId": "SchedulePresetsController_applyPresets", + "parameters": [ + { + "name": "email", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "preset", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "start", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "201": { + "description": "" + } + }, + "tags": [ + "SchedulePresets" + ] + } } }, "info": { @@ -1551,20 +1590,10 @@ "example": 40, "description": "pay-period`s regular hours" }, - "evening_hours": { - "type": "number", + "other_hours": { + "type": "object", "example": 0, - "description": "pay-period`s evening hours" - }, - "emergency_hours": { - "type": "number", - "example": 0, - "description": "pay-period`s emergency hours" - }, - "overtime_hours": { - "type": "number", - "example": 2, - "description": "pay-period`s overtime hours" + "description": "pay-period`s other hours" }, "expenses": { "type": "number", @@ -1585,9 +1614,7 @@ "required": [ "employee_name", "regular_hours", - "evening_hours", - "emergency_hours", - "overtime_hours", + "other_hours", "expenses", "mileage", "is_approved" diff --git a/src/modules/leave-requests/utils/leave-request.util.ts b/src/modules/leave-requests/utils/leave-request.util.ts index 746a568..6688f1a 100644 --- a/src/modules/leave-requests/utils/leave-request.util.ts +++ b/src/modules/leave-requests/utils/leave-request.util.ts @@ -62,17 +62,21 @@ export class LeaveRequestsUtils { await this.shiftsCommand.upsertShiftsByDate(email, iso_date, { old_shift: existing ? { + date: existing.date, start_time: existing.start_time.toISOString().slice(11, 16), end_time: existing.end_time.toISOString().slice(11, 16), type: existing.bank_code?.type ?? type, is_remote: existing.is_remote, + is_approved:existing.is_approved, comment: existing.comment ?? undefined, } : undefined, new_shift: { + date: existing?.date ?? '', start_time: toHHmm(start_minutes), end_time: toHHmm(end_minutes), is_remote: existing?.is_remote ?? false, + is_approved:existing?.is_approved ?? false, comment: comment ?? existing?.comment ?? "", type: type, }, @@ -97,10 +101,12 @@ export class LeaveRequestsUtils { await this.shiftsCommand.upsertShiftsByDate(email, iso_date, { old_shift: { + date: existing.date, start_time: existing.start_time.toISOString().slice(11, 16), end_time: existing.end_time.toISOString().slice(11, 16), type: existing.bank_code?.type ?? type, is_remote: existing.is_remote, + is_approved:existing.is_approved, comment: existing.comment ?? undefined, }, }); diff --git a/src/modules/pay-periods/dtos/overview-employee-period.dto.ts b/src/modules/pay-periods/dtos/overview-employee-period.dto.ts index 861c783..1ea6937 100644 --- a/src/modules/pay-periods/dtos/overview-employee-period.dto.ts +++ b/src/modules/pay-periods/dtos/overview-employee-period.dto.ts @@ -1,48 +1,54 @@ import { ApiProperty } from '@nestjs/swagger'; export class EmployeePeriodOverviewDto { - // @ApiProperty({ - // example: 42, - // description: "Employees.id (clé primaire num.)", - // }) - // @Allow() - // @IsOptional() - // employee_id: number; + // @ApiProperty({ + // example: 42, + // description: "Employees.id (clé primaire num.)", + // }) + // @Allow() + // @IsOptional() + // employee_id: number; - email:string; + email: string; - @ApiProperty({ - example: 'Alex Dupont', - description: 'Nom complet de lemployé', - }) - employee_name: string; + @ApiProperty({ + example: 'Alex Dupont', + description: 'Nom complet de lemployé', + }) + employee_name: string; - @ApiProperty({ example: 40, description: 'pay-period`s regular hours' }) - regular_hours: number; + @ApiProperty({ example: 40, description: 'pay-period`s regular hours' }) + regular_hours: number; - @ApiProperty({ example: 0, description: 'pay-period`s evening hours' }) - evening_hours: number; + @ApiProperty({ example: 0, description: 'pay-period`s other hours' }) + other_hours: { + evening_hours: number; - @ApiProperty({ example: 0, description: 'pay-period`s emergency hours' }) - emergency_hours: number; + emergency_hours: number; - @ApiProperty({ example: 2, description: 'pay-period`s overtime hours' }) - overtime_hours: number; + overtime_hours: number; - total_hours: number; + sick_hours: number; - @ApiProperty({ example: 420.69, description: 'pay-period`s total expenses ($)' }) - expenses: number; + holiday_hours: number; - @ApiProperty({ example: 40, description: 'pay-period total mileages (km)' }) - mileage: number; + vacation_hours: number; + }; - @ApiProperty({ - example: true, - description: 'Tous les timesheets de la période sont approuvés pour cet employé', - }) - is_approved: boolean; + total_hours: number; - is_remote: boolean; + @ApiProperty({ example: 420.69, description: 'pay-period`s total expenses ($)' }) + expenses: number; + + @ApiProperty({ example: 40, description: 'pay-period total mileages (km)' }) + mileage: number; + + @ApiProperty({ + example: true, + description: 'Tous les timesheets de la période sont approuvés pour cet employé', + }) + is_approved: boolean; + + is_remote: boolean; } diff --git a/src/modules/pay-periods/services/pay-periods-query.service.ts b/src/modules/pay-periods/services/pay-periods-query.service.ts index 8e0a952..0e6aac0 100644 --- a/src/modules/pay-periods/services/pay-periods-query.service.ts +++ b/src/modules/pay-periods/services/pay-periods-query.service.ts @@ -9,365 +9,399 @@ import { mapPayPeriodToDto } from "../mappers/pay-periods.mapper"; @Injectable() export class PayPeriodsQueryService { - constructor( private readonly prisma: PrismaService) {} + constructor(private readonly prisma: PrismaService) { } - async getOverview(pay_period_no: number): Promise { - const period = await this.prisma.payPeriods.findFirst({ - where: { pay_period_no }, - orderBy: { pay_year: "desc" }, - }); - if (!period) throw new NotFoundException(`Period #${pay_period_no} not found`); + async getOverview(pay_period_no: number): Promise { + const period = await this.prisma.payPeriods.findFirst({ + where: { pay_period_no }, + orderBy: { pay_year: "desc" }, + }); + if (!period) throw new NotFoundException(`Period #${pay_period_no} not found`); - return this.buildOverview({ - period_start: period.period_start, - period_end : period.period_end, - payday : period.payday, - period_no : period.pay_period_no, - pay_year : period.pay_year, - label : period.label, - }); - } - - async getOverviewByYearPeriod(pay_year: number, period_no: number): Promise { - const period = computePeriod(pay_year, period_no); - return this.buildOverview({ - period_start: period.period_start, - period_end : period.period_end, - period_no : period.period_no, - pay_year : period.pay_year, - payday : period.payday, - label :period.label, - } as any); - } - - //find crew member associated with supervisor - private async resolveCrew(supervisor_id: number, include_subtree: boolean): - Promise> { - const result: Array<{ id: number; first_name: string; last_name: string; email: string; }> = []; - - let frontier = await this.prisma.employees.findMany({ - where: { supervisor_id: supervisor_id }, - select: { id: true, user: { select: { first_name: true, last_name: true, email: true } } }, - }); - result.push(...frontier.map(emp => ({ - id: emp.id, first_name: emp.user.first_name, last_name: emp.user.last_name, email: emp.user.email - }))); - - if (!include_subtree) return result; - - while (frontier.length) { - const parent_ids = frontier.map(emp => emp.id); - const next = await this.prisma.employees.findMany({ - where: { supervisor_id: { in: parent_ids } }, - select: { id: true, user: { select: { first_name: true, last_name: true, email: true } } }, - }); - if (next.length === 0) break; - result.push(...next.map(emp => ({ - id: emp.id, first_name: emp.user.first_name, last_name: emp.user.last_name, email: emp.user.email - }))); - frontier = next; + return this.buildOverview({ + period_start: period.period_start, + period_end: period.period_end, + payday: period.payday, + period_no: period.pay_period_no, + pay_year: period.pay_year, + label: period.label, + }); } - return result; - } - //fetchs crew emails - async resolveCrewEmails(supervisor_id: number, include_subtree: boolean): Promise> { - const crew = await this.resolveCrew(supervisor_id, include_subtree); - return new Set(crew.map(crew_member => crew_member.email).filter(Boolean)); - } - - async getCrewOverview(pay_year: number, period_no: number, email: string, include_subtree: boolean): - Promise { - // 1) Search for the period - const period = await this.prisma.payPeriods.findFirst({ where: { pay_year, pay_period_no: period_no } }); - if (!period) throw new NotFoundException(`Pay period ${pay_year}-${period_no} not found`); + async getOverviewByYearPeriod(pay_year: number, period_no: number): Promise { + const period = computePeriod(pay_year, period_no); + return this.buildOverview({ + period_start: period.period_start, + period_end: period.period_end, + period_no: period.period_no, + pay_year: period.pay_year, + payday: period.payday, + label: period.label, + } as any); + } - // 2) fetch supervisor - const supervisor = await this.prisma.employees.findFirst({ - where: { user: { email: email }}, - select: { - id: true, - is_supervisor: true, - }, - }); + //find crew member associated with supervisor + private async resolveCrew(supervisor_id: number, include_subtree: boolean): + Promise> { + const result: Array<{ id: number; first_name: string; last_name: string; email: string; }> = []; - if (!supervisor) throw new NotFoundException('No employee record linked to current user'); - if (!supervisor.is_supervisor) throw new ForbiddenException('Employee is not a supervisor'); + let frontier = await this.prisma.employees.findMany({ + where: { supervisor_id: supervisor_id }, + select: { id: true, user: { select: { first_name: true, last_name: true, email: true } } }, + }); + result.push(...frontier.map(emp => ({ + id: emp.id, first_name: emp.user.first_name, last_name: emp.user.last_name, email: emp.user.email + }))); - // 3)fetchs crew members - const crew = await this.resolveCrew(supervisor.id, include_subtree); // [{ id, first_name, last_name }] - const crew_ids = crew.map(c => c.id); - // seed names map for employee without data - const seed_names = new Map( - crew.map(crew => [ - crew.id, - { name:`${crew.first_name} ${crew.last_name}`.trim(), - email: crew.email } - ] - ) - ); + if (!include_subtree) return result; - // 4) overview build - return this.buildOverview({ - period_no : period.pay_period_no, - period_start: period.period_start, - period_end : period.period_end, - payday : period.payday, - pay_year : period.pay_year, - label : period.label, - //add is_approved - }, { filtered_employee_ids: crew_ids, seed_names }); - } + while (frontier.length) { + const parent_ids = frontier.map(emp => emp.id); + const next = await this.prisma.employees.findMany({ + where: { supervisor_id: { in: parent_ids } }, + select: { id: true, user: { select: { first_name: true, last_name: true, email: true } } }, + }); + if (next.length === 0) break; + result.push(...next.map(emp => ({ + id: emp.id, first_name: emp.user.first_name, last_name: emp.user.last_name, email: emp.user.email + }))); + frontier = next; + } + return result; + } - private async buildOverview( - period: { period_start: string | Date; period_end: string | Date; payday: string | Date; - period_no: number; pay_year: number; label: string; }, //add is_approved - options?: { filtered_employee_ids?: number[]; seed_names?: Map} - ): Promise { - const toDateString = (d: Date) => d.toISOString().slice(0, 10); - const toMoney = (v: any) => (typeof v === "object" && "toNumber" in v ? v.toNumber() : (v as number)); + //fetchs crew emails + async resolveCrewEmails(supervisor_id: number, include_subtree: boolean): Promise> { + const crew = await this.resolveCrew(supervisor_id, include_subtree); + return new Set(crew.map(crew_member => crew_member.email).filter(Boolean)); + } - const start = period.period_start instanceof Date - ? period.period_start - : new Date(`${period.period_start}T00:00:00.000Z`); + async getCrewOverview(pay_year: number, period_no: number, email: string, include_subtree: boolean): + Promise { + // 1) Search for the period + const period = await this.prisma.payPeriods.findFirst({ where: { pay_year, pay_period_no: period_no } }); + if (!period) throw new NotFoundException(`Pay period ${pay_year}-${period_no} not found`); - const end = period.period_end instanceof Date - ? period.period_end - : new Date(`${period.period_end}T00:00:00.000Z`); + // 2) fetch supervisor + const supervisor = await this.prisma.employees.findFirst({ + where: { user: { email: email } }, + select: { + id: true, + is_supervisor: true, + }, + }); - const payd = period.payday instanceof Date - ? period.payday - : new Date (`${period.payday}T00:00:00.000Z`); + if (!supervisor) throw new NotFoundException('No employee record linked to current user'); + if (!supervisor.is_supervisor) throw new ForbiddenException('Employee is not a supervisor'); - //restrictEmployeeIds = filter for shifts and expenses by employees - const where_employee = options?.filtered_employee_ids?.length ? { employee_id: { in: options.filtered_employee_ids } }: {}; + // 3)fetchs crew members + const crew = await this.resolveCrew(supervisor.id, include_subtree); // [{ id, first_name, last_name }] + const crew_ids = crew.map(c => c.id); + // seed names map for employee without data + const seed_names = new Map( + crew.map(crew => [ + crew.id, + { + name: `${crew.first_name} ${crew.last_name}`.trim(), + email: crew.email + } + ] + ) + ); - // SHIFTS (filtered by crew) - const shifts = await this.prisma.shifts.findMany({ - where: { - date: { gte: start, lte: end }, - timesheet: where_employee, - }, - select: { - start_time: true, - end_time: true, - is_remote: true, - timesheet: { select: { - is_approved: true, - employee: { select: { - id: true, - user: { select: { - first_name: true, - last_name: true, - email: true, - } }, - } }, + // 4) overview build + return this.buildOverview({ + period_no: period.pay_period_no, + period_start: period.period_start, + period_end: period.period_end, + payday: period.payday, + pay_year: period.pay_year, + label: period.label, + //add is_approved + }, { filtered_employee_ids: crew_ids, seed_names }); + } + + private async buildOverview( + period: { + period_start: string | Date; period_end: string | Date; payday: string | Date; + period_no: number; pay_year: number; label: string; + }, //add is_approved + options?: { filtered_employee_ids?: number[]; seed_names?: Map } + ): Promise { + const toDateString = (d: Date) => d.toISOString().slice(0, 10); + const toMoney = (v: any) => (typeof v === "object" && "toNumber" in v ? v.toNumber() : (v as number)); + + const start = period.period_start instanceof Date + ? period.period_start + : new Date(`${period.period_start}T00:00:00.000Z`); + + const end = period.period_end instanceof Date + ? period.period_end + : new Date(`${period.period_end}T00:00:00.000Z`); + + const payd = period.payday instanceof Date + ? period.payday + : new Date(`${period.payday}T00:00:00.000Z`); + + //restrictEmployeeIds = filter for shifts and expenses by employees + const where_employee = options?.filtered_employee_ids?.length ? { employee_id: { in: options.filtered_employee_ids } } : {}; + + // SHIFTS (filtered by crew) + const shifts = await this.prisma.shifts.findMany({ + where: { + date: { gte: start, lte: end }, + timesheet: where_employee, + }, + select: { + start_time: true, + end_time: true, + is_remote: true, + timesheet: { + select: { + is_approved: true, + employee: { + select: { + id: true, + user: { + select: { + first_name: true, + last_name: true, + email: true, + } + }, + } + }, }, - }, - bank_code: { select: { categorie: true, type: true } }, - }, - }); - - // EXPENSES (filtered by crew) - const expenses = await this.prisma.expenses.findMany({ - where: { - date: { gte: start, lte: end }, - timesheet: where_employee, - }, - select: { - amount: true, - timesheet: { select: { - is_approved: true, - employee: { select: { - id: true, - user: { select: { - first_name: true, - last_name: true, - email: true, - } }, - } }, - } }, - bank_code: { select: { categorie: true, modifier: true, type: true } }, - }, - }); - - const by_employee = new Map(); - - // seed for employee without data - if (options?.seed_names) { - for (const [id, {name, email}] of options.seed_names.entries()) { - by_employee.set(id, { - email, - employee_name: name, - regular_hours: 0, - evening_hours: 0, - emergency_hours: 0, - overtime_hours: 0, - total_hours: 0, - expenses: 0, - mileage: 0, - is_approved: true, - is_remote: true, + }, + bank_code: { select: { categorie: true, type: true } }, + }, }); - } - } - const ensure = (id: number, name: string, email: string) => { - if (!by_employee.has(id)) { - by_employee.set(id, { - email, - employee_name: name, - regular_hours: 0, - evening_hours: 0, - emergency_hours: 0, - overtime_hours: 0, - total_hours: 0, - expenses: 0, - mileage: 0, - is_approved: true, - is_remote: true, + // EXPENSES (filtered by crew) + const expenses = await this.prisma.expenses.findMany({ + where: { + date: { gte: start, lte: end }, + timesheet: where_employee, + }, + select: { + amount: true, + timesheet: { + select: { + is_approved: true, + employee: { + select: { + id: true, + user: { + select: { + first_name: true, + last_name: true, + email: true, + } + }, + } + }, + } + }, + bank_code: { select: { categorie: true, modifier: true, type: true } }, + }, }); - } - return by_employee.get(id)!; - }; - for (const shift of shifts) { - const employee = shift.timesheet.employee; - const name = `${employee.user.first_name} ${employee.user.last_name}`.trim(); - const record = ensure(employee.id, name, employee.user.email); + const by_employee = new Map(); - const hours = computeHours(shift.start_time, shift.end_time); - const type = (shift.bank_code?.type ?? '').toUpperCase(); - switch (type) { - case "EVENING": record.evening_hours += hours; - record.total_hours += hours; - break; - case "EMERGENCY": record.emergency_hours += hours; - record.total_hours += hours; - break; - case "OVERTIME": record.overtime_hours += hours; - record.total_hours += hours; - break; - case "REGULAR" : record.regular_hours += hours; - record.total_hours += hours; - break; - } - - record.is_approved = record.is_approved && shift.timesheet.is_approved; - record.is_remote = record.is_remote || !!shift.is_remote; + // seed for employee without data + if (options?.seed_names) { + for (const [id, { name, email }] of options.seed_names.entries()) { + by_employee.set(id, { + email, + employee_name: name, + regular_hours: 0, + other_hours: { + evening_hours: 0, + emergency_hours: 0, + overtime_hours: 0, + sick_hours: 0, + holiday_hours: 0, + vacation_hours: 0, + }, + total_hours: 0, + expenses: 0, + mileage: 0, + is_approved: true, + is_remote: true, + }); + } + } + + const ensure = (id: number, name: string, email: string) => { + if (!by_employee.has(id)) { + by_employee.set(id, { + email, + employee_name: name, + regular_hours: 0, + other_hours: { + evening_hours: 0, + emergency_hours: 0, + overtime_hours: 0, + sick_hours: 0, + holiday_hours: 0, + vacation_hours: 0, + }, + total_hours: 0, + expenses: 0, + mileage: 0, + is_approved: true, + is_remote: true, + }); + } + return by_employee.get(id)!; + }; + + for (const shift of shifts) { + const employee = shift.timesheet.employee; + const name = `${employee.user.first_name} ${employee.user.last_name}`.trim(); + const record = ensure(employee.id, name, employee.user.email); + + const hours = computeHours(shift.start_time, shift.end_time); + const type = (shift.bank_code?.type ?? '').toUpperCase(); + switch (type) { + case "EVENING": record.other_hours.evening_hours += hours; + record.total_hours += hours; + break; + case "EMERGENCY": record.other_hours.emergency_hours += hours; + record.total_hours += hours; + break; + case "OVERTIME": record.other_hours.overtime_hours += hours; + record.total_hours += hours; + break; + case "SICK": record.other_hours.sick_hours += hours; + record.total_hours += hours; + break; + case "HOLIDAY": record.other_hours.holiday_hours += hours; + record.total_hours += hours; + break; + case "VACATION": record.other_hours.vacation_hours += hours; + record.total_hours += hours; + break; + case "REGULAR": record.regular_hours += hours; + record.total_hours += hours; + break; + } + + record.is_approved = record.is_approved && shift.timesheet.is_approved; + record.is_remote = record.is_remote || !!shift.is_remote; + } + + for (const expense of expenses) { + const exp = expense.timesheet.employee; + const name = `${exp.user.first_name} ${exp.user.last_name}`.trim(); + const record = ensure(exp.id, name, exp.user.email); + + const amount = toMoney(expense.amount); + record.expenses += amount; + + const type = (expense.bank_code?.type || "").toUpperCase(); + const rate = expense.bank_code?.modifier ?? 0; + if (type === "MILEAGE" && rate > 0) { + record.mileage += Math.round((amount / rate) * 100) / 100; + } + record.is_approved = record.is_approved && expense.timesheet.is_approved; + } + + const employees_overview = Array.from(by_employee.values()).sort((a, b) => + a.employee_name.localeCompare(b.employee_name, "fr", { sensitivity: "base" }), + ); + + return { + pay_period_no: period.period_no, + pay_year: period.pay_year, + payday: toDateString(payd), + period_start: toDateString(start), + period_end: toDateString(end), + label: period.label, + employees_overview, + }; } - for (const expense of expenses) { - const exp = expense.timesheet.employee; - const name = `${exp.user.first_name} ${exp.user.last_name}`.trim(); - const record = ensure(exp.id, name, exp.user.email); - - const amount = toMoney(expense.amount); - record.expenses += amount; - - const type = (expense.bank_code?.type || "").toUpperCase(); - const rate = expense.bank_code?.modifier ?? 0; - if (type === "MILEAGE" && rate > 0) { - record.mileage += Math.round((amount / rate) * 100) / 100; - } - record.is_approved = record.is_approved && expense.timesheet.is_approved; - } - - const employees_overview = Array.from(by_employee.values()).sort((a, b) => - a.employee_name.localeCompare(b.employee_name, "fr", { sensitivity: "base" }), - ); - - return { - pay_period_no: period.period_no, - pay_year: period.pay_year, - payday: toDateString(payd), - period_start: toDateString(start), - period_end: toDateString(end), - label: period.label, - employees_overview, - }; - } - - async getSupervisor(email:string) { - return this.prisma.employees.findFirst({ - where: { user: { email } }, - select: { id: true, is_supervisor: true }, - }); - } - - async findAll(): Promise { - const currentPayYear = payYearOfDate(new Date()); - return listPayYear(currentPayYear).map(period =>({ - pay_period_no: period.period_no, - pay_year: period.pay_year, - payday: period.payday, - period_start: period.period_start, - period_end: period.period_end, - label: period.label, - //add is_approved - })); - } - - async findOne(period_no: number): Promise { - const row = await this.prisma.payPeriods.findFirst({ - where: { pay_period_no: period_no }, - orderBy: { pay_year: "desc" }, - }); - if (!row) throw new NotFoundException(`Pay period #${period_no} not found`); - return mapPayPeriodToDto(row); - } - - async findCurrent(date?: string): Promise { - const iso_day = date ?? new Date().toISOString().slice(0,10); - return this.findByDate(iso_day); - } - - async findOneByYearPeriod(pay_year: number, period_no: number): Promise { - const row = await this.prisma.payPeriods.findFirst({ - where: { pay_year, pay_period_no: period_no }, - }); - if(row) return mapPayPeriodToDto(row); - - // fallback for outside of view periods - const period = computePeriod(pay_year, period_no); - return { - pay_period_no: period.period_no, - pay_year: period.pay_year, - period_start: period.period_start, - payday: period.payday, - period_end: period.period_end, - label: period.label + async getSupervisor(email: string) { + return this.prisma.employees.findFirst({ + where: { user: { email } }, + select: { id: true, is_supervisor: true }, + }); } - } - //function to cherry pick a Date to find a period - async findByDate(date: string): Promise { - const dt = new Date(date); - const row = await this.prisma.payPeriods.findFirst({ - where: { period_start: { lte: dt }, period_end: { gte: dt } }, - }); - if(row) return mapPayPeriodToDto(row); - - //fallback for outwside view periods - const pay_year = payYearOfDate(date); - const periods = listPayYear(pay_year); - const hit = periods.find(period => date >= period.period_start && date <= period.period_end); - if(!hit) throw new NotFoundException(`No period found for ${date}`); - - return { - pay_period_no: hit.period_no, - pay_year : hit.pay_year, - period_start : hit.period_start, - period_end : hit.period_end, - payday : hit.payday, - label : hit.label + async findAll(): Promise { + const currentPayYear = payYearOfDate(new Date()); + return listPayYear(currentPayYear).map(period => ({ + pay_period_no: period.period_no, + pay_year: period.pay_year, + payday: period.payday, + period_start: period.period_start, + period_end: period.period_end, + label: period.label, + //add is_approved + })); } - } - async getPeriodWindow(pay_year: number, period_no: number) { - return this.prisma.payPeriods.findFirst({ - where: {pay_year, pay_period_no: period_no }, - select: { period_start: true, period_end: true }, - }); - } + async findOne(period_no: number): Promise { + const row = await this.prisma.payPeriods.findFirst({ + where: { pay_period_no: period_no }, + orderBy: { pay_year: "desc" }, + }); + if (!row) throw new NotFoundException(`Pay period #${period_no} not found`); + return mapPayPeriodToDto(row); + } + + async findCurrent(date?: string): Promise { + const iso_day = date ?? new Date().toISOString().slice(0, 10); + return this.findByDate(iso_day); + } + + async findOneByYearPeriod(pay_year: number, period_no: number): Promise { + const row = await this.prisma.payPeriods.findFirst({ + where: { pay_year, pay_period_no: period_no }, + }); + if (row) return mapPayPeriodToDto(row); + + // fallback for outside of view periods + const period = computePeriod(pay_year, period_no); + return { + pay_period_no: period.period_no, + pay_year: period.pay_year, + period_start: period.period_start, + payday: period.payday, + period_end: period.period_end, + label: period.label + } + } + + //function to cherry pick a Date to find a period + async findByDate(date: string): Promise { + const dt = new Date(date); + const row = await this.prisma.payPeriods.findFirst({ + where: { period_start: { lte: dt }, period_end: { gte: dt } }, + }); + if (row) return mapPayPeriodToDto(row); + + //fallback for outwside view periods + const pay_year = payYearOfDate(date); + const periods = listPayYear(pay_year); + const hit = periods.find(period => date >= period.period_start && date <= period.period_end); + if (!hit) throw new NotFoundException(`No period found for ${date}`); + + return { + pay_period_no: hit.period_no, + pay_year: hit.pay_year, + period_start: hit.period_start, + period_end: hit.period_end, + payday: hit.payday, + label: hit.label + } + } + + async getPeriodWindow(pay_year: number, period_no: number) { + return this.prisma.payPeriods.findFirst({ + where: { pay_year, pay_period_no: period_no }, + select: { period_start: true, period_end: true }, + }); + } }