feat(Calendar UI fixes) - shrink
This commit is contained in:
@@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useState } from "react";
|
|||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { apiRequest, queryClient } from "@/lib/queryClient";
|
import { apiRequest } from "@/lib/queryClient";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Clock } from "lucide-react";
|
import { Clock } from "lucide-react";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
import { useDebounce } from "use-debounce";
|
import { useDebounce } from "use-debounce";
|
||||||
import {
|
import {
|
||||||
@@ -39,7 +39,6 @@ import {
|
|||||||
PatientSearch,
|
PatientSearch,
|
||||||
SearchCriteria,
|
SearchCriteria,
|
||||||
} from "@/components/patients/patient-search";
|
} from "@/components/patients/patient-search";
|
||||||
import { QK_PATIENTS_BASE } from "../patients/patient-table";
|
|
||||||
import { toast } from "@/hooks/use-toast";
|
import { toast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
interface AppointmentFormProps {
|
interface AppointmentFormProps {
|
||||||
@@ -59,7 +58,6 @@ export function AppointmentForm({
|
|||||||
}: AppointmentFormProps) {
|
}: AppointmentFormProps) {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const [prefillPatient, setPrefillPatient] = useState<Patient | null>(null);
|
const [prefillPatient, setPrefillPatient] = useState<Patient | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -70,8 +68,7 @@ export function AppointmentForm({
|
|||||||
return () => clearTimeout(timeout);
|
return () => clearTimeout(timeout);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const { data: staffMembersRaw = [] as Staff[], isLoading: isLoadingStaff } =
|
const { data: staffMembersRaw = [] as Staff[] } = useQuery<Staff[]>({
|
||||||
useQuery<Staff[]>({
|
|
||||||
queryKey: ["/api/staffs/"],
|
queryKey: ["/api/staffs/"],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const res = await apiRequest("GET", "/api/staffs/");
|
const res = await apiRequest("GET", "/api/staffs/");
|
||||||
|
|||||||
@@ -133,6 +133,10 @@ export function ClaimForm({
|
|||||||
const [serviceDate, setServiceDate] = useState<string>(
|
const [serviceDate, setServiceDate] = useState<string>(
|
||||||
formatLocalDate(new Date())
|
formatLocalDate(new Date())
|
||||||
);
|
);
|
||||||
|
const [serviceDateOpen, setServiceDateOpen] = useState(false);
|
||||||
|
const [openProcedureDateIndex, setOpenProcedureDateIndex] = useState<
|
||||||
|
number | null
|
||||||
|
>(null);
|
||||||
|
|
||||||
// Update service date when calendar date changes
|
// Update service date when calendar date changes
|
||||||
const onServiceDateChange = (date: Date | undefined) => {
|
const onServiceDateChange = (date: Date | undefined) => {
|
||||||
@@ -559,7 +563,10 @@ export function ClaimForm({
|
|||||||
{/* Service Date */}
|
{/* Service Date */}
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Label className="flex items-center">Service Date</Label>
|
<Label className="flex items-center">Service Date</Label>
|
||||||
<Popover>
|
<Popover
|
||||||
|
open={serviceDateOpen}
|
||||||
|
onOpenChange={setServiceDateOpen}
|
||||||
|
>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -569,11 +576,14 @@ export function ClaimForm({
|
|||||||
{form.serviceDate}
|
{form.serviceDate}
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-auto p-0" align="start">
|
<PopoverContent className="w-auto">
|
||||||
<Calendar
|
<Calendar
|
||||||
mode="single"
|
mode="single"
|
||||||
selected={serviceDateValue}
|
selected={serviceDateValue}
|
||||||
onSelect={onServiceDateChange}
|
onSelect={(date) => {
|
||||||
|
onServiceDateChange(date);
|
||||||
|
}}
|
||||||
|
onClose={() => setServiceDateOpen(false)}
|
||||||
/>
|
/>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
@@ -702,7 +712,12 @@ export function ClaimForm({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Date Picker */}
|
{/* Date Picker */}
|
||||||
<Popover>
|
<Popover
|
||||||
|
open={openProcedureDateIndex === i}
|
||||||
|
onOpenChange={(open) =>
|
||||||
|
setOpenProcedureDateIndex(open ? i : null)
|
||||||
|
}
|
||||||
|
>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -712,11 +727,16 @@ export function ClaimForm({
|
|||||||
{line.procedureDate || "Pick Date"}
|
{line.procedureDate || "Pick Date"}
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-auto p-0">
|
<PopoverContent className="w-auto">
|
||||||
<Calendar
|
<Calendar
|
||||||
mode="single"
|
mode="single"
|
||||||
selected={new Date(line.procedureDate)}
|
selected={
|
||||||
|
line.procedureDate
|
||||||
|
? new Date(line.procedureDate)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
onSelect={(date) => updateProcedureDate(i, date)}
|
onSelect={(date) => updateProcedureDate(i, date)}
|
||||||
|
onClose={() => setOpenProcedureDateIndex(null)}
|
||||||
/>
|
/>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ export function PatientSearch({
|
|||||||
onClearSearch,
|
onClearSearch,
|
||||||
isSearchActive,
|
isSearchActive,
|
||||||
}: PatientSearchProps) {
|
}: PatientSearchProps) {
|
||||||
|
const [dobOpen, setDobOpen] = useState(false);
|
||||||
|
const [advanceDobOpen, setAdvanceDobOpen] = useState(false);
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
const [searchBy, setSearchBy] = useState<SearchCriteria["searchBy"]>("name");
|
const [searchBy, setSearchBy] = useState<SearchCriteria["searchBy"]>("name");
|
||||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||||
@@ -84,7 +86,7 @@ export function PatientSearch({
|
|||||||
<div className="flex gap-2 mb-4">
|
<div className="flex gap-2 mb-4">
|
||||||
<div className="relative flex-1">
|
<div className="relative flex-1">
|
||||||
{searchBy === "dob" ? (
|
{searchBy === "dob" ? (
|
||||||
<Popover>
|
<Popover open={dobOpen} onOpenChange={setDobOpen}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -112,6 +114,7 @@ export function PatientSearch({
|
|||||||
if (date) {
|
if (date) {
|
||||||
const formattedDate = format(date, "yyyy-MM-dd");
|
const formattedDate = format(date, "yyyy-MM-dd");
|
||||||
setSearchTerm(String(formattedDate));
|
setSearchTerm(String(formattedDate));
|
||||||
|
setDobOpen(false);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={(date) => date > new Date()}
|
disabled={(date) => date > new Date()}
|
||||||
@@ -153,9 +156,10 @@ export function PatientSearch({
|
|||||||
|
|
||||||
<Select
|
<Select
|
||||||
value={searchBy}
|
value={searchBy}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) => {
|
||||||
setSearchBy(value as SearchCriteria["searchBy"])
|
setSearchBy(value as SearchCriteria["searchBy"]);
|
||||||
}
|
setSearchTerm("");
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-[180px]">
|
<SelectTrigger className="w-[180px]">
|
||||||
<SelectValue placeholder="Search by..." />
|
<SelectValue placeholder="Search by..." />
|
||||||
@@ -189,12 +193,13 @@ export function PatientSearch({
|
|||||||
</label>
|
</label>
|
||||||
<Select
|
<Select
|
||||||
value={advancedCriteria.searchBy}
|
value={advancedCriteria.searchBy}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) => {
|
||||||
updateAdvancedCriteria(
|
setAdvancedCriteria((prev) => ({
|
||||||
"searchBy",
|
...prev,
|
||||||
value as SearchCriteria["searchBy"]
|
searchBy: value as SearchCriteria["searchBy"],
|
||||||
)
|
searchTerm: "",
|
||||||
}
|
}));
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="col-span-3">
|
<SelectTrigger className="col-span-3">
|
||||||
<SelectValue placeholder="Name" />
|
<SelectValue placeholder="Name" />
|
||||||
@@ -215,9 +220,13 @@ export function PatientSearch({
|
|||||||
Search term
|
Search term
|
||||||
</label>
|
</label>
|
||||||
{advancedCriteria.searchBy === "dob" ? (
|
{advancedCriteria.searchBy === "dob" ? (
|
||||||
<Popover>
|
<Popover
|
||||||
|
open={advanceDobOpen}
|
||||||
|
onOpenChange={setAdvanceDobOpen}
|
||||||
|
>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter") handleSearch();
|
if (e.key === "Enter") handleSearch();
|
||||||
@@ -251,6 +260,7 @@ export function PatientSearch({
|
|||||||
"searchTerm",
|
"searchTerm",
|
||||||
String(formattedDate)
|
String(formattedDate)
|
||||||
);
|
);
|
||||||
|
setAdvanceDobOpen(false);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={(date) => date > new Date()}
|
disabled={(date) => date > new Date()}
|
||||||
|
|||||||
@@ -13,20 +13,34 @@ type CalendarProps =
|
|||||||
mode: "single";
|
mode: "single";
|
||||||
selected?: Date;
|
selected?: Date;
|
||||||
onSelect?: (date: Date | undefined) => void;
|
onSelect?: (date: Date | undefined) => void;
|
||||||
|
closeOnSelect?: boolean /** whether to request closing after selection (default true for single) */;
|
||||||
|
onClose?: () => void;
|
||||||
})
|
})
|
||||||
| (BaseProps & {
|
| (BaseProps & {
|
||||||
mode: "range";
|
mode: "range";
|
||||||
selected?: DateRange;
|
selected?: DateRange;
|
||||||
onSelect?: (range: DateRange | undefined) => void;
|
onSelect?: (range: DateRange | undefined) => void;
|
||||||
|
closeOnSelect?: boolean; // will close only when range is complete
|
||||||
|
onClose?: () => void;
|
||||||
})
|
})
|
||||||
| (BaseProps & {
|
| (BaseProps & {
|
||||||
mode: "multiple";
|
mode: "multiple";
|
||||||
selected?: Date[];
|
selected?: Date[];
|
||||||
onSelect?: (dates: Date[] | undefined) => void;
|
onSelect?: (dates: Date[] | undefined) => void;
|
||||||
|
closeOnSelect?: boolean; // default false for multi
|
||||||
|
onClose?: () => void;
|
||||||
});
|
});
|
||||||
|
|
||||||
export function Calendar(props: CalendarProps) {
|
export function Calendar(props: CalendarProps) {
|
||||||
const { mode, selected, onSelect, className, ...rest } = props;
|
const {
|
||||||
|
mode,
|
||||||
|
selected,
|
||||||
|
onSelect,
|
||||||
|
className,
|
||||||
|
closeOnSelect,
|
||||||
|
onClose,
|
||||||
|
...rest
|
||||||
|
} = props;
|
||||||
|
|
||||||
const [internalSelected, setInternalSelected] =
|
const [internalSelected, setInternalSelected] =
|
||||||
useState<typeof selected>(selected);
|
useState<typeof selected>(selected);
|
||||||
@@ -37,7 +51,30 @@ export function Calendar(props: CalendarProps) {
|
|||||||
|
|
||||||
const handleSelect = (value: typeof selected) => {
|
const handleSelect = (value: typeof selected) => {
|
||||||
setInternalSelected(value);
|
setInternalSelected(value);
|
||||||
onSelect?.(value as any); // We'll narrow this properly below
|
// forward original callback
|
||||||
|
onSelect?.(value as any);
|
||||||
|
|
||||||
|
// Decide whether to request closing
|
||||||
|
const shouldClose =
|
||||||
|
typeof closeOnSelect !== "undefined"
|
||||||
|
? closeOnSelect
|
||||||
|
: mode === "single"
|
||||||
|
? true
|
||||||
|
: false;
|
||||||
|
|
||||||
|
if (!shouldClose) return;
|
||||||
|
|
||||||
|
// For range: only close when both from and to exist
|
||||||
|
if (mode === "range") {
|
||||||
|
const range = value as DateRange | undefined;
|
||||||
|
if (range?.from && range?.to) {
|
||||||
|
onClose?.();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For single or multiple (when allowed), close immediately
|
||||||
|
onClose?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useQuery, useMutation, keepPreviousData } from "@tanstack/react-query";
|
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||||
import { addDays, startOfToday, addMinutes } from "date-fns";
|
import { addDays, startOfToday, addMinutes } from "date-fns";
|
||||||
import {
|
import {
|
||||||
parseLocalDate,
|
parseLocalDate,
|
||||||
@@ -20,13 +20,6 @@ import { useToast } from "@/hooks/use-toast";
|
|||||||
import { Calendar } from "@/components/ui/calendar";
|
import { Calendar } from "@/components/ui/calendar";
|
||||||
import { apiRequest, queryClient } from "@/lib/queryClient";
|
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardHeader,
|
|
||||||
CardDescription,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import { DndProvider, useDrag, useDrop } from "react-dnd";
|
import { DndProvider, useDrag, useDrop } from "react-dnd";
|
||||||
import { HTML5Backend } from "react-dnd-html5-backend";
|
import { HTML5Backend } from "react-dnd-html5-backend";
|
||||||
import { Menu, Item, useContextMenu } from "react-contexify";
|
import { Menu, Item, useContextMenu } from "react-contexify";
|
||||||
@@ -39,6 +32,12 @@ import {
|
|||||||
Patient,
|
Patient,
|
||||||
UpdateAppointment,
|
UpdateAppointment,
|
||||||
} from "@repo/db/types";
|
} from "@repo/db/types";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
|
||||||
// Define types for scheduling
|
// Define types for scheduling
|
||||||
interface TimeSlot {
|
interface TimeSlot {
|
||||||
@@ -78,6 +77,7 @@ export default function AppointmentsPage() {
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
||||||
|
const [calendarOpen, setCalendarOpen] = useState(false);
|
||||||
const [editingAppointment, setEditingAppointment] = useState<
|
const [editingAppointment, setEditingAppointment] = useState<
|
||||||
Appointment | undefined
|
Appointment | undefined
|
||||||
>(undefined);
|
>(undefined);
|
||||||
@@ -233,10 +233,10 @@ export default function AppointmentsPage() {
|
|||||||
);
|
);
|
||||||
return await res.json();
|
return await res.json();
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: (appointment) => {
|
||||||
toast({
|
toast({
|
||||||
title: "Success",
|
title: "Appointment Scheduled",
|
||||||
description: "Appointment created successfully.",
|
description: appointment.message || "Appointment created successfully.",
|
||||||
});
|
});
|
||||||
// Invalidate both appointments and patients queries
|
// Invalidate both appointments and patients queries
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
@@ -269,10 +269,10 @@ export default function AppointmentsPage() {
|
|||||||
);
|
);
|
||||||
return await res.json();
|
return await res.json();
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: (appointment) => {
|
||||||
toast({
|
toast({
|
||||||
title: "Success",
|
title: "Appointment Scheduled",
|
||||||
description: "Appointment updated successfully.",
|
description: appointment.message || "Appointment created successfully.",
|
||||||
});
|
});
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: qkAppointmentsDay(formattedSelectedDate),
|
queryKey: qkAppointmentsDay(formattedSelectedDate),
|
||||||
@@ -697,10 +697,10 @@ export default function AppointmentsPage() {
|
|||||||
</Item>
|
</Item>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|
||||||
{/* Main Content - Split into Schedule and Calendar */}
|
{/* Main Content */}
|
||||||
<div className="flex flex-col lg:flex-row gap-6">
|
<div className="flex flex-col lg:flex-row gap-6">
|
||||||
{/* Left side - Schedule Grid */}
|
{/* Schedule Grid */}
|
||||||
<div className="w-full lg:w-3/4 overflow-x-auto bg-white rounded-md shadow">
|
<div className="w-full overflow-x-auto bg-white rounded-md shadow">
|
||||||
<div className="p-4 border-b">
|
<div className="p-4 border-b">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
@@ -722,6 +722,35 @@ export default function AppointmentsPage() {
|
|||||||
<ChevronRight className="h-4 w-4" />
|
<ChevronRight className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Top button with popover calendar */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Label className="hidden sm:flex">Selected</Label>
|
||||||
|
<Popover open={calendarOpen} onOpenChange={setCalendarOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-[160px] justify-start text-left font-normal"
|
||||||
|
>
|
||||||
|
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||||
|
{selectedDate
|
||||||
|
? selectedDate.toLocaleDateString()
|
||||||
|
: "Pick a date"}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
|
||||||
|
<PopoverContent className="w-auto">
|
||||||
|
<Calendar
|
||||||
|
mode="single"
|
||||||
|
selected={selectedDate}
|
||||||
|
onSelect={(date) => {
|
||||||
|
if (date) setSelectedDate(date);
|
||||||
|
}}
|
||||||
|
onClose={() => setCalendarOpen(false)}
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -770,28 +799,6 @@ export default function AppointmentsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</DndProvider>
|
</DndProvider>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right side - Calendar and Stats */}
|
|
||||||
<div className="w-full lg:w-1/4 space-y-6">
|
|
||||||
{/* Calendar Card */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle>Calendar</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Select a date to view or schedule appointments
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<Calendar
|
|
||||||
mode="single"
|
|
||||||
selected={selectedDate}
|
|
||||||
onSelect={(date) => {
|
|
||||||
if (date) setSelectedDate(date);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user