diff --git a/public/locale/en.json b/public/locale/en.json index 8ae766fc8e6..8999037a846 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -364,6 +364,7 @@ "allow_transfer": "Allow Transfer", "allowed_formats_are": "Allowed formats are", "already_a_member": "Already a member?", + "alternate_phone_number": "Alternate Phone Number", "ambulance_driver_name": "Name of ambulance driver", "ambulance_number": "Ambulance No", "ambulance_phone_number": "Phone number of Ambulance", @@ -502,6 +503,7 @@ "check_policy_eligibility": "Check Policy Eligibility", "check_status": "Check Status", "checked_in": "Checked-In", + "checking_availability": "Checking Availability", "checking_consent_status": "Consent request status is being checked!", "checking_eligibility": "Checking Eligibility", "checking_for_update": "Checking for update", @@ -646,6 +648,7 @@ "create_position_preset_description": "Creates a new position preset in Care from the current position of the camera for the given name", "create_preset_prerequisite": "To create presets for this bed, you'll need to link the camera to the bed first.", "create_resource_request": "Create Request", + "create_user": "Create User", "created": "Created", "created_by": "Created By", "created_date": "Created Date", @@ -726,6 +729,7 @@ "dob": "DOB", "dob_format": "Please enter date in DD/MM/YYYY format", "doc_will_visit_patient": "will visit the patient at the scheduled time.", + "doctor": "Doctor", "doctor_experience_error": "Please enter a valid number between 0 and 100.", "doctor_experience_required": "Years of experience is required", "doctor_not_found": "Doctor not found", @@ -770,7 +774,7 @@ "eg_mail_example_com": "Eg. mail@example.com", "eg_xyz": "Eg. XYZ", "eligible": "Eligible", - "email": "Email Address", + "email": "Email", "email_address": "Email Address", "email_discharge_summary_description": "Enter your valid email address to receive the discharge summary", "email_success": "We will be sending an email shortly. Please check your inbox.", @@ -1260,6 +1264,8 @@ "no_notices_for_you": "No notices for you.", "no_observations": "No Observations", "no_ongoing_medications": "No Ongoing Medications", + "no_patient_record_found": "No Patient Records Found", + "no_patient_record_text": "No existing records found with this phone number. Would you like to register a new patient?", "no_patients": "No patients found", "no_patients_found": "No Patients Found", "no_patients_found_phone_number": "No patients found with this phone number. Please create a new patient to proceed with booking appointment.", @@ -1304,6 +1310,7 @@ "number_of_beds_out_of_range_error": "Number of beds cannot be greater than 100", "number_of_chronic_diseased_dependents": "Number Of Chronic Diseased Dependents", "number_of_covid_vaccine_doses": "Number of Covid vaccine doses", + "nurse": "Nurse", "nursing_care": "Nursing Care", "nursing_information": "Nursing Information", "nutrition": "Nutrition", @@ -1365,6 +1372,7 @@ "patient__social-profile": "Social Profile", "patient__volunteer-contact": "Volunteer Contact", "patient_address": "Patient Address", + "patient_birth_year_for_identity": "Please enter the patient's year of birth to verify their identity", "patient_body": "Patient Body", "patient_category": "Patient Category", "patient_consultation__admission": "Date of admission", @@ -1631,6 +1639,7 @@ "search_icd11_placeholder": "Search for ICD-11 Diagnoses", "search_investigation_placeholder": "Search Investigation & Groups", "search_medication": "Search Medication", + "search_patient_page_text": "Search for existing patients using their phone number or create a new patient record", "search_patients": "Search Patients", "search_resource": "Search Resource", "search_user": "Search User", @@ -1714,6 +1723,7 @@ "source": "Source", "spokes": "Spoke Facilities", "srf_id": "SRF ID", + "staff": "Staff", "staff_list": "Staff List", "start_consultation": "Start Consultation", "start_datetime": "Start Date/Time", @@ -1878,6 +1888,7 @@ "vaccinated": "Vaccinated", "vaccine_name": "Vaccine name", "valid_otp_found": "Valid OTP found, Navigating to Appointments", + "valid_year_of_birth": "Please enter a valid year of birth (YYYY)", "vehicle_preference": "Vehicle preference", "vendor_name": "Vendor Name", "ventilator_interface": "Respiratory Support Type", @@ -1887,6 +1898,7 @@ "ventilator_oxygen_modality": "Oxygen Modality", "ventilator_oxygen_modality_oxygen_rate": "Oxygen Flow Rate", "ventilator_spo2": "SpO₂", + "verify": "Verify", "verify_and_link": "Verify and Link", "verify_otp": "Verify OTP", "verify_otp_error": "Failed to verify OTP. Please try again later.", @@ -1894,6 +1906,7 @@ "verify_otp_success_login": "OTP has been verified successfully. Logging in.", "verify_patient": "Verify Patient", "verify_patient_identifier": "Please verify the patient identifier", + "verify_patient_identity": "Verify Patient Identity", "verify_using": "Verify Using", "video_call": "Video Call", "video_conference_link": "Video Conference Link", @@ -1920,6 +1933,7 @@ "vitals_monitor": "Vitals Monitor", "vitals_present": "Vitals Monitor present", "voice_autofill": "Voice Autofill", + "volunteer": "Volunteer", "volunteer_assigned": "Volunteer assigned successfully", "volunteer_contact": "Volunteer Contact", "volunteer_contact_detail": "Provide the name and contact details of a volunteer who can assist the patient in emergencies. This should be someone outside the family.", @@ -1932,6 +1946,7 @@ "weekly_working_hours_error": "Average weekly working hours must be a number between 0 and 168", "what_facility_assign_the_patient_to": "What facility would you like to assign the patient to", "whatsapp_number": "Whatsapp Number", + "whatsapp_number_same_as_phone_number": "WhatsApp number is same as phone number", "why_the_asset_is_not_working": "Why the asset is not working?", "width": "Width ({{unit}})", "with": "with", diff --git a/src/components/Common/FilePreviewDialog.tsx b/src/components/Common/FilePreviewDialog.tsx index 4d6709d427d..d9eb4db62b3 100644 --- a/src/components/Common/FilePreviewDialog.tsx +++ b/src/components/Common/FilePreviewDialog.tsx @@ -11,6 +11,7 @@ import { useState, } from "react"; import { useTranslation } from "react-i18next"; +import useKeyboardShortcut from "use-keyboard-shortcut"; import CareIcon, { IconName } from "@/CAREUI/icons/CareIcon"; @@ -152,20 +153,11 @@ const FilePreviewDialog = (props: FilePreviewProps) => { : `rotate-${normalizedRotation}`; } - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if (!show) return; - if (e.key === "ArrowLeft" && index > 0) { - handleNext(index - 1); - } - if (e.key === "ArrowRight" && index < (uploadedFiles?.length || 0) - 1) { - handleNext(index + 1); - } - }; - - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, [show, index, uploadedFiles]); + useKeyboardShortcut(["ArrowLeft"], () => index > 0 && handleNext(index - 1)); + useKeyboardShortcut( + ["ArrowRight"], + () => index < (uploadedFiles?.length || 0) - 1 && handleNext(index + 1), + ); return ( { uploadedFiles[index] && uploadedFiles[index].created_date && (

- Created on{" "} + {t("created_on")}{" "} {new Date( uploadedFiles[index].created_date!, ).toLocaleString("en-US", { @@ -235,9 +227,6 @@ const FilePreviewDialog = (props: FilePreviewProps) => { onClick={() => handleNext(index - 1)} disabled={index <= 0} aria-label="Previous file" - onKeyDown={(e) => - e.key === "ArrowLeft" && handleNext(index - 1) - } > @@ -288,9 +277,6 @@ const FilePreviewDialog = (props: FilePreviewProps) => { onClick={() => handleNext(index + 1)} disabled={index >= uploadedFiles.length - 1} aria-label="Next file" - onKeyDown={(e) => - e.key === "ArrowRight" && handleNext(index + 1) - } > diff --git a/src/components/Patient/PatientIndex.tsx b/src/components/Patient/PatientIndex.tsx index 64b3480f925..ba416025b33 100644 --- a/src/components/Patient/PatientIndex.tsx +++ b/src/components/Patient/PatientIndex.tsx @@ -1,7 +1,9 @@ import { useMutation } from "@tanstack/react-query"; import { navigate } from "raviger"; import { useCallback, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { toast } from "sonner"; +import useKeyboardShortcut from "use-keyboard-shortcut"; import { cn } from "@/lib/utils"; @@ -51,6 +53,7 @@ export default function PatientIndex({ facilityId }: { facilityId: string }) { results: [], count: 0, }); + const { t } = useTranslation(); const handleCreatePatient = useCallback(() => { const queryParams = phoneNumber ? { phone_number: phoneNumber } : {}; @@ -60,6 +63,8 @@ export default function PatientIndex({ facilityId }: { facilityId: string }) { }); }, [facilityId, phoneNumber]); + useKeyboardShortcut(["shift", "p"], handleCreatePatient); + function AddPatientButton({ outline }: { outline?: boolean }) { return ( - + diff --git a/src/components/Users/CreateUserForm.tsx b/src/components/Users/CreateUserForm.tsx index 4e3f2d4fc28..eac348e4f91 100644 --- a/src/components/Users/CreateUserForm.tsx +++ b/src/components/Users/CreateUserForm.tsx @@ -1,9 +1,12 @@ import { zodResolver } from "@hookform/resolvers/zod"; +import { useQuery } from "@tanstack/react-query"; import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import * as z from "zod"; +import CareIcon from "@/CAREUI/icons/CareIcon"; + import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { @@ -24,13 +27,17 @@ import { SelectValue, } from "@/components/ui/select"; +import { validateRule } from "@/components/Users/UserFormValidations"; + import { GENDER_TYPES } from "@/common/constants"; import * as Notification from "@/Utils/Notifications"; +import query from "@/Utils/request/query"; import request from "@/Utils/request/request"; import OrganizationSelector from "@/pages/Organization/components/OrganizationSelector"; import { UserBase } from "@/types/user/user"; import UserApi from "@/types/user/userApi"; +import userApi from "@/types/user/userApi"; const userFormSchema = z .object({ @@ -108,6 +115,7 @@ export default function CreateUserForm({ onSubmitSuccess }: Props) { }); const userType = form.watch("user_type"); + const usernameInput = form.watch("username"); const phoneNumber = form.watch("phone_number"); const isWhatsApp = form.watch("phone_number_is_whatsapp"); @@ -115,7 +123,44 @@ export default function CreateUserForm({ onSubmitSuccess }: Props) { if (isWhatsApp) { form.setValue("alt_phone_number", phoneNumber); } - }, [phoneNumber, isWhatsApp, form]); + if (usernameInput && usernameInput.length > 0) { + form.trigger("username"); + } + }, [phoneNumber, isWhatsApp, form, usernameInput]); + + const { error, isLoading } = useQuery({ + queryKey: ["checkUsername", usernameInput], + queryFn: query(userApi.checkUsername, { + pathParams: { username: usernameInput }, + silent: true, + }), + enabled: !form.formState.errors.username, + }); + + const renderUsernameFeedback = (usernameInput: string) => { + const { + errors: { username }, + } = form.formState; + if (username?.message) { + return validateRule(false, username.message); + } else if (isLoading) { + return ( +

+ + + {t("checking_availability")} + +
+ ); + } else if (error) { + return validateRule(false, t("username_not_available")); + } else if (usernameInput) { + return validateRule(true, t("username_available")); + } + }; const onSubmit = async (data: UserFormValues) => { try { @@ -156,7 +201,7 @@ export default function CreateUserForm({ onSubmitSuccess }: Props) { name="user_type" render={({ field }) => ( - User Type + {t("user_type")} @@ -181,9 +226,9 @@ export default function CreateUserForm({ onSubmitSuccess }: Props) { name="first_name" render={({ field }) => ( - First Name + {t("first_name")} - + @@ -195,26 +240,27 @@ export default function CreateUserForm({ onSubmitSuccess }: Props) { name="last_name" render={({ field }) => ( - Last Name + {t("last_name")} - + )} /> - ( - Username + {t("username")} - +
+ +
- + {renderUsernameFeedback(usernameInput)}
)} /> @@ -225,8 +271,7 @@ export default function CreateUserForm({ onSubmitSuccess }: Props) { name="password" render={({ field }) => ( - Password - + {t("password")} @@ -239,7 +284,7 @@ export default function CreateUserForm({ onSubmitSuccess }: Props) { name="c_password" render={({ field }) => ( - Confirm Password + {t("confirm_password")} @@ -254,9 +299,9 @@ export default function CreateUserForm({ onSubmitSuccess }: Props) { name="email" render={({ field }) => ( - Email + {t("email")} - + @@ -269,7 +314,7 @@ export default function CreateUserForm({ onSubmitSuccess }: Props) { name="phone_number" render={({ field }) => ( - Phone Number + {t("phone_number")} @@ -283,7 +328,7 @@ export default function CreateUserForm({ onSubmitSuccess }: Props) { name="alt_phone_number" render={({ field }) => ( - Alternative Phone Number + {t("alternate_phone_number")}
- WhatsApp number is same as phone number + + {t("whatsapp_number_same_as_phone_number")} +
)} @@ -322,7 +369,7 @@ export default function CreateUserForm({ onSubmitSuccess }: Props) { name="date_of_birth" render={({ field }) => ( - Date of Birth + {t("date_of_birth")} @@ -336,7 +383,7 @@ export default function CreateUserForm({ onSubmitSuccess }: Props) { name="gender" render={({ field }) => ( - Gender + {t("gender")} + @@ -384,11 +431,11 @@ export default function CreateUserForm({ onSubmitSuccess }: Props) { name="doctor_experience_commenced_on" render={({ field }) => ( - Years of Experience + {t("years_of_experience")} @@ -402,10 +449,10 @@ export default function CreateUserForm({ onSubmitSuccess }: Props) { name="doctor_medical_council_registration" render={({ field }) => ( - Medical Council Registration + {t("medical_council_registration")} @@ -416,7 +463,6 @@ export default function CreateUserForm({ onSubmitSuccess }: Props) { )} - diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx index b6ad223fbed..c3d5eb952ea 100644 --- a/src/components/ui/sidebar.tsx +++ b/src/components/ui/sidebar.tsx @@ -2,6 +2,7 @@ import { Slot } from "@radix-ui/react-slot"; import { VariantProps, cva } from "class-variance-authority"; import { PanelLeftClose, PanelRightClose } from "lucide-react"; import * as React from "react"; +import useKeyboardShortcut from "use-keyboard-shortcut"; import { cn } from "@/lib/utils"; @@ -19,6 +20,8 @@ import { import { useIsMobile } from "@/hooks/use-mobile"; +import { isAppleDevice } from "@/Utils/utils"; + const SIDEBAR_COOKIE_NAME = "sidebar:state"; const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; const SIDEBAR_WIDTH = "16rem"; @@ -97,20 +100,10 @@ const SidebarProvider = React.forwardRef< }, [isMobile, setOpen, setOpenMobile]); // Adds a keyboard shortcut to toggle the sidebar. - React.useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - if ( - event.key === SIDEBAR_KEYBOARD_SHORTCUT && - (event.metaKey || event.ctrlKey) - ) { - event.preventDefault(); - toggleSidebar(); - } - }; - - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, [toggleSidebar]); + useKeyboardShortcut( + [isAppleDevice ? "Meta" : "Control", SIDEBAR_KEYBOARD_SHORTCUT], + toggleSidebar, + ); // We add a state so that we can do data-state="expanded" or "collapsed". // This makes it easier to style the sidebar with Tailwind classes. diff --git a/src/types/user/userApi.ts b/src/types/user/userApi.ts index 61292d5c473..d522b301473 100644 --- a/src/types/user/userApi.ts +++ b/src/types/user/userApi.ts @@ -21,4 +21,9 @@ export default { TRes: Type(), }, + checkUsername: { + path: "/api/v1/users/{username}/check_availability/", + method: HttpMethod.GET, + TRes: Type, + }, };