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:
parent
7c7edea768
commit
5a1017f82b
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 },
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user