feature: step 2 wizard UX for working hours improved. appointment grid height increased.
This commit is contained in:
@@ -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');
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user