feat(schedule-presets): created an apply service to auto-create shifts using presets
This commit is contained in:
parent
83792e596a
commit
7c7edea768
|
|
@ -1,4 +1,4 @@
|
||||||
import { Body, Controller, Get, NotFoundException, Param, Put, Query } from "@nestjs/common";
|
import { BadRequestException, Body, Controller, Get, NotFoundException, Param, Post, Put, Query } from "@nestjs/common";
|
||||||
import { SchedulePresetsDto } from "../dtos/create-schedule-presets.dto";
|
import { SchedulePresetsDto } from "../dtos/create-schedule-presets.dto";
|
||||||
import { SchedulePresetsCommandService } from "../services/schedule-presets-command.service";
|
import { SchedulePresetsCommandService } from "../services/schedule-presets-command.service";
|
||||||
import { UpsertAction } from "src/modules/shared/types/upsert-actions.types";
|
import { UpsertAction } from "src/modules/shared/types/upsert-actions.types";
|
||||||
|
|
@ -11,6 +11,7 @@ export class SchedulePresetsController {
|
||||||
private readonly queryService: SchedulePresetsQueryService,
|
private readonly queryService: SchedulePresetsQueryService,
|
||||||
){}
|
){}
|
||||||
|
|
||||||
|
//used to create, update or delete a schedule preset
|
||||||
@Put(':email')
|
@Put(':email')
|
||||||
async upsert(
|
async upsert(
|
||||||
@Param('email') email: string,
|
@Param('email') email: string,
|
||||||
|
|
@ -22,10 +23,22 @@ export class SchedulePresetsController {
|
||||||
return this.commandService.upsertSchedulePreset(email, action, dto);
|
return this.commandService.upsertSchedulePreset(email, action, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//used to show the list of available schedule presets
|
||||||
@Get(':email')
|
@Get(':email')
|
||||||
async findListByEmail(
|
async findListByEmail(
|
||||||
@Param('email') email: string,
|
@Param('email') email: string,
|
||||||
) {
|
) {
|
||||||
return this.queryService.findSchedulePresetsByEmail(email);
|
return this.queryService.findSchedulePresetsByEmail(email);
|
||||||
}
|
}
|
||||||
|
//used to apply a preset to a timesheet
|
||||||
|
@Post('/apply-presets/:email')
|
||||||
|
async applyPresets(
|
||||||
|
@Param('email') email: string,
|
||||||
|
@Query('preset') preset_name: string,
|
||||||
|
@Query('start') start_date: string,
|
||||||
|
) {
|
||||||
|
if(!preset_name?.trim()) throw new BadRequestException('Query "preset" is required');
|
||||||
|
if(!start_date?.trim()) throw new BadRequestException('Query "start" is required YYYY-MM-DD');
|
||||||
|
return this.applyPresets(email, preset_name, start_date);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { Weekday } from "@prisma/client";
|
||||||
|
|
||||||
|
export const WEEKDAY: Weekday[] = ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'];
|
||||||
|
|
@ -5,6 +5,7 @@ import { SchedulePresetsController } from "./controller/schedule-presets.control
|
||||||
import { EmployeeIdEmailResolver } from "../shared/utils/resolve-email-id.utils";
|
import { EmployeeIdEmailResolver } from "../shared/utils/resolve-email-id.utils";
|
||||||
import { BankCodesResolver } from "../shared/utils/resolve-bank-type-id.utils";
|
import { BankCodesResolver } from "../shared/utils/resolve-bank-type-id.utils";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
import { SchedulePresetsApplyService } from "./services/schedule-presets-apply.service";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [],
|
imports: [],
|
||||||
|
|
@ -13,11 +14,13 @@ import { PrismaService } from "src/prisma/prisma.service";
|
||||||
PrismaService,
|
PrismaService,
|
||||||
SchedulePresetsCommandService,
|
SchedulePresetsCommandService,
|
||||||
SchedulePresetsQueryService,
|
SchedulePresetsQueryService,
|
||||||
|
SchedulePresetsApplyService,
|
||||||
EmployeeIdEmailResolver,
|
EmployeeIdEmailResolver,
|
||||||
BankCodesResolver,
|
BankCodesResolver,
|
||||||
],
|
],
|
||||||
exports:[
|
exports:[
|
||||||
SchedulePresetsCommandService,
|
SchedulePresetsCommandService,
|
||||||
SchedulePresetsQueryService
|
SchedulePresetsQueryService,
|
||||||
|
SchedulePresetsApplyService,
|
||||||
],
|
],
|
||||||
}) export class SchedulePresetsModule {}
|
}) export class SchedulePresetsModule {}
|
||||||
|
|
@ -1,45 +1,128 @@
|
||||||
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
|
import { BadRequestException, ConflictException, Injectable, NotFoundException } from "@nestjs/common";
|
||||||
import { EmployeeIdEmailResolver } from "src/modules/shared/utils/resolve-email-id.utils";
|
import { EmployeeIdEmailResolver } from "src/modules/shared/utils/resolve-email-id.utils";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
import { ApplyResult } from "../types/schedule-presets.types";
|
import { ApplyResult } from "../types/schedule-presets.types";
|
||||||
|
import { Prisma, Weekday } from "@prisma/client";
|
||||||
|
import { WEEKDAY } from "../mappers/schedule-presets.mappers";
|
||||||
|
|
||||||
// @Injectable()
|
@Injectable()
|
||||||
// export class SchedulePresetsApplyService {
|
export class SchedulePresetsApplyService {
|
||||||
// constructor(
|
constructor(
|
||||||
// private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
// private readonly emailResolver: EmployeeIdEmailResolver,
|
private readonly emailResolver: EmployeeIdEmailResolver,
|
||||||
// ) {}
|
) {}
|
||||||
|
|
||||||
// async applyToTimesheet(
|
async applyToTimesheet(
|
||||||
// email: string,
|
email: string,
|
||||||
// preset_name: string,
|
preset_name: string,
|
||||||
// start_date_iso: string,
|
start_date_iso: string,
|
||||||
// ): Promise<ApplyResult> {
|
): Promise<ApplyResult> {
|
||||||
// if(!preset_name?.trim()) throw new BadRequestException('A preset_name is required');
|
if(!preset_name?.trim()) throw new BadRequestException('A preset_name is required');
|
||||||
// if(!DATE_ISO_FORMAT.test(start_date_iso)) throw new BadRequestException('start_date must be of format :YYYY-MM-DD');
|
if(!DATE_ISO_FORMAT.test(start_date_iso)) throw new BadRequestException('start_date must be of format :YYYY-MM-DD');
|
||||||
|
|
||||||
// const employee_id = await this.emailResolver.findIdByEmail(email);
|
const employee_id = await this.emailResolver.findIdByEmail(email);
|
||||||
// if(!employee_id) throw new NotFoundException(`Employee with email: ${email} not found`);
|
if(!employee_id) throw new NotFoundException(`Employee with email: ${email} not found`);
|
||||||
|
|
||||||
// const preset = await this.prisma.schedulePresets.findFirst({
|
const preset = await this.prisma.schedulePresets.findFirst({
|
||||||
// where: { employee_id, name: preset_name },
|
where: { employee_id, name: preset_name },
|
||||||
// include: {
|
include: {
|
||||||
// shifts: {
|
shifts: {
|
||||||
// orderBy: [{ week_day: 'asc'}, { sort_order: 'asc'}],
|
orderBy: [{ week_day: 'asc'}, { sort_order: 'asc'}],
|
||||||
// select: {
|
select: {
|
||||||
// week_day: true,
|
week_day: true,
|
||||||
// sort_order: true,
|
sort_order: true,
|
||||||
// start_time: true,
|
start_time: true,
|
||||||
// end_time: true,
|
end_time: true,
|
||||||
// is_remote: true,
|
is_remote: true,
|
||||||
// bank_code_id: true,
|
bank_code_id: true,
|
||||||
// },
|
},
|
||||||
// },
|
},
|
||||||
// },
|
},
|
||||||
// });
|
});
|
||||||
// if(!preset) throw new NotFoundException(`Preset ${preset} not found`);
|
if(!preset) throw new NotFoundException(`Preset ${preset} not found`);
|
||||||
|
|
||||||
// const start_date = new Date(`${start_date_iso}T00:00:00.000Z`)
|
const start_date = new Date(`${start_date_iso}T00:00:00.000Z`);
|
||||||
|
const timesheet = await this.prisma.timesheets.upsert({
|
||||||
|
where: { employee_id_start_date: { employee_id, start_date: start_date} },
|
||||||
|
update: {},
|
||||||
|
create: { employee_id, start_date: start_date },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
// }
|
//index shifts by weekday
|
||||||
// }
|
const index_by_day = new Map<Weekday, typeof preset.shifts>();
|
||||||
|
for (const shift of preset.shifts) {
|
||||||
|
const list = index_by_day.get(shift.week_day) ?? [];
|
||||||
|
list.push(shift);
|
||||||
|
index_by_day.set(shift.week_day, list);
|
||||||
|
}
|
||||||
|
|
||||||
|
const addDays = (date: Date, days: number) =>
|
||||||
|
new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate() + days));
|
||||||
|
|
||||||
|
const overlaps = (aStart: Date, aEnd: Date, bStart: Date, bEnd: Date) =>
|
||||||
|
aStart.getTime() < bEnd.getTime() && aEnd.getTime() > bStart.getTime();
|
||||||
|
|
||||||
|
let created = 0;
|
||||||
|
let skipped = 0;
|
||||||
|
|
||||||
|
await this.prisma.$transaction(async (tx) => {
|
||||||
|
for(let i = 0; i < 7; i++) {
|
||||||
|
const date = addDays(start_date, i);
|
||||||
|
const week_day = WEEKDAY[date.getUTCDay()];
|
||||||
|
const shifts = index_by_day.get(week_day) ?? [];
|
||||||
|
|
||||||
|
if(shifts.length === 0) continue;
|
||||||
|
|
||||||
|
const existing = await tx.shifts.findMany({
|
||||||
|
where: { timesheet_id: timesheet.id, date: date },
|
||||||
|
orderBy: { start_time: 'asc' },
|
||||||
|
select: {
|
||||||
|
start_time: true,
|
||||||
|
end_time: true,
|
||||||
|
bank_code_id: true,
|
||||||
|
is_remote: true,
|
||||||
|
comment: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const payload: Prisma.ShiftsCreateManyInput[] = [];
|
||||||
|
|
||||||
|
for(const shift of shifts) {
|
||||||
|
if(shift.end_time.getTime() <= shift.start_time.getTime()) {
|
||||||
|
throw new ConflictException(`Invalid time range in preset day: ${week_day}, order: ${shift.sort_order}`);
|
||||||
|
}
|
||||||
|
const conflict = existing.find((existe)=> overlaps(
|
||||||
|
shift.start_time, shift.end_time ,
|
||||||
|
existe.start_time, existe.end_time,
|
||||||
|
));
|
||||||
|
if(conflict) {
|
||||||
|
throw new ConflictException({
|
||||||
|
error_code: 'SHIFT_OVERLAP_WITH_EXISTING',
|
||||||
|
mesage: `Preset shift overlaps existing shift on ${start_date} + ${i}(week day ${week_day})`,
|
||||||
|
conflict: {
|
||||||
|
existing_start: conflict.start_time.toISOString().slice(11,16),
|
||||||
|
existing_end: conflict.end_time.toISOString().slice(11,16),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
payload.push({
|
||||||
|
timesheet_id: timesheet.id,
|
||||||
|
date: date,
|
||||||
|
start_time: shift.start_time,
|
||||||
|
end_time: shift.end_time,
|
||||||
|
is_remote: shift.is_remote,
|
||||||
|
comment: null,
|
||||||
|
bank_code_id: shift.bank_code_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if(payload.length) {
|
||||||
|
const response = await tx.shifts.createMany({ data: payload, skipDuplicates: true });
|
||||||
|
created += response.count;
|
||||||
|
skipped += payload.length - response.count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { timesheet_id: timesheet.id, created, skipped };
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user