diff --git a/public/locale/en.json b/public/locale/en.json index 654cfa019f3..e060c325ab8 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -437,6 +437,7 @@ "authorize_shift_delete": "Authorize shift delete", "auto_generated_for_care": "Auto Generated for Care", "autofilled_fields": "Autofilled Fields", + "availabilities": "Availabilities", "available_features": "Available Features", "available_in": "Available in", "available_time_slots": "Available Time Slots", @@ -657,6 +658,7 @@ "created_by": "Created By", "created_date": "Created Date", "created_on": "Created On", + "creating": "Creating...", "criticality": "Criticality", "csv_file_in_the_specified_format": "Select a CSV file in the specified format", "current_address": "Current Address", @@ -1240,11 +1242,13 @@ "new_password_confirmation": "Confirm New Password", "new_password_same_as_old": "New password is same as old password, please enter a different new password.", "new_password_validation": "New password is not valid.", + "new_session": "New Session", "next_sessions": "Next Sessions", "no": "No", "no_address_provided": "No address provided", "no_appointments": "No appointments found", "no_attachments_found": "This communication has no attachments.", + "no_availabilities_yet": "No availabilities yet", "no_bed_asset_linked_allocated": "No bed/asset linked allocated", "no_bed_types_found": "No Bed Types found", "no_beds_available": "No beds available", @@ -1624,8 +1628,8 @@ "scan_asset_qr": "Scan Asset QR!", "schedule": "Schedule", "schedule_appointment": "Schedule Appointment", + "schedule_availability_created_successfully": "Availability created successfully", "schedule_availability_deleted_successfully": "Schedule availability deleted successfully", - "schedule_availability_updated_successfully": "Schedule availability updated successfully", "schedule_calendar": "Schedule Calendar", "schedule_end_time": "End Time", "schedule_for": "Scheduled for", @@ -1633,7 +1637,6 @@ "schedule_remarks": "Remarks", "schedule_remarks_placeholder": "Any additional notes about this session", "schedule_session_time": "Session Time", - "schedule_session_title": "Session Title", "schedule_session_type": "Session Type", "schedule_sessions": "Sessions", "schedule_sessions_min_error": "Add at least one session", @@ -1644,7 +1647,6 @@ "schedule_template": "Schedule Template", "schedule_template_name": "Template Name", "schedule_template_name_placeholder": "Regular OP Day", - "schedule_tokens_per_slot": "Patients per Slot", "schedule_valid_from_till_range": "Valid from {{from_date}} till {{to_date}}", "schedule_weekdays": "Weekdays", "schedule_weekdays_description": "Select the weekdays applicable for the template", @@ -1704,6 +1706,7 @@ "send_sample_to_collection_centre_title": "Send sample to collection centre", "serial_number": "Serial Number", "serviced_on": "Serviced on", + "session_capacity": "Session Capacity", "session_expired": "Session Expired", "session_expired_msg": "It appears that your session has expired. This could be due to inactivity. Please login again to continue.", "session_title": "Session Title", @@ -1738,6 +1741,7 @@ "skill_add_error": "Error while adding skill", "skill_added_successfully": "Skill added successfully", "skills": "Skills", + "slot_configuration": "Slot Configuration", "slots_left": "slots left", "social_profile": "Social Profile", "social_profile_detail": "Include occupation, ration card category, socioeconomic status, and domestic healthcare support for a complete profile.", @@ -1803,6 +1807,7 @@ "total_amount": "Total Amount", "total_beds": "Total Beds", "total_patients": "Total Patients", + "total_slots": "Total Slots", "total_staff": "Total Staff", "total_users": "Total Users", "transcribe_again": "Transcribe Again", diff --git a/src/CAREUI/interactive/WeekdayCheckbox.tsx b/src/CAREUI/interactive/WeekdayCheckbox.tsx index 1660d14ef02..55ebc304516 100644 --- a/src/CAREUI/interactive/WeekdayCheckbox.tsx +++ b/src/CAREUI/interactive/WeekdayCheckbox.tsx @@ -1,31 +1,32 @@ import { useTranslation } from "react-i18next"; -import { cn } from "@/lib/utils"; - -import { Checkbox } from "@/components/ui/checkbox"; +import { Button } from "@/components/ui/button"; // 0 is Monday, 6 is Sunday - Python's convention. -const DAYS_OF_WEEK = { - MONDAY: 0, - TUESDAY: 1, - WEDNESDAY: 2, - THURSDAY: 3, - FRIDAY: 4, - SATURDAY: 5, - SUNDAY: 6, -} as const; - -export type DayOfWeekValue = (typeof DAYS_OF_WEEK)[keyof typeof DAYS_OF_WEEK]; +export enum DayOfWeek { + MONDAY = 0, + TUESDAY = 1, + WEDNESDAY = 2, + THURSDAY = 3, + FRIDAY = 4, + SATURDAY = 5, + SUNDAY = 6, +} interface Props { - value?: DayOfWeekValue[]; - onChange?: (value: DayOfWeekValue[]) => void; + value?: DayOfWeek[]; + onChange?: (value: DayOfWeek[]) => void; + format?: "alphabet" | "short" | "long"; } -export default function WeekdayCheckbox({ value = [], onChange }: Props) { +export default function WeekdayCheckbox({ + value = [], + onChange, + format = "alphabet", +}: Props) { const { t } = useTranslation(); - const handleDayToggle = (day: DayOfWeekValue) => { + const handleDayToggle = (day: DayOfWeek) => { if (!onChange) return; if (value.includes(day)) { @@ -36,36 +37,35 @@ export default function WeekdayCheckbox({ value = [], onChange }: Props) { }; return ( - + ); } diff --git a/src/components/Schedule/ScheduleTemplateEditForm.tsx b/src/components/Schedule/ScheduleTemplateEditForm.tsx index 081255c7294..a2e952fe3cc 100644 --- a/src/components/Schedule/ScheduleTemplateEditForm.tsx +++ b/src/components/Schedule/ScheduleTemplateEditForm.tsx @@ -1,20 +1,21 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { ArrowRightIcon } from "lucide-react"; +import { useState } from "react"; import { useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; +import { Trans } from "react-i18next"; import { toast } from "sonner"; import * as z from "zod"; +import Callout from "@/CAREUI/display/Callout"; import CareIcon from "@/CAREUI/icons/CareIcon"; +import WeekdayCheckbox, { + DayOfWeek, +} from "@/CAREUI/interactive/WeekdayCheckbox"; import { Button } from "@/components/ui/button"; import { DatePicker } from "@/components/ui/date-picker"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; import { Form, FormControl, @@ -24,20 +25,30 @@ import { FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { Textarea } from "@/components/ui/textarea"; -import { getSlotsPerSession } from "@/components/Schedule/helpers"; +import { + getSlotsPerSession, + getTokenDuration, +} from "@/components/Schedule/helpers"; import { formatAvailabilityTime } from "@/components/Users/UserAvailabilityTab"; import mutate from "@/Utils/request/mutate"; +import { Time } from "@/Utils/types"; import { dateQueryString } from "@/Utils/utils"; import { AvailabilityDateTime, ScheduleAvailability, - ScheduleAvailabilityUpdateRequest, + ScheduleAvailabilityCreateRequest, ScheduleTemplate, } from "@/types/scheduling/schedule"; -import { DayOfWeek } from "@/types/scheduling/schedule"; import scheduleApis from "@/types/scheduling/scheduleApis"; export default function ScheduleTemplateEditForm({ @@ -49,13 +60,26 @@ export default function ScheduleTemplateEditForm({ facilityId: string; userId: string; }) { + const { t } = useTranslation(); + return ( -
+
+ +
+

