From 594984cf235ea92e8f0418c9bedef43d5612ad21 Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Tue, 11 Feb 2025 18:21:02 +0530 Subject: [PATCH 01/11] devices --- public/locale/en.json | 67 ++- .../settings/devices/CreateDevice.tsx | 30 ++ .../Facility/settings/devices/DevicesList.tsx | 135 ++++++ .../devices/components/DeviceCard.tsx | 21 + .../devices/components/DeviceForm.tsx | 440 ++++++++++++++++++ .../devices/components/DeviceSheet.tsx | 45 ++ src/pages/Facility/settings/layout.tsx | 10 + .../CreateFacilityOrganizationSheet.tsx | 2 +- src/types/common/contactPoint.ts | 27 ++ src/types/device/device.ts | 50 ++ src/types/device/deviceApi.ts | 40 ++ vite.config.mts | 1 + 12 files changed, 865 insertions(+), 3 deletions(-) create mode 100644 src/pages/Facility/settings/devices/CreateDevice.tsx create mode 100644 src/pages/Facility/settings/devices/DevicesList.tsx create mode 100644 src/pages/Facility/settings/devices/components/DeviceCard.tsx create mode 100644 src/pages/Facility/settings/devices/components/DeviceForm.tsx create mode 100644 src/pages/Facility/settings/devices/components/DeviceSheet.tsx create mode 100644 src/types/common/contactPoint.ts create mode 100644 src/types/device/device.ts create mode 100644 src/types/device/deviceApi.ts diff --git a/public/locale/en.json b/public/locale/en.json index a962bbde003..ca8c611a028 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -2349,5 +2349,68 @@ "yet_to_be_decided": "Yet to be decided", "you_need_at_least_a_location_to_create_an_assest": "You need at least a location to create an assest.", "zoom_in": "Zoom In", - "zoom_out": "Zoom Out" -} + "zoom_out": "Zoom Out", + "devices": "Devices", + "add_device": "Add Device", + "no_devices_found": "No devices found", + "no_devices_available": "No devices available", + "edit_device": "Edit Device", + "add_device_description": "Add a new device to the facility", + "edit_device_description": "Edit the details of the device", + "registered_name": "Registered Name", + "enter_registered_name": "Enter the registered name of the device", + "user_friendly_name": "User Friendly Name", + "enter_user_friendly_name": "Enter a user friendly name for the device", + "status": "Status", + "select_status": "Select device status", + "device_status_active": "Active", + "device_status_inactive": "Inactive", + "device_status_entered_in_error": "Entered in Error", + "availability_status": "Availability Status", + "select_availability_status": "Select availability status", + "device_availability_status_lost": "Lost", + "device_availability_status_damaged": "Damaged", + "device_availability_status_destroyed": "Destroyed", + "device_availability_status_available": "Available", + "identifier": "Identifier", + "enter_identifier": "Enter device identifier", + "manufacturer": "Manufacturer", + "enter_manufacturer": "Enter manufacturer name", + "manufacturer_date": "Manufacture Date", + "expiration_date": "Expiration Date", + "lot_number": "Lot Number", + "enter_lot_number": "Enter lot number", + "serial_number": "Serial Number", + "enter_serial_number": "Enter serial number", + "model_number": "Model Number", + "enter_model_number": "Enter model number", + "part_number": "Part Number", + "enter_part_number": "Enter part number", + "contact_points": "Contact Points", + "add_contact_point": "Add Contact Point", + "system": "System", + "select_contact_system": "Select contact system", + "contact_system_phone": "Phone", + "contact_system_fax": "Fax", + "contact_system_email": "Email", + "contact_system_pager": "Pager", + "contact_system_url": "URL", + "contact_system_sms": "SMS", + "contact_system_other": "Other", + "value": "Value", + "enter_contact_value": "Enter contact value", + "use": "Use", + "select_contact_use": "Select contact use", + "contact_use_home": "Home", + "contact_use_work": "Work", + "contact_use_temp": "Temporary", + "contact_use_old": "Old", + "contact_use_mobile": "Mobile", + "remove": "Remove", + "saving": "Saving...", + "save": "Save", + "no_devices_found": "No devices found", + "no_devices_available": "No devices available", + "devices": "Devices", + "search_by_name": "Search by name" +} \ No newline at end of file diff --git a/src/pages/Facility/settings/devices/CreateDevice.tsx b/src/pages/Facility/settings/devices/CreateDevice.tsx new file mode 100644 index 00000000000..6fd4e609637 --- /dev/null +++ b/src/pages/Facility/settings/devices/CreateDevice.tsx @@ -0,0 +1,30 @@ +import { navigate } from "raviger"; +import { useTranslation } from "react-i18next"; + +import DeviceForm from "@/pages/Facility/settings/devices/components/DeviceForm"; + +interface Props { + facilityId: string; +} + +export default function CreateDevice({ facilityId }: Props) { + const { t } = useTranslation(); + + return ( +
+
+

{t("add_device")}

+

{t("add_device_description")}

+
+ +
+ { + navigate(`/facility/${facilityId}/settings/devices`); + }} + /> +
+
+ ); +} diff --git a/src/pages/Facility/settings/devices/DevicesList.tsx b/src/pages/Facility/settings/devices/DevicesList.tsx new file mode 100644 index 00000000000..e0553f8ccb9 --- /dev/null +++ b/src/pages/Facility/settings/devices/DevicesList.tsx @@ -0,0 +1,135 @@ +import { useQuery } from "@tanstack/react-query"; +import { navigate, useQueryParams } from "raviger"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; + +import CareIcon from "@/CAREUI/icons/CareIcon"; + +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; + +import Pagination from "@/components/Common/Pagination"; +import { CardGridSkeleton } from "@/components/Common/SkeletonLoading"; + +import query from "@/Utils/request/query"; +import DeviceCard from "@/pages/Facility/settings/devices/components/DeviceCard"; +import DeviceSheet from "@/pages/Facility/settings/devices/components/DeviceSheet"; +import deviceApi from "@/types/device/deviceApi"; + +interface Props { + facilityId: string; +} + +interface PageQueryParams { + device_id: string | null; + sheet?: "view" | "link-to-location" | null; +} + +export default function DevicesList({ facilityId }: Props) { + const { t } = useTranslation(); + const [page, setPage] = useState(1); + const [searchQuery, setSearchQuery] = useState(""); + const [{ device_id, sheet }, setQueryParams] = + useQueryParams(); + + const limit = 12; + + const { data, isLoading } = useQuery({ + queryKey: ["devices", facilityId, page, limit, searchQuery], + queryFn: query.debounced(deviceApi.list, { + pathParams: { facility_id: facilityId }, + queryParams: { + offset: (page - 1) * limit, + limit, + name: searchQuery || undefined, + }, + }), + }); + + const handleAddDevice = () => { + navigate(`/facility/${facilityId}/settings/devices/create`); + }; + + const handleViewDevice = (deviceId: string) => { + setQueryParams({ sheet: "view", device_id: deviceId }); + }; + + const handleLinkDevice = (deviceId: string) => { + setQueryParams({ sheet: "link-to-location", device_id: deviceId }); + }; + + return ( +
+
+
+

{t("devices")}

+ +
+
+ { + setSearchQuery(e.target.value); + setPage(1); + }} + className="w-full" + /> +
+
+ + {isLoading ? ( +
+ +
+ ) : ( +
+
+ {data?.results?.length ? ( + data.results.map((device) => ( + + )) + ) : ( + + + {searchQuery + ? t("no_devices_found") + : t("no_devices_available")} + + + )} +
+ {data && data.count > limit && ( +
+ setPage(page)} + defaultPerPage={limit} + cPage={page} + /> +
+ )} +
+ )} + setQueryParams({ sheet: null, device_id })} + facilityId={facilityId} + device={ + device_id + ? data?.results?.find((device) => device.id === device_id) + : undefined + } + /> +
+ ); +} diff --git a/src/pages/Facility/settings/devices/components/DeviceCard.tsx b/src/pages/Facility/settings/devices/components/DeviceCard.tsx new file mode 100644 index 00000000000..716b83b72da --- /dev/null +++ b/src/pages/Facility/settings/devices/components/DeviceCard.tsx @@ -0,0 +1,21 @@ +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +import { DeviceList } from "@/types/device/device"; + +interface Props { + device: DeviceList; + onView: (deviceId: string) => void; + onLink: (deviceId: string) => void; +} + +export default function DeviceCard({ device, onView, onLink }: Props) { + return ( + + + + {device.registered_name} + + + + ); +} diff --git a/src/pages/Facility/settings/devices/components/DeviceForm.tsx b/src/pages/Facility/settings/devices/components/DeviceForm.tsx new file mode 100644 index 00000000000..df50fc880a8 --- /dev/null +++ b/src/pages/Facility/settings/devices/components/DeviceForm.tsx @@ -0,0 +1,440 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { t } from "i18next"; +import { useEffect } from "react"; +import { useFieldArray, useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import { z } from "zod"; + +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; + +import mutate from "@/Utils/request/mutate"; +import { + ContactPointSystems, + ContactPointUses, +} from "@/types/common/contactPoint"; +import { + DeviceAvailabilityStatuses, + DeviceList, + DeviceStatuses, +} from "@/types/device/device"; +import deviceApi from "@/types/device/deviceApi"; + +const formSchema = z.object({ + identifier: z.string().optional(), + status: z.enum(DeviceStatuses), + availability_status: z.enum(DeviceAvailabilityStatuses), + manufacturer: z.string().optional(), + manufacturer_date: z.string().optional(), + expiration_date: z.string().optional(), + lot_number: z.string().optional(), + serial_number: z.string().optional(), + registered_name: z.string().min(1, { message: t("required") }), + user_friendly_name: z.string().optional(), + model_number: z.string().optional(), + part_number: z.string().optional(), + contact: z.array( + z.object({ + system: z.enum(ContactPointSystems), + value: z.string(), + use: z.enum(ContactPointUses), + }), + ), +}); + +interface Props { + facilityId: string; + device?: DeviceList; + onSuccess?: () => void; +} + +const defaultValues: z.infer = { + identifier: "", + status: "active", + availability_status: "available", + manufacturer: "", + manufacturer_date: "", + registered_name: "", + contact: [], +}; + +export default function DeviceForm({ facilityId, device, onSuccess }: Props) { + const { t } = useTranslation(); + const queryClient = useQueryClient(); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues, + }); + + const { fields, append, remove } = useFieldArray({ + control: form.control, + name: "contact", + }); + + const { mutate: submitForm, isPending } = useMutation({ + mutationFn: device?.id + ? mutate(deviceApi.update, { + pathParams: { facility_id: facilityId, id: device.id }, + }) + : mutate(deviceApi.create, { + pathParams: { facility_id: facilityId }, + }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["devices"] }); + onSuccess?.(); + }, + }); + + useEffect(() => { + if (device) { + form.reset({ ...device }); + } + }, [device, form]); + + function onSubmit(values: z.infer) { + submitForm({ ...values }); + } + + return ( +
+ +
+ ( + + {t("registered_name")} + + + + + + )} + /> + + ( + + {t("user_friendly_name")} + + + + + + )} + /> + + ( + + {t("status")} + + + + )} + /> + + ( + + {t("availability_status")} + + + + )} + /> + + ( + + {t("identifier")} + + + + + + )} + /> + + ( + + {t("manufacturer")} + + + + + + )} + /> + + ( + + {t("manufacturer_date")} + + + + + + )} + /> + + ( + + {t("expiration_date")} + + + + + + )} + /> + + ( + + {t("lot_number")} + + + + + + )} + /> + + ( + + {t("serial_number")} + + + + + + )} + /> + + ( + + {t("model_number")} + + + + + + )} + /> + + ( + + {t("part_number")} + + + + + + )} + /> +
+ +
+
+

