feat(staff column order in aptmpt page)
This commit is contained in:
@@ -1,15 +1,12 @@
|
|||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import { storage } from "../storage";
|
import { storage } from "../storage";
|
||||||
import { z } from "zod";
|
import {
|
||||||
import { StaffUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
|
StaffCreateBody,
|
||||||
|
StaffCreateInput,
|
||||||
type Staff = z.infer<typeof StaffUncheckedCreateInputObjectSchema>;
|
staffCreateSchema,
|
||||||
|
staffUpdateSchema,
|
||||||
const staffCreateSchema = StaffUncheckedCreateInputObjectSchema;
|
} from "@repo/db/types";
|
||||||
const staffUpdateSchema = (
|
|
||||||
StaffUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
|
||||||
).partial();
|
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@@ -17,12 +14,14 @@ router.post("/", async (req: Request, res: Response): Promise<any> => {
|
|||||||
try {
|
try {
|
||||||
const userId = req.user!.id; // from auth middleware
|
const userId = req.user!.id; // from auth middleware
|
||||||
|
|
||||||
const validatedData = staffCreateSchema.parse({
|
const body = staffCreateSchema.parse(req.body) as StaffCreateBody;
|
||||||
...req.body,
|
|
||||||
userId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const newStaff = await storage.createStaff(validatedData);
|
const data: StaffCreateInput = {
|
||||||
|
...body,
|
||||||
|
userId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const newStaff = await storage.createStaff(data);
|
||||||
res.status(200).json(newStaff);
|
res.status(200).json(newStaff);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to create staff:", error);
|
console.error("Failed to create staff:", error);
|
||||||
@@ -52,12 +51,17 @@ router.put("/:id", async (req: Request, res: Response): Promise<any> => {
|
|||||||
const validatedData = staffUpdateSchema.parse(req.body);
|
const validatedData = staffUpdateSchema.parse(req.body);
|
||||||
const updatedStaff = await storage.updateStaff(
|
const updatedStaff = await storage.updateStaff(
|
||||||
parsedStaffId,
|
parsedStaffId,
|
||||||
validatedData
|
validatedData,
|
||||||
);
|
);
|
||||||
if (!updatedStaff) return res.status(404).send("Staff not found");
|
if (!updatedStaff) return res.status(404).send("Staff not found");
|
||||||
|
|
||||||
res.json(updatedStaff);
|
res.json(updatedStaff);
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
|
if (error.message?.includes("displayOrder")) {
|
||||||
|
return res.status(400).json({
|
||||||
|
message: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
console.error("Failed to update staff:", error);
|
console.error("Failed to update staff:", error);
|
||||||
res.status(500).send("Failed to update staff");
|
res.status(500).send("Failed to update staff");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import { Staff } from "@repo/db/types";
|
import { Staff, StaffCreateInput, StaffUpdateInput } from "@repo/db/types";
|
||||||
import { prisma as db } from "@repo/db/client";
|
import { prisma as db } from "@repo/db/client";
|
||||||
|
|
||||||
export interface IStorage {
|
export interface IStorage {
|
||||||
getStaff(id: number): Promise<Staff | undefined>;
|
getStaff(id: number): Promise<Staff | undefined>;
|
||||||
getAllStaff(): Promise<Staff[]>;
|
getAllStaff(): Promise<Staff[]>;
|
||||||
createStaff(staff: Staff): Promise<Staff>;
|
createStaff(staff: StaffCreateInput): Promise<Staff>;
|
||||||
updateStaff(id: number, updates: Partial<Staff>): Promise<Staff | undefined>;
|
updateStaff(
|
||||||
|
id: number,
|
||||||
|
updates: StaffUpdateInput,
|
||||||
|
): Promise<Staff | undefined>;
|
||||||
deleteStaff(id: number): Promise<boolean>;
|
deleteStaff(id: number): Promise<boolean>;
|
||||||
countAppointmentsByStaffId(staffId: number): Promise<number>;
|
countAppointmentsByStaffId(staffId: number): Promise<number>;
|
||||||
countClaimsByStaffId(staffId: number): Promise<number>;
|
countClaimsByStaffId(staffId: number): Promise<number>;
|
||||||
@@ -19,38 +22,122 @@ export const staffStorage: IStorage = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async getAllStaff(): Promise<Staff[]> {
|
async getAllStaff(): Promise<Staff[]> {
|
||||||
const staff = await db.staff.findMany();
|
return db.staff.findMany({
|
||||||
return staff;
|
orderBy: { displayOrder: "asc" },
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async createStaff(staff: Staff): Promise<Staff> {
|
async createStaff(staff: StaffCreateInput): Promise<Staff> {
|
||||||
const createdStaff = await db.staff.create({
|
const max = await db.staff.aggregate({
|
||||||
data: staff,
|
where: {
|
||||||
|
userId: staff.userId,
|
||||||
|
displayOrder: { gt: 0 },
|
||||||
|
},
|
||||||
|
_max: { displayOrder: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
return db.staff.create({
|
||||||
|
data: {
|
||||||
|
...staff,
|
||||||
|
displayOrder: (max._max.displayOrder ?? 0) + 1,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
return createdStaff;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async updateStaff(
|
async updateStaff(
|
||||||
id: number,
|
id: number,
|
||||||
updates: Partial<Staff>
|
updates: StaffUpdateInput,
|
||||||
): Promise<Staff | undefined> {
|
): Promise<Staff | undefined> {
|
||||||
const updatedStaff = await db.staff.update({
|
return db.$transaction(async (tx: any) => {
|
||||||
|
const staff = await tx.staff.findUnique({ where: { id } });
|
||||||
|
if (!staff) return undefined;
|
||||||
|
|
||||||
|
const { userId, displayOrder: oldOrder } = staff;
|
||||||
|
const newOrder = updates.displayOrder;
|
||||||
|
|
||||||
|
if (newOrder === undefined || newOrder === oldOrder) {
|
||||||
|
return tx.staff.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: updates,
|
data: updates,
|
||||||
});
|
});
|
||||||
return updatedStaff ?? undefined;
|
}
|
||||||
|
|
||||||
|
const totalStaff = await tx.staff.count({ where: { userId } });
|
||||||
|
|
||||||
|
if (newOrder < 1 || newOrder > totalStaff) {
|
||||||
|
throw new Error(`displayOrder must be between 1 and ${totalStaff}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const occupyingStaff = await tx.staff.findFirst({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
displayOrder: newOrder,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// CASE 1: staff already had a slot → SWAP
|
||||||
|
if (oldOrder && oldOrder > 0 && occupyingStaff) {
|
||||||
|
await tx.staff.update({
|
||||||
|
where: { id: occupyingStaff.id },
|
||||||
|
data: { displayOrder: oldOrder },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// CASE 2: first-time assignment (oldOrder = 0)
|
||||||
|
if ((!oldOrder || oldOrder === 0) && occupyingStaff) {
|
||||||
|
// find first free slot
|
||||||
|
const usedOrders = await tx.staff.findMany({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
displayOrder: { gt: 0 },
|
||||||
|
},
|
||||||
|
select: { displayOrder: true },
|
||||||
|
orderBy: { displayOrder: "asc" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const usedSet = new Set(usedOrders.map((s: any) => s.displayOrder));
|
||||||
|
let freeSlot = 1;
|
||||||
|
while (usedSet.has(freeSlot)) freeSlot++;
|
||||||
|
|
||||||
|
await tx.staff.update({
|
||||||
|
where: { id: occupyingStaff.id },
|
||||||
|
data: { displayOrder: freeSlot },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.staff.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
...updates,
|
||||||
|
displayOrder: newOrder,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async deleteStaff(id: number): Promise<boolean> {
|
async deleteStaff(id: number): Promise<boolean> {
|
||||||
try {
|
return db.$transaction(async (tx: any) => {
|
||||||
await db.staff.delete({ where: { id } });
|
const staff = await tx.staff.findUnique({ where: { id } });
|
||||||
return true;
|
if (!staff) return false;
|
||||||
} catch (error) {
|
|
||||||
console.error("Error deleting staff:", error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
|
const { userId, displayOrder } = staff;
|
||||||
|
|
||||||
|
await tx.staff.delete({ where: { id } });
|
||||||
|
|
||||||
|
// Shift left to remove gap
|
||||||
|
await tx.staff.updateMany({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
displayOrder: { gt: displayOrder },
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
displayOrder: { decrement: 1 },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
},
|
||||||
async countAppointmentsByStaffId(staffId: number): Promise<number> {
|
async countAppointmentsByStaffId(staffId: number): Promise<number> {
|
||||||
return await db.appointment.count({ where: { staffId } });
|
return await db.appointment.count({ where: { staffId } });
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Staff } from "@repo/db/types";
|
import { Staff, StaffFormData } from "@repo/db/types";
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
|
|
||||||
interface StaffFormProps {
|
interface StaffFormProps {
|
||||||
initialData?: Partial<Staff>;
|
initialData?: Partial<Staff>;
|
||||||
onSubmit: (data: Omit<Staff, "id" | "createdAt">) => void;
|
onSubmit: (data: StaffFormData) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
}
|
}
|
||||||
@@ -21,6 +21,8 @@ export function StaffForm({
|
|||||||
|
|
||||||
const [hasTypedRole, setHasTypedRole] = useState(false);
|
const [hasTypedRole, setHasTypedRole] = useState(false);
|
||||||
|
|
||||||
|
const [displayOrder, setDisplayOrder] = useState<number | "">("");
|
||||||
|
|
||||||
// Set initial values once on mount
|
// Set initial values once on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialData) {
|
if (initialData) {
|
||||||
@@ -28,6 +30,9 @@ export function StaffForm({
|
|||||||
if (initialData.email) setEmail(initialData.email);
|
if (initialData.email) setEmail(initialData.email);
|
||||||
if (initialData.role) setRole(initialData.role);
|
if (initialData.role) setRole(initialData.role);
|
||||||
if (initialData.phone) setPhone(initialData.phone);
|
if (initialData.phone) setPhone(initialData.phone);
|
||||||
|
if (initialData?.displayOrder !== undefined) {
|
||||||
|
setDisplayOrder(initialData.displayOrder);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, []); // run once only
|
}, []); // run once only
|
||||||
|
|
||||||
@@ -43,6 +48,7 @@ export function StaffForm({
|
|||||||
email: email.trim() || undefined,
|
email: email.trim() || undefined,
|
||||||
role: role.trim(),
|
role: role.trim(),
|
||||||
phone: phone.trim() || undefined,
|
phone: phone.trim() || undefined,
|
||||||
|
displayOrder: displayOrder === "" ? undefined : displayOrder,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -95,6 +101,24 @@ export function StaffForm({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700">Phone</label>
|
<label className="block text-sm font-medium text-gray-700">Phone</label>
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import {
|
|||||||
Patient,
|
Patient,
|
||||||
PatientStatus,
|
PatientStatus,
|
||||||
UpdateAppointment,
|
UpdateAppointment,
|
||||||
|
Staff as DBStaff,
|
||||||
} from "@repo/db/types";
|
} from "@repo/db/types";
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
@@ -61,12 +62,9 @@ interface TimeSlot {
|
|||||||
displayTime: string;
|
displayTime: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Staff {
|
type StaffWithColor = DBStaff & {
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
role: "doctor" | "hygienist";
|
|
||||||
color: string;
|
color: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
interface ScheduledAppointment {
|
interface ScheduledAppointment {
|
||||||
id?: number;
|
id?: number;
|
||||||
@@ -99,10 +97,10 @@ export default function AppointmentsPage() {
|
|||||||
number | null
|
number | null
|
||||||
>(null);
|
>(null);
|
||||||
const [proceduresPatientId, setProceduresPatientId] = useState<number | null>(
|
const [proceduresPatientId, setProceduresPatientId] = useState<number | null>(
|
||||||
null
|
null,
|
||||||
);
|
);
|
||||||
const [proceduresPatient, setProceduresPatient] = useState<Patient | null>(
|
const [proceduresPatient, setProceduresPatient] = useState<Patient | null>(
|
||||||
null
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [calendarOpen, setCalendarOpen] = useState(false);
|
const [calendarOpen, setCalendarOpen] = useState(false);
|
||||||
@@ -117,7 +115,7 @@ export default function AppointmentsPage() {
|
|||||||
}>({ open: false });
|
}>({ open: false });
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const batchTask = useAppSelector(
|
const batchTask = useAppSelector(
|
||||||
(state) => state.seleniumEligibilityBatchCheckTask
|
(state) => state.seleniumEligibilityBatchCheckTask,
|
||||||
);
|
);
|
||||||
const [isCheckingAllElig, setIsCheckingAllElig] = useState(false);
|
const [isCheckingAllElig, setIsCheckingAllElig] = useState(false);
|
||||||
const [processedAppointmentIds, setProcessedAppointmentIds] = useState<
|
const [processedAppointmentIds, setProcessedAppointmentIds] = useState<
|
||||||
@@ -153,7 +151,7 @@ export default function AppointmentsPage() {
|
|||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const res = await apiRequest(
|
const res = await apiRequest(
|
||||||
"GET",
|
"GET",
|
||||||
`/api/appointments/day?date=${formattedSelectedDate}`
|
`/api/appointments/day?date=${formattedSelectedDate}`,
|
||||||
);
|
);
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error("Failed to load appointments for date");
|
throw new Error("Failed to load appointments for date");
|
||||||
@@ -168,7 +166,7 @@ export default function AppointmentsPage() {
|
|||||||
const patientsFromDay = dayPayload.patients ?? [];
|
const patientsFromDay = dayPayload.patients ?? [];
|
||||||
|
|
||||||
// Staff memebers
|
// Staff memebers
|
||||||
const { data: staffMembersRaw = [] as Staff[] } = useQuery<Staff[]>({
|
const { data: staffMembersRaw = [] as DBStaff[] } = useQuery<DBStaff[]>({
|
||||||
queryKey: ["/api/staffs/"],
|
queryKey: ["/api/staffs/"],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const res = await apiRequest("GET", "/api/staffs/");
|
const res = await apiRequest("GET", "/api/staffs/");
|
||||||
@@ -186,11 +184,18 @@ export default function AppointmentsPage() {
|
|||||||
"bg-orange-500", // softer warm orange
|
"bg-orange-500", // softer warm orange
|
||||||
];
|
];
|
||||||
|
|
||||||
// Assign colors cycling through the list
|
// Assign colors cycling through the list, and order them by display order for the page column.
|
||||||
const staffMembers = staffMembersRaw.map((staff, index) => ({
|
|
||||||
...staff,
|
|
||||||
|
|
||||||
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
|
// 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(
|
sessionStorage.setItem(
|
||||||
"newAppointmentData",
|
"newAppointmentData",
|
||||||
JSON.stringify(newAppointmentData)
|
JSON.stringify(newAppointmentData),
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// If sessionStorage parsing fails, overwrite with a fresh object
|
// If sessionStorage parsing fails, overwrite with a fresh object
|
||||||
sessionStorage.setItem(
|
sessionStorage.setItem(
|
||||||
"newAppointmentData",
|
"newAppointmentData",
|
||||||
JSON.stringify({ patientId: patientId })
|
JSON.stringify({ patientId: patientId }),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,7 +277,7 @@ export default function AppointmentsPage() {
|
|||||||
const res = await apiRequest(
|
const res = await apiRequest(
|
||||||
"POST",
|
"POST",
|
||||||
"/api/appointments/upsert",
|
"/api/appointments/upsert",
|
||||||
appointment
|
appointment,
|
||||||
);
|
);
|
||||||
return await res.json();
|
return await res.json();
|
||||||
},
|
},
|
||||||
@@ -308,7 +313,7 @@ export default function AppointmentsPage() {
|
|||||||
const res = await apiRequest(
|
const res = await apiRequest(
|
||||||
"PUT",
|
"PUT",
|
||||||
`/api/appointments/${id}`,
|
`/api/appointments/${id}`,
|
||||||
appointment
|
appointment,
|
||||||
);
|
);
|
||||||
return await res.json();
|
return await res.json();
|
||||||
},
|
},
|
||||||
@@ -359,7 +364,7 @@ export default function AppointmentsPage() {
|
|||||||
|
|
||||||
// Handle appointment submission (create or update)
|
// Handle appointment submission (create or update)
|
||||||
const handleAppointmentSubmit = (
|
const handleAppointmentSubmit = (
|
||||||
appointmentData: InsertAppointment | UpdateAppointment
|
appointmentData: InsertAppointment | UpdateAppointment,
|
||||||
) => {
|
) => {
|
||||||
// Converts local date to exact UTC date with no offset issues
|
// Converts local date to exact UTC date with no offset issues
|
||||||
const rawDate = parseLocalDate(appointmentData.date);
|
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
|
// Handle creating a new appointment at a specific time slot and for a specific staff member
|
||||||
const handleCreateAppointmentAtSlot = (
|
const handleCreateAppointmentAtSlot = (
|
||||||
timeSlot: TimeSlot,
|
timeSlot: TimeSlot,
|
||||||
staffId: number
|
staffId: number,
|
||||||
) => {
|
) => {
|
||||||
// Calculate end time (30 minutes after start time)
|
// Calculate end time (30 minutes after start time)
|
||||||
const startHour = parseInt(timeSlot.time.split(":")[0] as string);
|
const startHour = parseInt(timeSlot.time.split(":")[0] as string);
|
||||||
@@ -532,7 +537,7 @@ export default function AppointmentsPage() {
|
|||||||
try {
|
try {
|
||||||
sessionStorage.setItem(
|
sessionStorage.setItem(
|
||||||
"newAppointmentData",
|
"newAppointmentData",
|
||||||
JSON.stringify(mergedAppointment)
|
JSON.stringify(mergedAppointment),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// ignore sessionStorage write failures
|
// ignore sessionStorage write failures
|
||||||
@@ -550,7 +555,7 @@ export default function AppointmentsPage() {
|
|||||||
const handleMoveAppointment = (
|
const handleMoveAppointment = (
|
||||||
appointmentId: number,
|
appointmentId: number,
|
||||||
newTimeSlot: TimeSlot,
|
newTimeSlot: TimeSlot,
|
||||||
newStaffId: number
|
newStaffId: number,
|
||||||
) => {
|
) => {
|
||||||
const appointment = appointments.find((a) => a.id === appointmentId);
|
const appointment = appointments.find((a) => a.id === appointmentId);
|
||||||
if (!appointment) return;
|
if (!appointment) return;
|
||||||
@@ -603,7 +608,7 @@ export default function AppointmentsPage() {
|
|||||||
staff,
|
staff,
|
||||||
}: {
|
}: {
|
||||||
appointment: ScheduledAppointment;
|
appointment: ScheduledAppointment;
|
||||||
staff: Staff;
|
staff: StaffWithColor;
|
||||||
}) {
|
}) {
|
||||||
const [{ isDragging }, drag] = useDrag(() => ({
|
const [{ isDragging }, drag] = useDrag(() => ({
|
||||||
type: ItemTypes.APPOINTMENT,
|
type: ItemTypes.APPOINTMENT,
|
||||||
@@ -624,7 +629,7 @@ export default function AppointmentsPage() {
|
|||||||
// Only allow edit on click if we're not dragging
|
// Only allow edit on click if we're not dragging
|
||||||
if (!isDragging) {
|
if (!isDragging) {
|
||||||
const fullAppointment = appointments.find(
|
const fullAppointment = appointments.find(
|
||||||
(a) => a.id === appointment.id
|
(a) => a.id === appointment.id,
|
||||||
);
|
);
|
||||||
if (fullAppointment) {
|
if (fullAppointment) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -659,7 +664,7 @@ export default function AppointmentsPage() {
|
|||||||
timeSlot: TimeSlot;
|
timeSlot: TimeSlot;
|
||||||
staffId: number;
|
staffId: number;
|
||||||
appointment: ScheduledAppointment | undefined;
|
appointment: ScheduledAppointment | undefined;
|
||||||
staff: Staff;
|
staff: StaffWithColor;
|
||||||
}) {
|
}) {
|
||||||
const [{ isOver, canDrop }, drop] = useDrop(() => ({
|
const [{ isOver, canDrop }, drop] = useDrop(() => ({
|
||||||
accept: ItemTypes.APPOINTMENT,
|
accept: ItemTypes.APPOINTMENT,
|
||||||
@@ -700,13 +705,13 @@ export default function AppointmentsPage() {
|
|||||||
// appointment page — update these handlers
|
// appointment page — update these handlers
|
||||||
const handleCheckEligibility = (appointmentId: number) => {
|
const handleCheckEligibility = (appointmentId: number) => {
|
||||||
setLocation(
|
setLocation(
|
||||||
`/insurance-status?appointmentId=${appointmentId}&action=eligibility`
|
`/insurance-status?appointmentId=${appointmentId}&action=eligibility`,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCheckClaimStatus = (appointmentId: number) => {
|
const handleCheckClaimStatus = (appointmentId: number) => {
|
||||||
setLocation(
|
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) => {
|
const handleChartPlan = (appointmentId: number) => {
|
||||||
console.log(
|
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({
|
setTaskStatus({
|
||||||
status: "pending",
|
status: "pending",
|
||||||
message: `Checking eligibility for appointments on ${dateParam}...`,
|
message: `Checking eligibility for appointments on ${dateParam}...`,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
setIsCheckingAllElig(true);
|
setIsCheckingAllElig(true);
|
||||||
@@ -755,7 +760,7 @@ export default function AppointmentsPage() {
|
|||||||
const res = await apiRequest(
|
const res = await apiRequest(
|
||||||
"POST",
|
"POST",
|
||||||
`/api/insurance-status/appointments/check-all-eligibilities?date=${dateParam}`,
|
`/api/insurance-status/appointments/check-all-eligibilities?date=${dateParam}`,
|
||||||
{}
|
{},
|
||||||
);
|
);
|
||||||
|
|
||||||
// read body for all cases so we can show per-appointment info
|
// read body for all cases so we can show per-appointment info
|
||||||
@@ -773,7 +778,7 @@ export default function AppointmentsPage() {
|
|||||||
setTaskStatus({
|
setTaskStatus({
|
||||||
status: "error",
|
status: "error",
|
||||||
message: `Batch eligibility failed: ${errMsg}`,
|
message: `Batch eligibility failed: ${errMsg}`,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
toast({
|
toast({
|
||||||
title: "Batch check failed",
|
title: "Batch check failed",
|
||||||
@@ -851,7 +856,7 @@ export default function AppointmentsPage() {
|
|||||||
setTaskStatus({
|
setTaskStatus({
|
||||||
status: skippedCount > 0 ? "error" : "success",
|
status: skippedCount > 0 ? "error" : "success",
|
||||||
message: `Batch processed ${results.length} appointments — success: ${successCount}, warnings: ${warningCount}, skipped: ${skippedCount}.`,
|
message: `Batch processed ${results.length} appointments — success: ${successCount}, warnings: ${warningCount}, skipped: ${skippedCount}.`,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// also show final toast summary
|
// also show final toast summary
|
||||||
@@ -866,7 +871,7 @@ export default function AppointmentsPage() {
|
|||||||
setTaskStatus({
|
setTaskStatus({
|
||||||
status: "error",
|
status: "error",
|
||||||
message: `Batch eligibility error: ${err?.message ?? String(err)}`,
|
message: `Batch eligibility error: ${err?.message ?? String(err)}`,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
toast({
|
toast({
|
||||||
title: "Batch check failed",
|
title: "Batch check failed",
|
||||||
@@ -970,7 +975,7 @@ export default function AppointmentsPage() {
|
|||||||
<Item
|
<Item
|
||||||
onClick={({ props }) => {
|
onClick={({ props }) => {
|
||||||
const fullAppointment = appointments.find(
|
const fullAppointment = appointments.find(
|
||||||
(a) => a.id === props.appointmentId
|
(a) => a.id === props.appointmentId,
|
||||||
);
|
);
|
||||||
if (fullAppointment) {
|
if (fullAppointment) {
|
||||||
handleEditAppointment(fullAppointment);
|
handleEditAppointment(fullAppointment);
|
||||||
@@ -1148,7 +1153,7 @@ export default function AppointmentsPage() {
|
|||||||
staffId={Number(staff.id)}
|
staffId={Number(staff.id)}
|
||||||
appointment={getAppointmentAtSlot(
|
appointment={getAppointmentAtSlot(
|
||||||
timeSlot,
|
timeSlot,
|
||||||
Number(staff.id)
|
Number(staff.id),
|
||||||
)}
|
)}
|
||||||
staff={staff}
|
staff={staff}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { StaffForm } from "@/components/staffs/staff-form";
|
|||||||
import { DeleteConfirmationDialog } from "@/components/ui/deleteDialog";
|
import { DeleteConfirmationDialog } from "@/components/ui/deleteDialog";
|
||||||
import { CredentialTable } from "@/components/settings/insuranceCredTable";
|
import { CredentialTable } from "@/components/settings/insuranceCredTable";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
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";
|
import { NpiProviderTable } from "@/components/settings/npiProviderTable";
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
@@ -44,9 +44,9 @@ export default function SettingsPage() {
|
|||||||
const addStaffMutate = useMutation<
|
const addStaffMutate = useMutation<
|
||||||
Staff, // Return type
|
Staff, // Return type
|
||||||
Error, // Error 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);
|
const res = await apiRequest("POST", "/api/staffs/", newStaff);
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const errorData = await res.json().catch(() => null);
|
const errorData = await res.json().catch(() => null);
|
||||||
@@ -75,7 +75,7 @@ export default function SettingsPage() {
|
|||||||
const updateStaffMutate = useMutation<
|
const updateStaffMutate = useMutation<
|
||||||
Staff,
|
Staff,
|
||||||
Error,
|
Error,
|
||||||
{ id: number; updatedFields: Partial<Staff> }
|
{ id: number; updatedFields: Partial<StaffFormData> }
|
||||||
>({
|
>({
|
||||||
mutationFn: async ({
|
mutationFn: async ({
|
||||||
id,
|
id,
|
||||||
@@ -157,7 +157,7 @@ export default function SettingsPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Handle form submit for Add or Edit
|
// Handle form submit for Add or Edit
|
||||||
const handleFormSubmit = (formData: Omit<Staff, "id" | "createdAt">) => {
|
const handleFormSubmit = (formData: StaffFormData) => {
|
||||||
if (editingStaff) {
|
if (editingStaff) {
|
||||||
// Editing existing staff
|
// Editing existing staff
|
||||||
if (editingStaff.id === undefined) {
|
if (editingStaff.id === undefined) {
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ model Staff {
|
|||||||
role String // e.g., "Dentist", "Hygienist", "Assistant"
|
role String // e.g., "Dentist", "Hygienist", "Assistant"
|
||||||
phone String?
|
phone String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
displayOrder Int @default(0)
|
||||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
appointments Appointment[]
|
appointments Appointment[]
|
||||||
claims Claim[] @relation("ClaimStaff")
|
claims Claim[] @relation("ClaimStaff")
|
||||||
@@ -128,7 +129,6 @@ model NpiProvider {
|
|||||||
@@index([userId])
|
@@index([userId])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
enum ProcedureSource {
|
enum ProcedureSource {
|
||||||
COMBO
|
COMBO
|
||||||
MANUAL
|
MANUAL
|
||||||
|
|||||||
@@ -2,3 +2,43 @@ import { StaffUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export type Staff = z.infer<typeof StaffUncheckedCreateInputObjectSchema>;
|
export type Staff = z.infer<typeof StaffUncheckedCreateInputObjectSchema>;
|
||||||
|
|
||||||
|
export const staffCreateSchema = (
|
||||||
|
StaffUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||||
|
).omit({
|
||||||
|
id: true,
|
||||||
|
createdAt: true,
|
||||||
|
displayOrder: true,
|
||||||
|
userId: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const staffUpdateSchema = (
|
||||||
|
StaffUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
||||||
|
)
|
||||||
|
.partial()
|
||||||
|
.omit({
|
||||||
|
id: true,
|
||||||
|
createdAt: true,
|
||||||
|
userId: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type StaffFormData = {
|
||||||
|
name: string;
|
||||||
|
email?: string;
|
||||||
|
role: string;
|
||||||
|
phone?: string;
|
||||||
|
displayOrder?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type StaffCreateInput = z.infer<
|
||||||
|
typeof StaffUncheckedCreateInputObjectSchema
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type StaffCreateBody = Omit<
|
||||||
|
StaffCreateInput,
|
||||||
|
"id" | "createdAt" | "userId" | "displayOrder"
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type StaffUpdateInput = Partial<
|
||||||
|
Omit<StaffCreateInput, "id" | "createdAt" | "userId">
|
||||||
|
>;
|
||||||
|
|||||||
Reference in New Issue
Block a user