feature/working-hours #42

Merged
admin merged 3 commits from feature/working-hours into master 2026-06-20 00:46:41 +03:30
2 changed files with 69 additions and 6 deletions
Showing only changes of commit 1f440086af - Show all commits

View File

@@ -6,7 +6,9 @@ import {
} from '@nestjs/common'; } from '@nestjs/common';
import { PrismaService } from '../../../prisma/prisma.service'; import { PrismaService } from '../../../prisma/prisma.service';
import { import {
appointmentWithinWorkingHours,
blocksForDay, blocksForDay,
localDayOfWeekMondayZero,
validateWorkingHoursBlocks, validateWorkingHoursBlocks,
type WorkingHoursBlockInput, type WorkingHoursBlockInput,
} from '../../common/working-hours'; } from '../../common/working-hours';
@@ -68,6 +70,11 @@ export class StaffWorkingHoursService {
} }
const normalizedBlocks = this.normalizeBlocks(dto.blocks); const normalizedBlocks = this.normalizeBlocks(dto.blocks);
await this.assertNoConflictingAppointments(
organizationId,
membership.userId,
normalizedBlocks,
);
await this.prisma.$transaction(async (tx) => { await this.prisma.$transaction(async (tx) => {
const schedule = await tx.staffWorkingHoursSchedule.upsert({ const schedule = await tx.staffWorkingHoursSchedule.upsert({
@@ -142,10 +149,66 @@ export class StaffWorkingHoursService {
})); }));
} }
private async assertNoConflictingAppointments(
organizationId: string,
providerUserId: string,
blocks: WorkingHoursBlockInput[],
) {
const now = new Date();
const appointments = await this.prisma.appointment.findMany({
where: {
organizationId,
providerUserId,
endAt: { gt: now },
},
include: {
patient: { select: { firstName: true, lastName: true } },
},
orderBy: { startAt: 'asc' },
});
const conflicts = appointments.filter((appointment) => {
const startAt = new Date(appointment.startAt);
const endAt = new Date(appointment.endAt);
const dayOfWeek = localDayOfWeekMondayZero(startAt.getDay());
const dayBlocks = blocksForDay(blocks, dayOfWeek);
if (dayBlocks.length === 0) {
return true;
}
return !appointmentWithinWorkingHours(startAt, endAt, dayBlocks);
});
if (conflicts.length === 0) {
return;
}
const examples = conflicts.slice(0, 3).map((appointment) => {
const startAt = new Date(appointment.startAt);
const patientName = `${appointment.patient.firstName} ${appointment.patient.lastName}`;
const when = startAt.toLocaleString(undefined, {
weekday: 'short',
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
});
return `${patientName} (${when})`;
});
const extra =
conflicts.length > examples.length
? ` and ${conflicts.length - examples.length} more`
: '';
throw new BadRequestException(
`Cannot save working hours: ${conflicts.length} upcoming appointment${conflicts.length === 1 ? '' : 's'} fall outside the new schedule (${examples.join(', ')}${extra}). Reschedule or remove those appointments first.`,
);
}
private async findMembership(membershipId: string, organizationId: string) { private async findMembership(membershipId: string, organizationId: string) {
const membership = await this.prisma.membership.findFirst({ const membership = await this.prisma.membership.findFirst({
where: { id: membershipId, organizationId }, where: { id: membershipId, organizationId },
select: { id: true, isOwner: true }, select: { id: true, isOwner: true, userId: true },
}); });
if (!membership) { if (!membership) {
throw new NotFoundException('Member not found'); throw new NotFoundException('Member not found');

View File

@@ -416,11 +416,6 @@ export default function StaffPage() {
setEditLoading(true); setEditLoading(true);
toast.setError(''); toast.setError('');
try { try {
await staffApi.updateMember(editing.id, {
name: editName.trim(),
permissionNames: permissionNamesFromFeatureState(editPerms),
});
if (editHasTreatmentEdit) { if (editHasTreatmentEdit) {
await staffApi.upsertWorkingHours( await staffApi.upsertWorkingHours(
editing.id, editing.id,
@@ -431,6 +426,11 @@ export default function StaffPage() {
); );
} }
await staffApi.updateMember(editing.id, {
name: editName.trim(),
permissionNames: permissionNamesFromFeatureState(editPerms),
});
toast.showSuccess('Member updated.'); toast.showSuccess('Member updated.');
setEditing(null); setEditing(null);
setEditStep(1); setEditStep(1);