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:
@@ -13,6 +13,7 @@ import {
|
||||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { AppointmentsService } from './appointments.service';
|
||||
import { ColumnProvidersQueryDto } from './dto/column-providers-query.dto';
|
||||
import { CreateAppointmentDto } from './dto/create-appointment.dto';
|
||||
import { ListAppointmentsDto } from './dto/list-appointments.dto';
|
||||
import { UpdateAppointmentDto } from './dto/update-appointment.dto';
|
||||
@@ -29,9 +30,16 @@ export class AppointmentsController {
|
||||
summary:
|
||||
'Staff columns: active non-owner members with TAB_TREATMENT_EDIT. Owners are excluded. Requires TAB_APPOINTMENTS_READ or owner.',
|
||||
})
|
||||
columnProviders(@Req() req: { user: { id: string; organizationId?: string } }) {
|
||||
columnProviders(
|
||||
@Query() query: ColumnProvidersQueryDto,
|
||||
@Req() req: { user: { id: string; organizationId?: string } },
|
||||
) {
|
||||
const organizationId = this.appointmentsService.getOrganizationIdFromUser(req.user);
|
||||
return this.appointmentsService.listColumnProviders(organizationId, req.user.id);
|
||||
return this.appointmentsService.listColumnProviders(
|
||||
organizationId,
|
||||
req.user.id,
|
||||
query.date,
|
||||
);
|
||||
}
|
||||
|
||||
@Get()
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PrismaService } from '../../../prisma/prisma.service';
|
||||
import { StaffModule } from '../staff/staff.module';
|
||||
import { AppointmentsController } from './appointments.controller';
|
||||
import { AppointmentsService } from './appointments.service';
|
||||
|
||||
@Module({
|
||||
imports: [StaffModule],
|
||||
controllers: [AppointmentsController],
|
||||
providers: [AppointmentsService, PrismaService],
|
||||
})
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { IsOptional, IsString, Matches } from 'class-validator';
|
||||
|
||||
export class ColumnProvidersQueryDto {
|
||||
/** Local calendar date (YYYY-MM-DD) used to resolve weekday working hours. */
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Matches(/^\d{4}-\d{2}-\d{2}$/)
|
||||
date?: string;
|
||||
}
|
||||
Reference in New Issue
Block a user