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:
2026-06-10 19:44:09 +03:30
parent 468572a50f
commit 48ecfd05e1
22 changed files with 1571 additions and 116 deletions

View File

@@ -5,6 +5,12 @@ import {
NotFoundException,
} from '@nestjs/common';
import { PrismaService } from '../../../prisma/prisma.service';
import {
appointmentWithinWorkingHours,
blocksForDay,
localDayOfWeekMondayZero,
} from '../../common/working-hours';
import { StaffWorkingHoursService } from '../staff/staff-working-hours.service';
import { CreateAppointmentDto } from './dto/create-appointment.dto';
import { ListAppointmentsDto } from './dto/list-appointments.dto';
import { UpdateAppointmentDto } from './dto/update-appointment.dto';
@@ -13,7 +19,10 @@ const MS_PER_DAY = 86_400_000;
@Injectable()
export class AppointmentsService {
constructor(private readonly prisma: PrismaService) {}
constructor(
private readonly prisma: PrismaService,
private readonly staffWorkingHoursService: StaffWorkingHoursService,
) {}
getOrganizationIdFromUser(user: { organizationId?: string }) {
if (!user?.organizationId) {
@@ -22,7 +31,7 @@ export class AppointmentsService {
return user.organizationId;
}
async listColumnProviders(organizationId: string, actorUserId: string) {
async listColumnProviders(organizationId: string, actorUserId: string, date?: string) {
await this.assertCanViewAppointments(actorUserId, organizationId);
const members = await this.prisma.membership.findMany({
@@ -44,7 +53,23 @@ export class AppointmentsService {
orderBy: [{ createdAt: 'asc' }],
});
const data = members.map((m) => ({ userId: m.user.id, name: m.user.name }));
const scheduleBlocksByMembership =
await this.staffWorkingHoursService.loadScheduleBlocksByMembershipIds(
members.map((m) => m.id),
);
const dayOfWeek = this.resolveDayOfWeekMondayZero(date);
const data = members.map((m) => {
const allBlocks = scheduleBlocksByMembership.get(m.id) ?? [];
const dayBlocks = blocksForDay(allBlocks, dayOfWeek);
return {
userId: m.user.id,
name: m.user.name,
hasWorkingHours: allBlocks.length > 0,
dayBlocks,
};
});
return { success: true, data };
}
@@ -109,6 +134,12 @@ export class AppointmentsService {
await this.ensurePatientInOrg(dto.patientId, organizationId);
await this.ensureProviderIsTreatmentEditor(dto.providerUserId, organizationId);
await this.ensureAppointmentWithinProviderWorkingHours(
dto.providerUserId,
organizationId,
startAt,
endAt,
);
const appointment = await this.prisma.appointment.create({
data: {
@@ -166,6 +197,12 @@ export class AppointmentsService {
await this.ensurePatientInOrg(patientId, organizationId);
await this.ensureProviderIsTreatmentEditor(providerUserId, organizationId);
await this.ensureAppointmentWithinProviderWorkingHours(
providerUserId,
organizationId,
startAt,
endAt,
);
const appointment = await this.prisma.appointment.update({
where: { id },
@@ -273,6 +310,47 @@ export class AppointmentsService {
}
}
private resolveDayOfWeekMondayZero(date?: string): number {
if (!date) {
return localDayOfWeekMondayZero(new Date().getDay());
}
const [y, m, d] = date.split('-').map(Number);
const parsed = new Date(y, m - 1, d, 12, 0, 0, 0);
if (Number.isNaN(parsed.getTime())) {
throw new BadRequestException('Invalid date query parameter');
}
return localDayOfWeekMondayZero(parsed.getDay());
}
private async ensureAppointmentWithinProviderWorkingHours(
providerUserId: string,
organizationId: string,
startAt: Date,
endAt: Date,
) {
const membership = await this.getMembership(providerUserId, organizationId);
if (!membership) {
throw new BadRequestException('Provider is not a member of this organization');
}
const scheduleBlocksByMembership =
await this.staffWorkingHoursService.loadScheduleBlocksByMembershipIds([membership.id]);
const allBlocks = scheduleBlocksByMembership.get(membership.id) ?? [];
if (allBlocks.length === 0) {
throw new BadRequestException('Provider has no working hours configured');
}
const dayOfWeek = localDayOfWeekMondayZero(startAt.getDay());
const dayBlocks = blocksForDay(allBlocks, dayOfWeek);
if (dayBlocks.length === 0) {
throw new BadRequestException('Provider is not working on this day');
}
if (!appointmentWithinWorkingHours(startAt, endAt, dayBlocks)) {
throw new BadRequestException('Appointment must fall within the provider working hours');
}
}
private async getMembership(userId: string, organizationId: string) {
return this.prisma.membership.findFirst({
where: { userId, organizationId },