247 lines
9.3 KiB
TypeScript
247 lines
9.3 KiB
TypeScript
import { BadRequestException, ConflictException, Injectable, NotFoundException, UnprocessableEntityException } from "@nestjs/common";
|
|
import { formatHHmm, toDateOnlyUTC, weekStartMondayUTC } from "../helpers/shifts-date-time-helpers";
|
|
import { normalizeShiftPayload, overlaps, resolveBankCodeByType } from "../utils/shifts.utils";
|
|
import { DayShiftResponse, UpsertAction } from "../types and interfaces/shifts-upsert.types";
|
|
import { Prisma, Shifts } from "@prisma/client";
|
|
import { UpsertShiftDto } from "../dtos/upsert-shift.dto";
|
|
import { BaseApprovalService } from "src/common/shared/base-approval.service";
|
|
import { PrismaService } from "src/prisma/prisma.service";
|
|
|
|
@Injectable()
|
|
export class ShiftsCommandService extends BaseApprovalService<Shifts> {
|
|
constructor(prisma: PrismaService) { super(prisma); }
|
|
|
|
//_____________________________________________________________________________________________
|
|
// APPROVAL AND DELEGATE METHODS
|
|
//_____________________________________________________________________________________________
|
|
protected get delegate() {
|
|
return this.prisma.shifts;
|
|
}
|
|
|
|
protected delegateFor(transaction: Prisma.TransactionClient) {
|
|
return transaction.shifts;
|
|
}
|
|
|
|
async updateApproval(id: number, is_approved: boolean): Promise<Shifts> {
|
|
return this.prisma.$transaction((transaction) =>
|
|
this.updateApprovalWithTransaction(transaction, id, is_approved),
|
|
);
|
|
}
|
|
|
|
//_____________________________________________________________________________________________
|
|
// MASTER CRUD METHOD
|
|
//_____________________________________________________________________________________________
|
|
async upsertShiftsByDate(email:string, date_string: string, dto: UpsertShiftDto):
|
|
Promise<{ action: UpsertAction; day: DayShiftResponse[] }> {
|
|
const { old_shift, new_shift } = dto;
|
|
|
|
if(!dto.old_shift && !dto.new_shift) {
|
|
throw new BadRequestException('At least one of old or new shift must be provided');
|
|
}
|
|
|
|
const date_only = toDateOnlyUTC(date_string);
|
|
|
|
//Resolve employee by email
|
|
const employee = await this.prisma.employees.findFirst({
|
|
where: { user: {email } },
|
|
select: { id: true },
|
|
});
|
|
if(!employee) throw new NotFoundException(`Employee not found for email : ${ email }`);
|
|
|
|
//making sure a timesheet exist in selected week
|
|
const start_of_week = weekStartMondayUTC(date_only);
|
|
let timesheet = await this.prisma.timesheets.findFirst({
|
|
where: {
|
|
employee_id: employee.id,
|
|
start_date: start_of_week
|
|
},
|
|
select: {
|
|
id: true
|
|
},
|
|
});
|
|
if(!timesheet) {
|
|
timesheet = await this.prisma.timesheets.create({
|
|
data: {
|
|
employee_id: employee.id,
|
|
start_date: start_of_week
|
|
},
|
|
select: {
|
|
id: true
|
|
},
|
|
});
|
|
}
|
|
|
|
//normalization of data to ensure a valid comparison between DB and payload
|
|
const old_norm = dto.old_shift
|
|
? normalizeShiftPayload(dto.old_shift)
|
|
: undefined;
|
|
const new_norm = dto.new_shift
|
|
? normalizeShiftPayload(dto.new_shift)
|
|
: undefined;
|
|
|
|
if (old_norm && old_norm.end_time.getTime() <= old_norm.start_time.getTime()) {
|
|
throw new UnprocessableEntityException(' old_shift.end_time must be > old_shift.start_time');
|
|
}
|
|
if (new_norm && new_norm.end_time.getTime() <= new_norm.start_time.getTime()) {
|
|
throw new UnprocessableEntityException(' new_shift.end_time must be > new_shift.start_time');
|
|
}
|
|
|
|
//Resolve bank_code_id with type
|
|
const old_bank_code_id = old_norm
|
|
? await resolveBankCodeByType(old_norm.type)
|
|
: undefined;
|
|
const new_bank_code_id = new_norm
|
|
? await resolveBankCodeByType(new_norm.type)
|
|
: undefined;
|
|
|
|
//fetch all shifts in a single day
|
|
const day_shifts = await this.prisma.shifts.findMany({
|
|
where: {
|
|
timesheet_id: timesheet.id,
|
|
date: date_only
|
|
},
|
|
include: {
|
|
bank_code: true
|
|
},
|
|
orderBy: {
|
|
start_time: 'asc'
|
|
},
|
|
});
|
|
|
|
const result = await this.prisma.$transaction(async (transaction)=> {
|
|
let action: UpsertAction;
|
|
|
|
const findExactOldShift = async ()=> {
|
|
if(!old_norm || old_bank_code_id === undefined) return undefined;
|
|
const old_comment = old_norm.comment ?? null;
|
|
|
|
return transaction.shifts.findFirst({
|
|
where: {
|
|
timesheet_id: timesheet.id,
|
|
date: date_only,
|
|
start_time: old_norm.start_time,
|
|
end_time: old_norm.end_time,
|
|
is_remote: old_norm.is_remote,
|
|
comment: old_comment,
|
|
bank_code_id: old_bank_code_id,
|
|
},
|
|
select: { id: true },
|
|
});
|
|
};
|
|
|
|
//checks for overlaping shifts
|
|
const assertNoOverlap = (exclude_shift_id?: number)=> {
|
|
if (!new_norm) return;
|
|
const overlap_with = day_shifts.filter((shift)=> {
|
|
if(exclude_shift_id && shift.id === exclude_shift_id) return false;
|
|
return overlaps(
|
|
new_norm.start_time.getTime(),
|
|
new_norm.end_time.getTime(),
|
|
shift.start_time.getTime(),
|
|
shift.end_time.getTime(),
|
|
);
|
|
});
|
|
|
|
if(overlap_with.length > 0) {
|
|
const conflicts = overlap_with.map((shift)=> ({
|
|
start_time: formatHHmm(shift.start_time),
|
|
end_time: formatHHmm(shift.end_time),
|
|
type: shift.bank_code?.type ?? 'UNKNOWN',
|
|
}));
|
|
throw new ConflictException({
|
|
error_code: 'SHIFT_OVERLAP',
|
|
message: 'New shift overlaps with existing shift(s)',
|
|
conflicts,
|
|
});
|
|
}
|
|
};
|
|
|
|
//_____________________________________________________________________________________________
|
|
// DELETE
|
|
//_____________________________________________________________________________________________
|
|
if ( old_shift && !new_shift ) {
|
|
const existing = await findExactOldShift();
|
|
if(!existing) {
|
|
throw new NotFoundException({
|
|
error_code: 'SHIFT_STALE',
|
|
message: 'The shift was modified or deleted by someone else',
|
|
});
|
|
}
|
|
await transaction.shifts.delete({ where: { id: existing.id } } );
|
|
action = 'deleted';
|
|
}
|
|
//_____________________________________________________________________________________________
|
|
// CREATE
|
|
//_____________________________________________________________________________________________
|
|
else if (!old_shift && new_shift) {
|
|
assertNoOverlap();
|
|
await transaction.shifts.create({
|
|
data: {
|
|
timesheet_id: timesheet.id,
|
|
date: date_only,
|
|
start_time: new_norm!.start_time,
|
|
end_time: new_norm!.end_time,
|
|
is_remote: new_norm!.is_remote,
|
|
comment: new_norm!.comment ?? null,
|
|
bank_code_id: new_bank_code_id!,
|
|
},
|
|
});
|
|
action = 'created';
|
|
}
|
|
//_____________________________________________________________________________________________
|
|
// UPDATE
|
|
//_____________________________________________________________________________________________
|
|
else if (old_shift && new_shift){
|
|
const existing = await findExactOldShift();
|
|
if(!existing) {
|
|
throw new NotFoundException({
|
|
error_code: 'SHIFT_STALE',
|
|
message: 'The shift was modified or deleted by someone else',
|
|
});
|
|
}
|
|
assertNoOverlap(existing.id);
|
|
await transaction.shifts.update({
|
|
where: {
|
|
id: existing.id
|
|
},
|
|
data: {
|
|
start_time: new_norm!.start_time,
|
|
end_time: new_norm!.end_time,
|
|
is_remote: new_norm!.is_remote,
|
|
comment: new_norm!.comment ?? null,
|
|
bank_code_id: new_bank_code_id,
|
|
},
|
|
});
|
|
action = 'updated';
|
|
} else {
|
|
throw new BadRequestException('At least one of old_shift or new_shift must be provided');
|
|
}
|
|
|
|
//Reload the day (truth source)
|
|
const fresh_day = await transaction.shifts.findMany({
|
|
where: {
|
|
date: date_only,
|
|
timesheet_id: timesheet.id,
|
|
},
|
|
include: {
|
|
bank_code: true
|
|
},
|
|
orderBy: {
|
|
start_time: 'asc'
|
|
},
|
|
});
|
|
|
|
return {
|
|
action,
|
|
day: fresh_day.map<DayShiftResponse>((shift)=> ({
|
|
start_time: formatHHmm(shift.start_time),
|
|
end_time: formatHHmm(shift.end_time),
|
|
type: shift.bank_code?.type ?? 'UNKNOWN',
|
|
is_remote: shift.is_remote,
|
|
comment: shift.comment ?? null,
|
|
})),
|
|
};
|
|
});
|
|
return result;
|
|
}
|
|
} |