fix(pay-period): change payload to send regular hours and other hours, rather than each individual shift type as a property

This commit is contained in:
Nicolas Drolet 2025-10-09 15:17:02 -04:00
parent 7c7edea768
commit 5a1017f82b
4 changed files with 456 additions and 383 deletions

View File

@ -1134,6 +1134,45 @@
"SchedulePresets" "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": { "info": {
@ -1551,20 +1590,10 @@
"example": 40, "example": 40,
"description": "pay-period`s regular hours" "description": "pay-period`s regular hours"
}, },
"evening_hours": { "other_hours": {
"type": "number", "type": "object",
"example": 0, "example": 0,
"description": "pay-period`s evening hours" "description": "pay-period`s other 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"
}, },
"expenses": { "expenses": {
"type": "number", "type": "number",
@ -1585,9 +1614,7 @@
"required": [ "required": [
"employee_name", "employee_name",
"regular_hours", "regular_hours",
"evening_hours", "other_hours",
"emergency_hours",
"overtime_hours",
"expenses", "expenses",
"mileage", "mileage",
"is_approved" "is_approved"

View File

@ -62,17 +62,21 @@ export class LeaveRequestsUtils {
await this.shiftsCommand.upsertShiftsByDate(email, iso_date, { await this.shiftsCommand.upsertShiftsByDate(email, iso_date, {
old_shift: existing old_shift: existing
? { ? {
date: existing.date,
start_time: existing.start_time.toISOString().slice(11, 16), start_time: existing.start_time.toISOString().slice(11, 16),
end_time: existing.end_time.toISOString().slice(11, 16), end_time: existing.end_time.toISOString().slice(11, 16),
type: existing.bank_code?.type ?? type, type: existing.bank_code?.type ?? type,
is_remote: existing.is_remote, is_remote: existing.is_remote,
is_approved:existing.is_approved,
comment: existing.comment ?? undefined, comment: existing.comment ?? undefined,
} }
: undefined, : undefined,
new_shift: { new_shift: {
date: existing?.date ?? '',
start_time: toHHmm(start_minutes), start_time: toHHmm(start_minutes),
end_time: toHHmm(end_minutes), end_time: toHHmm(end_minutes),
is_remote: existing?.is_remote ?? false, is_remote: existing?.is_remote ?? false,
is_approved:existing?.is_approved ?? false,
comment: comment ?? existing?.comment ?? "", comment: comment ?? existing?.comment ?? "",
type: type, type: type,
}, },
@ -97,10 +101,12 @@ export class LeaveRequestsUtils {
await this.shiftsCommand.upsertShiftsByDate(email, iso_date, { await this.shiftsCommand.upsertShiftsByDate(email, iso_date, {
old_shift: { old_shift: {
date: existing.date,
start_time: existing.start_time.toISOString().slice(11, 16), start_time: existing.start_time.toISOString().slice(11, 16),
end_time: existing.end_time.toISOString().slice(11, 16), end_time: existing.end_time.toISOString().slice(11, 16),
type: existing.bank_code?.type ?? type, type: existing.bank_code?.type ?? type,
is_remote: existing.is_remote, is_remote: existing.is_remote,
is_approved:existing.is_approved,
comment: existing.comment ?? undefined, comment: existing.comment ?? undefined,
}, },
}); });

View File

@ -1,48 +1,54 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
export class EmployeePeriodOverviewDto { export class EmployeePeriodOverviewDto {
// @ApiProperty({ // @ApiProperty({
// example: 42, // example: 42,
// description: "Employees.id (clé primaire num.)", // description: "Employees.id (clé primaire num.)",
// }) // })
// @Allow() // @Allow()
// @IsOptional() // @IsOptional()
// employee_id: number; // employee_id: number;
email:string; email: string;
@ApiProperty({ @ApiProperty({
example: 'Alex Dupont', example: 'Alex Dupont',
description: 'Nom complet de lemployé', description: 'Nom complet de lemployé',
}) })
employee_name: string; employee_name: string;
@ApiProperty({ example: 40, description: 'pay-period`s regular hours' }) @ApiProperty({ example: 40, description: 'pay-period`s regular hours' })
regular_hours: number; regular_hours: number;
@ApiProperty({ example: 0, description: 'pay-period`s evening hours' }) @ApiProperty({ example: 0, description: 'pay-period`s other hours' })
evening_hours: number; 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 ($)' }) holiday_hours: number;
expenses: number;
@ApiProperty({ example: 40, description: 'pay-period total mileages (km)' }) vacation_hours: number;
mileage: number; };
@ApiProperty({ total_hours: number;
example: true,
description: 'Tous les timesheets de la période sont approuvés pour cet employé',
})
is_approved: boolean;
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;
} }

View File

@ -9,365 +9,399 @@ import { mapPayPeriodToDto } from "../mappers/pay-periods.mapper";
@Injectable() @Injectable()
export class PayPeriodsQueryService { export class PayPeriodsQueryService {
constructor( private readonly prisma: PrismaService) {} constructor(private readonly prisma: PrismaService) { }
async getOverview(pay_period_no: number): Promise<PayPeriodOverviewDto> { async getOverview(pay_period_no: number): Promise<PayPeriodOverviewDto> {
const period = await this.prisma.payPeriods.findFirst({ const period = await this.prisma.payPeriods.findFirst({
where: { pay_period_no }, where: { pay_period_no },
orderBy: { pay_year: "desc" }, orderBy: { pay_year: "desc" },
}); });
if (!period) throw new NotFoundException(`Period #${pay_period_no} not found`); if (!period) throw new NotFoundException(`Period #${pay_period_no} not found`);
return this.buildOverview({ return this.buildOverview({
period_start: period.period_start, period_start: period.period_start,
period_end : period.period_end, period_end: period.period_end,
payday : period.payday, payday: period.payday,
period_no : period.pay_period_no, period_no: period.pay_period_no,
pay_year : period.pay_year, pay_year: period.pay_year,
label : period.label, label: period.label,
}); });
}
async getOverviewByYearPeriod(pay_year: number, period_no: number): Promise<PayPeriodOverviewDto> {
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<Array<{ id: number; first_name: string; last_name: string; email: string }>> {
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 result;
}
//fetchs crew emails async getOverviewByYearPeriod(pay_year: number, period_no: number): Promise<PayPeriodOverviewDto> {
async resolveCrewEmails(supervisor_id: number, include_subtree: boolean): Promise<Set<string>> { const period = computePeriod(pay_year, period_no);
const crew = await this.resolveCrew(supervisor_id, include_subtree); return this.buildOverview({
return new Set(crew.map(crew_member => crew_member.email).filter(Boolean)); 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);
}
async getCrewOverview(pay_year: number, period_no: number, email: string, include_subtree: boolean): //find crew member associated with supervisor
Promise<PayPeriodOverviewDto> { private async resolveCrew(supervisor_id: number, include_subtree: boolean):
// 1) Search for the period Promise<Array<{ id: number; first_name: string; last_name: string; email: string }>> {
const period = await this.prisma.payPeriods.findFirst({ where: { pay_year, pay_period_no: period_no } }); const result: Array<{ id: number; first_name: string; last_name: string; email: string; }> = [];
if (!period) throw new NotFoundException(`Pay period ${pay_year}-${period_no} not found`);
// 2) fetch supervisor let frontier = await this.prisma.employees.findMany({
const supervisor = await this.prisma.employees.findFirst({ where: { supervisor_id: supervisor_id },
where: { user: { email: email }}, select: { id: true, user: { select: { first_name: true, last_name: true, email: true } } },
select: { });
id: true, result.push(...frontier.map(emp => ({
is_supervisor: true, id: emp.id, first_name: emp.user.first_name, last_name: emp.user.last_name, email: emp.user.email
}, })));
});
if (!supervisor) throw new NotFoundException('No employee record linked to current user'); if (!include_subtree) return result;
if (!supervisor.is_supervisor) throw new ForbiddenException('Employee is not a supervisor');
// 3)fetchs crew members while (frontier.length) {
const crew = await this.resolveCrew(supervisor.id, include_subtree); // [{ id, first_name, last_name }] const parent_ids = frontier.map(emp => emp.id);
const crew_ids = crew.map(c => c.id); const next = await this.prisma.employees.findMany({
// seed names map for employee without data where: { supervisor_id: { in: parent_ids } },
const seed_names = new Map<number, { name: string; email: string }>( select: { id: true, user: { select: { first_name: true, last_name: true, email: true } } },
crew.map(crew => [ });
crew.id, if (next.length === 0) break;
{ name:`${crew.first_name} ${crew.last_name}`.trim(), result.push(...next.map(emp => ({
email: crew.email } id: emp.id, first_name: emp.user.first_name, last_name: emp.user.last_name, email: emp.user.email
] })));
) frontier = next;
); }
return result;
}
// 4) overview build //fetchs crew emails
return this.buildOverview({ async resolveCrewEmails(supervisor_id: number, include_subtree: boolean): Promise<Set<string>> {
period_no : period.pay_period_no, const crew = await this.resolveCrew(supervisor_id, include_subtree);
period_start: period.period_start, return new Set(crew.map(crew_member => crew_member.email).filter(Boolean));
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( async getCrewOverview(pay_year: number, period_no: number, email: string, include_subtree: boolean):
period: { period_start: string | Date; period_end: string | Date; payday: string | Date; Promise<PayPeriodOverviewDto> {
period_no: number; pay_year: number; label: string; }, //add is_approved // 1) Search for the period
options?: { filtered_employee_ids?: number[]; seed_names?: Map<number, {name: string, email: string}>} const period = await this.prisma.payPeriods.findFirst({ where: { pay_year, pay_period_no: period_no } });
): Promise<PayPeriodOverviewDto> { if (!period) throw new NotFoundException(`Pay period ${pay_year}-${period_no} not found`);
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 // 2) fetch supervisor
? period.period_start const supervisor = await this.prisma.employees.findFirst({
: new Date(`${period.period_start}T00:00:00.000Z`); where: { user: { email: email } },
select: {
id: true,
is_supervisor: true,
},
});
const end = period.period_end instanceof Date if (!supervisor) throw new NotFoundException('No employee record linked to current user');
? period.period_end if (!supervisor.is_supervisor) throw new ForbiddenException('Employee is not a supervisor');
: new Date(`${period.period_end}T00:00:00.000Z`);
const payd = period.payday instanceof Date // 3)fetchs crew members
? period.payday const crew = await this.resolveCrew(supervisor.id, include_subtree); // [{ id, first_name, last_name }]
: new Date (`${period.payday}T00:00:00.000Z`); const crew_ids = crew.map(c => c.id);
// seed names map for employee without data
const seed_names = new Map<number, { name: string; email: string }>(
crew.map(crew => [
crew.id,
{
name: `${crew.first_name} ${crew.last_name}`.trim(),
email: crew.email
}
]
)
);
//restrictEmployeeIds = filter for shifts and expenses by employees // 4) overview build
const where_employee = options?.filtered_employee_ids?.length ? { employee_id: { in: options.filtered_employee_ids } }: {}; 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 });
}
// SHIFTS (filtered by crew) private async buildOverview(
const shifts = await this.prisma.shifts.findMany({ period: {
where: { period_start: string | Date; period_end: string | Date; payday: string | Date;
date: { gte: start, lte: end }, period_no: number; pay_year: number; label: string;
timesheet: where_employee, }, //add is_approved
}, options?: { filtered_employee_ids?: number[]; seed_names?: Map<number, { name: string, email: string }> }
select: { ): Promise<PayPeriodOverviewDto> {
start_time: true, const toDateString = (d: Date) => d.toISOString().slice(0, 10);
end_time: true, const toMoney = (v: any) => (typeof v === "object" && "toNumber" in v ? v.toNumber() : (v as number));
is_remote: true,
timesheet: { select: { const start = period.period_start instanceof Date
is_approved: true, ? period.period_start
employee: { select: { : new Date(`${period.period_start}T00:00:00.000Z`);
id: true,
user: { select: { const end = period.period_end instanceof Date
first_name: true, ? period.period_end
last_name: true, : new Date(`${period.period_end}T00:00:00.000Z`);
email: true,
} }, 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 } }, 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<number, EmployeePeriodOverviewDto>();
// 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,
}); });
}
}
const ensure = (id: number, name: string, email: string) => { // EXPENSES (filtered by crew)
if (!by_employee.has(id)) { const expenses = await this.prisma.expenses.findMany({
by_employee.set(id, { where: {
email, date: { gte: start, lte: end },
employee_name: name, timesheet: where_employee,
regular_hours: 0, },
evening_hours: 0, select: {
emergency_hours: 0, amount: true,
overtime_hours: 0, timesheet: {
total_hours: 0, select: {
expenses: 0, is_approved: true,
mileage: 0, employee: {
is_approved: true, select: {
is_remote: true, 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 by_employee = new Map<number, EmployeePeriodOverviewDto>();
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); // seed for employee without data
const type = (shift.bank_code?.type ?? '').toUpperCase(); if (options?.seed_names) {
switch (type) { for (const [id, { name, email }] of options.seed_names.entries()) {
case "EVENING": record.evening_hours += hours; by_employee.set(id, {
record.total_hours += hours; email,
break; employee_name: name,
case "EMERGENCY": record.emergency_hours += hours; regular_hours: 0,
record.total_hours += hours; other_hours: {
break; evening_hours: 0,
case "OVERTIME": record.overtime_hours += hours; emergency_hours: 0,
record.total_hours += hours; overtime_hours: 0,
break; sick_hours: 0,
case "REGULAR" : record.regular_hours += hours; holiday_hours: 0,
record.total_hours += hours; vacation_hours: 0,
break; },
} total_hours: 0,
expenses: 0,
mileage: 0,
is_approved: true,
is_remote: true,
});
}
}
record.is_approved = record.is_approved && shift.timesheet.is_approved; const ensure = (id: number, name: string, email: string) => {
record.is_remote = record.is_remote || !!shift.is_remote; 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) { async getSupervisor(email: string) {
const exp = expense.timesheet.employee; return this.prisma.employees.findFirst({
const name = `${exp.user.first_name} ${exp.user.last_name}`.trim(); where: { user: { email } },
const record = ensure(exp.id, name, exp.user.email); select: { id: true, is_supervisor: true },
});
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<PayPeriodDto[]> {
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<PayPeriodDto> {
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<PayPeriodDto> {
const iso_day = date ?? new Date().toISOString().slice(0,10);
return this.findByDate(iso_day);
}
async findOneByYearPeriod(pay_year: number, period_no: number): Promise<PayPeriodDto> {
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 findAll(): Promise<PayPeriodDto[]> {
async findByDate(date: string): Promise<PayPeriodDto> { const currentPayYear = payYearOfDate(new Date());
const dt = new Date(date); return listPayYear(currentPayYear).map(period => ({
const row = await this.prisma.payPeriods.findFirst({ pay_period_no: period.period_no,
where: { period_start: { lte: dt }, period_end: { gte: dt } }, pay_year: period.pay_year,
}); payday: period.payday,
if(row) return mapPayPeriodToDto(row); period_start: period.period_start,
period_end: period.period_end,
//fallback for outwside view periods label: period.label,
const pay_year = payYearOfDate(date); //add is_approved
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) { async findOne(period_no: number): Promise<PayPeriodDto> {
return this.prisma.payPeriods.findFirst({ const row = await this.prisma.payPeriods.findFirst({
where: {pay_year, pay_period_no: period_no }, where: { pay_period_no: period_no },
select: { period_start: true, period_end: true }, orderBy: { pay_year: "desc" },
}); });
} if (!row) throw new NotFoundException(`Pay period #${period_no} not found`);
return mapPayPeriodToDto(row);
}
async findCurrent(date?: string): Promise<PayPeriodDto> {
const iso_day = date ?? new Date().toISOString().slice(0, 10);
return this.findByDate(iso_day);
}
async findOneByYearPeriod(pay_year: number, period_no: number): Promise<PayPeriodDto> {
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<PayPeriodDto> {
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 },
});
}
} }