feat(staff column order in aptmpt page)
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import { Staff } from "@repo/db/types";
|
||||
import { Staff, StaffFormData } from "@repo/db/types";
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
interface StaffFormProps {
|
||||
initialData?: Partial<Staff>;
|
||||
onSubmit: (data: Omit<Staff, "id" | "createdAt">) => void;
|
||||
onSubmit: (data: StaffFormData) => void;
|
||||
onCancel: () => void;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
@@ -21,6 +21,8 @@ export function StaffForm({
|
||||
|
||||
const [hasTypedRole, setHasTypedRole] = useState(false);
|
||||
|
||||
const [displayOrder, setDisplayOrder] = useState<number | "">("");
|
||||
|
||||
// Set initial values once on mount
|
||||
useEffect(() => {
|
||||
if (initialData) {
|
||||
@@ -28,6 +30,9 @@ export function StaffForm({
|
||||
if (initialData.email) setEmail(initialData.email);
|
||||
if (initialData.role) setRole(initialData.role);
|
||||
if (initialData.phone) setPhone(initialData.phone);
|
||||
if (initialData?.displayOrder !== undefined) {
|
||||
setDisplayOrder(initialData.displayOrder);
|
||||
}
|
||||
}
|
||||
}, []); // run once only
|
||||
|
||||
@@ -43,6 +48,7 @@ export function StaffForm({
|
||||
email: email.trim() || undefined,
|
||||
role: role.trim(),
|
||||
phone: phone.trim() || undefined,
|
||||
displayOrder: displayOrder === "" ? undefined : displayOrder,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -95,6 +101,24 @@ export function StaffForm({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
Column Order
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
className="mt-1 block w-full border rounded p-2"
|
||||
value={displayOrder}
|
||||
onChange={(e) =>
|
||||
setDisplayOrder(e.target.value === "" ? "" : Number(e.target.value))
|
||||
}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<p className="text-xs text-gray-500">
|
||||
Lower number appears first in appointment schedule
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Phone</label>
|
||||
<input
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
Patient,
|
||||
PatientStatus,
|
||||
UpdateAppointment,
|
||||
Staff as DBStaff,
|
||||
} from "@repo/db/types";
|
||||
import {
|
||||
Popover,
|
||||
@@ -61,12 +62,9 @@ interface TimeSlot {
|
||||
displayTime: string;
|
||||
}
|
||||
|
||||
interface Staff {
|
||||
id: string;
|
||||
name: string;
|
||||
role: "doctor" | "hygienist";
|
||||
type StaffWithColor = DBStaff & {
|
||||
color: string;
|
||||
}
|
||||
};
|
||||
|
||||
interface ScheduledAppointment {
|
||||
id?: number;
|
||||
@@ -99,10 +97,10 @@ export default function AppointmentsPage() {
|
||||
number | null
|
||||
>(null);
|
||||
const [proceduresPatientId, setProceduresPatientId] = useState<number | null>(
|
||||
null
|
||||
null,
|
||||
);
|
||||
const [proceduresPatient, setProceduresPatient] = useState<Patient | null>(
|
||||
null
|
||||
null,
|
||||
);
|
||||
|
||||
const [calendarOpen, setCalendarOpen] = useState(false);
|
||||
@@ -117,7 +115,7 @@ export default function AppointmentsPage() {
|
||||
}>({ open: false });
|
||||
const dispatch = useAppDispatch();
|
||||
const batchTask = useAppSelector(
|
||||
(state) => state.seleniumEligibilityBatchCheckTask
|
||||
(state) => state.seleniumEligibilityBatchCheckTask,
|
||||
);
|
||||
const [isCheckingAllElig, setIsCheckingAllElig] = useState(false);
|
||||
const [processedAppointmentIds, setProcessedAppointmentIds] = useState<
|
||||
@@ -153,7 +151,7 @@ export default function AppointmentsPage() {
|
||||
queryFn: async () => {
|
||||
const res = await apiRequest(
|
||||
"GET",
|
||||
`/api/appointments/day?date=${formattedSelectedDate}`
|
||||
`/api/appointments/day?date=${formattedSelectedDate}`,
|
||||
);
|
||||
if (!res.ok) {
|
||||
throw new Error("Failed to load appointments for date");
|
||||
@@ -168,7 +166,7 @@ export default function AppointmentsPage() {
|
||||
const patientsFromDay = dayPayload.patients ?? [];
|
||||
|
||||
// Staff memebers
|
||||
const { data: staffMembersRaw = [] as Staff[] } = useQuery<Staff[]>({
|
||||
const { data: staffMembersRaw = [] as DBStaff[] } = useQuery<DBStaff[]>({
|
||||
queryKey: ["/api/staffs/"],
|
||||
queryFn: async () => {
|
||||
const res = await apiRequest("GET", "/api/staffs/");
|
||||
@@ -186,11 +184,18 @@ export default function AppointmentsPage() {
|
||||
"bg-orange-500", // softer warm orange
|
||||
];
|
||||
|
||||
// Assign colors cycling through the list
|
||||
const staffMembers = staffMembersRaw.map((staff, index) => ({
|
||||
...staff,
|
||||
// Assign colors cycling through the list, and order them by display order for the page column.
|
||||
|
||||
color: colors[index % colors.length] || "bg-gray-400",
|
||||
const orderedStaff = staffMembersRaw.filter(
|
||||
(s): s is DBStaff & { displayOrder: number } =>
|
||||
typeof s.displayOrder === "number" && s.displayOrder > 0,
|
||||
);
|
||||
|
||||
orderedStaff.sort((a, b) => a.displayOrder - b.displayOrder);
|
||||
|
||||
const staffMembers: StaffWithColor[] = orderedStaff.map((staff, index) => ({
|
||||
...staff,
|
||||
color: colors[index % colors.length] ?? "bg-gray-400",
|
||||
}));
|
||||
|
||||
// Generate time slots from 8:00 AM to 6:00 PM in 15-minute increments
|
||||
@@ -245,13 +250,13 @@ export default function AppointmentsPage() {
|
||||
};
|
||||
sessionStorage.setItem(
|
||||
"newAppointmentData",
|
||||
JSON.stringify(newAppointmentData)
|
||||
JSON.stringify(newAppointmentData),
|
||||
);
|
||||
} catch (err) {
|
||||
// If sessionStorage parsing fails, overwrite with a fresh object
|
||||
sessionStorage.setItem(
|
||||
"newAppointmentData",
|
||||
JSON.stringify({ patientId: patientId })
|
||||
JSON.stringify({ patientId: patientId }),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -272,7 +277,7 @@ export default function AppointmentsPage() {
|
||||
const res = await apiRequest(
|
||||
"POST",
|
||||
"/api/appointments/upsert",
|
||||
appointment
|
||||
appointment,
|
||||
);
|
||||
return await res.json();
|
||||
},
|
||||
@@ -308,7 +313,7 @@ export default function AppointmentsPage() {
|
||||
const res = await apiRequest(
|
||||
"PUT",
|
||||
`/api/appointments/${id}`,
|
||||
appointment
|
||||
appointment,
|
||||
);
|
||||
return await res.json();
|
||||
},
|
||||
@@ -359,7 +364,7 @@ export default function AppointmentsPage() {
|
||||
|
||||
// Handle appointment submission (create or update)
|
||||
const handleAppointmentSubmit = (
|
||||
appointmentData: InsertAppointment | UpdateAppointment
|
||||
appointmentData: InsertAppointment | UpdateAppointment,
|
||||
) => {
|
||||
// Converts local date to exact UTC date with no offset issues
|
||||
const rawDate = parseLocalDate(appointmentData.date);
|
||||
@@ -479,7 +484,7 @@ export default function AppointmentsPage() {
|
||||
// Handle creating a new appointment at a specific time slot and for a specific staff member
|
||||
const handleCreateAppointmentAtSlot = (
|
||||
timeSlot: TimeSlot,
|
||||
staffId: number
|
||||
staffId: number,
|
||||
) => {
|
||||
// Calculate end time (30 minutes after start time)
|
||||
const startHour = parseInt(timeSlot.time.split(":")[0] as string);
|
||||
@@ -532,7 +537,7 @@ export default function AppointmentsPage() {
|
||||
try {
|
||||
sessionStorage.setItem(
|
||||
"newAppointmentData",
|
||||
JSON.stringify(mergedAppointment)
|
||||
JSON.stringify(mergedAppointment),
|
||||
);
|
||||
} catch (e) {
|
||||
// ignore sessionStorage write failures
|
||||
@@ -550,7 +555,7 @@ export default function AppointmentsPage() {
|
||||
const handleMoveAppointment = (
|
||||
appointmentId: number,
|
||||
newTimeSlot: TimeSlot,
|
||||
newStaffId: number
|
||||
newStaffId: number,
|
||||
) => {
|
||||
const appointment = appointments.find((a) => a.id === appointmentId);
|
||||
if (!appointment) return;
|
||||
@@ -603,7 +608,7 @@ export default function AppointmentsPage() {
|
||||
staff,
|
||||
}: {
|
||||
appointment: ScheduledAppointment;
|
||||
staff: Staff;
|
||||
staff: StaffWithColor;
|
||||
}) {
|
||||
const [{ isDragging }, drag] = useDrag(() => ({
|
||||
type: ItemTypes.APPOINTMENT,
|
||||
@@ -624,7 +629,7 @@ export default function AppointmentsPage() {
|
||||
// Only allow edit on click if we're not dragging
|
||||
if (!isDragging) {
|
||||
const fullAppointment = appointments.find(
|
||||
(a) => a.id === appointment.id
|
||||
(a) => a.id === appointment.id,
|
||||
);
|
||||
if (fullAppointment) {
|
||||
e.stopPropagation();
|
||||
@@ -659,7 +664,7 @@ export default function AppointmentsPage() {
|
||||
timeSlot: TimeSlot;
|
||||
staffId: number;
|
||||
appointment: ScheduledAppointment | undefined;
|
||||
staff: Staff;
|
||||
staff: StaffWithColor;
|
||||
}) {
|
||||
const [{ isOver, canDrop }, drop] = useDrop(() => ({
|
||||
accept: ItemTypes.APPOINTMENT,
|
||||
@@ -700,13 +705,13 @@ export default function AppointmentsPage() {
|
||||
// appointment page — update these handlers
|
||||
const handleCheckEligibility = (appointmentId: number) => {
|
||||
setLocation(
|
||||
`/insurance-status?appointmentId=${appointmentId}&action=eligibility`
|
||||
`/insurance-status?appointmentId=${appointmentId}&action=eligibility`,
|
||||
);
|
||||
};
|
||||
|
||||
const handleCheckClaimStatus = (appointmentId: number) => {
|
||||
setLocation(
|
||||
`/insurance-status?appointmentId=${appointmentId}&action=claim`
|
||||
`/insurance-status?appointmentId=${appointmentId}&action=claim`,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -720,7 +725,7 @@ export default function AppointmentsPage() {
|
||||
|
||||
const handleChartPlan = (appointmentId: number) => {
|
||||
console.log(
|
||||
`Viewing chart/treatment plan for appointment: ${appointmentId}`
|
||||
`Viewing chart/treatment plan for appointment: ${appointmentId}`,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -745,7 +750,7 @@ export default function AppointmentsPage() {
|
||||
setTaskStatus({
|
||||
status: "pending",
|
||||
message: `Checking eligibility for appointments on ${dateParam}...`,
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
setIsCheckingAllElig(true);
|
||||
@@ -755,7 +760,7 @@ export default function AppointmentsPage() {
|
||||
const res = await apiRequest(
|
||||
"POST",
|
||||
`/api/insurance-status/appointments/check-all-eligibilities?date=${dateParam}`,
|
||||
{}
|
||||
{},
|
||||
);
|
||||
|
||||
// read body for all cases so we can show per-appointment info
|
||||
@@ -773,7 +778,7 @@ export default function AppointmentsPage() {
|
||||
setTaskStatus({
|
||||
status: "error",
|
||||
message: `Batch eligibility failed: ${errMsg}`,
|
||||
})
|
||||
}),
|
||||
);
|
||||
toast({
|
||||
title: "Batch check failed",
|
||||
@@ -851,7 +856,7 @@ export default function AppointmentsPage() {
|
||||
setTaskStatus({
|
||||
status: skippedCount > 0 ? "error" : "success",
|
||||
message: `Batch processed ${results.length} appointments — success: ${successCount}, warnings: ${warningCount}, skipped: ${skippedCount}.`,
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
// also show final toast summary
|
||||
@@ -866,7 +871,7 @@ export default function AppointmentsPage() {
|
||||
setTaskStatus({
|
||||
status: "error",
|
||||
message: `Batch eligibility error: ${err?.message ?? String(err)}`,
|
||||
})
|
||||
}),
|
||||
);
|
||||
toast({
|
||||
title: "Batch check failed",
|
||||
@@ -970,7 +975,7 @@ export default function AppointmentsPage() {
|
||||
<Item
|
||||
onClick={({ props }) => {
|
||||
const fullAppointment = appointments.find(
|
||||
(a) => a.id === props.appointmentId
|
||||
(a) => a.id === props.appointmentId,
|
||||
);
|
||||
if (fullAppointment) {
|
||||
handleEditAppointment(fullAppointment);
|
||||
@@ -1148,7 +1153,7 @@ export default function AppointmentsPage() {
|
||||
staffId={Number(staff.id)}
|
||||
appointment={getAppointmentAtSlot(
|
||||
timeSlot,
|
||||
Number(staff.id)
|
||||
Number(staff.id),
|
||||
)}
|
||||
staff={staff}
|
||||
/>
|
||||
|
||||
@@ -8,7 +8,7 @@ import { StaffForm } from "@/components/staffs/staff-form";
|
||||
import { DeleteConfirmationDialog } from "@/components/ui/deleteDialog";
|
||||
import { CredentialTable } from "@/components/settings/insuranceCredTable";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { Staff } from "@repo/db/types";
|
||||
import { Staff, StaffFormData } from "@repo/db/types";
|
||||
import { NpiProviderTable } from "@/components/settings/npiProviderTable";
|
||||
|
||||
export default function SettingsPage() {
|
||||
@@ -44,9 +44,9 @@ export default function SettingsPage() {
|
||||
const addStaffMutate = useMutation<
|
||||
Staff, // Return type
|
||||
Error, // Error type
|
||||
Omit<Staff, "id" | "createdAt"> // Variables
|
||||
StaffFormData
|
||||
>({
|
||||
mutationFn: async (newStaff: Omit<Staff, "id" | "createdAt">) => {
|
||||
mutationFn: async (newStaff: StaffFormData) => {
|
||||
const res = await apiRequest("POST", "/api/staffs/", newStaff);
|
||||
if (!res.ok) {
|
||||
const errorData = await res.json().catch(() => null);
|
||||
@@ -75,7 +75,7 @@ export default function SettingsPage() {
|
||||
const updateStaffMutate = useMutation<
|
||||
Staff,
|
||||
Error,
|
||||
{ id: number; updatedFields: Partial<Staff> }
|
||||
{ id: number; updatedFields: Partial<StaffFormData> }
|
||||
>({
|
||||
mutationFn: async ({
|
||||
id,
|
||||
@@ -157,7 +157,7 @@ export default function SettingsPage() {
|
||||
};
|
||||
|
||||
// Handle form submit for Add or Edit
|
||||
const handleFormSubmit = (formData: Omit<Staff, "id" | "createdAt">) => {
|
||||
const handleFormSubmit = (formData: StaffFormData) => {
|
||||
if (editingStaff) {
|
||||
// Editing existing staff
|
||||
if (editingStaff.id === undefined) {
|
||||
|
||||
Reference in New Issue
Block a user