{t("contact_points")}

+ +
+ + {fields.map((field, index) => ( + + +
+ ( + + {t("system")} + + + + )} + /> + + ( + + {t("value")} + + + + + + )} + /> + + ( + + {t("use")} + + + + )} + /> +
+ +
+ +
+
+
+ ))} +
+ +
+ +
+
+ + ); +} diff --git a/src/pages/Facility/settings/devices/components/DeviceSheet.tsx b/src/pages/Facility/settings/devices/components/DeviceSheet.tsx new file mode 100644 index 00000000000..5b71c46fc59 --- /dev/null +++ b/src/pages/Facility/settings/devices/components/DeviceSheet.tsx @@ -0,0 +1,45 @@ +import { useTranslation } from "react-i18next"; + +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet"; + +import DeviceForm from "@/pages/Facility/settings/devices/components/DeviceForm"; +import { DeviceList } from "@/types/device/device"; + +interface Props { + open: boolean; + onOpenChange: (open: boolean) => void; + facilityId: string; + device?: DeviceList; +} + +export default function DeviceSheet({ + open, + onOpenChange, + facilityId, + device, +}: Props) { + const { t } = useTranslation(); + return ( + + + + {device ? t("edit_device") : t("add_device")} + + {device + ? t("edit_device_description") + : t("add_device_description")} + + +
+ +
+
+
+ ); +} diff --git a/src/pages/Facility/settings/layout.tsx b/src/pages/Facility/settings/layout.tsx index 9ba1d35ec39..88b4e324e31 100644 --- a/src/pages/Facility/settings/layout.tsx +++ b/src/pages/Facility/settings/layout.tsx @@ -4,6 +4,9 @@ import { useTranslation } from "react-i18next"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import CreateDevice from "@/pages/Facility/settings/devices/CreateDevice"; +import DevicesList from "@/pages/Facility/settings/devices/DevicesList"; + import { GeneralSettings } from "./general/general"; import LocationList from "./locations/LocationList"; import LocationView from "./locations/LocationView"; @@ -28,6 +31,8 @@ const getRoutes = (facilityId: string) => ({ "/location/:id": ({ id }: { id: string }) => ( ), + "/devices": () => , + "/devices/create": () => , "*": () =>
404
, }); @@ -57,6 +62,11 @@ export function SettingsLayout({ facilityId }: SettingsLayoutProps) { label: t("locations"), href: `${basePath}/locations`, }, + { + value: "devices", + label: t("devices"), + href: `${basePath}/devices`, + }, ]; // Extract the current tab from the URL diff --git a/src/pages/Facility/settings/organizations/components/CreateFacilityOrganizationSheet.tsx b/src/pages/Facility/settings/organizations/components/CreateFacilityOrganizationSheet.tsx index 0726a6d44fe..51132441d04 100644 --- a/src/pages/Facility/settings/organizations/components/CreateFacilityOrganizationSheet.tsx +++ b/src/pages/Facility/settings/organizations/components/CreateFacilityOrganizationSheet.tsx @@ -39,7 +39,7 @@ const ORG_TYPES = [ type OrgType = (typeof ORG_TYPES)[number]["value"]; -export default function CreateFacilityOrganizationSheet({ +export default function FacilityOrganizationSheet({ facilityId, parentId, }: Props) { diff --git a/src/types/common/contactPoint.ts b/src/types/common/contactPoint.ts new file mode 100644 index 00000000000..c8b37e38944 --- /dev/null +++ b/src/types/common/contactPoint.ts @@ -0,0 +1,27 @@ +export const ContactPointSystems = [ + "phone", + "fax", + "email", + "pager", + "url", + "sms", + "other", +] as const; + +export type ContactPointSystem = (typeof ContactPointSystems)[number]; + +export const ContactPointUses = [ + "home", + "work", + "temp", + "old", + "mobile", +] as const; + +export type ContactPointUse = (typeof ContactPointUses)[number]; + +export interface ContactPoint { + system: ContactPointSystem; + value: string; + use: ContactPointUse; +} diff --git a/src/types/device/device.ts b/src/types/device/device.ts new file mode 100644 index 00000000000..eeca06593fd --- /dev/null +++ b/src/types/device/device.ts @@ -0,0 +1,50 @@ +import { ContactPoint } from "@/types/common/contactPoint"; +import { Encounter } from "@/types/emr/encounter"; +import { LocationDetail } from "@/types/location/location"; + +export const DeviceStatuses = [ + "active", + "inactive", + "entered_in_error", +] as const; + +export type DeviceStatus = (typeof DeviceStatuses)[number]; + +export const DeviceAvailabilityStatuses = [ + "lost", + "damaged", + "destroyed", + "available", +] as const; + +export type DeviceAvailabilityStatus = + (typeof DeviceAvailabilityStatuses)[number]; + +export interface DeviceBase { + identifier?: string; + status: DeviceStatus; + availability_status: DeviceAvailabilityStatus; + manufacturer?: string; + manufacturer_date?: string; // datetime + expiration_date?: string; // datetime + lot_number?: string; + serial_number?: string; + registered_name: string; + user_friendly_name?: string; + model_number?: string; + part_number?: string; + contact: ContactPoint[]; + // care_type: string | undefined; +} + +export interface DeviceDetail extends DeviceBase { + id: string; + current_encounter: Encounter | undefined; + current_location: LocationDetail | undefined; // TODO: verify this type +} + +export interface DeviceList extends DeviceBase { + id: string; +} + +export type DeviceWrite = DeviceBase; diff --git a/src/types/device/deviceApi.ts b/src/types/device/deviceApi.ts new file mode 100644 index 00000000000..de6b632a109 --- /dev/null +++ b/src/types/device/deviceApi.ts @@ -0,0 +1,40 @@ +import { HttpMethod, Type } from "@/Utils/request/api"; +import { PaginatedResponse } from "@/Utils/request/types"; + +import { DeviceDetail, DeviceList, DeviceWrite } from "./device"; + +export default { + list: { + path: "/api/v1/facility/{facility_id}/device/", + method: HttpMethod.GET, + TRes: Type>(), + }, + create: { + path: "/api/v1/facility/{facility_id}/device/", + method: HttpMethod.POST, + TRes: Type(), + TBody: Type(), + }, + retrieve: { + path: "/api/v1/facility/{facility_id}/device/{id}/", + method: HttpMethod.GET, + TRes: Type(), + }, + update: { + path: "/api/v1/facility/{facility_id}/device/{id}/", + method: HttpMethod.PUT, + TRes: Type(), + TBody: Type(), + }, + delete: { + path: "/api/v1/facility/{facility_id}/device/{id}/", + method: HttpMethod.DELETE, + TRes: Type(), + }, + upsert: { + path: "/api/v1/facility/{facility_id}/device/upsert/", + method: HttpMethod.POST, + TRes: Type(), + TBody: Type(), + }, +}; diff --git a/vite.config.mts b/vite.config.mts index 749f188002c..308c942b7ad 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -288,6 +288,7 @@ export default defineConfig(({ mode }) => { server: { port: 4000, host: "0.0.0.0", + allowedHosts: true, }, preview: { headers: { From 70e99d87a206790cd2dea90e7fb35ff04fb38b07 Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Wed, 12 Feb 2025 14:41:06 +0530 Subject: [PATCH 02/11] Devices form --- public/locale/en.json | 130 ++++++------- .../settings/devices/CreateDevice.tsx | 2 +- .../Facility/settings/devices/DevicesList.tsx | 57 ++---- .../devices/components/DeviceCard.tsx | 4 +- .../devices/components/DeviceForm.tsx | 183 ++++++++---------- .../devices/components/DeviceSheet.tsx | 45 ----- src/types/common/contactPoint.ts | 60 +++++- 7 files changed, 213 insertions(+), 268 deletions(-) delete mode 100644 src/pages/Facility/settings/devices/components/DeviceSheet.tsx diff --git a/public/locale/en.json b/public/locale/en.json index ca8c611a028..afcd1127117 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -290,7 +290,10 @@ "add_beds_to_configure_presets": "Add beds to this location to configure presets for them.", "add_consultation": "Add consultation", "add_consultation_update": "Add Consultation Update", + "add_contact_point": "Add Contact Point", "add_details_of_patient": "Add Details of Patient", + "add_device": "Add Device", + "add_device_description": "Add a new device to the facility", "add_exception": "Add Exception", "add_facility": "Add Facility", "add_files": "Add Files", @@ -436,6 +439,7 @@ "auto_generated_for_care": "Auto Generated for Care", "autofilled_fields": "Autofilled Fields", "availabilities": "Availabilities", + "availability_status": "Availability Status", "available_features": "Available Features", "available_in": "Available in", "available_time_slots": "Available Time Slots", @@ -612,6 +616,25 @@ "contact_person_number": "Contact person number", "contact_phone": "Contact Person Number", "contact_phone_description": "Phone number to reach the contact person.", + "contact_point_placeholder__email": "Eg. john@example.com", + "contact_point_placeholder__fax": "Eg. +91 9876543210", + "contact_point_placeholder__pager": "Eg. 123456", + "contact_point_placeholder__phone": "Eg. +91 9876543210", + "contact_point_placeholder__sms": "Eg. +91 9876543210", + "contact_point_placeholder__url": "Eg. https://example.com", + "contact_points": "Contact Points", + "contact_system_email": "Email", + "contact_system_fax": "Fax", + "contact_system_other": "Other", + "contact_system_pager": "Pager", + "contact_system_phone": "Phone", + "contact_system_sms": "SMS", + "contact_system_url": "URL", + "contact_use_home": "Home", + "contact_use_mobile": "Mobile", + "contact_use_old": "Old", + "contact_use_temp": "Temporary", + "contact_use_work": "Work", "contact_with_confirmed_carrier": "Contact with confirmed carrier", "contact_with_suspected_carrier": "Contact with suspected carrier", "contact_your_admin_to_add_facilities": "Contact your admin to add facilities", @@ -707,6 +730,14 @@ "details_of_origin_facility": "Details of origin facility", "details_of_patient": "Details of patient", "details_of_shifting_approving_facility": "Details of shifting approving facility", + "device_availability_status_available": "Available", + "device_availability_status_damaged": "Damaged", + "device_availability_status_destroyed": "Destroyed", + "device_availability_status_lost": "Lost", + "device_status_active": "Active", + "device_status_entered_in_error": "Entered in Error", + "device_status_inactive": "Inactive", + "devices": "Devices", "diagnoses": "Diagnoses", "diagnosis": "Diagnosis", "diagnosis__confirmed": "Confirmed", @@ -782,6 +813,8 @@ "edit_avatar_permission_error": "You do not have permissions to edit the avatar of this user", "edit_caution_note": "A new prescription will be added to the consultation with the edited details and the current prescription will be discontinued.", "edit_cover_photo": "Edit Cover Photo", + "edit_device": "Edit Device", + "edit_device_description": "Edit the details of the device", "edit_facility": "Edit Facility", "edit_history": "Edit History", "edit_location": "Edit Location", @@ -927,18 +960,27 @@ "end_time_before_start_error": "End time cannot be before start time", "end_time_future_error": "End time cannot be in the future", "ended": "Ended", + "enter_contact_value": "Enter contact value", "enter_dosage_instructions": "Enter Dosage Instructions", "enter_file_name": "Enter File Name", + "enter_identifier": "Enter device identifier", + "enter_lot_number": "Enter lot number", + "enter_manufacturer": "Enter manufacturer name", "enter_message": "Start typing...", "enter_mobile_number": "Enter Mobile Number", "enter_mobile_otp": "Enter OTP sent to the given mobile number", + "enter_model_number": "Enter model number", "enter_otp": "Enter OTP sent to the registered mobile with the respective ID", + "enter_part_number": "Enter part number", "enter_phone_number": "Enter phone number", "enter_phone_number_to_login_register": "Enter phone number to login/register", + "enter_registered_name": "Enter the registered name of the device", + "enter_serial_number": "Enter serial number", "enter_tag_name": "Enter tag name", "enter_tag_slug": "Enter tag slug", "enter_the_file_name": "Enter the file name", "enter_the_verification_code": "Enter the verification code sent to your phone", + "enter_user_friendly_name": "Enter a user friendly name for the device", "enter_valid_age": "Please Enter Valid Age", "enter_valid_dob": "Enter a valid date of birth", "enter_valid_dob_age": "Please enter an age greater than 15 years", @@ -971,6 +1013,7 @@ "exceptions": "Exceptions", "expand_sidebar": "Expand Sidebar", "expected_burn_rate": "Expected Burn Rate", + "expiration_date": "Expiration Date", "expired": "Expired", "expired_on": "Expired On", "expires_on": "Expires On", @@ -1108,6 +1151,7 @@ "i_declare": "I hereby declare that:", "icd11_as_recommended": "As per ICD-11 recommended by WHO", "icmr_specimen_referral_form": "ICMR Specimen Referral Form", + "identifier": "Identifier", "immunisation-records": "Immunisation", "in_consultation": "In-Consultation", "in_progress": "In Progress", @@ -1256,6 +1300,7 @@ "logout": "Log Out", "longitude": "Longitude", "longitude_invalid": "Longitude must be between -180 and 180", + "lot_number": "Lot Number", "low": "Low", "lsg": "Lsg", "make_facility_public": "Make this facility public", @@ -1272,6 +1317,7 @@ "manage_tags_description": "Add or remove tags for this questionnaire", "manage_user": "Manage User", "manufacturer": "Manufacturer", + "manufacturer_date": "Manufacture Date", "map_acronym": "M.A.P.", "mark_active": "Mark Active", "mark_all_as_read": "Mark all as Read", @@ -1325,6 +1371,7 @@ "mobile_otp_send_success": "OTP has been sent to the given mobile number.", "mobile_otp_verify_error": "Failed to verify mobile number. Please try again later.", "mobile_otp_verify_success": "Mobile number has been verified successfully.", + "model_number": "Model Number", "moderate": "Moderate", "modification_caution_note": "No modifications possible once added", "modified": "Modified", @@ -1379,6 +1426,8 @@ "no_country_found": "No country found", "no_data_found": "No data found", "no_departments_teams_found": "No Departments or Teams found", + "no_devices_available": "No devices available", + "no_devices_found": "No devices found", "no_diagnoses_recorded": "No diagnoses recorded", "no_doctors_found": "No Doctors Found", "no_duplicate_facility": "You should not create duplicate facilities", @@ -1516,6 +1565,7 @@ "page_not_found": "Page Not Found", "pain": "Pain", "pain_chart_description": "Mark region and intensity of pain", + "part_number": "Part Number", "passport_number": "Passport Number", "password": "Password", "password_length_validation": "Use at least 8 characters", @@ -1744,6 +1794,7 @@ "register_hospital": "Register Hospital", "register_page_title": "Register As Hospital Administrator", "register_patient": "Register Patient", + "registered_name": "Registered Name", "reject": "Reject", "rejected": "Rejected", "relapse": "Relapse", @@ -1885,13 +1936,14 @@ "search_by": "Search by", "search_by_emergency_contact_phone_number": "Search by Emergency Contact Phone Number", "search_by_emergency_phone_number": "Search by Emergency Phone Number", - "search_by_name": "Search by Name", + "search_by_name": "Search by name", "search_by_patient_name": "Search by Patient Name", "search_by_patient_no": "Search by Patient Number", "search_by_phone_number": "Search by Phone Number", "search_by_resource_title": "Search by resource title", "search_by_username": "Search by username", "search_country": "Search country...", + "search_devices": "Search Devices", "search_encounters": "Search Encounters", "search_for_allergies_to_add": "Search for allergies to add", "search_for_diagnoses_to_add": "Search for diagnoses to add", @@ -1916,8 +1968,11 @@ "select_additional_instructions": "Select additional instructions", "select_admit_source": "Select Admit Source", "select_all": "Select All", + "select_availability_status": "Select availability status", "select_category": "Select a category", "select_class": "Select Class", + "select_contact_system": "Select contact system", + "select_contact_use": "Select contact use", "select_date": "Select date", "select_department": "Select Department", "select_diet_preference": "Select diet preference", @@ -1955,7 +2010,7 @@ "select_seven_day_period": "Select a seven day period", "select_site": "Select site", "select_skills": "Select and add some skills", - "select_status": "Select Status", + "select_status": "Select device status", "select_sub_department": "Select sub-department", "select_time": "Select time", "select_time_slot": "Select time slot", @@ -2079,6 +2134,7 @@ "symptom": "Symptom", "symptoms": "Symptoms", "symptoms_empty_message": "No symptoms recorded", + "system": "System", "systolic": "Systolic", "tachycardia": "Tachycardia", "tag_name": "Tag Name", @@ -2227,6 +2283,7 @@ "upload_headings__supporting_info": "Upload Supporting Info", "upload_report": "Upload Report", "uploading": "Uploading", + "use": "Use", "use_address_as_permanent": "Use this address for permanent address", "use_phone_number_for_emergency": "Use this phone number for emergency contact", "user_add_error": "Error while adding User", @@ -2237,6 +2294,7 @@ "user_details": "User Details", "user_details_update_error": "Error while updating user details", "user_details_update_success": "User details updated successfully", + "user_friendly_name": "User Friendly Name", "user_management": "User Management", "user_not_available_for_appointments": "This user is not available for appointments", "user_qualifications": "Qualifications", @@ -2267,6 +2325,7 @@ "valid_otp_found": "Valid OTP found, Navigating to Appointments", "valid_to": "Valid Till", "valid_year_of_birth": "Please enter a valid year of birth (YYYY)", + "value": "Value", "vehicle_preference": "Vehicle preference", "vendor_name": "Vendor Name", "ventilator_interface": "Respiratory Support Type", @@ -2349,68 +2408,5 @@ "yet_to_be_decided": "Yet to be decided", "you_need_at_least_a_location_to_create_an_assest": "You need at least a location to create an assest.", "zoom_in": "Zoom In", - "zoom_out": "Zoom Out", - "devices": "Devices", - "add_device": "Add Device", - "no_devices_found": "No devices found", - "no_devices_available": "No devices available", - "edit_device": "Edit Device", - "add_device_description": "Add a new device to the facility", - "edit_device_description": "Edit the details of the device", - "registered_name": "Registered Name", - "enter_registered_name": "Enter the registered name of the device", - "user_friendly_name": "User Friendly Name", - "enter_user_friendly_name": "Enter a user friendly name for the device", - "status": "Status", - "select_status": "Select device status", - "device_status_active": "Active", - "device_status_inactive": "Inactive", - "device_status_entered_in_error": "Entered in Error", - "availability_status": "Availability Status", - "select_availability_status": "Select availability status", - "device_availability_status_lost": "Lost", - "device_availability_status_damaged": "Damaged", - "device_availability_status_destroyed": "Destroyed", - "device_availability_status_available": "Available", - "identifier": "Identifier", - "enter_identifier": "Enter device identifier", - "manufacturer": "Manufacturer", - "enter_manufacturer": "Enter manufacturer name", - "manufacturer_date": "Manufacture Date", - "expiration_date": "Expiration Date", - "lot_number": "Lot Number", - "enter_lot_number": "Enter lot number", - "serial_number": "Serial Number", - "enter_serial_number": "Enter serial number", - "model_number": "Model Number", - "enter_model_number": "Enter model number", - "part_number": "Part Number", - "enter_part_number": "Enter part number", - "contact_points": "Contact Points", - "add_contact_point": "Add Contact Point", - "system": "System", - "select_contact_system": "Select contact system", - "contact_system_phone": "Phone", - "contact_system_fax": "Fax", - "contact_system_email": "Email", - "contact_system_pager": "Pager", - "contact_system_url": "URL", - "contact_system_sms": "SMS", - "contact_system_other": "Other", - "value": "Value", - "enter_contact_value": "Enter contact value", - "use": "Use", - "select_contact_use": "Select contact use", - "contact_use_home": "Home", - "contact_use_work": "Work", - "contact_use_temp": "Temporary", - "contact_use_old": "Old", - "contact_use_mobile": "Mobile", - "remove": "Remove", - "saving": "Saving...", - "save": "Save", - "no_devices_found": "No devices found", - "no_devices_available": "No devices available", - "devices": "Devices", - "search_by_name": "Search by name" -} \ No newline at end of file + "zoom_out": "Zoom Out" +} diff --git a/src/pages/Facility/settings/devices/CreateDevice.tsx b/src/pages/Facility/settings/devices/CreateDevice.tsx index 6fd4e609637..f836f395f22 100644 --- a/src/pages/Facility/settings/devices/CreateDevice.tsx +++ b/src/pages/Facility/settings/devices/CreateDevice.tsx @@ -17,7 +17,7 @@ export default function CreateDevice({ facilityId }: Props) {

