fix(shifts): rework update and create to match ShiftEntity
This commit is contained in:
parent
032e1de631
commit
c0189dc61d
|
|
@ -15,6 +15,11 @@ export class EmployeesController {
|
|||
private readonly archiveService: EmployeesArchivalService,
|
||||
) { }
|
||||
|
||||
@Get('profile/:email')
|
||||
findOneProfile(@Param('email') email: string): Promise<EmployeeProfileItemDto> {
|
||||
return this.employeesService.findOneProfile(email);
|
||||
}
|
||||
|
||||
@Get('employee-list')
|
||||
@RolesAllowed(...MANAGER_ROLES)
|
||||
findListEmployees(): Promise<EmployeeListItemDto[]> {
|
||||
|
|
@ -34,6 +39,8 @@ export class EmployeesController {
|
|||
return result;
|
||||
}
|
||||
|
||||
|
||||
|
||||
//_____________________________________________________________________________________________
|
||||
// Deprecated or unused methods
|
||||
//_____________________________________________________________________________________________
|
||||
|
|
@ -46,29 +53,6 @@ export class EmployeesController {
|
|||
// create(@Body() dto: CreateEmployeeDto): Promise<Employees> {
|
||||
// return this.employeesService.create(dto);
|
||||
// }
|
||||
// @Get()
|
||||
// //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ACCOUNTING)
|
||||
// @ApiOperation({summary: 'Find all employees' })
|
||||
// @ApiResponse({ status: 200, description: 'List of employees found', type: CreateEmployeeDto, isArray: true })
|
||||
// @ApiResponse({ status: 400, description: 'List of employees not found' })
|
||||
// findAll(): Promise<Employees[]> {
|
||||
// return this.employeesService.findAll();
|
||||
// }
|
||||
|
||||
|
||||
// @Get(':email')
|
||||
// //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR,RoleEnum.ACCOUNTING )
|
||||
// @ApiOperation({summary: 'Find employee' })
|
||||
// @ApiResponse({ status: 200, description: 'Employee found', type: CreateEmployeeDto })
|
||||
// @ApiResponse({ status: 400, description: 'Employee not found' })
|
||||
// findOne(@Param('email', ParseIntPipe) email: string): Promise<Employees> {
|
||||
// return this.employeesService.findOne(email);
|
||||
// }
|
||||
|
||||
@Get('profile/:email')
|
||||
findOneProfile(@Param('email') email: string): Promise<EmployeeProfileItemDto> {
|
||||
return this.employeesService.findOneProfile(email);
|
||||
}
|
||||
|
||||
// @Delete(':email')
|
||||
// //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR )
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ export class ShiftController {
|
|||
}
|
||||
|
||||
@Patch('update')
|
||||
updateBatch( @Body() dtos: UpdateShiftDto[]): Promise<UpdateShiftResult[]>{
|
||||
updateBatch( @Body() dtos: ShiftDto[]): Promise<UpdateShiftResult[]>{
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
export class GetShiftDto {
|
||||
shift_id: number;
|
||||
timesheet_id: number;
|
||||
type: string;
|
||||
date: string;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
export class ShiftEntity {
|
||||
id: number;
|
||||
timesheet_id: number;
|
||||
bank_code_id: number;
|
||||
date: string;
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
is_remote: boolean;
|
||||
is_approved: boolean;
|
||||
comment?: string;
|
||||
}
|
||||
|
|
@ -1,16 +1,15 @@
|
|||
import { CreateShiftResult, NormedOk, UpdateShiftResult, UpdateShiftPayload, UpdateShiftChanges, Normalized } from "src/time-and-attendance/utils/type.utils";
|
||||
import { CreateShiftResult, NormedOk, UpdateShiftResult, Normalized } from "src/time-and-attendance/utils/type.utils";
|
||||
import { overlaps, toStringFromHHmm, toStringFromDate, toDateFromString, toHHmmFromString } from "src/time-and-attendance/utils/date-time.utils";
|
||||
import { Injectable, BadRequestException, ConflictException, NotFoundException } from "@nestjs/common";
|
||||
import { shift_select, timesheet_select } from "src/time-and-attendance/utils/selects.utils";
|
||||
import { BankCodesResolver } from "src/time-and-attendance/utils/resolve-bank-type-id.utils";
|
||||
import { EmailToIdResolver } from "src/time-and-attendance/utils/resolve-email-id.utils";
|
||||
import { OvertimeService } from "src/time-and-attendance/domains/services/overtime.service";
|
||||
import { UpdateShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-update.dto";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { GetShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-get.dto";
|
||||
import { ShiftEntity } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-payload.dto";
|
||||
import { ShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-create.dto";
|
||||
|
||||
|
||||
import { response } from "express";
|
||||
|
||||
@Injectable()
|
||||
export class ShiftsUpsertService {
|
||||
|
|
@ -33,54 +32,59 @@ export class ShiftsUpsertService {
|
|||
if (!Array.isArray(dtos) || dtos.length === 0) return [];
|
||||
|
||||
const employee_id = await this.emailResolver.findIdByEmail(email);
|
||||
|
||||
const normed_shifts = await Promise.all(
|
||||
dtos.map(async (dto, index) => {
|
||||
try {
|
||||
const normed = await this.normalizeShiftDto(dto);
|
||||
if (normed.end_time <= normed.start_time) {
|
||||
const error = {
|
||||
error_code: 'SHIFT_OVERLAP',
|
||||
conflicts: {
|
||||
start_time: toStringFromHHmm(normed.start_time),
|
||||
end_time: toStringFromHHmm(normed.end_time),
|
||||
date: toStringFromDate(normed.date),
|
||||
},
|
||||
};
|
||||
return { index, error };
|
||||
}
|
||||
if (!normed.end_time) throw new BadRequestException('A shift needs an end_time');
|
||||
if (!normed.start_time) throw new BadRequestException('A shift needs a start_time');
|
||||
|
||||
const timesheet = await this.prisma.timesheets.findUnique({
|
||||
where: { id: dto.timesheet_id, employee_id },
|
||||
select: timesheet_select,
|
||||
});
|
||||
if (!timesheet) {
|
||||
const error = {
|
||||
error_code: 'INVALID_TIMESHEET',
|
||||
conflicts: {
|
||||
start_time: toStringFromHHmm(normed.start_time),
|
||||
end_time: toStringFromHHmm(normed.end_time),
|
||||
date: toStringFromDate(normed.date),
|
||||
},
|
||||
};
|
||||
return { index, error };
|
||||
}
|
||||
|
||||
return {
|
||||
index,
|
||||
dto,
|
||||
normed,
|
||||
timesheet_id: timesheet.id,
|
||||
const results: CreateShiftResult[] = [];
|
||||
const normed_shifts: (NormedOk | undefined)[] = await Promise.all(dtos.map(async (dto, index) => {
|
||||
try {
|
||||
const normed = await this.normalizeShiftDto(dto);
|
||||
if (normed.end_time <= normed.start_time) {
|
||||
const error = {
|
||||
error_code: 'SHIFT_OVERLAP',
|
||||
conflicts: {
|
||||
start_time: toStringFromHHmm(normed.start_time),
|
||||
end_time: toStringFromHHmm(normed.end_time),
|
||||
date: toStringFromDate(normed.date),
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return { index, error };
|
||||
results.push({ ok: false, error });
|
||||
}
|
||||
}));
|
||||
|
||||
const ok_items = normed_shifts.filter(
|
||||
(item): item is NormedOk & { timesheet_id: number } => "normed" in item);
|
||||
const timesheet = await this.prisma.timesheets.findUnique({
|
||||
where: { id: dto.timesheet_id, employee_id },
|
||||
select: timesheet_select,
|
||||
});
|
||||
if (!timesheet) {
|
||||
const error = {
|
||||
error_code: 'INVALID_TIMESHEET',
|
||||
conflicts: {
|
||||
start_time: toStringFromHHmm(normed.start_time),
|
||||
end_time: toStringFromHHmm(normed.end_time),
|
||||
date: toStringFromDate(normed.date),
|
||||
},
|
||||
};
|
||||
results.push({ ok: false, error });
|
||||
return;
|
||||
}
|
||||
const bank_code = await this.typeResolver.findBankCodeIDByType(dto.type);
|
||||
const entity: ShiftEntity = {
|
||||
bank_code_id: bank_code.id,
|
||||
...dto,
|
||||
};
|
||||
|
||||
return {
|
||||
index,
|
||||
dto: entity,
|
||||
normed,
|
||||
timesheet_id: timesheet.id,
|
||||
};
|
||||
} catch (error) {
|
||||
results.push({ ok: false, error });
|
||||
return;
|
||||
}
|
||||
|
||||
}));
|
||||
|
||||
const ok_items = normed_shifts.filter((item) => item !== undefined);
|
||||
|
||||
|
||||
const regroup_by_date = new Map<string, number[]>();
|
||||
ok_items.forEach(({ index, normed, timesheet_id }) => {
|
||||
|
|
@ -151,7 +155,7 @@ export class ShiftsUpsertService {
|
|||
existing_map.set(key, rows.map((row) => ({ start_time: row.start_time, end_time: row.end_time, date: row.date })));
|
||||
}
|
||||
|
||||
normed_shifts.forEach((x, i) => {
|
||||
ok_items.forEach((x, i) => {
|
||||
if ("error" in x) results[i] = { ok: false, error: x.error };
|
||||
});
|
||||
|
||||
|
|
@ -184,7 +188,7 @@ export class ShiftsUpsertService {
|
|||
const row = await tx.shifts.create({
|
||||
data: {
|
||||
timesheet_id: timesheet_id,
|
||||
bank_code_id: normed.id,
|
||||
bank_code_id: normed.bank_code_id,
|
||||
date: normed.date,
|
||||
start_time: normed.start_time,
|
||||
end_time: normed.end_time,
|
||||
|
|
@ -207,6 +211,7 @@ export class ShiftsUpsertService {
|
|||
const summary = await this.overtime.getWeekOvertimeSummary(timesheet_id, normed.date, tx);
|
||||
|
||||
const shift: GetShiftDto = {
|
||||
shift_id: row.id,
|
||||
timesheet_id: timesheet_id,
|
||||
type: bank_type,
|
||||
date: toStringFromDate(row.date),
|
||||
|
|
@ -235,26 +240,24 @@ export class ShiftsUpsertService {
|
|||
// update shifts in DB
|
||||
// recalculate overtime after update
|
||||
// return an updated version to display
|
||||
async updateShifts(dtos: UpdateShiftDto[]): Promise<UpdateShiftResult[]> {
|
||||
async updateShifts(dtos: ShiftDto[]): Promise<UpdateShiftResult[]> {
|
||||
if (!Array.isArray(dtos) || dtos.length === 0) throw new BadRequestException({ error_code: 'SHIFT_MISSING' });
|
||||
|
||||
const updates: UpdateShiftPayload[] = await Promise.all(dtos.map((item) => {
|
||||
const { shift_id, ...rest } = item;
|
||||
if (!shift_id) throw new BadRequestException({ error_code: 'SHIFT_INVALID' });
|
||||
|
||||
const changes: UpdateShiftChanges = {};
|
||||
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.type !== undefined) changes.type = rest.type;
|
||||
if (rest.is_remote !== undefined) changes.is_remote = rest.is_remote;
|
||||
if (rest.comment !== undefined) changes.comment = rest.comment;
|
||||
|
||||
return { shift_id, dto: changes };
|
||||
const updates: ShiftEntity[] = await Promise.all(dtos.map(async (item) => {
|
||||
try {
|
||||
const bank_code = await this.typeResolver.findBankCodeIDByType(item.type);
|
||||
return {
|
||||
bank_code_id: bank_code.id,
|
||||
...item,
|
||||
}
|
||||
} catch (error) {
|
||||
throw new BadRequestException('INVALID_SHIFT');
|
||||
}
|
||||
}));
|
||||
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
const shift_ids = updates.map(update_shift => update_shift.shift_id);
|
||||
|
||||
const shift_ids = updates.map(update_shift => update_shift.id);
|
||||
const rows = await tx.shifts.findMany({
|
||||
where: { id: { in: shift_ids } },
|
||||
select: shift_select,
|
||||
|
|
@ -262,31 +265,28 @@ export class ShiftsUpsertService {
|
|||
const regroup_id = new Map(rows.map(r => [r.id, r]));
|
||||
|
||||
for (const update of updates) {
|
||||
const existing = regroup_id.get(update.shift_id);
|
||||
const existing = regroup_id.get(update.id);
|
||||
if (!existing) {
|
||||
return updates.map(exist => exist.shift_id === update.shift_id
|
||||
? ({ ok: false, id: update.shift_id, error: new NotFoundException({ error_code: 'SHIFT_MISSING' }) } as UpdateShiftResult)
|
||||
: ({ ok: false, id: exist.shift_id, error: new BadRequestException({ error_code: 'SHIFT_INVALID' }) })
|
||||
return updates.map(exist => exist.id === update.id
|
||||
? ({ ok: false, id: update.id, error: new NotFoundException({ error_code: 'SHIFT_MISSING' }) } as UpdateShiftResult)
|
||||
: ({ ok: false, id: exist.id, error: new BadRequestException({ error_code: 'SHIFT_INVALID' }) })
|
||||
);
|
||||
}
|
||||
if (existing.is_approved) {
|
||||
return updates.map(exist => exist.shift_id === update.shift_id
|
||||
? ({ ok: false, id: update.shift_id, error: new BadRequestException({ error_code: 'SHIFT_INVALID' }) } as UpdateShiftResult)
|
||||
: ({ ok: false, id: exist.shift_id, error: new BadRequestException({ error_code: 'SHIFT_INVALID' }) })
|
||||
return updates.map(exist => exist.id === update.id
|
||||
? ({ ok: false, id: update.id, error: new BadRequestException({ error_code: 'SHIFT_INVALID' }) } as UpdateShiftResult)
|
||||
: ({ ok: false, id: exist.id, error: new BadRequestException({ error_code: 'SHIFT_INVALID' }) })
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const planned_updates = updates.map(update => {
|
||||
const exist_shift = regroup_id.get(update.shift_id)!;
|
||||
const date_string = update.dto.date ?? toStringFromDate(exist_shift.date);
|
||||
const start_string = update.dto.start_time ?? toStringFromHHmm(exist_shift.start_time);
|
||||
const end_string = update.dto.end_time ?? toStringFromHHmm(exist_shift.end_time);
|
||||
const exist_shift = regroup_id.get(update.id)!;
|
||||
const normed: Normalized = {
|
||||
date: toDateFromString(date_string),
|
||||
start_time: toHHmmFromString(start_string),
|
||||
end_time: toHHmmFromString(end_string),
|
||||
id: exist_shift.id,
|
||||
date: toDateFromString(update.date),
|
||||
start_time: toHHmmFromString(update.start_time),
|
||||
end_time: toHHmmFromString(update.end_time),
|
||||
bank_code_id: exist_shift.bank_code_id,
|
||||
};
|
||||
return { update, exist_shift, normed };
|
||||
});
|
||||
|
|
@ -329,9 +329,9 @@ export class ShiftsUpsertService {
|
|||
);
|
||||
if (conflict) {
|
||||
return updates.map(exist =>
|
||||
exist.shift_id === planned.exist_shift.id
|
||||
exist.id === planned.exist_shift.id
|
||||
? ({
|
||||
ok: false, id: exist.shift_id, error:{
|
||||
ok: false, id: exist.id, error: {
|
||||
error_code: 'SHIFT_OVERLAP',
|
||||
conflicts: {
|
||||
start_time: toStringFromHHmm(conflict.start),
|
||||
|
|
@ -340,7 +340,7 @@ export class ShiftsUpsertService {
|
|||
},
|
||||
}
|
||||
} as UpdateShiftResult)
|
||||
: ({ ok: false, id: exist.shift_id, error: new BadRequestException('Batch aborted due to overlap in another update') })
|
||||
: ({ ok: false, id: exist.id, error: new BadRequestException('Batch aborted due to overlap in another update') })
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -373,49 +373,59 @@ export class ShiftsUpsertService {
|
|||
},
|
||||
|
||||
};
|
||||
return updates.map(exist => ({ ok: false, id: exist.shift_id, error: error }));
|
||||
return updates.map(exist => ({ ok: false, id: exist.id, error: error }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const results: UpdateShiftResult[] = [];
|
||||
for (const planned of planned_updates) {
|
||||
const data: any = {};
|
||||
const { dto } = planned.update;
|
||||
if (dto.date !== undefined) data.date = planned.normed.date;
|
||||
if (dto.start_time !== undefined) data.start_time = planned.normed.start_time;
|
||||
if (dto.end_time !== undefined) data.end_time = planned.normed.end_time;
|
||||
if (dto.type !== undefined) data.type = dto.type;
|
||||
if (dto.is_remote !== undefined) data.is_remote = dto.is_remote;
|
||||
if (dto.comment !== undefined) data.comment = dto.comment ?? null;
|
||||
try {
|
||||
const date = toStringFromDate(planned.normed.date);
|
||||
const start_time = toStringFromHHmm(planned.normed.start_time);
|
||||
const end_time = toStringFromHHmm(planned.normed.end_time);
|
||||
|
||||
const row = await tx.shifts.update({
|
||||
where: { id: planned.exist_shift.id },
|
||||
data,
|
||||
select: shift_select,
|
||||
});
|
||||
const data: Partial<ShiftEntity> = {
|
||||
bank_code_id: planned.normed.bank_code_id,
|
||||
date: date,
|
||||
start_time: start_time,
|
||||
end_time: end_time,
|
||||
is_remote: planned.update.is_remote,
|
||||
is_approved: planned.exist_shift.is_approved,
|
||||
comment: planned.update.comment,
|
||||
};
|
||||
|
||||
const summary_new = await this.overtime.getWeekOvertimeSummary(row.timesheet_id, planned.exist_shift.date, tx);
|
||||
if (row.date.getTime() !== planned.exist_shift.date.getTime()) {
|
||||
await this.overtime.getWeekOvertimeSummary(row.timesheet_id, row.date, tx);
|
||||
const row = await tx.shifts.update({
|
||||
where: { id: planned.exist_shift.id },
|
||||
data,
|
||||
select: shift_select,
|
||||
});
|
||||
const summary_new = await this.overtime.getWeekOvertimeSummary(row.timesheet_id, planned.exist_shift.date, tx);
|
||||
if (row.date.getTime() !== planned.exist_shift.date.getTime()) {
|
||||
await this.overtime.getWeekOvertimeSummary(row.timesheet_id, row.date, tx);
|
||||
}
|
||||
|
||||
const type = await this.typeResolver.findTypeByBankCodeId(row.bank_code_id);
|
||||
|
||||
const dto: GetShiftDto = {
|
||||
shift_id: row.id,
|
||||
timesheet_id: row.timesheet_id,
|
||||
type: type.type,
|
||||
date: toStringFromDate(row.date),
|
||||
start_time: toStringFromHHmm(row.start_time),
|
||||
end_time: toStringFromHHmm(row.end_time),
|
||||
is_approved: row.is_approved,
|
||||
is_remote: row.is_remote,
|
||||
comment: row.comment ?? undefined,
|
||||
};
|
||||
|
||||
results.push({ ok: true, id: planned.exist_shift.id, data: { shift: dto, overtime: summary_new } });
|
||||
} catch (error) {
|
||||
throw new BadRequestException('INVALID_SHIFT');
|
||||
}
|
||||
|
||||
const shift: GetShiftDto = {
|
||||
timesheet_id: row.timesheet_id,
|
||||
type: data.type,
|
||||
date: toStringFromDate(row.date),
|
||||
start_time: toStringFromHHmm(row.start_time),
|
||||
end_time: toStringFromHHmm(row.end_time),
|
||||
is_approved: row.is_approved,
|
||||
is_remote: row.is_remote,
|
||||
comment: row.comment ?? undefined,
|
||||
};
|
||||
|
||||
results.push({ ok: true, id: planned.exist_shift.id, data: { shift, overtime: summary_new } });
|
||||
}
|
||||
return results;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
//_________________________________________________________________
|
||||
|
|
@ -451,6 +461,6 @@ export class ShiftsUpsertService {
|
|||
const date = toDateFromString(dto.date);
|
||||
const start_time = toHHmmFromString(dto.start_time);
|
||||
const end_time = toHHmmFromString(dto.end_time);
|
||||
return { date, start_time, end_time, id: bank_code_id };
|
||||
return { date, start_time, end_time, bank_code_id: bank_code_id };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { updateExpenseDto } from "src/time-and-attendance/expenses/dtos/expense-
|
|||
import { SchedulePresetsDto } from "src/time-and-attendance/time-tracker/schedule-presets/dtos/create-schedule-presets.dto";
|
||||
import { ShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-create.dto";
|
||||
import { GetShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-get.dto";
|
||||
import { ShiftEntity } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-payload.dto";
|
||||
import { UpdateShiftDto } from "src/time-and-attendance/time-tracker/shifts/dtos/shift-update.dto";
|
||||
import { leaveRequestsSelect } from "src/time-and-attendance/utils/selects.utils";
|
||||
|
||||
|
|
@ -25,7 +26,7 @@ export type TotalExpenses = {
|
|||
mileage: number;
|
||||
};
|
||||
|
||||
export type Normalized = { date: Date; start_time: Date; end_time: Date; id: number};
|
||||
export type Normalized = { date: Date; start_time: Date; end_time: Date; bank_code_id: number};
|
||||
|
||||
export type ShiftWithOvertimeDto = {
|
||||
shift: GetShiftDto;
|
||||
|
|
@ -51,7 +52,7 @@ export type DeleteExpenseResult = { ok: true; id: number; } | { ok: false; id: n
|
|||
|
||||
|
||||
|
||||
export type NormedOk = { index: number; dto: ShiftDto; normed: Normalized };
|
||||
export type NormedOk = { index: number; dto: ShiftEntity; normed: Normalized, timesheet_id: number };
|
||||
export type NormedErr = { index: number; error: any };
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user