feat(Calendar UI fixes) - shrink

This commit is contained in:
2025-09-14 18:40:35 +05:30
parent 7aa6f6bc6d
commit aa3d3cac3a
5 changed files with 142 additions and 71 deletions

View File

@@ -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,15 +68,14 @@ 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/"); return res.json();
return res.json(); },
}, enabled: !!user,
enabled: !!user, });
});
const colorMap: Record<string, string> = { const colorMap: Record<string, string> = {
"Dr. Kai Gao": "bg-blue-600", "Dr. Kai Gao": "bg-blue-600",

View File

@@ -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>

View File

@@ -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()}

View File

@@ -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 (

View File

@@ -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>