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:
@@ -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}`}
|
||||
|
||||
Reference in New Issue
Block a user