Merge branch 'main' of git.targo.ca:Targo/targo_backend

This commit is contained in:
Nicolas Drolet 2025-10-28 14:48:17 -04:00
commit 45e447f954
28 changed files with 287 additions and 266 deletions

View File

@ -377,19 +377,10 @@
]
}
},
"/shift/{timesheet_id}": {
"/shift/create": {
"post": {
"operationId": "ShiftController_createBatch",
"parameters": [
{
"name": "timesheet_id",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
}
],
"parameters": [],
"requestBody": {
"required": true,
"content": {
@ -413,10 +404,23 @@
]
}
},
"/shift": {
"/shift/update": {
"patch": {
"operationId": "ShiftController_updateBatch",
"parameters": [],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
},
"responses": {
"200": {
"description": ""

View File

@ -1,5 +1,5 @@
import { toDateFromString, toStringFromDate } from "src/time-and-attendance/utils/date-time-helpers";
import { toDateFromString, toStringFromDate } from "src/time-and-attendance/utils/date-time.utils";
import { Injectable, NotFoundException } from "@nestjs/common";
import { updateExpenseDto } from "../dtos/expense-update.dto";
import { GetExpenseDto } from "../dtos/expense-get.dto";

View File

@ -1,6 +1,6 @@
import { Prisma } from "@prisma/client";
import { LeaveRequestViewDto } from "../dtos/leave-request-view.dto";
import { LeaveRequestRow } from "src/time-and-attendance/utils/selects.utils";
import { LeaveRequestRow } from "src/time-and-attendance/utils/type.utils";
import { Prisma } from "@prisma/client";
const toNum = (value?: Prisma.Decimal | null) =>
value !== null && value !== undefined ? Number(value) : undefined;

View File

@ -1,8 +1,8 @@
import { LeaveRequestRow } from 'src/time-and-attendance/utils/selects.utils';
import { LeaveRequestArchiveRow } from './leave-requests-archive.select';
import { LeaveRequestViewDto } from '../dtos/leave-request-view.dto';
import { mapArchiveRowToView } from '../mappers/leave-requests-archive.mapper';
import { LeaveRequestRow } from 'src/time-and-attendance/utils/type.utils';
import { mapRowToView } from '../mappers/leave-requests.mapper';
import { LeaveRequestArchiveRow } from './leave-requests-archive.select';
/** Active (table leave_requests) : proxy to base mapper */
export function mapRowToViewWithDays(row: LeaveRequestRow): LeaveRequestViewDto {

View File

@ -4,7 +4,7 @@ import { PrismaService } from "src/prisma/prisma.service";
import { LeaveTypes } from "@prisma/client";
// import { toDateOnly, toStringFromDate } from "src/time-and-attendance/modules/shared/helpers/date-time.helpers";
import { UpsertAction } from "src/time-and-attendance/modules/shared/types/upsert-actions.types";
import { toDateFromString, toStringFromDate } from "src/time-and-attendance/utils/date-time-helpers";
import { toDateFromString, toStringFromDate } from "src/time-and-attendance/utils/date-time.utils";
// import { ShiftsUpsertService } from "src/time-and-attendance/modules/time-tracker/shifts/services/shifts-upsert.service";
@Injectable()

View File

@ -3,9 +3,9 @@ import { PrismaService } from "src/prisma/prisma.service";
import { computeHours } from "src/common/utils/date-utils";
import { PayPeriodOverviewDto } from "../dtos/overview-pay-period.dto";
import { EmployeePeriodOverviewDto } from "../dtos/overview-employee-period.dto";
import { computePeriod, listPayYear, payYearOfDate } from "../utils/pay-year.util";
import { PayPeriodDto } from "../dtos/pay-period.dto";
import { mapPayPeriodToDto } from "../mappers/pay-periods.mapper";
import { computePeriod, listPayYear, payYearOfDate } from "src/time-and-attendance/utils/date-time.utils";
@Injectable()
export class PayPeriodsQueryService {

View File

@ -1,37 +0,0 @@
import { toStringFromDate, toUTCDate } from "src/time-and-attendance/utils/date-time-helpers";
export const ANCHOR_ISO = '2023-12-17'; // ancre date
const PERIOD_DAYS = 14;
const PERIODS_PER_YEAR = 26;
const MS_PER_DAY = 86_400_000;
export function payYearOfDate(date: string | Date, anchorISO = ANCHOR_ISO): number {
const ANCHOR = toUTCDate(anchorISO);
const d = toUTCDate(date);
const days = Math.floor((+d - +ANCHOR) / MS_PER_DAY);
const cycles = Math.floor(days / (PERIODS_PER_YEAR * PERIOD_DAYS));
return ANCHOR.getUTCFullYear() + 1 + cycles;
}
//compute labels for periods
export function computePeriod(pay_year: number, period_no: number, anchorISO = ANCHOR_ISO) {
const ANCHOR = toUTCDate(anchorISO);
const cycles = pay_year - (ANCHOR.getUTCFullYear() + 1);
const offsetPeriods = cycles * PERIODS_PER_YEAR + (period_no - 1);
const start = new Date(+ANCHOR + offsetPeriods * PERIOD_DAYS * 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 {
period_no: period_no,
pay_year: pay_year,
payday: toStringFromDate(pay),
period_start: toStringFromDate(start),
period_end: toStringFromDate(end),
label: `${toStringFromDate(start)}.${toStringFromDate(end)}`,
start, end,
};
}
//list of all 26 periods for a full year
export function listPayYear(pay_year: number, anchorISO = ANCHOR_ISO) {
return Array.from({ length: PERIODS_PER_YEAR }, (_, i) => computePeriod(pay_year, i + 1, anchorISO));
}

View File

@ -1,6 +1,6 @@
import { Injectable, NotFoundException } from "@nestjs/common";
import { Prisma, PrismaClient } from "@prisma/client";
import { weekStartSunday } from "src/time-and-attendance/utils/date-time-helpers";
import { weekStartSunday } from "src/time-and-attendance/utils/date-time.utils";
import { PrismaService } from "src/prisma/prisma.service";
import { EmailToIdResolver } from "./resolve-email-id.utils";

View File

@ -1,8 +1,8 @@
import { BadRequestException, Body, Controller, Get, NotFoundException, Param, Post, Put, Query } from "@nestjs/common";
import { SchedulePresetsDto } from "../dtos/create-schedule-presets.dto";
import { SchedulePresetsCommandService } from "../services/schedule-presets-command.service";
import { UpsertAction } from "src/time-and-attendance/modules/shared/types/upsert-actions.types";
import { SchedulePresetsQueryService } from "../services/schedule-presets-query.service";
import { SchedulePresetsDto } from "../dtos/create-schedule-presets.dto";
import { UpsertAction } from "src/time-and-attendance/modules/shared/types/upsert-actions.types";
@Controller('schedule-presets')
export class SchedulePresetsController {

View File

@ -1,4 +1,4 @@
import { ArrayMinSize, IsArray, IsBoolean, IsEmail, IsOptional, IsString } from "class-validator";
import { ArrayMinSize, IsArray, IsBoolean, IsOptional, IsString } from "class-validator";
import { SchedulePresetShiftsDto } from "./create-schedule-preset-shifts.dto";
export class SchedulePresetsDto {

View File

@ -1,9 +1,9 @@
import { BadRequestException, ConflictException, Injectable, NotFoundException } from "@nestjs/common";
import { EmailToIdResolver } from "src/time-and-attendance/modules/shared/utils/resolve-email-id.utils";
import { PrismaService } from "src/prisma/prisma.service";
import { ApplyResult } from "../types/schedule-presets.types";
import { Prisma, Weekday } from "@prisma/client";
import { WEEKDAY } from "../mappers/schedule-presets.mappers";
import { WEEKDAY } from "../../../../utils/mappers.utils";
import { ApplyResult } from "src/time-and-attendance/utils/type.utils";
@Injectable()
export class SchedulePresetsApplyService {

View File

@ -1,8 +1,8 @@
import { Injectable, NotFoundException } from "@nestjs/common";
import { EmailToIdResolver } from "src/time-and-attendance/modules/shared/utils/resolve-email-id.utils";
import { PrismaService } from "src/prisma/prisma.service";
import { PresetResponse, ShiftResponse } from "../types/schedule-presets.types";
import { Prisma } from "@prisma/client";
import { PresetResponse, ShiftResponse } from "src/time-and-attendance/utils/type.utils";
@Injectable()
export class SchedulePresetsQueryService {

View File

@ -1,21 +0,0 @@
export type ShiftResponse = {
week_day: string;
sort_order: number;
start_time: string;
end_time: string;
is_remote: boolean;
type: string;
};
export type PresetResponse = {
id: number;
name: string;
is_default: boolean;
shifts: ShiftResponse[];
}
export type ApplyResult = {
timesheet_id: number;
created: number;
skipped: number;
}

View File

@ -1,32 +1,30 @@
import { BadRequestException, Body, Controller, Delete, Param, ParseIntPipe, Patch, Post } from "@nestjs/common";
import { CreateResult, ShiftsUpsertService, UpdateResult } from "../services/shifts-upsert.service";
import { updateShiftDto } from "../dtos/shift-update.dto";
import { BadRequestException, Body, Controller, Delete, Param, Patch, Post } from "@nestjs/common";
import { CreateResult, UpdateResult } from "src/time-and-attendance/utils/type.utils";
import { ShiftsUpsertService } from "src/time-and-attendance/modules/time-tracker/shifts/services/shifts-upsert.service";
import { ShiftDto } from "../dtos/shift-create.dto";
import { UpdateShiftDto } from "src/time-and-attendance/modules/time-tracker/shifts/dtos/shift-update.dto";
@Controller('shift')
export class ShiftController {
constructor( private readonly upsert_service: ShiftsUpsertService ){}
@Post(':timesheet_id')
@Post('create')
createBatch(
@Param('timesheet_id', ParseIntPipe) timesheet_id: number,
@Body()dtos: ShiftDto[]): Promise<CreateResult[]> {
const list = Array.isArray(dtos) ? dtos : [];
if(list.length === 0) throw new BadRequestException('Body is missing or invalid (create shifts)')
return this.upsert_service.createShifts(timesheet_id, dtos)
if(list.length === 0) throw new BadRequestException('Body is missing or invalid (create shifts)');
return this.upsert_service.createShifts(dtos)
}
@Patch()
//change Body to receive dtos
@Patch('update')
updateBatch(
@Body() body: { updates: { id: number; dto: updateShiftDto }[] }): Promise<UpdateResult[]>{
const updates = Array.isArray(body?.updates)
? body.updates.filter(update => Number.isFinite(update?.id) && typeof update.dto === "object")
: [];
if(updates.length === 0) {
throw new BadRequestException(`Body is missing or invalid (update shifts)`);
}
return this.upsert_service.updateShifts(updates);
@Body() dtos: UpdateShiftDto[]): Promise<UpdateResult[]>{
const list = Array.isArray(dtos) ? dtos: [];
if(list.length === 0) throw new BadRequestException('Body is missing or invalid (update shifts)');
return this.upsert_service.updateShifts(dtos);
}
@Delete(':shift_id')

View File

@ -1,6 +1,7 @@
import { IsBoolean, IsInt, IsOptional, IsString, MaxLength } from "class-validator";
export class ShiftDto {
@IsInt() @IsOptional() id: number;
@IsInt() timesheet_id!: number;
@IsInt() bank_code_id!: number;

View File

@ -1,7 +1,11 @@
import { PartialType, OmitType } from "@nestjs/swagger";
import { IsInt } from "class-validator";
import { ShiftDto } from "./shift-create.dto";
export class updateShiftDto extends PartialType (
export class UpdateShiftDto extends PartialType(
// allows update using ShiftDto and preventing OmitType variables to be modified
OmitType(ShiftDto, [ 'is_approved', 'timesheet_id'] as const)
){}
OmitType(ShiftDto, ['is_approved', 'timesheet_id'] as const),
) {
@IsInt()
id!: number;
}

View File

@ -2,7 +2,6 @@ import { Injectable } from "@nestjs/common";
import { ShiftsArchive } from "@prisma/client";
import { PrismaService } from "src/prisma/prisma.service";
/**
* _____________________________________________________________________________________
*

View File

@ -1,7 +1,7 @@
import { Injectable, NotFoundException } from "@nestjs/common";
import { PrismaService } from "src/prisma/prisma.service";
import { GetShiftDto } from "../dtos/shift-get.dto";
import { toStringFromDate, toStringFromHHmm } from "../../../../utils/date-time-helpers";
import { toStringFromDate, toStringFromHHmm } from "../../../../utils/date-time.utils";
import { shift_select } from "src/time-and-attendance/utils/selects.utils";
/**

View File

@ -1,26 +1,12 @@
import { toDateFromString, toHHmmFromString, toStringFromDate, toStringFromHHmm } from "../../../../utils/date-time-helpers";
import { CreateResult, NormedOk, NormedErr, UpdatePayload, UpdateResult, Normalized, UpdateChanges } from "src/time-and-attendance/utils/type.utils";
import { toDateFromString, toHHmmFromString, toStringFromDate, toStringFromHHmm } from "../../../../utils/date-time.utils";
import { BadRequestException, ConflictException, Injectable, NotFoundException } from "@nestjs/common";
import { OvertimeService, WeekOvertimeSummary } from "src/time-and-attendance/domains/services/overtime.service";
import { updateShiftDto } from "../dtos/shift-update.dto";
import { OvertimeService } from "src/time-and-attendance/domains/services/overtime.service";
import { PrismaService } from "src/prisma/prisma.service";
import { shift_select } from "src/time-and-attendance/utils/selects.utils";
import { GetShiftDto } from "../dtos/shift-get.dto";
import { ShiftDto } from "../dtos/shift-create.dto";
import { shift_select } from "src/time-and-attendance/utils/selects.utils";
type Normalized = { date: Date; start_time: Date; end_time: Date; };
export type ShiftWithOvertimeDto = {
shift: GetShiftDto;
overtime: WeekOvertimeSummary;
};
export type CreateResult = { ok: true; data: ShiftWithOvertimeDto } | { ok: false; error: any };
export type UpdatePayload = { id: number; dto: updateShiftDto };
export type UpdateResult = { ok: true; id: number; data: ShiftWithOvertimeDto } | { ok: false; id: number; error: any };
export type DeleteResult = { ok: true; id: number; overtime: WeekOvertimeSummary } | { ok: false; id: number; error: any };
type NormedOk = { index: number; dto: ShiftDto; normed: Normalized };
type NormedErr = { index: number; error: any };
import { UpdateShiftDto } from "src/time-and-attendance/modules/time-tracker/shifts/dtos/shift-update.dto";
const overlaps = (a: { start: Date; end: Date }, b: { start: Date; end: Date }) =>
!(a.end <= b.start || a.start >= b.end);
@ -40,7 +26,7 @@ export class ShiftsUpsertService {
//checks for overlaping shifts
//create new shifts
//calculate overtime
async createShifts(timesheet_id: number, dtos: ShiftDto[]): Promise<CreateResult[]> {
async createShifts(dtos: ShiftDto[]): Promise<CreateResult[]> {
if (!Array.isArray(dtos) || dtos.length === 0) return [];
const normed_shift: Array<NormedOk | NormedErr> = dtos.map((dto, index) => {
@ -99,7 +85,7 @@ export class ShiftsUpsertService {
const existing_date = new Map<number, { start_time: Date; end_time: Date }[]>();
for (const d of unique_dates) {
const rows = await tx.shifts.findMany({
where: { timesheet_id, date: d },
where: { date: d },
select: { start_time: true, end_time: true },
});
existing_date.set(d.getTime(), rows.map(r => ({ start_time: r.start_time, end_time: r.end_time })));
@ -129,7 +115,7 @@ export class ShiftsUpsertService {
const row = await tx.shifts.create({
data: {
timesheet_id,
timesheet_id: dto.timesheet_id,
bank_code_id: dto.bank_code_id,
date: normed.date,
start_time: normed.start_time,
@ -142,7 +128,7 @@ export class ShiftsUpsertService {
existing.push({ start_time: row.start_time, end_time: row.end_time });
const summary = await this.overtime.getWeekOvertimeSummary(timesheet_id, normed.date, tx);
const summary = await this.overtime.getWeekOvertimeSummary(dto.timesheet_id, normed.date, tx);
const shift: GetShiftDto = {
timesheet_id: row.timesheet_id,
bank_code_id: row.bank_code_id,
@ -172,8 +158,25 @@ export class ShiftsUpsertService {
// update shifts in DB
// recalculate overtime after update
// return an updated version to display
async updateShifts(updates: UpdatePayload[]): Promise<UpdateResult[]> {
if (!Array.isArray(updates) || updates.length === 0) return [];
async updateShifts(dtos: UpdateShiftDto[]): Promise<UpdateResult[]> {
if (!Array.isArray(dtos) || dtos.length === 0) return [];
const updates: UpdatePayload[] = dtos.map((item) => {
const { id, ...rest } = item;
if (!Number.isInteger(id)) {
throw new BadRequestException('Update shift payload is missing a valid id');
}
const changes: UpdateChanges = {};
if (rest.date !== undefined) changes.date = rest.date;
if (rest.start_time !== undefined) changes.start_time = rest.start_time;
if (rest.end_time !== undefined) changes.end_time = rest.end_time;
if (rest.bank_code_id !== undefined) changes.bank_code_id = rest.bank_code_id;
if (rest.is_remote !== undefined) changes.is_remote = rest.is_remote;
if (rest.comment !== undefined) changes.comment = rest.comment;
return { id, dto: changes };
});
return this.prisma.$transaction(async (tx) => {
const shift_ids = updates.map(update_shift => update_shift.id);

View File

@ -1,36 +0,0 @@
export function weekStartSunday(date_local: Date): Date {
const start = new Date(Date.UTC(date_local.getFullYear(), date_local.getMonth(), date_local.getDate()));
const dow = start.getDay();
start.setDate(start.getDate() - dow);
start.setHours(0, 0, 0, 0);
return start;
}
export const toDateFromString = ( date: Date | string):Date => {
const d = new Date(date);
return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
}
export const sevenDaysFrom = (date: Date | string): Date[] => {
return Array.from({length: 7 }, (_,i) => {
const d = new Date(date);
d.setUTCDate(d.getUTCDate() + i );
return d;
});
}
export const toStringFromDate = (date: Date | string): string => {
const d = toDateFromString(date);
const year = d.getUTCFullYear();
const month = String(d.getUTCMonth() + 1).padStart(2, '0');
const day = String(d.getUTCDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
export const toHHmmFromDate = (input: Date | string): string => {
const date = new Date(input);
const hh = String(date.getUTCHours()).padStart(2, '0');
const mm = String(date.getUTCMinutes()).padStart(2, '0');
return `${hh}:${mm}`;
}

View File

@ -1,25 +1,7 @@
import { sevenDaysFrom, toDateFromString, toHHmmFromDate, toStringFromDate } from "../helpers/timesheets-date-time-helpers";
import { Injectable, NotFoundException } from "@nestjs/common";
import { PrismaService } from "src/prisma/prisma.service";
type TotalHours = {
regular: number;
evening: number;
emergency: number;
overtime: number;
vacation: number;
holiday: number;
sick: number;
};
type TotalExpenses = {
expenses: number;
per_diem: number;
on_call: number;
mileage: number;
};
const NUMBER_OF_TIMESHEETS_TO_RETURN = 2;
import { sevenDaysFrom, toStringFromDate, toHHmmFromDate, toDateFromString } from "src/time-and-attendance/utils/date-time.utils";
import { TotalExpenses, TotalHours } from "src/time-and-attendance/utils/type.utils";
@Injectable()
export class GetTimesheetsOverviewService {

View File

@ -0,0 +1,12 @@
const NUMBER_OF_TIMESHEETS_TO_RETURN = 2;
const DAILY_LIMIT_HOURS = 8;
const WEEKLY_LIMIT_HOURS = 40;
const PAY_PERIOD_ANCHOR = 2023-12-17;
const ANCHOR_ISO = '2023-12-17'; // ancre date
const PERIOD_DAYS = 14;
const PERIODS_PER_YEAR = 26;
const MS_PER_DAY = 86_400_000;
//REGEX CONSTANTS
const DATE_ISO_FORMAT = /^\d{4}-\d{2}-\d{2}$/;
const HH_MM_REGEX = /^([01]\d|2[0-3]):[0-5]\d$/;

View File

@ -1,37 +0,0 @@
//ensures the week starts from sunday
export function weekStartSunday(date_local: Date): Date {
const start = new Date(Date.UTC(date_local.getFullYear(), date_local.getMonth(), date_local.getDate()));
const dow = start.getDay(); // 0 = dimanche
start.setDate(start.getDate() - dow);
start.setHours(0, 0, 0, 0);
return start;
}
//converts string to HHmm format
export const toStringFromHHmm = (date: Date): string => {
const hh = date.getUTCHours().toString().padStart(2, '0');
const mm = date.getUTCMinutes().toString().padStart(2, '0');
return `${hh}:${mm}`;
}
//converts string to Date format
export const toStringFromDate = (date: Date) =>
date.toISOString().slice(0,10);
//converts HHmm format to string
export const toHHmmFromString = (hhmm: string): Date => {
const [hh, mm] = hhmm.split(':').map(Number);
const date = new Date('1970-01-01T00:00:00.000Z');
date.setUTCHours(hh, mm, 0, 0);
return new Date(date);
}
//converts Date format to string
export const toDateFromString = (ymd: string): Date => {
return new Date(`${ymd}T00:00:00:000Z`);
}
export const toUTCDate = (iso: string | Date) => {
const d = typeof iso === 'string' ? new Date(iso + 'T00:00:00.000Z') : iso;
return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
};

View File

@ -0,0 +1,86 @@
//ensures the week starts from sunday
export function weekStartSunday(date_local: Date): Date {
const start = new Date(Date.UTC(date_local.getFullYear(), date_local.getMonth(), date_local.getDate()));
const dow = start.getDay(); // 0 = dimanche
start.setDate(start.getDate() - dow);
start.setHours(0, 0, 0, 0);
return start;
}
//converts string to HHmm format
export const toStringFromHHmm = (date: Date): string => {
const hh = date.getUTCHours().toString().padStart(2, '0');
const mm = date.getUTCMinutes().toString().padStart(2, '0');
return `${hh}:${mm}`;
}
//converts string to Date format
export const toStringFromDate = (date: Date) =>
date.toISOString().slice(0,10);
//converts HHmm format to string
export const toHHmmFromString = (hhmm: string): Date => {
const [hh, mm] = hhmm.split(':').map(Number);
const date = new Date('1970-01-01T00:00:00.000Z');
date.setUTCHours(hh, mm, 0, 0);
return new Date(date);
}
//converts string to HHmm format
export const toHHmmFromDate = (input: Date | string): string => {
const date = new Date(input);
const hh = String(date.getUTCHours()).padStart(2, '0');
const mm = String(date.getUTCMinutes()).padStart(2, '0');
return `${hh}:${mm}`;
}
//converts Date format to string
export const toDateFromString = (ymd: string | Date): Date => {
return new Date(`${ymd}T00:00:00:000Z`);
}
export const toUTCDateFromString = (iso: string | Date) => {
const d = typeof iso === 'string' ? new Date(iso + 'T00:00:00.000Z') : iso;
return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
};
export const sevenDaysFrom = (date: Date | string): Date[] => {
return Array.from({length: 7 }, (_,i) => {
const d = new Date(date);
d.setUTCDate(d.getUTCDate() + i );
return d;
});
}
export function payYearOfDate(date: string | Date, anchorISO = ANCHOR_ISO): number {
const ANCHOR = toUTCDateFromString(anchorISO);
const d = toUTCDateFromString(date);
const days = Math.floor((+d - +ANCHOR) / MS_PER_DAY);
const cycles = Math.floor(days / (PERIODS_PER_YEAR * PERIOD_DAYS));
return ANCHOR.getUTCFullYear() + 1 + cycles;
}
//compute labels for periods
export function computePeriod(pay_year: number, period_no: number, anchorISO = ANCHOR_ISO) {
const ANCHOR = toUTCDateFromString(anchorISO);
const cycles = pay_year - (ANCHOR.getUTCFullYear() + 1);
const offsetPeriods = cycles * PERIODS_PER_YEAR + (period_no - 1);
const start = new Date(+ANCHOR + offsetPeriods * PERIOD_DAYS * 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 {
period_no: period_no,
pay_year: pay_year,
payday: toStringFromDate(pay),
period_start: toStringFromDate(start),
period_end: toStringFromDate(end),
label: `${toStringFromDate(start)}.${toStringFromDate(end)}`,
start, end,
};
}
//list of all 26 periods for a full year
export function listPayYear(pay_year: number, anchorISO = ANCHOR_ISO) {
return Array.from({ length: PERIODS_PER_YEAR }, (_, i) => computePeriod(pay_year, i + 1, anchorISO));
}

View File

@ -1,2 +0,0 @@
const DATE_ISO_FORMAT = /^\d{4}-\d{2}-\d{2}$/;
const HH_MM_REGEX = /^([01]\d|2[0-3]):[0-5]\d$/;

View File

@ -1,6 +1,5 @@
import { Prisma } from "@prisma/client";
export const expense_select = {
id: true,
timesheet_id: true,
@ -35,14 +34,16 @@ export const leaveRequestsSelect = {
requested_hours: true,
comment: true,
approval_status: true,
employee: { select: {
employee: {
select: {
id: true,
user: { select: {
user: {
select: {
email: true,
first_name: true,
last_name: true,
}},
}},
},
},
}
},
} satisfies Prisma.LeaveRequestsSelect;
export type LeaveRequestRow = Prisma.LeaveRequestsGetPayload<{ select: typeof leaveRequestsSelect}>;

View File

@ -0,0 +1,64 @@
import { WeekOvertimeSummary } from "src/time-and-attendance/domains/services/overtime.service";
import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils";
import { UpdateShiftDto } from "src/time-and-attendance/modules/time-tracker/shifts/dtos/shift-update.dto";
import { GetShiftDto } from "src/time-and-attendance/modules/time-tracker/shifts/dtos/shift-get.dto";
import { ShiftDto } from "src/time-and-attendance/modules/time-tracker/shifts/dtos/shift-create.dto";
import { Prisma } from "@prisma/client";
export type TotalHours = {
regular: number;
evening: number;
emergency: number;
overtime: number;
vacation: number;
holiday: number;
sick: number;
};
export type TotalExpenses = {
expenses: number;
per_diem: number;
on_call: number;
mileage: number;
};
export type Normalized = { date: Date; start_time: Date; end_time: Date; };
export type ShiftWithOvertimeDto = {
shift: GetShiftDto;
overtime: WeekOvertimeSummary;
};
export type CreateResult = { ok: true; data: ShiftWithOvertimeDto } | { ok: false; error: any };
export type UpdateChanges = Omit<UpdateShiftDto, 'id'>;
export type UpdatePayload = { id: number; dto: UpdateChanges };
export type UpdateResult = { ok: true; id: number; data: ShiftWithOvertimeDto } | { ok: false; id: number; error: any };
export type DeleteResult = { ok: true; id: number; overtime: WeekOvertimeSummary } | { ok: false; id: number; error: any };
export type NormedOk = { index: number; dto: ShiftDto; normed: Normalized };
export type NormedErr = { index: number; error: any };
export type ShiftResponse = {
week_day: string;
sort_order: number;
start_time: string;
end_time: string;
is_remote: boolean;
type: string;
};
export type PresetResponse = {
id: number;
name: string;
is_default: boolean;
shifts: ShiftResponse[];
}
export type ApplyResult = {
timesheet_id: number;
created: number;
skipped: number;
}
export type LeaveRequestRow = Prisma.LeaveRequestsGetPayload<{ select: typeof leaveRequestsSelect}>;