feat: schedule page UX improvements + CDT combo price skip

- Move Select Procedures above Check Eligibility in appointment right-click menu
- Show 3 blank service lines by default when opening Select Procedures with no saved procedures
- Fix serviceLines not being preserved when API returns empty procedures list
- CDT combo buttons no longer auto-fill price (only fill codes); user maps price via Map Price button
- Overlap detection in schedule: shorten earlier appointment display span when a later one starts within its range
- Procedures dialog: replace single manual-add row with 3 pre-filled blank rows grid + Add Line / Save Lines buttons

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ff
2026-05-30 19:37:12 -04:00
parent 70f36fc13c
commit 4bf8cb1a94
4 changed files with 139 additions and 97 deletions

View File

@@ -597,10 +597,40 @@ export default function AppointmentsPage() {
return Math.max(1, Math.round(diff / 15));
};
// Compute display span — same as getSlotSpan but truncated if a later appointment in the
// same staff column starts within this appointment's time range (overlap case).
const getDisplaySpan = (apt: ScheduledAppointment): number => {
const fullSpan = getSlotSpan(apt);
const startStr = (typeof apt.startTime === "string" ? apt.startTime : formatLocalTime(apt.startTime)).substring(0, 5);
const [startH, startM] = startStr.split(":").map(Number);
const startMinutes = (startH ?? 0) * 60 + (startM ?? 0);
const nextOverlap = (processedAppointments ?? [])
.filter((other) => {
if (other.id === apt.id || other.staffId !== apt.staffId) return false;
const otherStart = (typeof other.startTime === "string" ? other.startTime : formatLocalTime(other.startTime)).substring(0, 5);
const [oh, om] = otherStart.split(":").map(Number);
const otherMin = (oh ?? 0) * 60 + (om ?? 0);
return otherMin > startMinutes && otherMin < startMinutes + fullSpan * 15;
})
.sort((a, b) => {
const aStart = (typeof a.startTime === "string" ? a.startTime : formatLocalTime(a.startTime)).substring(0, 5);
const bStart = (typeof b.startTime === "string" ? b.startTime : formatLocalTime(b.startTime)).substring(0, 5);
return aStart.localeCompare(bStart);
})[0];
if (!nextOverlap) return fullSpan;
const nextStart = (typeof nextOverlap.startTime === "string" ? nextOverlap.startTime : formatLocalTime(nextOverlap.startTime)).substring(0, 5);
const [nh, nm] = nextStart.split(":").map(Number);
const nextMin = (nh ?? 0) * 60 + (nm ?? 0);
return Math.max(1, Math.round((nextMin - startMinutes) / 15));
};
// Slots that are "continued" rows of a multi-slot appointment (should not render a td)
const coveredSlots = new Set<string>();
(processedAppointments ?? []).forEach((apt) => {
const span = getSlotSpan(apt);
const span = getDisplaySpan(apt);
if (span <= 1) return;
const startStr = (typeof apt.startTime === "string" ? apt.startTime : formatLocalTime(apt.startTime)).substring(0, 5);
const [startH, startM] = startStr.split(":").map(Number);
@@ -1556,16 +1586,6 @@ export default function AppointmentsPage() {
</span>
</Item>
{/* Check Eligibility */}
<Item
onClick={({ props }) => handleCheckEligibility(props.appointmentId)}
>
<span className="flex items-center gap-2">
<Shield className="h-4 w-4" />
Check Eligibility
</span>
</Item>
{/* Select Procedures */}
<Item
onClick={({ props }) => handleSelectProcedures(props.appointmentId)}
@@ -1576,6 +1596,16 @@ export default function AppointmentsPage() {
</span>
</Item>
{/* Check Eligibility */}
<Item
onClick={({ props }) => handleCheckEligibility(props.appointmentId)}
>
<span className="flex items-center gap-2">
<Shield className="h-4 w-4" />
Check Eligibility
</span>
</Item>
{/* Claims / PreAuth */}
<Item
onClick={({ props }) => handleClaimsPreAuth(props.appointmentId)}
@@ -1806,7 +1836,7 @@ export default function AppointmentsPage() {
{staffMembers.map((staff, staffIndex) => {
if (coveredSlots.has(`${timeSlot.time}-${staff.id}`)) return null;
const apt = getAppointmentAtSlot(timeSlot, Number(staff.id));
const span = apt ? getSlotSpan(apt) : 1;
const span = apt ? getDisplaySpan(apt) : 1;
return (
<DroppableTimeSlot
key={`${timeSlot.time}-${staff.id}`}