{t("availabilities")}

+
+ + {template.availabilities.length === 0 && ( +
+

{t("no_availabilities_yet")}

+
+ )} + {template.availabilities.map((availability) => ( ))} + +
); } @@ -144,7 +174,7 @@ const ScheduleTemplateEditor = ({ )} /> -
+
{ - toast.success(t("schedule_availability_updated_successfully")); - queryClient.invalidateQueries({ - queryKey: ["user-schedule-templates", { facilityId, userId }], - }); - }, - }); - const { mutate: deleteAvailability, isPending: isDeleting } = useMutation({ mutationFn: mutate(scheduleApis.templates.availabilities.delete, { pathParams: { @@ -233,58 +247,6 @@ const AvailabilityEditor = ({ }, }); - return availability.slot_type === "appointment" ? ( - - ) : ( - - ); -}; - -const AppointmentAvailabilityEditor = ({ - availability, - updateAvailability, - deleteAvailability, - isProcessing, -}: { - availability: ScheduleAvailability & { slot_type: "appointment" }; - updateAvailability: (request: ScheduleAvailabilityUpdateRequest) => void; - deleteAvailability: () => void; - isProcessing: boolean; -}) => { - const { t } = useTranslation(); - const appointmentAvailabilityFormSchema = z.object({ - name: z.string().min(1, t("field_required")), - tokens_per_slot: z.number().min(1, t("number_min_error", { min: 1 })), - reason: z.string(), - }); - - const form = useForm>({ - resolver: zodResolver(appointmentAvailabilityFormSchema), - defaultValues: { - name: availability.name, - tokens_per_slot: availability.tokens_per_slot, - reason: availability.reason || "", - }, - }); - - function onSubmit(values: z.infer) { - updateAvailability({ - name: values.name, - tokens_per_slot: values.tokens_per_slot, - reason: values.reason, - }); - } - // Group availabilities by day of week const availabilitiesByDay = availability.availability.reduce( (acc, curr) => { @@ -298,218 +260,272 @@ const AppointmentAvailabilityEditor = ({ {} as Record, ); - // Calculate total slots - const totalSlots = Math.floor( - getSlotsPerSession( - availability.availability[0].start_time, - availability.availability[0].end_time, + // Calculate slots and duration for appointment type + const { totalSlots, tokenDuration } = (() => { + if (availability.slot_type !== "appointment") + return { totalSlots: null, tokenDuration: null }; + + const slots = Math.floor( + getSlotsPerSession( + availability.availability[0].start_time, + availability.availability[0].end_time, + availability.slot_size_in_minutes, + ) ?? 0, + ); + + const duration = getTokenDuration( availability.slot_size_in_minutes, - ) ?? 0, - ); + availability.tokens_per_slot, + ); + + return { totalSlots: slots, tokenDuration: duration }; + })(); return (
-
+
-
- {availability.name} -

- {availability.slot_type} - | - {totalSlots} slots - | - {availability.slot_size_in_minutes} min. -

-
+ {availability.name} + + {t(`SCHEDULE_AVAILABILITY_TYPE__${availability.slot_type}`)} +
- - - - - - deleteAvailability()} - disabled={isProcessing} - className="text-red-600" - > - - {t("delete")} - - - +
-
- -
- ( - - {t("schedule_session_title")} - - - - - - )} - /> - - ( - - - {t("schedule_tokens_per_slot")} - - - field.onChange(e.target.valueAsNumber)} - /> - - - - )} - /> -
+
+ {availability.slot_type === "appointment" && ( +
+
+ + {t("slot_configuration")} + +
+ + {availability.slot_size_in_minutes} + + min + × + + {availability.tokens_per_slot} + + + patients + +
+ + ≈ {tokenDuration?.toFixed(1).replace(".0", "")} min per patient + +
-
- - {t("schedule")} - -
- {Object.entries(availabilitiesByDay).map(([day, times]) => ( -

- - {DayOfWeek[parseInt(day)].charAt(0) + - DayOfWeek[parseInt(day)].slice(1).toLowerCase()} - - - {times - .map((time) => formatAvailabilityTime([time])) - .join(", ")} - -

- ))} +
+ + {t("session_capacity")} + +
+ + {totalSlots} + + slots + × + + {availability.tokens_per_slot} + + + patients + +
+ + = {totalSlots ? totalSlots * availability.tokens_per_slot : 0}{" "} + total patients +
+ )} + +
+ + {t("remarks")} + +

+ {availability.reason || t("no_remarks")} +

+
- ( - - Remarks - -