refactor(pay-period): ajusted logics services and controller of model pay-periods
This commit is contained in:
parent
ae6ce4bf97
commit
f765a99273
|
|
@ -3035,22 +3035,27 @@
|
||||||
"PayPeriodDto": {
|
"PayPeriodDto": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"period_number": {
|
"pay_period_no": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"example": 1,
|
"example": 1,
|
||||||
"description": "numéro cyclique de la période entre 1 et 26"
|
"description": "numéro cyclique de la période entre 1 et 26"
|
||||||
},
|
},
|
||||||
"start_date": {
|
"period_start": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "2023-12-17",
|
"example": "2023-12-17",
|
||||||
"format": "date"
|
"format": "date"
|
||||||
},
|
},
|
||||||
"end_date": {
|
"period_end": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "2023-12-30",
|
"example": "2023-12-30",
|
||||||
"format": "date"
|
"format": "date"
|
||||||
},
|
},
|
||||||
"year": {
|
"payday": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "2023-01-04",
|
||||||
|
"format": "date"
|
||||||
|
},
|
||||||
|
"pay_year": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"example": 2023
|
"example": 2023
|
||||||
},
|
},
|
||||||
|
|
@ -3060,10 +3065,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"period_number",
|
"pay_period_no",
|
||||||
"start_date",
|
"period_start",
|
||||||
"end_date",
|
"period_end",
|
||||||
"year",
|
"payday",
|
||||||
|
"pay_year",
|
||||||
"label"
|
"label"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -3155,28 +3161,34 @@
|
||||||
"PayPeriodOverviewDto": {
|
"PayPeriodOverviewDto": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"period_number": {
|
"pay_period_no": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"example": 1,
|
"example": 1,
|
||||||
"description": "Period number (1–26)"
|
"description": "Period number (1–26)"
|
||||||
},
|
},
|
||||||
"year": {
|
"pay_year": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"example": 2023,
|
"example": 2023,
|
||||||
"description": "Calendar year of the period"
|
"description": "Calendar year of the period"
|
||||||
},
|
},
|
||||||
"start_date": {
|
"period_start": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "2023-12-17",
|
"example": "2023-12-17",
|
||||||
"format": "date",
|
"format": "date",
|
||||||
"description": "Period start date (YYYY-MM-DD)"
|
"description": "Period start date (YYYY-MM-DD)"
|
||||||
},
|
},
|
||||||
"end_date": {
|
"period_end": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "2023-12-30",
|
"example": "2023-12-30",
|
||||||
"format": "date",
|
"format": "date",
|
||||||
"description": "Period end date (YYYY-MM-DD)"
|
"description": "Period end date (YYYY-MM-DD)"
|
||||||
},
|
},
|
||||||
|
"payday": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "2023-12-30",
|
||||||
|
"format": "date",
|
||||||
|
"description": "Period pay day(YYYY-MM-DD)"
|
||||||
|
},
|
||||||
"label": {
|
"label": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "2023-12-17 → 2023-12-30",
|
"example": "2023-12-17 → 2023-12-30",
|
||||||
|
|
@ -3191,10 +3203,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"period_number",
|
"pay_period_no",
|
||||||
"year",
|
"pay_year",
|
||||||
"start_date",
|
"period_start",
|
||||||
"end_date",
|
"period_end",
|
||||||
|
"payday",
|
||||||
"label",
|
"label",
|
||||||
"employees_overview"
|
"employees_overview"
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -5,39 +5,41 @@
|
||||||
CREATE OR REPLACE VIEW pay_period AS
|
CREATE OR REPLACE VIEW pay_period AS
|
||||||
WITH
|
WITH
|
||||||
anchor AS (
|
anchor AS (
|
||||||
SELECT '2023-12-17'::date AS anchor_date
|
SELECT '2023-12-17'::date AS anchor_sunday
|
||||||
),
|
|
||||||
current_pay_period AS(
|
|
||||||
SELECT
|
|
||||||
((now()::date - anchor_date) % 14) +1 AS current_day_in_pay_period
|
|
||||||
FROM anchor
|
|
||||||
),
|
|
||||||
bounds AS (
|
|
||||||
SELECT
|
|
||||||
(now()::date
|
|
||||||
- INTERVAL '6 months'
|
|
||||||
- (current_day_in_pay_period || ' days')::INTERVAL
|
|
||||||
)::date AS start_bound,
|
|
||||||
(now()::date + INTERVAL '1 month'
|
|
||||||
- (current_day_in_pay_period || ' days')::INTERVAL
|
|
||||||
)::date AS end_bound,
|
|
||||||
anchor.anchor_date
|
|
||||||
FROM anchor
|
|
||||||
CROSS JOIN current_pay_period
|
|
||||||
),
|
),
|
||||||
series AS (
|
series AS (
|
||||||
SELECT
|
SELECT
|
||||||
generate_series(bounds.start_bound, bounds.end_bound, '14 days') AS period_start,
|
gs::date AS period_start, -- Dimanche
|
||||||
bounds.anchor_date
|
(gs + INTERVAL '13 days')::date AS period_end, -- Samedi
|
||||||
FROM bounds
|
(gs + INTERVAL '18 days')::date AS payday -- Jeudi suivant pour viser l'année fiscale
|
||||||
|
FROM generate_series(
|
||||||
|
(SELECT anchor_sunday FROM anchor),
|
||||||
|
(CURRENT_DATE + INTERVAL '1 month')::date,
|
||||||
|
INTERVAL '14 days'
|
||||||
|
) AS gs
|
||||||
|
),
|
||||||
|
numbered AS (
|
||||||
|
SELECT
|
||||||
|
period_start,
|
||||||
|
period_end,
|
||||||
|
payday,
|
||||||
|
EXTRACT(YEAR FROM payday)::int AS pay_year,
|
||||||
|
ROW_NUMBER() OVER (
|
||||||
|
PARTITION BY EXTRACT(YEAR FROM payday)
|
||||||
|
ORDER BY payday
|
||||||
|
) AS pay_period_no
|
||||||
|
FROM series
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
((row_number() OVER (ORDER BY period_start) - 1) % 26) + 1 AS period_number,
|
pay_year,
|
||||||
period_start AS start_date,
|
pay_period_no,
|
||||||
period_start + INTERVAL '13 days' AS end_date,
|
period_start,
|
||||||
EXTRACT(YEAR FROM period_start)::int AS year,
|
period_end,
|
||||||
period_start || ' -> ' ||
|
payday,
|
||||||
to_char(period_start + INTERVAL '13 days', 'YYYY-MM-DD')
|
to_char(period_start, 'YYYY-MM-DD') || '->' ||
|
||||||
AS label
|
to_char(period_end, 'YYYY-MM-DD') AS label
|
||||||
FROM series
|
FROM numbered
|
||||||
ORDER BY period_start;
|
|
||||||
|
WHERE payday BETWEEN (CURRENT_DATE - INTERVAL '6 months')::date
|
||||||
|
AND (CURRENT_DATE + INTERVAL '1 month')::date
|
||||||
|
ORDER BY period_start;
|
||||||
|
|
@ -135,12 +135,13 @@ model LeaveRequestsArchive {
|
||||||
|
|
||||||
//pay-period vue
|
//pay-period vue
|
||||||
view PayPeriods {
|
view PayPeriods {
|
||||||
period_number Int
|
pay_year Int
|
||||||
start_date DateTime @db.Date
|
pay_period_no Int
|
||||||
end_date DateTime @db.Date
|
payday DateTime @db.Date
|
||||||
year Int
|
period_start DateTime @db.Date
|
||||||
label String
|
period_end DateTime @db.Date
|
||||||
|
label String
|
||||||
|
|
||||||
@@map("pay_period")
|
@@map("pay_period")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ export class HolidayService {
|
||||||
|
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
//switch employeeId for email
|
||||||
private async computeHoursPrevious4Weeks(employeeId: number, holidayDate: Date): Promise<number> {
|
private async computeHoursPrevious4Weeks(employeeId: number, holidayDate: Date): Promise<number> {
|
||||||
//sets the end of the window to 1ms before the week with the holiday
|
//sets the end of the window to 1ms before the week with the holiday
|
||||||
const holidayWeekStart = getWeekStart(holidayDate);
|
const holidayWeekStart = getWeekStart(holidayDate);
|
||||||
|
|
@ -31,6 +32,7 @@ export class HolidayService {
|
||||||
return dailyHours;
|
return dailyHours;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//switch employeeId for email
|
||||||
async calculateHolidayPay( employeeId: number, holidayDate: Date, modifier: number): Promise<number> {
|
async calculateHolidayPay( employeeId: number, holidayDate: Date, modifier: number): Promise<number> {
|
||||||
const hours = await this.computeHoursPrevious4Weeks(employeeId, holidayDate);
|
const hours = await this.computeHoursPrevious4Weeks(employeeId, holidayDate);
|
||||||
const dailyRate = Math.min(hours, 8);
|
const dailyRate = Math.min(hours, 8);
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ export class OvertimeService {
|
||||||
}
|
}
|
||||||
|
|
||||||
//calculate Weekly overtime
|
//calculate Weekly overtime
|
||||||
|
//switch employeeId for email
|
||||||
async getWeeklyOvertimeHours(employeeId: number, refDate: Date): Promise<number> {
|
async getWeeklyOvertimeHours(employeeId: number, refDate: Date): Promise<number> {
|
||||||
const weekStart = getWeekStart(refDate);
|
const weekStart = getWeekStart(refDate);
|
||||||
const weekEnd = getWeekEnd(weekStart);
|
const weekEnd = getWeekEnd(weekStart);
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ export class SickLeaveService {
|
||||||
|
|
||||||
private readonly logger = new Logger(SickLeaveService.name);
|
private readonly logger = new Logger(SickLeaveService.name);
|
||||||
|
|
||||||
|
//switch employeeId for email
|
||||||
async calculateSickLeavePay(employeeId: number, referenceDate: Date, daysRequested: number, modifier: number): Promise<number> {
|
async calculateSickLeavePay(employeeId: number, referenceDate: Date, daysRequested: number, modifier: number): Promise<number> {
|
||||||
//sets the year to jan 1st to dec 31st
|
//sets the year to jan 1st to dec 31st
|
||||||
const periodStart = getYearStart(referenceDate);
|
const periodStart = getYearStart(referenceDate);
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ export class VacationService {
|
||||||
* @param modifier Coefficient of hours(1)
|
* @param modifier Coefficient of hours(1)
|
||||||
* @returns amount of payable hours
|
* @returns amount of payable hours
|
||||||
*/
|
*/
|
||||||
|
//switch employeeId for email
|
||||||
async calculateVacationPay(employeeId: number, startDate: Date, daysRequested: number, modifier: number): Promise<number> {
|
async calculateVacationPay(employeeId: number, startDate: Date, daysRequested: number, modifier: number): Promise<number> {
|
||||||
//fetch hiring date
|
//fetch hiring date
|
||||||
const employee = await this.prisma.employees.findUnique({
|
const employee = await this.prisma.employees.findUnique({
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ export class CsvExportController {
|
||||||
|
|
||||||
//filters by type
|
//filters by type
|
||||||
const filtered = all.filter(r => {
|
const filtered = all.filter(r => {
|
||||||
switch (r.bankCode.toLocaleLowerCase()) {
|
switch (r.bank_code.toLocaleLowerCase()) {
|
||||||
case 'holiday' : return types.includes(ExportType.HOLIDAY);
|
case 'holiday' : return types.includes(ExportType.HOLIDAY);
|
||||||
case 'vacation' : return types.includes(ExportType.VACATION);
|
case 'vacation' : return types.includes(ExportType.VACATION);
|
||||||
case 'sick-leave': return types.includes(ExportType.SICK_LEAVE);
|
case 'sick-leave': return types.includes(ExportType.SICK_LEAVE);
|
||||||
|
|
|
||||||
|
|
@ -3,33 +3,33 @@ import { ExportCompany } from "../dtos/export-csv-options.dto";
|
||||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||||
|
|
||||||
export interface CsvRow {
|
export interface CsvRow {
|
||||||
companyCode: number;
|
company_code: number;
|
||||||
externalPayrollId: number;
|
external_payroll_id: number;
|
||||||
fullName: string;
|
full_name: string;
|
||||||
bankCode: string;
|
bank_code: string;
|
||||||
quantityHours?: number;
|
quantity_hours?: number;
|
||||||
amount?: number;
|
amount?: number;
|
||||||
weekNumber: number;
|
week_number: number;
|
||||||
payDate: string;
|
pay_date: string;
|
||||||
holidayDate?: string;
|
holiday_date?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CsvExportService {
|
export class CsvExportService {
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
async collectTransaction(periodId: number, companies: ExportCompany[]): Promise<CsvRow[]> {
|
async collectTransaction(period_id: number, companies: ExportCompany[]): Promise<CsvRow[]> {
|
||||||
const companyCodes = companies.map(c => c === ExportCompany.TARGO ? 1 : 2);
|
const companyCodes = companies.map(c => c === ExportCompany.TARGO ? 1 : 2);
|
||||||
|
|
||||||
const period = await this.prisma.payPeriods.findFirst({
|
const period = await this.prisma.payPeriods.findFirst({
|
||||||
where: { period_number: periodId },
|
where: { pay_period_no: period_id },
|
||||||
});
|
});
|
||||||
if(!period) {
|
if(!period) {
|
||||||
throw new NotFoundException(`Pay period ${periodId} not found`);
|
throw new NotFoundException(`Pay period ${period_id} not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const startDate = period.start_date;
|
const startDate = period.period_start;
|
||||||
const endDate = period.end_date;
|
const endDate = period.period_end;
|
||||||
|
|
||||||
//fetching shifts
|
//fetching shifts
|
||||||
const shifts = await this.prisma.shifts.findMany({
|
const shifts = await this.prisma.shifts.findMany({
|
||||||
|
|
@ -72,39 +72,39 @@ export class CsvExportService {
|
||||||
const rows: CsvRow[] = [];
|
const rows: CsvRow[] = [];
|
||||||
|
|
||||||
//Shifts Mapping
|
//Shifts Mapping
|
||||||
for (const s of shifts) {
|
for (const shift of shifts) {
|
||||||
const emp = s.timesheet.employee;
|
const emp = shift.timesheet.employee;
|
||||||
const weekNumber = this.computeWeekNumber(startDate, s.date);
|
const week_number = this.computeWeekNumber(startDate, shift.date);
|
||||||
const hours = this.computeHours(s.start_time, s.end_time);
|
const hours = this.computeHours(shift.start_time, shift.end_time);
|
||||||
|
|
||||||
rows.push({
|
rows.push({
|
||||||
companyCode: emp.company_code,
|
company_code: emp.company_code,
|
||||||
externalPayrollId: emp.external_payroll_id,
|
external_payroll_id: emp.external_payroll_id,
|
||||||
fullName: `${emp.user.first_name} ${emp.user.last_name}`,
|
full_name: `${emp.user.first_name} ${emp.user.last_name}`,
|
||||||
bankCode: s.bank_code.bank_code,
|
bank_code: shift.bank_code.bank_code,
|
||||||
quantityHours: hours,
|
quantity_hours: hours,
|
||||||
amount: undefined,
|
amount: undefined,
|
||||||
weekNumber,
|
week_number,
|
||||||
payDate: this.formatDate(endDate),
|
pay_date: this.formatDate(endDate),
|
||||||
holidayDate: undefined,
|
holiday_date: undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//Expenses Mapping
|
//Expenses Mapping
|
||||||
for (const e of expenses) {
|
for (const e of expenses) {
|
||||||
const emp = e.timesheet.employee;
|
const emp = e.timesheet.employee;
|
||||||
const weekNumber = this.computeWeekNumber(startDate, e.date);
|
const week_number = this.computeWeekNumber(startDate, e.date);
|
||||||
|
|
||||||
rows.push({
|
rows.push({
|
||||||
companyCode: emp.company_code,
|
company_code: emp.company_code,
|
||||||
externalPayrollId: emp.external_payroll_id,
|
external_payroll_id: emp.external_payroll_id,
|
||||||
fullName: `${emp.user.first_name} ${emp.user.last_name}`,
|
full_name: `${emp.user.first_name} ${emp.user.last_name}`,
|
||||||
bankCode: e.bank_code.bank_code,
|
bank_code: e.bank_code.bank_code,
|
||||||
quantityHours: undefined,
|
quantity_hours: undefined,
|
||||||
amount: Number(e.amount),
|
amount: Number(e.amount),
|
||||||
weekNumber,
|
week_number,
|
||||||
payDate: this.formatDate(endDate),
|
pay_date: this.formatDate(endDate),
|
||||||
holidayDate: undefined,
|
holiday_date: undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -115,56 +115,56 @@ export class CsvExportService {
|
||||||
const start = l.start_date_time;
|
const start = l.start_date_time;
|
||||||
const end = l.end_date_time ?? start;
|
const end = l.end_date_time ?? start;
|
||||||
|
|
||||||
const weekNumber = this.computeWeekNumber(startDate, start);
|
const week_number = this.computeWeekNumber(startDate, start);
|
||||||
const hours = this.computeHours(start, end);
|
const hours = this.computeHours(start, end);
|
||||||
|
|
||||||
rows.push({
|
rows.push({
|
||||||
companyCode: emp.company_code,
|
company_code: emp.company_code,
|
||||||
externalPayrollId: emp.external_payroll_id,
|
external_payroll_id: emp.external_payroll_id,
|
||||||
fullName: `${emp.user.first_name} ${emp.user.last_name}`,
|
full_name: `${emp.user.first_name} ${emp.user.last_name}`,
|
||||||
bankCode: l.bank_code.bank_code,
|
bank_code: l.bank_code.bank_code,
|
||||||
quantityHours: hours,
|
quantity_hours: hours,
|
||||||
amount: undefined,
|
amount: undefined,
|
||||||
weekNumber,
|
week_number,
|
||||||
payDate: this.formatDate(endDate),
|
pay_date: this.formatDate(endDate),
|
||||||
holidayDate: undefined,
|
holiday_date: undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//Final Mapping and sorts
|
//Final Mapping and sorts
|
||||||
return rows.sort((a,b) => {
|
return rows.sort((a,b) => {
|
||||||
if(a.externalPayrollId !== b.externalPayrollId) {
|
if(a.external_payroll_id !== b.external_payroll_id) {
|
||||||
return a.externalPayrollId - b.externalPayrollId;
|
return a.external_payroll_id - b.external_payroll_id;
|
||||||
}
|
}
|
||||||
if(a.bankCode !== b.bankCode) {
|
if(a.bank_code !== b.bank_code) {
|
||||||
return a.bankCode.localeCompare(b.bankCode);
|
return a.bank_code.localeCompare(b.bank_code);
|
||||||
}
|
}
|
||||||
return a.weekNumber - b.weekNumber;
|
return a.week_number - b.week_number;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
generateCsv(rows: CsvRow[]): Buffer {
|
generateCsv(rows: CsvRow[]): Buffer {
|
||||||
const header = [
|
const header = [
|
||||||
'companyCode',
|
'company_code',
|
||||||
'externalPayrolId',
|
'external_payrol_id',
|
||||||
'fullName',
|
'full_name',
|
||||||
'bankCode',
|
'bank_code',
|
||||||
'quantityHours',
|
'quantity_hours',
|
||||||
'amount',
|
'amount',
|
||||||
'weekNumber',
|
'week_number',
|
||||||
'payDate',
|
'pay_date',
|
||||||
'holidayDate',
|
'holiday_date',
|
||||||
].join(',') + '\n';
|
].join(',') + '\n';
|
||||||
|
|
||||||
const body = rows.map(r => [
|
const body = rows.map(r => [
|
||||||
r.companyCode,
|
r.company_code,
|
||||||
r.externalPayrollId,
|
r.external_payroll_id,
|
||||||
`${r.fullName.replace(/"/g, '""')}"`,
|
`${r.full_name.replace(/"/g, '""')}"`,
|
||||||
r.bankCode,
|
r.bank_code,
|
||||||
r.quantityHours?.toFixed(2) ?? '',
|
r.quantity_hours?.toFixed(2) ?? '',
|
||||||
r.weekNumber,
|
r.week_number,
|
||||||
r.payDate,
|
r.pay_date,
|
||||||
r.holidayDate ?? '',
|
r.holiday_date ?? '',
|
||||||
].join(',')).join('\n');
|
].join(',')).join('\n');
|
||||||
|
|
||||||
return Buffer.from('\uFEFF' + header + body, 'utf8');
|
return Buffer.from('\uFEFF' + header + body, 'utf8');
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
import { Controller, ForbiddenException, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Query } from "@nestjs/common";
|
import { Controller, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Query } from "@nestjs/common";
|
||||||
import { ApiNotFoundResponse, ApiOperation, ApiParam, ApiQuery, ApiResponse, ApiTags } from "@nestjs/swagger";
|
import { ApiNotFoundResponse, ApiOperation, ApiParam, ApiQuery, ApiResponse, ApiTags } from "@nestjs/swagger";
|
||||||
import { PayPeriodDto } from "../dtos/pay-period.dto";
|
import { PayPeriodDto } from "../dtos/pay-period.dto";
|
||||||
import { PayPeriodOverviewDto } from "../dtos/overview-pay-period.dto";
|
import { PayPeriodOverviewDto } from "../dtos/overview-pay-period.dto";
|
||||||
import { PayPeriodsQueryService } from "../services/pay-periods-query.service";
|
import { PayPeriodsQueryService } from "../services/pay-periods-query.service";
|
||||||
import { RolesAllowed } from "src/common/decorators/roles.decorators";
|
import { RolesAllowed } from "src/common/decorators/roles.decorators";
|
||||||
import { Roles as RoleEnum } from '.prisma/client';
|
import { Roles as RoleEnum } from '.prisma/client';
|
||||||
import { Req } from '@nestjs/common';
|
|
||||||
import { Request } from 'express';
|
|
||||||
import { PayPeriodsCommandService } from "../services/pay-periods-command.service";
|
import { PayPeriodsCommandService } from "../services/pay-periods-command.service";
|
||||||
import { PayPeriodBundleDto } from "../dtos/bundle-pay-period.dto";
|
import { PayPeriodBundleDto } from "../dtos/bundle-pay-period.dto";
|
||||||
|
|
||||||
|
|
@ -54,9 +52,9 @@ export class PayPeriodsController {
|
||||||
@ApiNotFoundResponse({ description: "Pay period not found" })
|
@ApiNotFoundResponse({ description: "Pay period not found" })
|
||||||
async findOneByYear(
|
async findOneByYear(
|
||||||
@Param("year", ParseIntPipe) year: number,
|
@Param("year", ParseIntPipe) year: number,
|
||||||
@Param("periodNumber", ParseIntPipe) periodNumber: number,
|
@Param("periodNumber", ParseIntPipe) period_no: number,
|
||||||
) {
|
) {
|
||||||
return this.queryService.findOneByYearPeriod(year, periodNumber);
|
return this.queryService.findOneByYearPeriod(year, period_no);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch("approval/:year/:periodNumber")
|
@Patch("approval/:year/:periodNumber")
|
||||||
|
|
@ -67,10 +65,10 @@ export class PayPeriodsController {
|
||||||
@ApiResponse({ status: 200, description: "Pay period approved" })
|
@ApiResponse({ status: 200, description: "Pay period approved" })
|
||||||
async approve(
|
async approve(
|
||||||
@Param("year", ParseIntPipe) year: number,
|
@Param("year", ParseIntPipe) year: number,
|
||||||
@Param("periodNumber", ParseIntPipe) periodNumber: number,
|
@Param("periodNumber", ParseIntPipe) period_no: number,
|
||||||
) {
|
) {
|
||||||
await this.commandService.approvalPayPeriod(year, periodNumber);
|
await this.commandService.approvalPayPeriod(year, period_no);
|
||||||
return { message: `Pay-period ${year}-${periodNumber} approved` };
|
return { message: `Pay-period ${year}-${period_no} approved` };
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':year/:periodNumber/:email')
|
@Get(':year/:periodNumber/:email')
|
||||||
|
|
@ -83,12 +81,11 @@ export class PayPeriodsController {
|
||||||
@ApiNotFoundResponse({ description: 'Pay period not found' })
|
@ApiNotFoundResponse({ description: 'Pay period not found' })
|
||||||
async getCrewOverview(
|
async getCrewOverview(
|
||||||
@Param('year', ParseIntPipe) year: number,
|
@Param('year', ParseIntPipe) year: number,
|
||||||
@Param('periodNumber', ParseIntPipe) periodNumber: number,
|
@Param('periodNumber', ParseIntPipe) period_no: number,
|
||||||
@Param('email') email: string,
|
@Param('email') email: string,
|
||||||
@Query('includeSubtree', new ParseBoolPipe({ optional: true })) includeSubtree = false,
|
@Query('includeSubtree', new ParseBoolPipe({ optional: true })) include_subtree = false,
|
||||||
@Req() req: Request,
|
|
||||||
): Promise<PayPeriodOverviewDto> {
|
): Promise<PayPeriodOverviewDto> {
|
||||||
return this.queryService.getCrewOverview(year, periodNumber, email, includeSubtree);
|
return this.queryService.getCrewOverview(year, period_no, email, include_subtree);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('overview/:year/:periodNumber')
|
@Get('overview/:year/:periodNumber')
|
||||||
|
|
@ -99,8 +96,8 @@ export class PayPeriodsController {
|
||||||
@ApiNotFoundResponse({ description: 'Pay period not found' })
|
@ApiNotFoundResponse({ description: 'Pay period not found' })
|
||||||
async getOverviewByYear(
|
async getOverviewByYear(
|
||||||
@Param('year', ParseIntPipe) year: number,
|
@Param('year', ParseIntPipe) year: number,
|
||||||
@Param('periodNumber', ParseIntPipe) periodNumber: number,
|
@Param('periodNumber', ParseIntPipe) period_no: number,
|
||||||
): Promise<PayPeriodOverviewDto> {
|
): Promise<PayPeriodOverviewDto> {
|
||||||
return this.queryService.getOverviewByYearPeriod(year, periodNumber);
|
return this.queryService.getOverviewByYearPeriod(year, period_no);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,10 @@ import { EmployeePeriodOverviewDto } from './overview-employee-period.dto';
|
||||||
|
|
||||||
export class PayPeriodOverviewDto {
|
export class PayPeriodOverviewDto {
|
||||||
@ApiProperty({ example: 1, description: 'Period number (1–26)' })
|
@ApiProperty({ example: 1, description: 'Period number (1–26)' })
|
||||||
period_number: number;
|
pay_period_no: number;
|
||||||
|
|
||||||
@ApiProperty({ example: 2023, description: 'Calendar year of the period' })
|
@ApiProperty({ example: 2023, description: 'Calendar year of the period' })
|
||||||
year: number;
|
pay_year: number;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
example: '2023-12-17',
|
example: '2023-12-17',
|
||||||
|
|
@ -14,7 +14,7 @@ export class PayPeriodOverviewDto {
|
||||||
format: 'date',
|
format: 'date',
|
||||||
description: "Period start date (YYYY-MM-DD)",
|
description: "Period start date (YYYY-MM-DD)",
|
||||||
})
|
})
|
||||||
start_date: string;
|
period_start: string;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
example: '2023-12-30',
|
example: '2023-12-30',
|
||||||
|
|
@ -22,7 +22,15 @@ export class PayPeriodOverviewDto {
|
||||||
format: 'date',
|
format: 'date',
|
||||||
description: "Period end date (YYYY-MM-DD)",
|
description: "Period end date (YYYY-MM-DD)",
|
||||||
})
|
})
|
||||||
end_date: string;
|
period_end: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: '2023-12-30',
|
||||||
|
type: String,
|
||||||
|
format: 'date',
|
||||||
|
description: "Period pay day(YYYY-MM-DD)",
|
||||||
|
})
|
||||||
|
payday: string;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
example: '2023-12-17 → 2023-12-30',
|
example: '2023-12-17 → 2023-12-30',
|
||||||
|
|
|
||||||
|
|
@ -3,18 +3,22 @@ import { ApiProperty } from "@nestjs/swagger";
|
||||||
export class PayPeriodDto {
|
export class PayPeriodDto {
|
||||||
@ApiProperty({ example: 1,
|
@ApiProperty({ example: 1,
|
||||||
description: 'numéro cyclique de la période entre 1 et 26' })
|
description: 'numéro cyclique de la période entre 1 et 26' })
|
||||||
period_number: number;
|
pay_period_no: number;
|
||||||
|
|
||||||
@ApiProperty({ example: '2023-12-17',
|
@ApiProperty({ example: '2023-12-17',
|
||||||
type: String, format: 'date' })
|
type: String, format: 'date' })
|
||||||
start_date: String;
|
period_start: string;
|
||||||
|
|
||||||
@ApiProperty({ example: '2023-12-30',
|
@ApiProperty({ example: '2023-12-30',
|
||||||
type: String, format: 'date' })
|
type: String, format: 'date' })
|
||||||
end_date: String;
|
period_end: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '2023-01-04',
|
||||||
|
type: String, format: 'date' })
|
||||||
|
payday: string;
|
||||||
|
|
||||||
@ApiProperty({ example: 2023 })
|
@ApiProperty({ example: 2023 })
|
||||||
year: number;
|
pay_year: number;
|
||||||
|
|
||||||
@ApiProperty({ example: '2023-12-17 → 2023-12-30' })
|
@ApiProperty({ example: '2023-12-17 → 2023-12-30' })
|
||||||
label: string;
|
label: string;
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,19 @@
|
||||||
import { PayPeriods } from "@prisma/client";
|
import { PayPeriods } from "@prisma/client";
|
||||||
import { PayPeriodDto } from "../dtos/pay-period.dto";
|
import { PayPeriodDto } from "../dtos/pay-period.dto";
|
||||||
import { payYearOfDate } from "../utils/pay-year.util";
|
|
||||||
|
|
||||||
const toDateString = (d: Date) => d.toISOString().slice(0, 10); // "YYYY-MM-DD"
|
const toDateString = (date: Date) => date.toISOString().slice(0, 10); // "YYYY-MM-DD"
|
||||||
|
|
||||||
export function mapPayPeriodToDto(row: PayPeriods): PayPeriodDto {
|
export function mapPayPeriodToDto(row: PayPeriods): PayPeriodDto {
|
||||||
const s = toDateString(row.start_date);
|
const start = toDateString(row.period_start);
|
||||||
const e = toDateString(row.end_date);
|
const end = toDateString(row.period_end);
|
||||||
|
const pay = toDateString(row.payday);
|
||||||
return {
|
return {
|
||||||
period_number: row.period_number,
|
pay_period_no: row.pay_period_no,
|
||||||
start_date: toDateString(row.start_date),
|
period_start: toDateString(row.period_start),
|
||||||
end_date: toDateString(row.end_date),
|
period_end: toDateString(row.period_end),
|
||||||
year: payYearOfDate(s),
|
payday:pay,
|
||||||
label: `${s} => ${e}`,
|
pay_year: new Date(pay).getFullYear(),
|
||||||
|
label: `${start} => ${end}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,26 +6,26 @@ import { PrismaService } from "src/prisma/prisma.service";
|
||||||
export class PayPeriodsCommandService {
|
export class PayPeriodsCommandService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
private readonly timesheetsApproval: TimesheetsCommandService,
|
private readonly timesheets_approval: TimesheetsCommandService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async approvalPayPeriod(year: number , periodNumber: number): Promise<void> {
|
async approvalPayPeriod(pay_year: number , period_no: number): Promise<void> {
|
||||||
const period = await this.prisma.payPeriods.findFirst({
|
const period = await this.prisma.payPeriods.findFirst({
|
||||||
where: { year, period_number: periodNumber},
|
where: { pay_year, pay_period_no: period_no},
|
||||||
});
|
});
|
||||||
if (!period) throw new NotFoundException(`PayPeriod #${year}-${periodNumber} not found`);
|
if (!period) throw new NotFoundException(`PayPeriod #${pay_year}-${period_no} not found`);
|
||||||
|
|
||||||
//fetches timesheet of selected period if the timesheet has atleast 1 shift or 1 expense
|
//fetches timesheet of selected period if the timesheet has atleast 1 shift or 1 expense
|
||||||
const timesheetList = await this.prisma.timesheets.findMany({
|
const timesheet_ist = await this.prisma.timesheets.findMany({
|
||||||
where: {
|
where: {
|
||||||
OR: [
|
OR: [
|
||||||
{ shift: {some: { date: { gte: period.start_date,
|
{ shift: {some: { date: { gte: period.period_start,
|
||||||
lte: period.end_date,
|
lte: period.period_end,
|
||||||
},
|
},
|
||||||
}},
|
}},
|
||||||
},
|
},
|
||||||
{ expense: { some: { date: { gte: period.start_date,
|
{ expense: { some: { date: { gte: period.period_start,
|
||||||
lte: period.end_date,
|
lte: period.period_end,
|
||||||
},
|
},
|
||||||
}},
|
}},
|
||||||
},
|
},
|
||||||
|
|
@ -36,8 +36,8 @@ export class PayPeriodsCommandService {
|
||||||
|
|
||||||
//approval of both timesheet (cascading to the approval of related shifts and expenses)
|
//approval of both timesheet (cascading to the approval of related shifts and expenses)
|
||||||
await this.prisma.$transaction(async (transaction)=> {
|
await this.prisma.$transaction(async (transaction)=> {
|
||||||
for(const {id} of timesheetList) {
|
for(const {id} of timesheet_ist) {
|
||||||
await this.timesheetsApproval.updateApprovalWithTx(transaction,id, true);
|
await this.timesheets_approval.updateApprovalWithTx(transaction,id, true);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,53 +9,65 @@ import { mapPayPeriodToDto } from "../mappers/pay-periods.mapper";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PayPeriodsQueryService {
|
export class PayPeriodsQueryService {
|
||||||
constructor(
|
constructor( private readonly prisma: PrismaService) {}
|
||||||
private readonly prisma: PrismaService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async getOverview(periodNumber: number): Promise<PayPeriodOverviewDto> {
|
async getOverviewByYearPeriod(pay_year: number, period_no: number): Promise<PayPeriodOverviewDto> {
|
||||||
const period = await this.prisma.payPeriods.findFirst({
|
const period = computePeriod(pay_year, period_no);
|
||||||
where: { period_number: periodNumber },
|
|
||||||
orderBy: { year: "desc" },
|
|
||||||
});
|
|
||||||
if (!period) throw new NotFoundException(`Period #${periodNumber} not found`);
|
|
||||||
return this.buildOverview(period);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getOverviewByYearPeriod(year: number, periodNumber: number): Promise<PayPeriodOverviewDto> {
|
|
||||||
const period = computePeriod(year, periodNumber);
|
|
||||||
return this.buildOverview({
|
return this.buildOverview({
|
||||||
start_date: period.start_date,
|
period_start: period.period_start,
|
||||||
end_date : period.end_date,
|
period_end : period.period_end,
|
||||||
period_number: period.period_number,
|
period_no : period.period_no,
|
||||||
year: period.year,
|
pay_year : period.pay_year,
|
||||||
label:period.label,
|
payday : period.payday,
|
||||||
|
label :period.label,
|
||||||
} as any);
|
} as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getOverview(pay_period_no: number): Promise<PayPeriodOverviewDto> {
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private async buildOverview(
|
private async buildOverview(
|
||||||
period: { start_date: string | Date; end_date: string | Date; period_number: number; year: number; label: string; },
|
period: { period_start: string | Date; period_end: string | Date; payday: string | Date;
|
||||||
options?: { filteredEmployeeIds?: number[]; seedNames?: Map<number, string> },
|
period_no: number; pay_year: number; label: string; },
|
||||||
|
options?: { filtered_employee_ids?: number[]; seed_names?: Map<number, string> },
|
||||||
): Promise<PayPeriodOverviewDto> {
|
): Promise<PayPeriodOverviewDto> {
|
||||||
const toDateString = (d: Date) => d.toISOString().slice(0, 10);
|
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 toMoney = (v: any) => (typeof v === "object" && "toNumber" in v ? v.toNumber() : (v as number));
|
||||||
|
|
||||||
const start = period.start_date instanceof Date
|
const start = period.period_start instanceof Date
|
||||||
? period.start_date
|
? period.period_start
|
||||||
: new Date(`${period.start_date}T00:00:00.000Z`);
|
: new Date(`${period.period_start}T00:00:00.000Z`);
|
||||||
|
|
||||||
const end = period.end_date instanceof Date
|
const end = period.period_end instanceof Date
|
||||||
? period.end_date
|
? period.period_end
|
||||||
: new Date(`${period.end_date}T00:00:00.000Z`);
|
: 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
|
//restrictEmployeeIds = filter for shifts and expenses by employees
|
||||||
const whereEmployee = options?.filteredEmployeeIds?.length ? { employee_id: { in: options.filteredEmployeeIds } }: {};
|
const where_employee = options?.filtered_employee_ids?.length ? { employee_id: { in: options.filtered_employee_ids } }: {};
|
||||||
|
|
||||||
// SHIFTS (filtered by crew)
|
// SHIFTS (filtered by crew)
|
||||||
const shifts = await this.prisma.shifts.findMany({
|
const shifts = await this.prisma.shifts.findMany({
|
||||||
where: {
|
where: {
|
||||||
date: { gte: start, lte: end },
|
date: { gte: start, lte: end },
|
||||||
timesheet: whereEmployee,
|
timesheet: where_employee,
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
start_time: true,
|
start_time: true,
|
||||||
|
|
@ -79,7 +91,7 @@ export class PayPeriodsQueryService {
|
||||||
const expenses = await this.prisma.expenses.findMany({
|
const expenses = await this.prisma.expenses.findMany({
|
||||||
where: {
|
where: {
|
||||||
date: { gte: start, lte: end },
|
date: { gte: start, lte: end },
|
||||||
timesheet: whereEmployee,
|
timesheet: where_employee,
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
amount: true,
|
amount: true,
|
||||||
|
|
@ -97,12 +109,12 @@ export class PayPeriodsQueryService {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const byEmployee = new Map<number, EmployeePeriodOverviewDto>();
|
const by_employee = new Map<number, EmployeePeriodOverviewDto>();
|
||||||
|
|
||||||
// seed for employee without data
|
// seed for employee without data
|
||||||
if (options?.seedNames) {
|
if (options?.seed_names) {
|
||||||
for (const [id, name] of options.seedNames.entries()) {
|
for (const [id, name] of options.seed_names.entries()) {
|
||||||
byEmployee.set(id, {
|
by_employee.set(id, {
|
||||||
employee_id: id,
|
employee_id: id,
|
||||||
employee_name: name,
|
employee_name: name,
|
||||||
regular_hours: 0,
|
regular_hours: 0,
|
||||||
|
|
@ -117,8 +129,8 @@ export class PayPeriodsQueryService {
|
||||||
}
|
}
|
||||||
|
|
||||||
const ensure = (id: number, name: string) => {
|
const ensure = (id: number, name: string) => {
|
||||||
if (!byEmployee.has(id)) {
|
if (!by_employee.has(id)) {
|
||||||
byEmployee.set(id, {
|
by_employee.set(id, {
|
||||||
employee_id: id,
|
employee_id: id,
|
||||||
employee_name: name,
|
employee_name: name,
|
||||||
regular_hours: 0,
|
regular_hours: 0,
|
||||||
|
|
@ -130,24 +142,24 @@ export class PayPeriodsQueryService {
|
||||||
is_approved: true,
|
is_approved: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return byEmployee.get(id)!;
|
return by_employee.get(id)!;
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const shift of shifts) {
|
for (const shift of shifts) {
|
||||||
const employee = shift.timesheet.employee;
|
const employee = shift.timesheet.employee;
|
||||||
const name = `${employee.user.first_name} ${employee.user.last_name}`.trim();
|
const name = `${employee.user.first_name} ${employee.user.last_name}`.trim();
|
||||||
const rec = ensure(employee.id, name);
|
const record = ensure(employee.id, name);
|
||||||
|
|
||||||
const hours = computeHours(shift.start_time, shift.end_time);
|
const hours = computeHours(shift.start_time, shift.end_time);
|
||||||
const categorie = (shift.bank_code?.categorie || "REGULAR").toUpperCase();
|
const categorie = (shift.bank_code?.categorie || "REGULAR").toUpperCase();
|
||||||
switch (categorie) {
|
switch (categorie) {
|
||||||
case "EVENING": rec.evening_hours += hours; break;
|
case "EVENING": record.evening_hours += hours; break;
|
||||||
case "EMERGENCY":
|
case "EMERGENCY":
|
||||||
case "URGENT": rec.emergency_hours += hours; break;
|
case "URGENT": record.emergency_hours += hours; break;
|
||||||
case "OVERTIME": rec.overtime_hours += hours; break;
|
case "OVERTIME": record.overtime_hours += hours; break;
|
||||||
default: rec.regular_hours += hours; break;
|
default: record.regular_hours += hours; break;
|
||||||
}
|
}
|
||||||
rec.is_approved = rec.is_approved && shift.timesheet.is_approved;
|
record.is_approved = record.is_approved && shift.timesheet.is_approved;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const expense of expenses) {
|
for (const expense of expenses) {
|
||||||
|
|
@ -166,25 +178,27 @@ export class PayPeriodsQueryService {
|
||||||
record.is_approved = record.is_approved && expense.timesheet.is_approved;
|
record.is_approved = record.is_approved && expense.timesheet.is_approved;
|
||||||
}
|
}
|
||||||
|
|
||||||
const employees_overview = Array.from(byEmployee.values()).sort((a, b) =>
|
const employees_overview = Array.from(by_employee.values()).sort((a, b) =>
|
||||||
a.employee_name.localeCompare(b.employee_name, "fr", { sensitivity: "base" }),
|
a.employee_name.localeCompare(b.employee_name, "fr", { sensitivity: "base" }),
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
period_number: period.period_number,
|
pay_period_no: period.period_no,
|
||||||
year: period.year,
|
pay_year: period.pay_year,
|
||||||
start_date: toDateString(start),
|
payday: toDateString(payd),
|
||||||
end_date: toDateString(end),
|
period_start: toDateString(start),
|
||||||
|
period_end: toDateString(end),
|
||||||
label: period.label,
|
label: period.label,
|
||||||
employees_overview,
|
employees_overview,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async getCrewOverview(year: number, periodNumber: number, email: string, includeSubtree: boolean): Promise<PayPeriodOverviewDto> {
|
async getCrewOverview(pay_year: number, period_no: number, email: string, include_subtree: boolean):
|
||||||
|
Promise<PayPeriodOverviewDto> {
|
||||||
// 1) Search for the period
|
// 1) Search for the period
|
||||||
const period = await this.prisma.payPeriods.findFirst({ where: { year, period_number: periodNumber } });
|
const period = await this.prisma.payPeriods.findFirst({ where: { pay_year, pay_period_no: period_no } });
|
||||||
if (!period) throw new NotFoundException(`Pay period ${year}-${periodNumber} not found`);
|
if (!period) throw new NotFoundException(`Pay period ${pay_year}-${period_no} not found`);
|
||||||
|
|
||||||
// 2) fetch supervisor
|
// 2) fetch supervisor
|
||||||
const supervisor = await this.prisma.employees.findFirst({
|
const supervisor = await this.prisma.employees.findFirst({
|
||||||
|
|
@ -199,34 +213,42 @@ export class PayPeriodsQueryService {
|
||||||
if (!supervisor.is_supervisor) throw new ForbiddenException('Employee is not a supervisor');
|
if (!supervisor.is_supervisor) throw new ForbiddenException('Employee is not a supervisor');
|
||||||
|
|
||||||
// 3)fetchs crew members
|
// 3)fetchs crew members
|
||||||
const crew = await this.resolveCrew(supervisor.id, includeSubtree); // [{ id, first_name, last_name }]
|
const crew = await this.resolveCrew(supervisor.id, include_subtree); // [{ id, first_name, last_name }]
|
||||||
const crewIds = crew.map(c => c.id);
|
const crew_ids = crew.map(c => c.id);
|
||||||
// seed names map for employee without data
|
// seed names map for employee without data
|
||||||
const seedNames = new Map<number, string>(crew.map(c => [c.id, `${c.first_name} ${c.last_name}`.trim()]));
|
const seed_names = new Map<number, string>(crew.map(crew => [crew.id, `${crew.first_name} ${crew.last_name}`.trim()]));
|
||||||
|
|
||||||
// 4) overview build
|
// 4) overview build
|
||||||
return this.buildOverview(period, { filteredEmployeeIds: crewIds, seedNames });
|
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,
|
||||||
|
}, { filtered_employee_ids: crew_ids, seed_names });
|
||||||
}
|
}
|
||||||
|
|
||||||
private async resolveCrew(supervisorId: number, includeSubtree: boolean): Promise<Array<{ id: number; first_name: string; last_name: string }>> {
|
private async resolveCrew(supervisor_id: number, include_subtree: boolean):
|
||||||
|
Promise<Array<{ id: number; first_name: string; last_name: string }>> {
|
||||||
const result: Array<{ id: number; first_name: string; last_name: string }> = [];
|
const result: Array<{ id: number; first_name: string; last_name: string }> = [];
|
||||||
|
|
||||||
let frontier = await this.prisma.employees.findMany({
|
let frontier = await this.prisma.employees.findMany({
|
||||||
where: { supervisor_id: supervisorId },
|
where: { supervisor_id: supervisor_id },
|
||||||
select: { id: true, user: { select: { first_name: true, last_name: true } } },
|
select: { id: true, user: { select: { first_name: true, last_name: true } } },
|
||||||
});
|
});
|
||||||
result.push(...frontier.map(e => ({ id: e.id, first_name: e.user.first_name, last_name: e.user.last_name })));
|
result.push(...frontier.map(emp => ({ id: emp.id, first_name: emp.user.first_name, last_name: emp.user.last_name })));
|
||||||
|
|
||||||
if (!includeSubtree) return result;
|
if (!include_subtree) return result;
|
||||||
|
|
||||||
while (frontier.length) {
|
while (frontier.length) {
|
||||||
const parentIds = frontier.map(e => e.id);
|
const parent_ids = frontier.map(emp => emp.id);
|
||||||
const next = await this.prisma.employees.findMany({
|
const next = await this.prisma.employees.findMany({
|
||||||
where: { supervisor_id: { in: parentIds } },
|
where: { supervisor_id: { in: parent_ids } },
|
||||||
select: { id: true, user: { select: { first_name: true, last_name: true } } },
|
select: { id: true, user: { select: { first_name: true, last_name: true } } },
|
||||||
});
|
});
|
||||||
if (next.length === 0) break;
|
if (next.length === 0) break;
|
||||||
result.push(...next.map(e => ({ id: e.id, first_name: e.user.first_name, last_name: e.user.last_name })));
|
result.push(...next.map(emp => ({ id: emp.id, first_name: emp.user.first_name, last_name: emp.user.last_name })));
|
||||||
frontier = next;
|
frontier = next;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -237,54 +259,69 @@ private async resolveCrew(supervisorId: number, includeSubtree: boolean): Promis
|
||||||
async findAll(): Promise<PayPeriodDto[]> {
|
async findAll(): Promise<PayPeriodDto[]> {
|
||||||
const currentPayYear = payYearOfDate(new Date());
|
const currentPayYear = payYearOfDate(new Date());
|
||||||
return listPayYear(currentPayYear).map(period =>({
|
return listPayYear(currentPayYear).map(period =>({
|
||||||
period_number: period.period_number,
|
pay_period_no: period.period_no,
|
||||||
year: period.year,
|
pay_year: period.pay_year,
|
||||||
start_date: period.start_date,
|
payday: period.payday,
|
||||||
end_date: period.end_date,
|
period_start: period.period_start,
|
||||||
|
period_end: period.period_end,
|
||||||
label: period.label,
|
label: period.label,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOne(periodNumber: number): Promise<PayPeriodDto> {
|
async findOne(period_no: number): Promise<PayPeriodDto> {
|
||||||
const row = await this.prisma.payPeriods.findFirst({
|
const row = await this.prisma.payPeriods.findFirst({
|
||||||
where: { period_number: periodNumber },
|
where: { pay_period_no: period_no },
|
||||||
orderBy: { year: "desc" },
|
orderBy: { pay_year: "desc" },
|
||||||
});
|
});
|
||||||
if (!row) throw new NotFoundException(`Pay period #${periodNumber} not found`);
|
if (!row) throw new NotFoundException(`Pay period #${period_no} not found`);
|
||||||
return mapPayPeriodToDto(row);
|
return mapPayPeriodToDto(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOneByYearPeriod(year: number, periodNumber: number): Promise<PayPeriodDto> {
|
async findOneByYearPeriod(pay_year: number, period_no: number): Promise<PayPeriodDto> {
|
||||||
const row = await this.prisma.payPeriods.findFirst({
|
const row = await this.prisma.payPeriods.findFirst({
|
||||||
where: { year, period_number: periodNumber },
|
where: { pay_year, pay_period_no: period_no },
|
||||||
});
|
});
|
||||||
if(row) return mapPayPeriodToDto(row);
|
if(row) return mapPayPeriodToDto(row);
|
||||||
|
|
||||||
// fallback for outside of view periods
|
// fallback for outside of view periods
|
||||||
const p = computePeriod(year, periodNumber);
|
const period = computePeriod(pay_year, period_no);
|
||||||
return {period_number: p.period_number, year: p.year, start_date: p.start_date, end_date: p.end_date, label: p.label}
|
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
|
//function to cherry pick a Date to find a period
|
||||||
async findByDate(date: string): Promise<PayPeriodDto> {
|
async findByDate(date: string): Promise<PayPeriodDto> {
|
||||||
const dt = new Date(date);
|
const dt = new Date(date);
|
||||||
const row = await this.prisma.payPeriods.findFirst({
|
const row = await this.prisma.payPeriods.findFirst({
|
||||||
where: { start_date: { lte: dt }, end_date: { gte: dt } },
|
where: { period_start: { lte: dt }, period_end: { gte: dt } },
|
||||||
});
|
});
|
||||||
if(row) return mapPayPeriodToDto(row);
|
if(row) return mapPayPeriodToDto(row);
|
||||||
|
|
||||||
//fallback for outwside view periods
|
//fallback for outwside view periods
|
||||||
const payYear = payYearOfDate(date);
|
const pay_year = payYearOfDate(date);
|
||||||
const periods = listPayYear(payYear);
|
const periods = listPayYear(pay_year);
|
||||||
const hit = periods.find(p => date >= p.start_date && date <= p.end_date);
|
const hit = periods.find(period => date >= period.period_start && date <= period.period_end);
|
||||||
if(!hit) throw new NotFoundException(`No period found for ${date}`);
|
if(!hit) throw new NotFoundException(`No period found for ${date}`);
|
||||||
|
|
||||||
return { period_number: hit.period_number, year: hit.year, start_date: hit.start_date, end_date:hit.end_date, label: hit.label}
|
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 findCurrent(date?: string): Promise<PayPeriodDto> {
|
async findCurrent(date?: string): Promise<PayPeriodDto> {
|
||||||
const isoDay = date ?? new Date().toISOString().slice(0,10);
|
const iso_day = date ?? new Date().toISOString().slice(0,10);
|
||||||
return this.findByDate(isoDay);
|
return this.findByDate(iso_day);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,6 @@ const toUTCDate = (iso: string | Date) => {
|
||||||
};
|
};
|
||||||
export const toDateString = (d: Date) => d.toISOString().slice(0, 10);
|
export const toDateString = (d: Date) => d.toISOString().slice(0, 10);
|
||||||
|
|
||||||
const ANCHOR = toUTCDate(ANCHOR_ISO);
|
|
||||||
|
|
||||||
export function payYearOfDate(date: string | Date, anchorISO = ANCHOR_ISO): number {
|
export function payYearOfDate(date: string | Date, anchorISO = ANCHOR_ISO): number {
|
||||||
const ANCHOR = toUTCDate(anchorISO);
|
const ANCHOR = toUTCDate(anchorISO);
|
||||||
const d = toUTCDate(date);
|
const d = toUTCDate(date);
|
||||||
|
|
@ -19,23 +17,25 @@ export function payYearOfDate(date: string | Date, anchorISO = ANCHOR_ISO): numb
|
||||||
return ANCHOR.getUTCFullYear() + 1 + cycles;
|
return ANCHOR.getUTCFullYear() + 1 + cycles;
|
||||||
}
|
}
|
||||||
//compute labels for periods
|
//compute labels for periods
|
||||||
export function computePeriod(payYear: number, periodNumber: number, anchorISO = ANCHOR_ISO) {
|
export function computePeriod(pay_year: number, period_no: number, anchorISO = ANCHOR_ISO) {
|
||||||
const ANCHOR = toUTCDate(anchorISO);
|
const ANCHOR = toUTCDate(anchorISO);
|
||||||
const cycles = payYear - (ANCHOR.getUTCFullYear() + 1);
|
const cycles = pay_year - (ANCHOR.getUTCFullYear() + 1);
|
||||||
const offsetPeriods = cycles * PERIODS_PER_YEAR + (periodNumber - 1);
|
const offsetPeriods = cycles * PERIODS_PER_YEAR + (period_no - 1);
|
||||||
const start = new Date(+ANCHOR + offsetPeriods * PERIOD_DAYS * MS_PER_DAY);
|
const start = new Date(+ANCHOR + offsetPeriods * PERIOD_DAYS * MS_PER_DAY);
|
||||||
const end = new Date(+start + (PERIOD_DAYS - 1) * MS_PER_DAY);
|
const end = new Date(+start + (PERIOD_DAYS - 1) * MS_PER_DAY);
|
||||||
|
const pay = new Date(end.getTime() + 6 * MS_PER_DAY);
|
||||||
return {
|
return {
|
||||||
period_number: periodNumber,
|
period_no: period_no,
|
||||||
year: payYear,
|
pay_year: pay_year,
|
||||||
start_date: toDateString(start),
|
payday: toDateString(pay),
|
||||||
end_date: toDateString(end),
|
period_start: toDateString(start),
|
||||||
|
period_end: toDateString(end),
|
||||||
label: `${toDateString(start)} → ${toDateString(end)}`,
|
label: `${toDateString(start)} → ${toDateString(end)}`,
|
||||||
start, end,
|
start, end,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
//list of all 26 periods for a full year
|
//list of all 26 periods for a full year
|
||||||
export function listPayYear(payYear: number, anchorISO = ANCHOR_ISO) {
|
export function listPayYear(pay_year: number, anchorISO = ANCHOR_ISO) {
|
||||||
return Array.from({ length: PERIODS_PER_YEAR }, (_, i) => computePeriod(payYear, i + 1, anchorISO));
|
return Array.from({ length: PERIODS_PER_YEAR }, (_, i) => computePeriod(pay_year, i + 1, anchorISO));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -117,16 +117,16 @@ export class ShiftsQueryService {
|
||||||
async getSummary(period_id: number): Promise<OverviewRow[]> {
|
async getSummary(period_id: number): Promise<OverviewRow[]> {
|
||||||
//fetch pay-period to display
|
//fetch pay-period to display
|
||||||
const period = await this.prisma.payPeriods.findFirst({
|
const period = await this.prisma.payPeriods.findFirst({
|
||||||
where: { period_number: period_id },
|
where: { pay_period_no: period_id },
|
||||||
});
|
});
|
||||||
if(!period) {
|
if(!period) {
|
||||||
throw new NotFoundException(`pay-period ${period_id} not found`);
|
throw new NotFoundException(`pay-period ${period_id} not found`);
|
||||||
}
|
}
|
||||||
const { start_date, end_date } = period;
|
const { period_start, period_end } = period;
|
||||||
|
|
||||||
//prepare shifts and expenses for display
|
//prepare shifts and expenses for display
|
||||||
const shifts = await this.prisma.shifts.findMany({
|
const shifts = await this.prisma.shifts.findMany({
|
||||||
where: { date: { gte: start_date, lte: end_date } },
|
where: { date: { gte: period_start, lte: period_end } },
|
||||||
include: {
|
include: {
|
||||||
bank_code: true,
|
bank_code: true,
|
||||||
timesheet: { include: {
|
timesheet: { include: {
|
||||||
|
|
@ -139,7 +139,7 @@ export class ShiftsQueryService {
|
||||||
});
|
});
|
||||||
|
|
||||||
const expenses = await this.prisma.expenses.findMany({
|
const expenses = await this.prisma.expenses.findMany({
|
||||||
where: { date: { gte: start_date, lte: end_date } },
|
where: { date: { gte: period_start, lte: period_end } },
|
||||||
include: {
|
include: {
|
||||||
bank_code: true,
|
bank_code: true,
|
||||||
timesheet: { include: { employee: {
|
timesheet: { include: { employee: {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user