feat: add Procedure Duration/Time Slot settings and custom appointment type

- Add Settings > Advanced > Procedure Duration/Time Slot page with three sections:
  1. Procedure Duration: CDT codes with durations (editable table, save per section)
  2. Doctor Time Slot: drag-to-block visual grid (A/B/C columns, 8 AM–9 PM, edit/delete slots)
  3. Hygienist Time Slot: procedure descriptions with durations
- Backend: ProcedureTimeslot Prisma model, storage, and GET/PUT /api/procedure-timeslot route
- DB migration: procedure_timeslot table
- Appointment form: when type is "Other", show a free-text input for custom description; saved as other:<description> and decoded for display on the schedule card

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Gitead
2026-05-05 23:08:34 -04:00
parent fea0dd4d59
commit ceb95f1915
11 changed files with 900 additions and 4 deletions

View File

@@ -55,6 +55,10 @@ export function AppointmentForm({
const { user } = useAuth();
const inputRef = useRef<HTMLInputElement>(null);
const [prefillPatient, setPrefillPatient] = useState<Patient | null>(null);
const [otherTypeDesc, setOtherTypeDesc] = useState<string>(() => {
const t = appointment?.type ?? "";
return t.startsWith("other:") ? t.slice(6) : "";
});
useEffect(() => {
const timeout = setTimeout(() => {
@@ -105,7 +109,7 @@ export function AppointmentForm({
date: parseLocalDate(appointment.date),
startTime: appointment.startTime || "09:00", // Default "09:00"
endTime: appointment.endTime || "09:30", // Default "09:30"
type: appointment.type,
type: appointment.type?.startsWith("other:") ? "other" : appointment.type,
notes: appointment.notes || "",
status: appointment.status || "scheduled",
staffId:
@@ -326,6 +330,11 @@ export function AppointmentForm({
const formattedDate = formatLocalDate(data.date);
const resolvedType =
data.type === "other" && otherTypeDesc.trim()
? `other:${otherTypeDesc.trim()}`
: data.type;
onSubmit({
...data,
userId: Number(user?.id),
@@ -335,6 +344,7 @@ export function AppointmentForm({
date: formattedDate,
startTime: data.startTime,
endTime: data.endTime,
type: resolvedType,
});
};
@@ -559,7 +569,10 @@ export function AppointmentForm({
<FormLabel>Appointment Type</FormLabel>
<Select
disabled={isLoading}
onValueChange={field.onChange}
onValueChange={(val) => {
field.onChange(val);
if (val !== "other") setOtherTypeDesc("");
}}
value={field.value}
defaultValue={field.value}
>
@@ -581,6 +594,16 @@ export function AppointmentForm({
<SelectItem value="other">Other</SelectItem>
</SelectContent>
</Select>
{field.value === "other" && (
<Input
className="mt-2"
placeholder="Describe the appointment type…"
value={otherTypeDesc}
onChange={(e) => setOtherTypeDesc(e.target.value)}
disabled={isLoading}
autoFocus
/>
)}
<FormMessage />
</FormItem>
)}