{t("add_device_description")}

-
+
{ diff --git a/src/pages/Facility/settings/devices/DevicesList.tsx b/src/pages/Facility/settings/devices/DevicesList.tsx index e0553f8ccb9..f693e0993d3 100644 --- a/src/pages/Facility/settings/devices/DevicesList.tsx +++ b/src/pages/Facility/settings/devices/DevicesList.tsx @@ -1,5 +1,5 @@ import { useQuery } from "@tanstack/react-query"; -import { navigate, useQueryParams } from "raviger"; +import { Link } from "raviger"; import { useState } from "react"; import { useTranslation } from "react-i18next"; @@ -14,24 +14,16 @@ import { CardGridSkeleton } from "@/components/Common/SkeletonLoading"; import query from "@/Utils/request/query"; import DeviceCard from "@/pages/Facility/settings/devices/components/DeviceCard"; -import DeviceSheet from "@/pages/Facility/settings/devices/components/DeviceSheet"; import deviceApi from "@/types/device/deviceApi"; interface Props { facilityId: string; } -interface PageQueryParams { - device_id: string | null; - sheet?: "view" | "link-to-location" | null; -} - export default function DevicesList({ facilityId }: Props) { const { t } = useTranslation(); const [page, setPage] = useState(1); const [searchQuery, setSearchQuery] = useState(""); - const [{ device_id, sheet }, setQueryParams] = - useQueryParams(); const limit = 12; @@ -47,39 +39,27 @@ export default function DevicesList({ facilityId }: Props) { }), }); - const handleAddDevice = () => { - navigate(`/facility/${facilityId}/settings/devices/create`); - }; - - const handleViewDevice = (deviceId: string) => { - setQueryParams({ sheet: "view", device_id: deviceId }); - }; - - const handleLinkDevice = (deviceId: string) => { - setQueryParams({ sheet: "link-to-location", device_id: deviceId }); - }; - return (
-

{t("devices")}

- -
-
{ setSearchQuery(e.target.value); setPage(1); }} - className="w-full" + className="w-72" />
+ +
{isLoading ? ( @@ -91,12 +71,7 @@ export default function DevicesList({ facilityId }: Props) {
{data?.results?.length ? ( data.results.map((device) => ( - + )) ) : ( @@ -120,16 +95,6 @@ export default function DevicesList({ facilityId }: Props) { )}
)} - setQueryParams({ sheet: null, device_id })} - facilityId={facilityId} - device={ - device_id - ? data?.results?.find((device) => device.id === device_id) - : undefined - } - />
); } diff --git a/src/pages/Facility/settings/devices/components/DeviceCard.tsx b/src/pages/Facility/settings/devices/components/DeviceCard.tsx index 716b83b72da..53b91729402 100644 --- a/src/pages/Facility/settings/devices/components/DeviceCard.tsx +++ b/src/pages/Facility/settings/devices/components/DeviceCard.tsx @@ -4,11 +4,9 @@ import { DeviceList } from "@/types/device/device"; interface Props { device: DeviceList; - onView: (deviceId: string) => void; - onLink: (deviceId: string) => void; } -export default function DeviceCard({ device, onView, onLink }: Props) { +export default function DeviceCard({ device }: Props) { return ( diff --git a/src/pages/Facility/settings/devices/components/DeviceForm.tsx b/src/pages/Facility/settings/devices/components/DeviceForm.tsx index df50fc880a8..c18cb7f473e 100644 --- a/src/pages/Facility/settings/devices/components/DeviceForm.tsx +++ b/src/pages/Facility/settings/devices/components/DeviceForm.tsx @@ -6,8 +6,9 @@ import { useFieldArray, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { z } from "zod"; +import CareIcon from "@/CAREUI/icons/CareIcon"; + import { Button } from "@/components/ui/button"; -import { Card, CardContent } from "@/components/ui/card"; import { Form, FormControl, @@ -17,6 +18,7 @@ import { FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; +import { PhoneInput } from "@/components/ui/phone-input"; import { Select, SelectContent, @@ -28,7 +30,7 @@ import { import mutate from "@/Utils/request/mutate"; import { ContactPointSystems, - ContactPointUses, + contactPointSchema, } from "@/types/common/contactPoint"; import { DeviceAvailabilityStatuses, @@ -50,13 +52,7 @@ const formSchema = z.object({ user_friendly_name: z.string().optional(), model_number: z.string().optional(), part_number: z.string().optional(), - contact: z.array( - z.object({ - system: z.enum(ContactPointSystems), - value: z.string(), - use: z.enum(ContactPointUses), - }), - ), + contact: z.array(contactPointSchema), }); interface Props { @@ -313,9 +309,11 @@ export default function DeviceForm({ facilityId, device, onSuccess }: Props) { />
-
+
-

{t("contact_points")}

+

+ {t("contact_points")} +

{fields.map((field, index) => ( - - -
- ( - - {t("system")} - - - - )} - /> +
+ ( + + + + + )} + /> - ( - - {t("value")} - + { + const system = form.watch(`contact.${index}.system`); + return ( + + + {system === "phone" || + system === "fax" || + system === "sms" ? ( + + ) : ( - - - - )} - /> + )} + + + + ); + }} + /> - ( - - {t("use")} - - - - )} - /> -
+ ( + + )} + /> -
- -
- - + +
))}
diff --git a/src/pages/Facility/settings/devices/components/DeviceSheet.tsx b/src/pages/Facility/settings/devices/components/DeviceSheet.tsx deleted file mode 100644 index 5b71c46fc59..00000000000 --- a/src/pages/Facility/settings/devices/components/DeviceSheet.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { useTranslation } from "react-i18next"; - -import { - Sheet, - SheetContent, - SheetDescription, - SheetHeader, - SheetTitle, -} from "@/components/ui/sheet"; - -import DeviceForm from "@/pages/Facility/settings/devices/components/DeviceForm"; -import { DeviceList } from "@/types/device/device"; - -interface Props { - open: boolean; - onOpenChange: (open: boolean) => void; - facilityId: string; - device?: DeviceList; -} - -export default function DeviceSheet({ - open, - onOpenChange, - facilityId, - device, -}: Props) { - const { t } = useTranslation(); - return ( - - - - {device ? t("edit_device") : t("add_device")} - - {device - ? t("edit_device_description") - : t("add_device_description")} - - -
- -
-
-
- ); -} diff --git a/src/types/common/contactPoint.ts b/src/types/common/contactPoint.ts index c8b37e38944..62134d0390f 100644 --- a/src/types/common/contactPoint.ts +++ b/src/types/common/contactPoint.ts @@ -1,3 +1,7 @@ +import { z } from "zod"; + +import validators from "@/Utils/validators"; + export const ContactPointSystems = [ "phone", "fax", @@ -10,13 +14,7 @@ export const ContactPointSystems = [ export type ContactPointSystem = (typeof ContactPointSystems)[number]; -export const ContactPointUses = [ - "home", - "work", - "temp", - "old", - "mobile", -] as const; +export const ContactPointUses = ["home", "work", "temp", "mobile"] as const; export type ContactPointUse = (typeof ContactPointUses)[number]; @@ -25,3 +23,51 @@ export interface ContactPoint { value: string; use: ContactPointUse; } + +export const contactPointSchema = z.discriminatedUnion("system", [ + // Phone numbers + z.object({ + system: z.literal("phone"), + value: validators.phoneNumber.required, + use: z.enum(ContactPointUses), + }), + // Fax numbers (also using phone validation since they follow same format) + z.object({ + system: z.literal("fax"), + value: validators.phoneNumber.required, + use: z.enum(ContactPointUses), + }), + // Email addresses + z.object({ + system: z.literal("email"), + value: z.string().email(), + use: z.enum(ContactPointUses), + }), + // URLs + z.object({ + system: z.literal("url"), + value: z.string().url(), + use: z.enum(ContactPointUses), + }), + // SMS (also using phone validation) + z.object({ + system: z.literal("sms"), + value: validators.phoneNumber.required, + use: z.enum(ContactPointUses), + }), + // Pager (typically numeric, but can vary) + z.object({ + system: z.literal("pager"), + value: z + .string() + .min(1, { message: "Required" }) + .max(20, { message: "Pager number too long" }), + use: z.enum(ContactPointUses), + }), + // Other (catch-all with basic validation) + z.object({ + system: z.literal("other"), + value: z.string().min(1, { message: "Required" }), + use: z.enum(ContactPointUses), + }), +]); From ff5a99d8347556ec896c58951449ce2d44f1ac06 Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Wed, 12 Feb 2025 15:24:24 +0530 Subject: [PATCH 03/11] some changes --- public/locale/en.json | 24 +- .../settings/devices/DeviceDetail.tsx | 235 ++++++++++++++++++ .../settings/devices/UpdateDevice.tsx | 59 +++++ .../devices/components/DeviceCard.tsx | 80 +++++- src/pages/Facility/settings/layout.tsx | 8 + 5 files changed, 391 insertions(+), 15 deletions(-) create mode 100644 src/pages/Facility/settings/devices/DeviceDetail.tsx create mode 100644 src/pages/Facility/settings/devices/UpdateDevice.tsx diff --git a/public/locale/en.json b/public/locale/en.json index ffc4bfd9e39..963175eed49 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -618,12 +618,13 @@ "contact_person_number": "Contact person number", "contact_phone": "Contact Person Number", "contact_phone_description": "Phone number to reach the contact person.", - "contact_point_placeholder__email": "Eg. john@example.com", - "contact_point_placeholder__fax": "Eg. +91 9876543210", - "contact_point_placeholder__pager": "Eg. 123456", - "contact_point_placeholder__phone": "Eg. +91 9876543210", - "contact_point_placeholder__sms": "Eg. +91 9876543210", - "contact_point_placeholder__url": "Eg. https://example.com", + "contact_point_placeholder__email": "Enter email address", + "contact_point_placeholder__fax": "Enter fax number", + "contact_point_placeholder__other": "Enter contact value", + "contact_point_placeholder__pager": "Enter pager number", + "contact_point_placeholder__phone": "Enter phone number", + "contact_point_placeholder__sms": "Enter SMS number", + "contact_point_placeholder__url": "Enter URL", "contact_points": "Contact Points", "contact_system_email": "Email", "contact_system_fax": "Fax", @@ -711,6 +712,7 @@ "date_of_return": "Date of Return", "date_of_test": "Date of sample collection for Covid testing", "date_range": "Date Range", + "dates_and_identifiers": "Dates & Identifiers", "day": "Day", "death_report": "Death Report", "delete": "Delete", @@ -737,6 +739,9 @@ "device_availability_status_damaged": "Damaged", "device_availability_status_destroyed": "Destroyed", "device_availability_status_lost": "Lost", + "device_contact_description": "Contact points associated with this device", + "device_information": "Device Information", + "device_not_found": "Device not found", "device_status_active": "Active", "device_status_entered_in_error": "Entered in Error", "device_status_inactive": "Inactive", @@ -1021,6 +1026,7 @@ "expiration_date": "Expiration Date", "expired": "Expired", "expired_on": "Expired On", + "expires": "Expires", "expires_on": "Expires On", "export": "Export", "export_live_patients": "Export Live Patients", @@ -1324,6 +1330,7 @@ "manage_tags": "Manage Tags", "manage_tags_description": "Add or remove tags for this questionnaire", "manage_user": "Manage User", + "manufactured": "Manufactured", "manufacturer": "Manufacturer", "manufacturer_date": "Manufacture Date", "map_acronym": "M.A.P.", @@ -1984,7 +1991,7 @@ "select_availability_status": "Select availability status", "select_category": "Select a category", "select_class": "Select Class", - "select_contact_system": "Select contact system", + "select_contact_system": "Select contact type", "select_contact_use": "Select contact use", "select_date": "Select date", "select_department": "Select Department", @@ -2023,7 +2030,7 @@ "select_seven_day_period": "Select a seven day period", "select_site": "Select site", "select_skills": "Select and add some skills", - "select_status": "Select device status", + "select_status": "Select status", "select_sub_department": "Select sub-department", "select_time": "Select time", "select_time_slot": "Select time slot", @@ -2264,6 +2271,7 @@ "update_available": "Update Available", "update_bed": "Update Bed", "update_department": "Update Department", + "update_device": "Update Device", "update_encounter": "Update Encounter", "update_encounter_details": "Update Encounter Details", "update_existing_facility": "Update the details of the existing facility.", diff --git a/src/pages/Facility/settings/devices/DeviceDetail.tsx b/src/pages/Facility/settings/devices/DeviceDetail.tsx new file mode 100644 index 00000000000..886c3d069b0 --- /dev/null +++ b/src/pages/Facility/settings/devices/DeviceDetail.tsx @@ -0,0 +1,235 @@ +import { useQuery } from "@tanstack/react-query"; +import { Link } from "raviger"; +import { useTranslation } from "react-i18next"; + +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Separator } from "@/components/ui/separator"; + +import Loading from "@/components/Common/Loading"; +import PageTitle from "@/components/Common/PageTitle"; + +import query from "@/Utils/request/query"; +import { ContactPoint } from "@/types/common/contactPoint"; +import deviceApi from "@/types/device/deviceApi"; + +interface Props { + facilityId: string; + deviceId: string; +} + +export default function DeviceDetail({ facilityId, deviceId }: Props) { + const { t } = useTranslation(); + + const { data: device, isLoading } = useQuery({ + queryKey: ["device", facilityId, deviceId], + queryFn: query(deviceApi.retrieve, { + pathParams: { facility_id: facilityId, id: deviceId }, + }), + }); + + if (isLoading) { + return ; + } + + if (!device) { + return null; + } + + const getStatusColor = (status: string) => { + switch (status) { + case "active": + return "bg-green-100 text-green-800 hover:bg-green-100/80"; + case "inactive": + return "bg-gray-100 text-gray-800 hover:bg-gray-100/80"; + case "entered_in_error": + return "bg-red-100 text-red-800 hover:bg-red-100/80"; + default: + return "bg-gray-100 text-gray-800 hover:bg-gray-100/80"; + } + }; + + const getAvailabilityStatusColor = (status: string) => { + switch (status) { + case "available": + return "bg-green-100 text-green-800 hover:bg-green-100/80"; + case "lost": + return "bg-yellow-100 text-yellow-800 hover:bg-yellow-100/80"; + case "damaged": + case "destroyed": + return "bg-red-100 text-red-800 hover:bg-red-100/80"; + default: + return "bg-gray-100 text-gray-800 hover:bg-gray-100/80"; + } + }; + + const renderContactInfo = (contact: ContactPoint) => { + return ( +
+

+ {t(contact.system)} ({t(contact.use)}) +

+

{contact.value}

+
+ ); + }; + + return ( +
+
+ + + + +
+ +
+ + + {t("device_information")} + + +
+
+

+ {t("registered_name")} +

+

{device.registered_name}

+
+ {device.user_friendly_name && ( +
+

+ {t("user_friendly_name")} +

+

{device.user_friendly_name}

+
+ )} +
+ + {t(`device_status_${device.status}`)} + + + {t( + `device_availability_status_${device.availability_status}`, + )} + +
+
+ + + +
+ {device.manufacturer && ( +
+

+ {t("manufacturer")} +

+

{device.manufacturer}

+
+ )} + {device.model_number && ( +
+

+ {t("model_number")} +

+

{device.model_number}

+
+ )} + {device.serial_number && ( +
+

+ {t("serial_number")} +

+

{device.serial_number}

+
+ )} + {device.part_number && ( +
+

+ {t("part_number")} +

+

{device.part_number}

+
+ )} +
+
+
+ + + + {t("dates_and_identifiers")} + + + {device.identifier && ( +
+

+ {t("identifier")} +

+

{device.identifier}

+
+ )} + {device.lot_number && ( +
+

+ {t("lot_number")} +

+

{device.lot_number}

+
+ )} + {device.manufacturer_date && ( +
+

+ {t("manufacturer_date")} +

+

+ {new Date(device.manufacturer_date).toLocaleDateString()} +

+
+ )} + {device.expiration_date && ( +
+

+ {t("expiration_date")} +

+

+ {new Date(device.expiration_date).toLocaleDateString()} +

+
+ )} +
+
+ + {device.contact?.length > 0 && ( + + + {t("contact_information")} + + {t("device_contact_description")} + + + +
+ {device.contact.map(renderContactInfo)} +
+
+
+ )} +
+
+ ); +} diff --git a/src/pages/Facility/settings/devices/UpdateDevice.tsx b/src/pages/Facility/settings/devices/UpdateDevice.tsx new file mode 100644 index 00000000000..7bb40b96d65 --- /dev/null +++ b/src/pages/Facility/settings/devices/UpdateDevice.tsx @@ -0,0 +1,59 @@ +import { useQuery } from "@tanstack/react-query"; +import { Link } from "raviger"; +import { useTranslation } from "react-i18next"; + +import { Button } from "@/components/ui/button"; + +import Loading from "@/components/Common/Loading"; +import PageTitle from "@/components/Common/PageTitle"; + +import query from "@/Utils/request/query"; +import deviceApi from "@/types/device/deviceApi"; + +import DeviceForm from "./components/DeviceForm"; + +interface Props { + facilityId: string; + deviceId: string; +} + +export default function UpdateDevice({ facilityId, deviceId }: Props) { + const { t } = useTranslation(); + + const { data: device, isLoading } = useQuery({ + queryKey: ["device", facilityId, deviceId], + queryFn: query(deviceApi.retrieve, { + pathParams: { facility_id: facilityId, id: deviceId }, + }), + }); + + return ( +
+
+ + + + +
+ + {isLoading ? ( + + ) : device ? ( + { + window.history.back(); + }} + /> + ) : ( +
+

{t("device_not_found")}

+ + + +
+ )} +
+ ); +} diff --git a/src/pages/Facility/settings/devices/components/DeviceCard.tsx b/src/pages/Facility/settings/devices/components/DeviceCard.tsx index 53b91729402..4bc53392872 100644 --- a/src/pages/Facility/settings/devices/components/DeviceCard.tsx +++ b/src/pages/Facility/settings/devices/components/DeviceCard.tsx @@ -1,4 +1,14 @@ -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Link } from "raviger"; +import { useTranslation } from "react-i18next"; + +import { Badge } from "@/components/ui/badge"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; import { DeviceList } from "@/types/device/device"; @@ -7,13 +17,69 @@ interface Props { } export default function DeviceCard({ device }: Props) { + const { t } = useTranslation(); + + const getStatusColor = (status: string) => { + switch (status) { + case "active": + return "bg-green-100 text-green-800 hover:bg-green-100/80"; + case "inactive": + return "bg-gray-100 text-gray-800 hover:bg-gray-100/80"; + case "entered_in_error": + return "bg-red-100 text-red-800 hover:bg-red-100/80"; + default: + return "bg-gray-100 text-gray-800 hover:bg-gray-100/80"; + } + }; + + const getAvailabilityStatusColor = (status: string) => { + switch (status) { + case "available": + return "bg-green-100 text-green-800 hover:bg-green-100/80"; + case "lost": + return "bg-yellow-100 text-yellow-800 hover:bg-yellow-100/80"; + case "damaged": + case "destroyed": + return "bg-red-100 text-red-800 hover:bg-red-100/80"; + default: + return "bg-gray-100 text-gray-800 hover:bg-gray-100/80"; + } + }; + return ( - - - - {device.registered_name} + + + +
+
+ + {device.registered_name} + + {device.user_friendly_name && ( + + {device.user_friendly_name} + + )} +
+
-
-
+ +
+ + {t(`device_status_${device.status}`)} + + + {t(`device_availability_status_${device.availability_status}`)} + +
+
+ + ); } diff --git a/src/pages/Facility/settings/layout.tsx b/src/pages/Facility/settings/layout.tsx index 2e01ad75ba7..6d256ec4cff 100644 --- a/src/pages/Facility/settings/layout.tsx +++ b/src/pages/Facility/settings/layout.tsx @@ -7,7 +7,9 @@ import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import ErrorPage from "@/components/ErrorPages/DefaultErrorPage"; import CreateDevice from "@/pages/Facility/settings/devices/CreateDevice"; +import DeviceDetail from "@/pages/Facility/settings/devices/DeviceDetail"; import DevicesList from "@/pages/Facility/settings/devices/DevicesList"; +import UpdateDevice from "@/pages/Facility/settings/devices/UpdateDevice"; import { GeneralSettings } from "./general/general"; import LocationList from "./locations/LocationList"; @@ -35,6 +37,12 @@ const getRoutes = (facilityId: string) => ({ ), "/devices": () => , "/devices/create": () => , + "/devices/:id": ({ id }: { id: string }) => ( + + ), + "/devices/:id/edit": ({ id }: { id: string }) => ( + + ), "*": () => , }); From d061307dc1a65b9731cf09df1ef9ff3aea32b447 Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Wed, 12 Feb 2025 15:32:15 +0530 Subject: [PATCH 04/11] support for deleting devices --- public/locale/en.json | 2 + .../settings/devices/DeviceDetail.tsx | 63 +++++++++++++++++-- src/types/device/deviceApi.ts | 1 + 3 files changed, 60 insertions(+), 6 deletions(-) diff --git a/public/locale/en.json b/public/locale/en.json index 963175eed49..f42dd84de42 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -719,6 +719,8 @@ "delete_account": "Delete account", "delete_account_btn": "Yes, delete this account", "delete_account_note": "Deleting this account will remove all associated data and cannot be undone.", + "delete_device": "Delete Device", + "delete_device_confirmation": "Are you sure you want to delete this device? This action cannot be undone.", "delete_facility": "Delete Facility", "delete_item": "Delete {{name}}", "delete_record": "Delete Record", diff --git a/src/pages/Facility/settings/devices/DeviceDetail.tsx b/src/pages/Facility/settings/devices/DeviceDetail.tsx index 886c3d069b0..fa6a41bf852 100644 --- a/src/pages/Facility/settings/devices/DeviceDetail.tsx +++ b/src/pages/Facility/settings/devices/DeviceDetail.tsx @@ -1,9 +1,22 @@ -import { useQuery } from "@tanstack/react-query"; -import { Link } from "raviger"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { Link, navigate } from "raviger"; import { useTranslation } from "react-i18next"; +import { cn } from "@/lib/utils"; + +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog"; import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; +import { Button, buttonVariants } from "@/components/ui/button"; import { Card, CardContent, @@ -16,6 +29,7 @@ import { Separator } from "@/components/ui/separator"; import Loading from "@/components/Common/Loading"; import PageTitle from "@/components/Common/PageTitle"; +import mutate from "@/Utils/request/mutate"; import query from "@/Utils/request/query"; import { ContactPoint } from "@/types/common/contactPoint"; import deviceApi from "@/types/device/deviceApi"; @@ -27,6 +41,7 @@ interface Props { export default function DeviceDetail({ facilityId, deviceId }: Props) { const { t } = useTranslation(); + const queryClient = useQueryClient(); const { data: device, isLoading } = useQuery({ queryKey: ["device", facilityId, deviceId], @@ -35,6 +50,16 @@ export default function DeviceDetail({ facilityId, deviceId }: Props) { }), }); + const { mutate: deleteDevice, isPending: isDeleting } = useMutation({ + mutationFn: mutate(deviceApi.delete, { + pathParams: { facility_id: facilityId, id: deviceId }, + }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["devices"] }); + navigate(`/facility/${facilityId}/settings/devices`); + }, + }); + if (isLoading) { return ; } @@ -85,9 +110,35 @@ export default function DeviceDetail({ facilityId, deviceId }: Props) {
- - - +
+ + + + + + + + + + + {t("delete_device")} + + {t("delete_device_confirmation")} + + + + {t("cancel")} + deleteDevice()} + className={cn(buttonVariants({ variant: "destructive" }))} + disabled={isDeleting} + > + {isDeleting ? t("deleting") : t("delete")} + + + + +
diff --git a/src/types/device/deviceApi.ts b/src/types/device/deviceApi.ts index de6b632a109..6446741a04f 100644 --- a/src/types/device/deviceApi.ts +++ b/src/types/device/deviceApi.ts @@ -30,6 +30,7 @@ export default { path: "/api/v1/facility/{facility_id}/device/{id}/", method: HttpMethod.DELETE, TRes: Type(), + TBody: Type(), }, upsert: { path: "/api/v1/facility/{facility_id}/device/upsert/", From e8657063652c8a2d71df8f96ac495f5790262e92 Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Wed, 12 Feb 2025 15:40:01 +0530 Subject: [PATCH 05/11] minor fixes --- .../Facility/settings/devices/DevicesList.tsx | 20 ++++--------------- .../settings/devices/UpdateDevice.tsx | 7 +------ 2 files changed, 5 insertions(+), 22 deletions(-) diff --git a/src/pages/Facility/settings/devices/DevicesList.tsx b/src/pages/Facility/settings/devices/DevicesList.tsx index f693e0993d3..2eaa341f6b7 100644 --- a/src/pages/Facility/settings/devices/DevicesList.tsx +++ b/src/pages/Facility/settings/devices/DevicesList.tsx @@ -7,8 +7,8 @@ import CareIcon from "@/CAREUI/icons/CareIcon"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; -import { Input } from "@/components/ui/input"; +import PageTitle from "@/components/Common/PageTitle"; import Pagination from "@/components/Common/Pagination"; import { CardGridSkeleton } from "@/components/Common/SkeletonLoading"; @@ -23,18 +23,16 @@ interface Props { export default function DevicesList({ facilityId }: Props) { const { t } = useTranslation(); const [page, setPage] = useState(1); - const [searchQuery, setSearchQuery] = useState(""); const limit = 12; const { data, isLoading } = useQuery({ - queryKey: ["devices", facilityId, page, limit, searchQuery], + queryKey: ["devices", facilityId, page, limit], queryFn: query.debounced(deviceApi.list, { pathParams: { facility_id: facilityId }, queryParams: { offset: (page - 1) * limit, limit, - name: searchQuery || undefined, }, }), }); @@ -43,15 +41,7 @@ export default function DevicesList({ facilityId }: Props) {
- { - setSearchQuery(e.target.value); - setPage(1); - }} - className="w-72" - /> +
- -
+ {isLoading ? ( From 15b2f1537dd1f477b02e9b9f61bb87265fec951b Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Wed, 12 Feb 2025 19:14:01 +0530 Subject: [PATCH 06/11] tiny cleanups --- public/locale/en.json | 28 ++ .../settings/devices/DeviceDetail.tsx | 245 ++++++++++++------ .../components/AssociateLocationSheet.tsx | 88 +++++++ .../settings/locations/LocationForm.tsx | 26 +- .../settings/locations/LocationView.tsx | 4 +- .../locations/components/LocationCard.tsx | 4 +- src/types/device/device.ts | 9 + src/types/device/deviceApi.ts | 18 +- src/types/location/location.ts | 55 ++-- 9 files changed, 338 insertions(+), 139 deletions(-) create mode 100644 src/pages/Facility/settings/devices/components/AssociateLocationSheet.tsx diff --git a/public/locale/en.json b/public/locale/en.json index f42dd84de42..3a93f6de759 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -418,6 +418,10 @@ "assigned_facility": "Facility assigned", "assigned_to": "Assigned to", "assigned_volunteer": "Assigned Volunteer", + "associate": "Associate", + "associate_location": "Associate Location", + "associate_location_description": "Select a location to associate with this device", + "associating": "Associating...", "at_time": "at {{time}}", "atypical_presentation_details": "Atypical presentation details", "audio__allow_permission": "Please allow microphone permission in site settings", @@ -511,9 +515,11 @@ "category_description": "Choose the category that best describes the resource needed.", "caution": "Caution", "central_nursing_station": "Central Nursing Station", + "change": "Change", "change_avatar": "Change Avatar", "change_avatar_note": "JPG, GIF or PNG. 1MB max.", "change_file": "Change File", + "change_location": "Change Location", "change_phone_number": "Change Phone Number", "change_status": "Change Status", "chat_on_whatsapp": "Chat on Whatsapp", @@ -687,6 +693,7 @@ "criticality": "Criticality", "csv_file_in_the_specified_format": "Select a CSV file in the specified format", "current_address": "Current Address", + "current_location_description": "The current location of this device", "current_organizations": "Current Organizations", "current_password": "Current Password", "current_role": "Current Role", @@ -1297,12 +1304,31 @@ "local_ip_address": "Local IP Address", "local_ip_address_example": "e.g. 192.168.0.123", "location": "Location", + "location_associated_successfully": "Location associated successfully", "location_beds_empty": "No beds available in this location", "location_created": "Location Created", + "location_description": "Location Description", "location_details": "Location Details", "location_form": "Location Form", + "location_form__area": "Area", + "location_form__bd": "Bed", + "location_form__bu": "Building", + "location_form__ca": "Cabinet", + "location_form__co": "Corridor", + "location_form__ho": "House", + "location_form__jdn": "Jurisdiction", + "location_form__lvl": "Level", + "location_form__rd": "Road", + "location_form__ro": "Room", + "location_form__si": "Site", + "location_form__ve": "Vehicle", + "location_form__vi": "Virtual", + "location_form__wa": "Ward", + "location_form__wi": "Wing", "location_history": "Location History", "location_management": "Location Management", + "location_name": "Location Name", + "location_status": "Location Status", "location_updated": "Location Updated", "location_updated_successfully": "Location updated successfully", "locations": "Locations", @@ -1460,6 +1486,8 @@ "no_investigation": "No investigation Reports found", "no_investigation_suggestions": "No Investigation Suggestions", "no_linked_facilities": "No Linked Facilities", + "no_location": "No location assigned", + "no_location_description": "This device is not currently assigned to any location", "no_locations_available": "No locations available", "no_locations_found": "No locations found", "no_log_update_delta": "No changes since previous log update", diff --git a/src/pages/Facility/settings/devices/DeviceDetail.tsx b/src/pages/Facility/settings/devices/DeviceDetail.tsx index fa6a41bf852..a6f040e8513 100644 --- a/src/pages/Facility/settings/devices/DeviceDetail.tsx +++ b/src/pages/Facility/settings/devices/DeviceDetail.tsx @@ -1,5 +1,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { ExternalLink } from "lucide-react"; import { Link, navigate } from "raviger"; +import { useState } from "react"; import { useTranslation } from "react-i18next"; import { cn } from "@/lib/utils"; @@ -34,6 +36,8 @@ import query from "@/Utils/request/query"; import { ContactPoint } from "@/types/common/contactPoint"; import deviceApi from "@/types/device/deviceApi"; +import AssociateLocationSheet from "./components/AssociateLocationSheet"; + interface Props { facilityId: string; deviceId: string; @@ -42,6 +46,7 @@ interface Props { export default function DeviceDetail({ facilityId, deviceId }: Props) { const { t } = useTranslation(); const queryClient = useQueryClient(); + const [isLocationSheetOpen, setIsLocationSheetOpen] = useState(false); const { data: device, isLoading } = useQuery({ queryKey: ["device", facilityId, deviceId], @@ -96,18 +101,47 @@ export default function DeviceDetail({ facilityId, deviceId }: Props) { }; const renderContactInfo = (contact: ContactPoint) => { + const getContactLink = (system: string, value: string) => { + switch (system) { + case "phone": + case "fax": + return `tel:${value}`; + case "email": + return `mailto:${value}`; + case "url": + return value; + case "sms": + return `sms:${value}`; + default: + return null; + } + }; + + const link = getContactLink(contact.system, contact.value); + return (

{t(contact.system)} ({t(contact.use)})

-

{contact.value}

+ {link ? ( + + {contact.value} + + ) : ( +

{contact.value}

+ )}
); }; return ( -
+
@@ -141,7 +175,7 @@ export default function DeviceDetail({ facilityId, deviceId }: Props) {
-
+
{t("device_information")} @@ -162,6 +196,35 @@ export default function DeviceDetail({ facilityId, deviceId }: Props) {

{device.user_friendly_name}

)} +
+

+ {t("location")} +

+
+
+ {device.current_location ? ( + <> + + {device.current_location.name} + + + + ) : ( + {t("no_location")} + )} +
+ +
+
- {device.manufacturer && ( -
-

- {t("manufacturer")} -

-

{device.manufacturer}

-
- )} - {device.model_number && ( -
-

- {t("model_number")} -

-

{device.model_number}

-
- )} - {device.serial_number && ( -
-

- {t("serial_number")} -

-

{device.serial_number}

-
- )} - {device.part_number && ( -
-

- {t("part_number")} -

-

{device.part_number}

-
- )} -
- - - - - - {t("dates_and_identifiers")} - - - {device.identifier && ( -
-

- {t("identifier")} -

-

{device.identifier}

+
+ {(device.identifier || device.lot_number) && ( + <> + {device.identifier && ( +
+

+ {t("identifier")} +

+

{device.identifier}

+
+ )} + {device.lot_number && ( +
+

+ {t("lot_number")} +

+

{device.lot_number}

+
+ )} + + )}
- )} - {device.lot_number && ( -
-

- {t("lot_number")} -

-

{device.lot_number}

+ +
+ {(device.manufacturer || device.model_number) && ( + <> + {device.manufacturer && ( +
+

+ {t("manufacturer")} +

+

{device.manufacturer}

+
+ )} + {device.model_number && ( +
+

+ {t("model_number")} +

+

{device.model_number}

+
+ )} + + )}
- )} - {device.manufacturer_date && ( -
-

- {t("manufacturer_date")} -

-

- {new Date(device.manufacturer_date).toLocaleDateString()} -

+ +
+ {(device.serial_number || device.part_number) && ( + <> + {device.serial_number && ( +
+

+ {t("serial_number")} +

+

{device.serial_number}

+
+ )} + {device.part_number && ( +
+

+ {t("part_number")} +

+

{device.part_number}

+
+ )} + + )}
- )} - {device.expiration_date && ( -
-

- {t("expiration_date")} -

-

- {new Date(device.expiration_date).toLocaleDateString()} -

+ +
+ {(device.manufacturer_date || device.expiration_date) && ( + <> + {device.manufacturer_date && ( +
+

+ {t("manufacturer_date")} +

+

+ {new Date( + device.manufacturer_date, + ).toLocaleDateString()} +

+
+ )} + {device.expiration_date && ( +
+

+ {t("expiration_date")} +

+

+ {new Date( + device.expiration_date, + ).toLocaleDateString()} +

+
+ )} + + )}
- )} +
@@ -281,6 +367,13 @@ export default function DeviceDetail({ facilityId, deviceId }: Props) { )}
+ +
); } diff --git a/src/pages/Facility/settings/devices/components/AssociateLocationSheet.tsx b/src/pages/Facility/settings/devices/components/AssociateLocationSheet.tsx new file mode 100644 index 00000000000..5eddfad70c1 --- /dev/null +++ b/src/pages/Facility/settings/devices/components/AssociateLocationSheet.tsx @@ -0,0 +1,88 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; + +import { Button } from "@/components/ui/button"; +import { + Sheet, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet"; + +import { LocationSearch } from "@/components/Location/LocationSearch"; + +import mutate from "@/Utils/request/mutate"; +import deviceApi from "@/types/device/deviceApi"; +import { LocationList } from "@/types/location/location"; + +interface Props { + open: boolean; + onOpenChange: (open: boolean) => void; + facilityId: string; + deviceId: string; +} + +export default function AssociateLocationSheet({ + open, + onOpenChange, + facilityId, + deviceId, +}: Props) { + const { t } = useTranslation(); + const queryClient = useQueryClient(); + const [selectedLocation, setSelectedLocation] = useState( + null, + ); + + const { mutate: associateLocation, isPending } = useMutation({ + mutationFn: mutate(deviceApi.associateLocation, { + pathParams: { facility_id: facilityId, id: deviceId }, + }), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: ["device", facilityId, deviceId], + }); + toast.success(t("location_associated_successfully")); + onOpenChange(false); + setSelectedLocation(null); + }, + }); + + const handleSubmit = () => { + if (!selectedLocation) return; + associateLocation({ location: selectedLocation.id }); + }; + + return ( + + + + {t("associate_location")} + + {t("associate_location_description")} + + +
+ +
+ + + +
+
+ ); +} diff --git a/src/pages/Facility/settings/locations/LocationForm.tsx b/src/pages/Facility/settings/locations/LocationForm.tsx index dc9b3f96bfd..8255471d9a5 100644 --- a/src/pages/Facility/settings/locations/LocationForm.tsx +++ b/src/pages/Facility/settings/locations/LocationForm.tsx @@ -28,10 +28,10 @@ import { Textarea } from "@/components/ui/textarea"; import mutate from "@/Utils/request/mutate"; import query from "@/Utils/request/query"; import { + LocationFormOptions, LocationWrite, OperationalStatus, Status, - locationFormOptions, } from "@/types/location/location"; import locationApi from "@/types/location/locationApi"; @@ -40,23 +40,7 @@ const formSchema = z.object({ description: z.string().optional(), status: z.enum(["active", "inactive", "unknown"] as const), operational_status: z.enum(["C", "H", "O", "U", "K", "I"] as const), - form: z.enum([ - "si", - "bu", - "wi", - "wa", - "lvl", - "co", - "ro", - "bd", - "ve", - "ho", - "ca", - "rd", - "area", - "jdn", - "vi", - ] as const), + form: z.enum(LocationFormOptions), parent: z.string().optional().nullable(), organizations: z.array(z.string()).default([]), availability_status: z.enum(["available", "unavailable"] as const), @@ -224,9 +208,9 @@ export default function LocationForm({ - {locationFormOptions.map((option) => ( - - {option.label} + {LocationFormOptions.map((option) => ( + + {t(`location_form__${option}`)} ))} diff --git a/src/pages/Facility/settings/locations/LocationView.tsx b/src/pages/Facility/settings/locations/LocationView.tsx index 9bab726b813..52685711fa0 100644 --- a/src/pages/Facility/settings/locations/LocationView.tsx +++ b/src/pages/Facility/settings/locations/LocationView.tsx @@ -23,7 +23,7 @@ import { CardGridSkeleton } from "@/components/Common/SkeletonLoading"; import LinkDepartmentsSheet from "@/components/Patient/LinkDepartmentsSheet"; import query from "@/Utils/request/query"; -import { LocationList, getLocationFormLabel } from "@/types/location/location"; +import { LocationList } from "@/types/location/location"; import locationApi from "@/types/location/locationApi"; import LocationSheet from "./LocationSheet"; @@ -154,7 +154,7 @@ export default function LocationView({ id, facilityId }: Props) {

{t("locations")}

- {getLocationFormLabel(location?.form)} + {t(`location_form__${location?.form}`)}

- {getLocationFormLabel(location.form)} + {t(`location_form__${location.form}`)}

diff --git a/src/types/device/device.ts b/src/types/device/device.ts index eeca06593fd..b3698e6d44e 100644 --- a/src/types/device/device.ts +++ b/src/types/device/device.ts @@ -1,6 +1,7 @@ import { ContactPoint } from "@/types/common/contactPoint"; import { Encounter } from "@/types/emr/encounter"; import { LocationDetail } from "@/types/location/location"; +import { UserBase } from "@/types/user/user"; export const DeviceStatuses = [ "active", @@ -41,6 +42,8 @@ export interface DeviceDetail extends DeviceBase { id: string; current_encounter: Encounter | undefined; current_location: LocationDetail | undefined; // TODO: verify this type + created_by: UserBase; + updated_by: UserBase; } export interface DeviceList extends DeviceBase { @@ -48,3 +51,9 @@ export interface DeviceList extends DeviceBase { } export type DeviceWrite = DeviceBase; + +export interface DeviceLocationHistory { + id: string; + location: LocationDetail; + start: string; // datetime +} diff --git a/src/types/device/deviceApi.ts b/src/types/device/deviceApi.ts index 6446741a04f..91efe73b1ec 100644 --- a/src/types/device/deviceApi.ts +++ b/src/types/device/deviceApi.ts @@ -1,7 +1,12 @@ import { HttpMethod, Type } from "@/Utils/request/api"; import { PaginatedResponse } from "@/Utils/request/types"; -import { DeviceDetail, DeviceList, DeviceWrite } from "./device"; +import { + DeviceDetail, + DeviceList, + DeviceLocationHistory, + DeviceWrite, +} from "./device"; export default { list: { @@ -38,4 +43,15 @@ export default { TRes: Type(), TBody: Type(), }, + associateLocation: { + path: "/api/v1/facility/{facility_id}/device/{id}/associate_location/", + method: HttpMethod.POST, + TRes: Type(), + TBody: Type<{ location: string }>(), + }, + locationHistory: { + path: "/api/v1/facility/{facility_id}/device/{id}/location_history/", + method: HttpMethod.GET, + TRes: Type>(), + }, }; diff --git a/src/types/location/location.ts b/src/types/location/location.ts index 3738dce63f7..3c67a72e6cf 100644 --- a/src/types/location/location.ts +++ b/src/types/location/location.ts @@ -9,22 +9,7 @@ export type OperationalStatus = "C" | "H" | "O" | "U" | "K" | "I"; export type LocationMode = "instance" | "kind"; -export type LocationForm = - | "si" - | "bu" - | "wi" - | "wa" - | "lvl" - | "co" - | "ro" - | "bd" - | "ve" - | "ho" - | "ca" - | "rd" - | "area" - | "jdn" - | "vi"; +export type LocationForm = (typeof LocationFormOptions)[number]; export interface LocationBase { status: Status; @@ -55,24 +40,20 @@ export interface LocationWrite extends LocationBase { mode: LocationMode; } -export const locationFormOptions = [ - { value: "si", label: "Site" }, - { value: "bu", label: "Building" }, - { value: "wi", label: "Wing" }, - { value: "wa", label: "Ward" }, - { value: "lvl", label: "Level" }, - { value: "co", label: "Corridor" }, - { value: "ro", label: "Room" }, - { value: "bd", label: "Bed" }, - { value: "ve", label: "Vehicle" }, - { value: "ho", label: "House" }, - { value: "ca", label: "Cabinet" }, - { value: "rd", label: "Road" }, - { value: "area", label: "Area" }, - { value: "jdn", label: "Jurisdiction" }, - { value: "vi", label: "Virtual" }, -]; - -export const getLocationFormLabel = (value: LocationForm) => { - return locationFormOptions.find((option) => option.value === value)?.label; -}; +export const LocationFormOptions = [ + "si", + "bu", + "wi", + "wa", + "lvl", + "co", + "ro", + "bd", + "ve", + "ho", + "ca", + "rd", + "area", + "jdn", + "vi", +] as const; From f05ceb875ae36c5fd7855ead0b845ff2d182615e Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Thu, 13 Feb 2025 12:12:26 +0530 Subject: [PATCH 07/11] Enhance Location Search and Device Details UI --- public/locale/en.json | 3 ++ src/components/Location/LocationSearch.tsx | 22 ++++++++++++- .../settings/devices/DeviceDetail.tsx | 32 +++++++++---------- .../components/AssociateLocationSheet.tsx | 1 - .../devices/components/DeviceForm.tsx | 13 ++++++-- 5 files changed, 50 insertions(+), 21 deletions(-) diff --git a/public/locale/en.json b/public/locale/en.json index 3a93f6de759..f6a6bff3125 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -1329,6 +1329,9 @@ "location_management": "Location Management", "location_name": "Location Name", "location_status": "Location Status", + "location_status__active": "Active", + "location_status__inactive": "Inactive", + "location_status__unknown": "Unknown", "location_updated": "Location Updated", "location_updated_successfully": "Location updated successfully", "locations": "Locations", diff --git a/src/components/Location/LocationSearch.tsx b/src/components/Location/LocationSearch.tsx index bd21bb26273..73c410a5345 100644 --- a/src/components/Location/LocationSearch.tsx +++ b/src/components/Location/LocationSearch.tsx @@ -1,5 +1,6 @@ import { useQuery } from "@tanstack/react-query"; import { useState } from "react"; +import { useTranslation } from "react-i18next"; import { Command, @@ -33,6 +34,7 @@ export function LocationSearch({ disabled, value, }: LocationSearchProps) { + const { t } = useTranslation(); const [open, setOpen] = useState(false); const [search, setSearch] = useState(""); @@ -61,6 +63,7 @@ export function LocationSearch({ No locations found. @@ -74,7 +77,15 @@ export function LocationSearch({ setOpen(false); }} > - {location.name} + {location.name} + + {t(`location_form__${location.form}`)} + {" in "} + {formatLocationParent(location)} + + + {t(`location_status__${location.status}`)} + ))} @@ -83,3 +94,12 @@ export function LocationSearch({ ); } + +const formatLocationParent = (location: LocationList) => { + const parents: string[] = []; + while (location.parent?.name) { + parents.push(location.parent?.name); + location = location.parent; + } + return parents.reverse().join(" > "); +}; diff --git a/src/pages/Facility/settings/devices/DeviceDetail.tsx b/src/pages/Facility/settings/devices/DeviceDetail.tsx index a6f040e8513..5ab30bc1036 100644 --- a/src/pages/Facility/settings/devices/DeviceDetail.tsx +++ b/src/pages/Facility/settings/devices/DeviceDetail.tsx @@ -200,24 +200,22 @@ export default function DeviceDetail({ facilityId, deviceId }: Props) {

{t("location")}

-
-
- {device.current_location ? ( - <> - - {device.current_location.name} - - - - ) : ( - {t("no_location")} - )} -
+
+ {device.current_location ? ( + <> + + {device.current_location.name} + + + + ) : ( + {t("no_location")} + )}