Merge branch 'main' of git.targo.ca:Targo/targo_backend
This commit is contained in:
commit
6f9ba599a0
93
Dockerfile
93
Dockerfile
|
|
@ -1,46 +1,47 @@
|
||||||
# Use the official Node.js image as the base image
|
# Use the official Node.js image as the base image
|
||||||
FROM node:22
|
FROM node:22
|
||||||
|
|
||||||
# Set the working directory inside the container
|
# Set the working directory inside the container
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Set the environment variables
|
#Commentaire de fred pour faire un test
|
||||||
ENV DATABASE_URL_PROD="postgresql://apptargo:6wLAZrb0HZnd3mrmqXiArPcqLyui0o9e@10.100.0.116/app_targo_db?schema=public"
|
# Set the environment variables
|
||||||
ENV DATABASE_URL_STAGING="postgresql://apptargo:6wLAZrb0HZnd3mrmqXiArPcqLyui0o9e@10.100.0.116/app_targo_db_staging?schema=public"
|
ENV DATABASE_URL_PROD="postgresql://apptargo:6wLAZrb0HZnd3mrmqXiArPcqLyui0o9e@10.100.0.116/app_targo_db?schema=public"
|
||||||
ENV DATABASE_URL_DEV="postgresql://apptargo:6wLAZrb0HZnd3mrmqXiArPcqLyui0o9e@10.100.0.116/app_targo_db_dev?schema=public"
|
ENV DATABASE_URL_STAGING="postgresql://apptargo:6wLAZrb0HZnd3mrmqXiArPcqLyui0o9e@10.100.0.116/app_targo_db_staging?schema=public"
|
||||||
|
ENV DATABASE_URL_DEV="postgresql://apptargo:6wLAZrb0HZnd3mrmqXiArPcqLyui0o9e@10.100.0.116/app_targo_db_dev?schema=public"
|
||||||
ENV AUTHENTIK_ISSUER="https://auth.targo.ca/application/o/montargo/"
|
|
||||||
ENV AUTHENTIK_CLIENT_ID="KUmSmvpu2aDDy4JfNwas7XriNFtPcj2Ka2PyLO5v"
|
ENV AUTHENTIK_ISSUER="https://auth.targo.ca/application/o/montargo/"
|
||||||
ENV AUTHENTIK_CLIENT_SECRET="N55BgX1mxT7eiY99LOo5zXr5cKz9FgTsaCA9MdC7D8ZuhOGqozvqtNXVGbpY1eCg2kkYwJeJLP89sQ8R4cYybIJI7EwKijb19bzZQpUPwBosWwG3irUwdTnZOyw8yW5i"
|
ENV AUTHENTIK_CLIENT_ID="KUmSmvpu2aDDy4JfNwas7XriNFtPcj2Ka2PyLO5v"
|
||||||
ENV AUTHENTIK_CALLBACK_URL="http://10.100.251.2:3420/auth/callback"
|
ENV AUTHENTIK_CLIENT_SECRET="N55BgX1mxT7eiY99LOo5zXr5cKz9FgTsaCA9MdC7D8ZuhOGqozvqtNXVGbpY1eCg2kkYwJeJLP89sQ8R4cYybIJI7EwKijb19bzZQpUPwBosWwG3irUwdTnZOyw8yW5i"
|
||||||
ENV AUTHENTIK_AUTH_URL="https://auth.targo.ca/application/o/authorize/"
|
ENV AUTHENTIK_CALLBACK_URL="http://10.100.251.2:3420/auth/callback"
|
||||||
ENV AUTHENTIK_TOKEN_URL="https://auth.targo.ca/application/o/token/"
|
ENV AUTHENTIK_AUTH_URL="https://auth.targo.ca/application/o/authorize/"
|
||||||
ENV AUTHENTIK_USERINFO_URL="https://auth.targo.ca/application/o/userinfo/"
|
ENV AUTHENTIK_TOKEN_URL="https://auth.targo.ca/application/o/token/"
|
||||||
|
ENV AUTHENTIK_USERINFO_URL="https://auth.targo.ca/application/o/userinfo/"
|
||||||
ENV TARGO_FRONTEND_URI="http://10.100.251.2/"
|
|
||||||
|
ENV TARGO_FRONTEND_URI="http://10.100.251.2/"
|
||||||
ENV ATTACHMENTS_SERVER_ID="server"
|
|
||||||
ENV ATTACHMENTS_ROOT=C:/
|
ENV ATTACHMENTS_SERVER_ID="server"
|
||||||
ENV MAX_UPLOAD_MB=25
|
ENV ATTACHMENTS_ROOT=C:/
|
||||||
ENV ALLOWED_MIME=image/jpeg,image/png,image/webp,application/pdf
|
ENV MAX_UPLOAD_MB=25
|
||||||
|
ENV ALLOWED_MIME=image/jpeg,image/png,image/webp,application/pdf
|
||||||
# Copy package.json and package-lock.json to the working directory
|
|
||||||
COPY package*.json ./
|
# Copy package.json and package-lock.json to the working directory
|
||||||
|
COPY package*.json ./
|
||||||
# Install the application dependencies
|
|
||||||
RUN npm install
|
# Install the application dependencies
|
||||||
|
RUN npm install
|
||||||
# Copy the rest of the application files
|
|
||||||
COPY . .
|
# Copy the rest of the application files
|
||||||
|
COPY . .
|
||||||
# Generate Prisma client
|
|
||||||
RUN npx prisma generate
|
# Generate Prisma client
|
||||||
|
RUN npx prisma generate
|
||||||
# Build the NestJS application
|
|
||||||
RUN npm run build
|
# Build the NestJS application
|
||||||
|
RUN npm run build
|
||||||
# Expose the application port
|
|
||||||
EXPOSE 3000
|
# Expose the application port
|
||||||
|
EXPOSE 3000
|
||||||
# Command to run the application
|
|
||||||
CMD ["node", "dist/src/main"]
|
# Command to run the application
|
||||||
|
CMD ["node", "dist/src/main"]
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,13 @@ export class PayPeriodOverviewDto {
|
||||||
|
|
||||||
export class EmployeePeriodOverviewDto {
|
export class EmployeePeriodOverviewDto {
|
||||||
email: string;
|
email: string;
|
||||||
employee_name: string;
|
employee_first_name: string;
|
||||||
|
employee_last_name: string;
|
||||||
|
supervisor?: {
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
|
email: string;
|
||||||
|
} | null;
|
||||||
is_active: boolean;
|
is_active: boolean;
|
||||||
regular_hours: number;
|
regular_hours: number;
|
||||||
other_hours: {
|
other_hours: {
|
||||||
|
|
@ -25,7 +31,6 @@ export class EmployeePeriodOverviewDto {
|
||||||
expenses: number;
|
expenses: number;
|
||||||
mileage: number;
|
mileage: number;
|
||||||
is_approved: boolean;
|
is_approved: boolean;
|
||||||
is_remote: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PayPeriodBundleDto {
|
export class PayPeriodBundleDto {
|
||||||
|
|
@ -40,4 +45,19 @@ export class PayPeriodDto {
|
||||||
payday: string;
|
payday: string;
|
||||||
pay_year: number;
|
pay_year: number;
|
||||||
label: string;
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Overview = {
|
||||||
|
period_start: string;
|
||||||
|
period_end: string;
|
||||||
|
payday: string;
|
||||||
|
period_no: number;
|
||||||
|
pay_year: number;
|
||||||
|
label: string;
|
||||||
|
options?: options;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type options = {
|
||||||
|
filtered_employee_ids?: number[];
|
||||||
|
seed_names?: Map<number, { first_name: string, last_name: string, email: string }>
|
||||||
}
|
}
|
||||||
|
|
@ -1,33 +1,21 @@
|
||||||
import { Body, Controller, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Query, UnauthorizedException } from "@nestjs/common";
|
import { Body, Controller, Get, Param, ParseIntPipe, Patch } from "@nestjs/common";
|
||||||
import { PayPeriodBundleDto, 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 { PayPeriodsCommandService } from "./services/pay-periods-command.service";
|
import { PayPeriodsCommandService } from "./services/pay-periods-command.service";
|
||||||
import { Result } from "src/common/errors/result-error.factory";
|
import { Result } from "src/common/errors/result-error.factory";
|
||||||
import { ModuleAccessAllowed } from "src/common/decorators/modules-guard.decorators";
|
import { ModuleAccessAllowed } from "src/common/decorators/modules-guard.decorators";
|
||||||
import { Modules as ModulesEnum } from ".prisma/client";
|
import { Modules as ModulesEnum } from ".prisma/client";
|
||||||
import { Access } from "src/common/decorators/module-access.decorators";
|
import { GetOverviewService } from "src/time-and-attendance/pay-period/services/pay-periods-build-overview.service";
|
||||||
import { BulkCrewApprovalDto } from "src/time-and-attendance/pay-period/dtos/bulk-crew-approval.dto";
|
|
||||||
|
|
||||||
@Controller('pay-periods')
|
@Controller('pay-periods')
|
||||||
export class PayPeriodsController {
|
export class PayPeriodsController {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly queryService: PayPeriodsQueryService,
|
private readonly queryService: PayPeriodsQueryService,
|
||||||
|
private readonly getOverviewService: GetOverviewService,
|
||||||
private readonly commandService: PayPeriodsCommandService,
|
private readonly commandService: PayPeriodsCommandService,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
@Get('current-and-all')
|
|
||||||
@ModuleAccessAllowed(ModulesEnum.timesheets)
|
|
||||||
async getCurrentAndAll(@Query('date') date?: string): Promise<Result<PayPeriodBundleDto, string>> {
|
|
||||||
const current = await this.queryService.findCurrent(date);
|
|
||||||
if (!current.success) return { success: false, error: 'INVALID_PAY_PERIOD' };
|
|
||||||
|
|
||||||
const periods = await this.queryService.findAll();
|
|
||||||
if (!periods.success) return { success: false, error: 'INVALID_PAY_PERIOD' };
|
|
||||||
|
|
||||||
return { success: true, data: { current: current.data, periods: periods.data } };
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get("date/:date")
|
@Get("date/:date")
|
||||||
@ModuleAccessAllowed(ModulesEnum.timesheets)
|
@ModuleAccessAllowed(ModulesEnum.timesheets)
|
||||||
async findByDate(@Param("date") date: string) {
|
async findByDate(@Param("date") date: string) {
|
||||||
|
|
@ -56,22 +44,12 @@ export class PayPeriodsController {
|
||||||
return this.commandService.bulkApproveEmployee(email, timesheet_ids, is_approved);
|
return this.commandService.bulkApproveEmployee(email, timesheet_ids, is_approved);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('crew/:year/:periodNumber')
|
|
||||||
@ModuleAccessAllowed(ModulesEnum.timesheets_approval)
|
|
||||||
async getCrewOverview(@Access('email') email: string,
|
|
||||||
@Param('year', ParseIntPipe) year: number,
|
|
||||||
@Param('periodNumber', ParseIntPipe) period_no: number,
|
|
||||||
@Query('includeSubtree', new ParseBoolPipe({ optional: true })) include_subtree = false,
|
|
||||||
): Promise<Result<PayPeriodOverviewDto, string>> {
|
|
||||||
return this.queryService.getCrewOverview(year, period_no, email, include_subtree);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('overview/:year/:periodNumber')
|
@Get('overview/:year/:periodNumber')
|
||||||
@ModuleAccessAllowed(ModulesEnum.timesheets_approval)
|
@ModuleAccessAllowed(ModulesEnum.timesheets_approval)
|
||||||
async getOverviewByYear(
|
async getOverviewByYear(
|
||||||
@Param('year', ParseIntPipe) year: number,
|
@Param('year', ParseIntPipe) year: number,
|
||||||
@Param('periodNumber', ParseIntPipe) period_no: number,
|
@Param('periodNumber', ParseIntPipe) period_no: number,
|
||||||
): Promise<Result<PayPeriodOverviewDto, string>> {
|
): Promise<Result<PayPeriodOverviewDto, string>> {
|
||||||
return this.queryService.getOverviewByYearPeriod(year, period_no);
|
return this.getOverviewService.getOverviewByYearPeriod(year, period_no);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,15 @@ import { Module } from "@nestjs/common";
|
||||||
import { PayPeriodsCommandService } from "src/time-and-attendance/pay-period/services/pay-periods-command.service";
|
import { PayPeriodsCommandService } from "src/time-and-attendance/pay-period/services/pay-periods-command.service";
|
||||||
import { TimesheetsModule } from "src/time-and-attendance/timesheets/timesheets.module";
|
import { TimesheetsModule } from "src/time-and-attendance/timesheets/timesheets.module";
|
||||||
import { EmailToIdResolver } from "src/common/mappers/email-id.mapper";
|
import { EmailToIdResolver } from "src/common/mappers/email-id.mapper";
|
||||||
|
import { GetOverviewService } from "src/time-and-attendance/pay-period/services/pay-periods-build-overview.service";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports:[TimesheetsModule],
|
imports:[TimesheetsModule],
|
||||||
controllers: [PayPeriodsController],
|
controllers: [PayPeriodsController],
|
||||||
providers: [
|
providers: [
|
||||||
PayPeriodsQueryService,
|
PayPeriodsQueryService,
|
||||||
PayPeriodsCommandService,
|
PayPeriodsCommandService,
|
||||||
|
GetOverviewService,
|
||||||
EmailToIdResolver,
|
EmailToIdResolver,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,222 @@
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { Result } from "src/common/errors/result-error.factory";
|
||||||
|
import { computeHours, computePeriod, toDateFromString } from "src/common/utils/date-utils";
|
||||||
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
import { EmployeePeriodOverviewDto, Overview, PayPeriodOverviewDto } from "src/time-and-attendance/pay-period/dtos/overview-pay-period.dto";
|
||||||
|
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GetOverviewService {
|
||||||
|
constructor(
|
||||||
|
private readonly prisma: PrismaService,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
async getOverviewByYearPeriod(pay_year: number, period_no: number): Promise<Result<PayPeriodOverviewDto, string>> {
|
||||||
|
const period = computePeriod(pay_year, period_no);
|
||||||
|
const overview = await 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,
|
||||||
|
|
||||||
|
});
|
||||||
|
if (!overview.success) return { success: false, error: 'INVALID_PAY_PERIOD' }
|
||||||
|
return { success: true, data: overview.data }
|
||||||
|
}
|
||||||
|
|
||||||
|
async buildOverview(overview: Overview): Promise<Result<PayPeriodOverviewDto, string>> {
|
||||||
|
const employee_overviews = await this.prisma.employees.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ last_work_day: { gte: toDateFromString(overview.period_start) } },
|
||||||
|
{ last_work_day: null },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
last_work_day: true,
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
first_name: true,
|
||||||
|
last_name: true,
|
||||||
|
email: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
supervisor: {
|
||||||
|
select: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
first_name: true,
|
||||||
|
last_name: true,
|
||||||
|
email: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
timesheet: {
|
||||||
|
where: {
|
||||||
|
start_date: { gte: toDateFromString(overview.period_start), lte: toDateFromString(overview.period_end) },
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
is_approved: true,
|
||||||
|
shift: {
|
||||||
|
select: {
|
||||||
|
start_time: true,
|
||||||
|
end_time: true,
|
||||||
|
bank_code: { select: { type: true } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expense: {
|
||||||
|
select: {
|
||||||
|
amount: true,
|
||||||
|
bank_code: { select: { type: true, modifier: true } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: { user: { first_name: 'asc' } },
|
||||||
|
});
|
||||||
|
|
||||||
|
const by_employee = new Map<number, EmployeePeriodOverviewDto>();
|
||||||
|
|
||||||
|
// seed for employee without data
|
||||||
|
if (overview.options?.seed_names) {
|
||||||
|
for (const [id, { first_name, last_name, email }] of overview.options.seed_names.entries()) {
|
||||||
|
by_employee.set(id, this.createEmployeeSeeds(email, first_name, last_name));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const employee of employee_overviews) {
|
||||||
|
const record = this.createEmployeeSeeds(
|
||||||
|
employee.user.email,
|
||||||
|
employee.user.first_name,
|
||||||
|
employee.user.last_name,
|
||||||
|
employee.supervisor?.user ?? null,
|
||||||
|
);
|
||||||
|
by_employee.set(employee.id, record);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ensure = (id: number, first_name: string, last_name: string, email: string) => {
|
||||||
|
if (!by_employee.has(id)) {
|
||||||
|
by_employee.set(id, this.createEmployeeSeeds(email, first_name, last_name));
|
||||||
|
}
|
||||||
|
return by_employee.get(id)!;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const employee of employee_overviews) {
|
||||||
|
const record = ensure(
|
||||||
|
employee.id,
|
||||||
|
employee.user.first_name,
|
||||||
|
employee.user.last_name,
|
||||||
|
employee.user.email
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const timesheet of employee.timesheet) {
|
||||||
|
//totals by types for shifts
|
||||||
|
for (const shift of timesheet.shift) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//totals by type for expenses
|
||||||
|
for (const expense of timesheet.expense) {
|
||||||
|
const amount = Number(expense.amount)
|
||||||
|
record.expenses = Number((record.expenses + amount).toFixed(2));
|
||||||
|
const type = (expense.bank_code?.type || "").toUpperCase();
|
||||||
|
const rate = expense.bank_code?.modifier ?? 1;
|
||||||
|
const mileage = amount / rate;
|
||||||
|
if (type === "MILEAGE" && rate > 0) {
|
||||||
|
record.mileage = Number((record.mileage += Math.round(mileage)).toFixed(2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const employee of employee_overviews) {
|
||||||
|
const record = by_employee.get(employee.id);
|
||||||
|
if (!record) continue;
|
||||||
|
const timesheets = employee.timesheet;
|
||||||
|
const has_data = timesheets.some(timesheet => timesheet.shift.length > 0 || timesheet.expense.length > 0);
|
||||||
|
|
||||||
|
const cutoff_date = new Date();
|
||||||
|
cutoff_date.setDate(cutoff_date.getDate() + 14);
|
||||||
|
const is_active = employee.last_work_day ? employee.last_work_day.getTime() >= cutoff_date.getTime() : true;
|
||||||
|
|
||||||
|
record.is_approved = has_data && timesheets.every(timesheet => timesheet.is_approved === true);
|
||||||
|
record.is_active = is_active;
|
||||||
|
}
|
||||||
|
|
||||||
|
const employees_overview = Array.from(by_employee.values()).sort((a, b) =>
|
||||||
|
a.employee_first_name.localeCompare(b.employee_first_name, "fr", { sensitivity: "base" }),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
pay_period_no: overview.period_no,
|
||||||
|
pay_year: overview.pay_year,
|
||||||
|
payday: overview.payday,
|
||||||
|
period_start: (overview.period_start),
|
||||||
|
period_end: overview.period_end,
|
||||||
|
label: overview.label,
|
||||||
|
employees_overview,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
createEmployeeSeeds = (
|
||||||
|
email: string,
|
||||||
|
employee_first_name: string,
|
||||||
|
employee_last_name: string,
|
||||||
|
supervisor: {
|
||||||
|
first_name: string;
|
||||||
|
last_name:string;
|
||||||
|
email: string;
|
||||||
|
} | null = null,
|
||||||
|
): EmployeePeriodOverviewDto => ({
|
||||||
|
email,
|
||||||
|
employee_first_name,
|
||||||
|
employee_last_name,
|
||||||
|
supervisor: supervisor ?? null,
|
||||||
|
is_active: true,
|
||||||
|
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: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
import { PayPeriodsQueryService } from "./pay-periods-query.service";
|
|
||||||
import { TimesheetApprovalService } from "src/time-and-attendance/timesheets/services/timesheet-approval.service";
|
|
||||||
import { Result } from "src/common/errors/result-error.factory";
|
import { Result } from "src/common/errors/result-error.factory";
|
||||||
import { EmailToIdResolver } from "src/common/mappers/email-id.mapper";
|
import { EmailToIdResolver } from "src/common/mappers/email-id.mapper";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
|
|
@ -12,8 +10,6 @@ import { Prisma } from "@prisma/client";
|
||||||
export class PayPeriodsCommandService {
|
export class PayPeriodsCommandService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
private readonly timesheetsApproval: TimesheetApprovalService,
|
|
||||||
private readonly query: PayPeriodsQueryService,
|
|
||||||
private readonly emailResolver: EmailToIdResolver,
|
private readonly emailResolver: EmailToIdResolver,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,415 +1,14 @@
|
||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
import { computeHours, computePeriod, listPayYear, payYearOfDate, toStringFromDate } from "src/common/utils/date-utils";
|
import { computePeriod, listPayYear, payYearOfDate } from "src/common/utils/date-utils";
|
||||||
import { EmployeePeriodOverviewDto, PayPeriodDto, PayPeriodOverviewDto } from "../dtos/overview-pay-period.dto";
|
import { PayPeriodDto } from "../dtos/overview-pay-period.dto";
|
||||||
import { Result } from "src/common/errors/result-error.factory";
|
import { Result } from "src/common/errors/result-error.factory";
|
||||||
import { mapPayPeriodToDto } from "src/time-and-attendance/pay-period/pay-periods.mapper";
|
import { mapPayPeriodToDto } from "src/time-and-attendance/pay-period/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<Result<PayPeriodOverviewDto, string>> {
|
|
||||||
const period = await this.prisma.payPeriods.findFirst({
|
|
||||||
where: { pay_period_no },
|
|
||||||
orderBy: { pay_year: "desc" },
|
|
||||||
});
|
|
||||||
if (!period) return { success: false, error: `PAY_PERIOD_NOT_FOUND` };
|
|
||||||
|
|
||||||
const overview = await 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,
|
|
||||||
});
|
|
||||||
if (!overview.success) return { success: false, error: 'INVALID_PAY_PERIOD' }
|
|
||||||
|
|
||||||
return { success: true, data: overview.data }
|
|
||||||
}
|
|
||||||
|
|
||||||
async getOverviewByYearPeriod(pay_year: number, period_no: number): Promise<Result<PayPeriodOverviewDto, string>> {
|
|
||||||
const period = computePeriod(pay_year, period_no);
|
|
||||||
const overview = await 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,
|
|
||||||
});
|
|
||||||
if (!overview.success) return { success: false, error: 'INVALID_PAY_PERIOD' }
|
|
||||||
return { success: true, data: overview.data }
|
|
||||||
}
|
|
||||||
|
|
||||||
//find crew member associated with supervisor
|
|
||||||
private async resolveCrew(supervisor_id: number, include_subtree: boolean):
|
|
||||||
Promise<Result<Array<{ id: number; first_name: string; last_name: string; email: string }>, 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 { success: true, data: 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 { success: true, data: result };
|
|
||||||
}
|
|
||||||
|
|
||||||
//fetchs crew emails
|
|
||||||
async resolveCrewEmails(supervisor_id: number, include_subtree: boolean): Promise<Result<Set<string>, string>> {
|
|
||||||
const crew = await this.resolveCrew(supervisor_id, include_subtree);
|
|
||||||
if (!crew.success) return { success: false, error: crew.error }
|
|
||||||
return { success: true, data: new Set(crew.data.map(crew_member => crew_member.email).filter(Boolean)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
async getCrewOverview(pay_year: number, period_no: number, email: string, include_subtree: boolean):
|
|
||||||
Promise<Result<PayPeriodOverviewDto, string>> {
|
|
||||||
// 1) Search for the period
|
|
||||||
const period = await this.prisma.payPeriods.findFirst({ where: { pay_year, pay_period_no: period_no } });
|
|
||||||
if (!period) return { success: false, error: 'PAY_PERIOD_NOT_FOUND' }
|
|
||||||
|
|
||||||
// 2) fetch supervisor
|
|
||||||
const supervisor = await this.prisma.employees.findFirst({
|
|
||||||
where: { user: { email: email } },
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
is_supervisor: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!supervisor) return { success: false, error: 'EMPLOYEE_NOT_FOUND' }
|
|
||||||
if (!supervisor.is_supervisor) return { success: false, error: 'INVALID_EMPLOYEE' }
|
|
||||||
|
|
||||||
// 3)fetchs crew members
|
|
||||||
const crew = await this.resolveCrew(supervisor.id, include_subtree); // [{ id, first_name, last_name }]
|
|
||||||
if (!crew.success) return { success: false, error: crew.error }
|
|
||||||
const crew_ids = crew.data.map(c => c.id);
|
|
||||||
// seed names map for employee without data
|
|
||||||
const seed_names = new Map<number, { name: string; email: string }>(
|
|
||||||
crew.data.map(crew => [
|
|
||||||
crew.id,
|
|
||||||
{
|
|
||||||
name: `${crew.first_name} ${crew.last_name}`.trim(),
|
|
||||||
email: crew.email
|
|
||||||
}
|
|
||||||
]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const overview = await 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 })
|
|
||||||
if (!overview.success) return { success: false, error: 'INVALID_PAY_PERIOD' }
|
|
||||||
|
|
||||||
// 4) overview build
|
|
||||||
return { success: true, data: overview.data }
|
|
||||||
}
|
|
||||||
|
|
||||||
private async buildOverview(
|
|
||||||
period: {
|
|
||||||
period_start: string | Date; period_end: string | Date; payday: string | Date;
|
|
||||||
period_no: number; pay_year: number; label: string;
|
|
||||||
}, //add is_approved
|
|
||||||
options?: { filtered_employee_ids?: number[]; seed_names?: Map<number, { name: string, email: string }> }
|
|
||||||
): Promise<Result<PayPeriodOverviewDto, string>> {
|
|
||||||
const toDateString = (d: Date) => d.toISOString().slice(0, 10);
|
|
||||||
const toMoney = (v: any) => (typeof v === "object" && "toNumber" in v ? v.toNumber() : (v as number));
|
|
||||||
|
|
||||||
const start = period.period_start instanceof Date
|
|
||||||
? period.period_start
|
|
||||||
: new Date(`${period.period_start}T00:00:00.000Z`);
|
|
||||||
|
|
||||||
const end = period.period_end instanceof Date
|
|
||||||
? period.period_end
|
|
||||||
: new Date(`${period.period_end}T00:00:00.000Z`);
|
|
||||||
|
|
||||||
const payd = period.payday instanceof Date
|
|
||||||
? period.payday
|
|
||||||
: new Date(`${period.payday}T00:00:00.000Z`);
|
|
||||||
|
|
||||||
//restrictEmployeeIds = filter for shifts and expenses by employees
|
|
||||||
const where_employee = options?.filtered_employee_ids?.length ?
|
|
||||||
{
|
|
||||||
date: { gte: start, lte: end },
|
|
||||||
timesheet: { employee_id: { in: options.filtered_employee_ids } },
|
|
||||||
}
|
|
||||||
:
|
|
||||||
{ date: { gte: start, lte: end } };
|
|
||||||
|
|
||||||
// SHIFTS (filtered by crew)
|
|
||||||
const shifts = await this.prisma.shifts.findMany({
|
|
||||||
where: where_employee,
|
|
||||||
select: {
|
|
||||||
start_time: true,
|
|
||||||
end_time: true,
|
|
||||||
is_remote: true,
|
|
||||||
timesheet: {
|
|
||||||
select: {
|
|
||||||
is_approved: true,
|
|
||||||
employee: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
user: {
|
|
||||||
select: {
|
|
||||||
first_name: true,
|
|
||||||
last_name: true,
|
|
||||||
email: true,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
bank_code: { select: { categorie: true, type: true } },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// EXPENSES (filtered by crew)
|
|
||||||
const expenses = await this.prisma.expenses.findMany({
|
|
||||||
where: 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,
|
|
||||||
is_active: true,
|
|
||||||
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: false,
|
|
||||||
is_remote: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const all_employees = await this.prisma.employees.findMany({
|
|
||||||
include: {
|
|
||||||
user: {
|
|
||||||
select: {
|
|
||||||
first_name: true,
|
|
||||||
last_name: true,
|
|
||||||
email: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const employee of all_employees) {
|
|
||||||
let is_active = true;
|
|
||||||
|
|
||||||
if (employee.last_work_day !== null) {
|
|
||||||
is_active = this.checkForInactiveDate(employee.last_work_day)
|
|
||||||
}
|
|
||||||
|
|
||||||
by_employee.set(employee.id, {
|
|
||||||
email: employee.user.email,
|
|
||||||
employee_name: employee.user.first_name + ' ' + employee.user.last_name,
|
|
||||||
is_active: is_active,
|
|
||||||
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: false,
|
|
||||||
is_remote: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const ensure = (id: number, name: string, email: string) => {
|
|
||||||
if (!by_employee.has(id)) {
|
|
||||||
by_employee.set(id, {
|
|
||||||
email,
|
|
||||||
employee_name: name,
|
|
||||||
is_active: true,
|
|
||||||
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: false,
|
|
||||||
is_remote: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
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 = 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 = Number((record.expenses += amount).toFixed(2));
|
|
||||||
|
|
||||||
const type = (expense.bank_code?.type || "").toUpperCase();
|
|
||||||
const rate = expense.bank_code?.modifier ?? 0;
|
|
||||||
if (type === "MILEAGE" && rate > 0) {
|
|
||||||
record.mileage = Number((record.mileage += Math.round((amount / rate) * 100) / 100).toFixed(2));
|
|
||||||
}
|
|
||||||
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 {
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
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<Result<PayPeriodDto[], string>> {
|
|
||||||
const currentPayYear = payYearOfDate(new Date());
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
data: 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,
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async findOne(period_no: number): Promise<Result<PayPeriodDto, string>> {
|
|
||||||
const row = await this.prisma.payPeriods.findFirst({
|
|
||||||
where: { pay_period_no: period_no },
|
|
||||||
orderBy: { pay_year: "desc" },
|
|
||||||
});
|
|
||||||
if (!row) return { success: false, error: `PAY_PERIOD_NOT_FOUND` }
|
|
||||||
return { success: true, data: mapPayPeriodToDto(row) };
|
|
||||||
}
|
|
||||||
|
|
||||||
async findCurrent(date?: string): Promise<Result<PayPeriodDto, string>> {
|
|
||||||
const iso_day = date ?? new Date().toISOString().slice(0, 10);
|
|
||||||
const pay_period = await this.findByDate(iso_day);
|
|
||||||
if (!pay_period.success) return { success: false, error: 'INVALID_PAY_PERIOD' }
|
|
||||||
return { success: true, data: pay_period.data }
|
|
||||||
}
|
|
||||||
|
|
||||||
async findOneByYearPeriod(pay_year: number, period_no: number): Promise<Result<PayPeriodDto, string>> {
|
async findOneByYearPeriod(pay_year: number, period_no: number): Promise<Result<PayPeriodDto, string>> {
|
||||||
const row = await this.prisma.payPeriods.findFirst({
|
const row = await this.prisma.payPeriods.findFirst({
|
||||||
|
|
@ -458,20 +57,4 @@ export class PayPeriodsQueryService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private checkForInactiveDate = (last_work_day: Date) => {
|
|
||||||
const limit = new Date(last_work_day);
|
|
||||||
limit.setDate(limit.getDate() + 14);
|
|
||||||
if (limit >= new Date()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import { EmployeeTimesheetResolver } from "src/common/mappers/timesheet.mapper";
|
||||||
|
|
||||||
import { PayperiodsModule } from "src/time-and-attendance/pay-period/pay-periods.module";
|
import { PayperiodsModule } from "src/time-and-attendance/pay-period/pay-periods.module";
|
||||||
import { PayPeriodsController } from "src/time-and-attendance/pay-period/pay-periods.controller";
|
import { PayPeriodsController } from "src/time-and-attendance/pay-period/pay-periods.controller";
|
||||||
|
import { GetOverviewService } from "src/time-and-attendance/pay-period/services/pay-periods-build-overview.service";
|
||||||
import { PayPeriodsQueryService } from "src/time-and-attendance/pay-period/services/pay-periods-query.service";
|
import { PayPeriodsQueryService } from "src/time-and-attendance/pay-period/services/pay-periods-query.service";
|
||||||
import { PayPeriodsCommandService } from "src/time-and-attendance/pay-period/services/pay-periods-command.service";
|
import { PayPeriodsCommandService } from "src/time-and-attendance/pay-period/services/pay-periods-command.service";
|
||||||
|
|
||||||
|
|
@ -73,6 +74,7 @@ import { SchedulePresetsApplyService } from "src/time-and-attendance/schedule-pr
|
||||||
BankCodesResolver,
|
BankCodesResolver,
|
||||||
TimesheetApprovalService,
|
TimesheetApprovalService,
|
||||||
EmployeeTimesheetResolver,
|
EmployeeTimesheetResolver,
|
||||||
|
GetOverviewService,
|
||||||
PayPeriodsQueryService,
|
PayPeriodsQueryService,
|
||||||
PayPeriodsCommandService,
|
PayPeriodsCommandService,
|
||||||
CsvExportService,
|
CsvExportService,
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ export const expense_select = {
|
||||||
comment: true,
|
comment: true,
|
||||||
supervisor_comment: true,
|
supervisor_comment: true,
|
||||||
is_approved: true,
|
is_approved: true,
|
||||||
|
|
||||||
} satisfies Prisma.ExpensesSelect;
|
} satisfies Prisma.ExpensesSelect;
|
||||||
|
|
||||||
export const shift_select = {
|
export const shift_select = {
|
||||||
|
|
@ -61,4 +61,25 @@ export const timesheet_select = {
|
||||||
expense: true,
|
expense: true,
|
||||||
start_date: true,
|
start_date: true,
|
||||||
is_approved: true,
|
is_approved: true,
|
||||||
} satisfies Prisma.TimesheetsSelect;
|
} satisfies Prisma.TimesheetsSelect;
|
||||||
|
|
||||||
|
export const select_employee_timesheet = {
|
||||||
|
timesheet: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
is_approved: true,
|
||||||
|
employee: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
first_name: true,
|
||||||
|
last_name: true,
|
||||||
|
email: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user