feature: users with treatment edit permission should now have working hours defined. the appointment grid is now being drawn based on the doctor's working hours.
This commit is contained in:
118
backend/src/common/working-hours.ts
Normal file
118
backend/src/common/working-hours.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
export const MINUTES_PER_DAY = 24 * 60;
|
||||
|
||||
export type WorkingHoursBlockInput = {
|
||||
dayOfWeek: number;
|
||||
startMinute: number;
|
||||
endMinute: number;
|
||||
sortOrder?: number;
|
||||
};
|
||||
|
||||
export type WorkingHoursDayBlock = {
|
||||
startMinute: number;
|
||||
endMinute: number;
|
||||
};
|
||||
|
||||
export function localDayOfWeekMondayZero(dayOfWeekJs: number): number {
|
||||
return dayOfWeekJs === 0 ? 6 : dayOfWeekJs - 1;
|
||||
}
|
||||
|
||||
export function validateWorkingHoursBlocks(blocks: WorkingHoursBlockInput[]): string | null {
|
||||
if (!Array.isArray(blocks)) {
|
||||
return 'Working hours blocks must be an array';
|
||||
}
|
||||
|
||||
const byDay = new Map<number, WorkingHoursBlockInput[]>();
|
||||
for (const block of blocks) {
|
||||
if (!Number.isInteger(block.dayOfWeek) || block.dayOfWeek < 0 || block.dayOfWeek > 6) {
|
||||
return 'dayOfWeek must be an integer from 0 (Monday) to 6 (Sunday)';
|
||||
}
|
||||
if (!Number.isInteger(block.startMinute) || block.startMinute < 0 || block.startMinute >= MINUTES_PER_DAY) {
|
||||
return 'startMinute must be between 0 and 1439';
|
||||
}
|
||||
if (!Number.isInteger(block.endMinute) || block.endMinute <= 0 || block.endMinute > MINUTES_PER_DAY) {
|
||||
return 'endMinute must be between 1 and 1440';
|
||||
}
|
||||
if (block.endMinute <= block.startMinute) {
|
||||
return 'Each shift end time must be after its start time';
|
||||
}
|
||||
const list = byDay.get(block.dayOfWeek) ?? [];
|
||||
list.push(block);
|
||||
byDay.set(block.dayOfWeek, list);
|
||||
}
|
||||
|
||||
for (const [, dayBlocks] of byDay) {
|
||||
const sorted = [...dayBlocks].sort((a, b) => a.startMinute - b.startMinute);
|
||||
for (let i = 1; i < sorted.length; i += 1) {
|
||||
if (sorted[i].startMinute < sorted[i - 1].endMinute) {
|
||||
return 'Shifts on the same day cannot overlap';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function blocksForDay(
|
||||
blocks: WorkingHoursBlockInput[],
|
||||
dayOfWeek: number,
|
||||
): WorkingHoursDayBlock[] {
|
||||
return blocks
|
||||
.filter((b) => b.dayOfWeek === dayOfWeek)
|
||||
.sort((a, b) => a.startMinute - b.startMinute || a.endMinute - b.endMinute)
|
||||
.map((b) => ({ startMinute: b.startMinute, endMinute: b.endMinute }));
|
||||
}
|
||||
|
||||
export function isMinuteWithinWorkingBlocks(minute: number, dayBlocks: WorkingHoursDayBlock[]): boolean {
|
||||
return dayBlocks.some((b) => minute >= b.startMinute && minute < b.endMinute);
|
||||
}
|
||||
|
||||
export function isRangeWithinWorkingBlocks(
|
||||
startMinute: number,
|
||||
endMinute: number,
|
||||
dayBlocks: WorkingHoursDayBlock[],
|
||||
): boolean {
|
||||
if (endMinute <= startMinute) {
|
||||
return false;
|
||||
}
|
||||
for (let m = startMinute; m < endMinute; m += 1) {
|
||||
if (!isMinuteWithinWorkingBlocks(m, dayBlocks)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function dateToLocalMinutes(date: Date): number {
|
||||
return date.getHours() * 60 + date.getMinutes();
|
||||
}
|
||||
|
||||
export function appointmentWithinWorkingHours(
|
||||
startAt: Date,
|
||||
endAt: Date,
|
||||
dayBlocks: WorkingHoursDayBlock[],
|
||||
): boolean {
|
||||
const startMinute = dateToLocalMinutes(startAt);
|
||||
const endMinute = dateToLocalMinutes(endAt);
|
||||
return isRangeWithinWorkingBlocks(startMinute, endMinute, dayBlocks);
|
||||
}
|
||||
|
||||
export function unionDayBlockRange(dayBlocksList: WorkingHoursDayBlock[][]): {
|
||||
startMinute: number;
|
||||
endMinute: number;
|
||||
} | null {
|
||||
let startMinute: number | null = null;
|
||||
let endMinute: number | null = null;
|
||||
|
||||
for (const dayBlocks of dayBlocksList) {
|
||||
for (const block of dayBlocks) {
|
||||
startMinute = startMinute == null ? block.startMinute : Math.min(startMinute, block.startMinute);
|
||||
endMinute = endMinute == null ? block.endMinute : Math.max(endMinute, block.endMinute);
|
||||
}
|
||||
}
|
||||
|
||||
if (startMinute == null || endMinute == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { startMinute, endMinute };
|
||||
}
|
||||
Reference in New Issue
Block a user