diff --git a/cypress/e2e/resource_spec/ResourcesHomepage.cy.ts b/cypress/e2e/resource_spec/ResourcesHomepage.cy.ts index 160884978fd..15f3c88a059 100644 --- a/cypress/e2e/resource_spec/ResourcesHomepage.cy.ts +++ b/cypress/e2e/resource_spec/ResourcesHomepage.cy.ts @@ -73,6 +73,10 @@ describe("Resource Page", () => { cy.awaitUrl(createdResource); resourcePage.clickUpdateStatus(); resourcePage.updateStatus("APPROVED"); + cy.clickAndSelectOption( + "input[name='assigned_facility_object']", + "Dummy Request Fulfilment Center, Ernakulam", + ); resourcePage.clickSubmitButton(); resourcePage.verifySuccessNotification( "Resource request updated successfully", diff --git a/cypress/pageobject/Facility/FacilityCreation.ts b/cypress/pageobject/Facility/FacilityCreation.ts index 9776433e523..a3f4de6c733 100644 --- a/cypress/pageobject/Facility/FacilityCreation.ts +++ b/cypress/pageobject/Facility/FacilityCreation.ts @@ -244,7 +244,7 @@ class FacilityPage { ) { cy.get("#refering_facility_contact_name").type(name); cy.get("#refering_facility_contact_number").type(phone_number); - cy.get("[name='approving_facility']") + cy.get("[name='approving_facility_object']") .type(facility) .then(() => { cy.get("[role='option']").first().click(); diff --git a/public/locale/en.json b/public/locale/en.json index 2ceaf310fac..40b87f3c94a 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -331,6 +331,7 @@ "approve": "Approve", "approved_by_district_covid_control_room": "Approved by District COVID Control Room", "approving_facility": "Name of Approving Facility", + "approving_facility_error": "Name of the referring facility is required.", "archive": "Archive", "archived": "Archived", "are_non_editable_fields": "are non-editable fields", @@ -348,9 +349,11 @@ "assign": "Assign", "assign_a_volunteer_to": "Assign a volunteer to {{name}}", "assign_bed": "Assign Bed", + "assign_facility_label": "What facility would you like to assign the request to", "assign_to_volunteer": "Assign to a Volunteer", "assigned_doctor": "Assigned Doctor", "assigned_facility": "Facility assigned", + "assigned_quantity_error": "Value can't be smaller than 0", "assigned_to": "Assigned to", "assigned_volunteer": "Assigned Volunteer", "async_operation_warning": "This operation may take some time. Please check back later.", @@ -687,6 +690,9 @@ "emergency_contact_person_name_volunteer": "Emergency Contact Person Name (Volunteer)", "emergency_contact_volunteer": "Emergency Contact (Volunteer)", "empty_date_time": "--:-- --; --/--/----", + "empty_description_error": "Description is required field.", + "empty_resource_error": "Resource approving facility cannot be empty.", + "empty_title_error": "Title is required field.", "encounter_date_field_label__A": "Date & Time of Admission to the Facility", "encounter_date_field_label__DC": "Date & Time of Domiciliary Care commencement", "encounter_date_field_label__DD": "Date & Time of Consultation", @@ -1216,10 +1222,12 @@ "ration_card__NO_CARD": "Non-card holder", "ration_card_category": "Ration Card Category", "reason": "Reason", + "reason_error": "Description of the resource request is mandatory.", "reason_for_discontinuation": "Reason for discontinuation", "reason_for_edit": "Reason for edit", "reason_for_referral": "Reason for referral", "reason_for_shift": "Reason for shift", + "reason_invalid": "Please enter a description for the resource request.", "recommended_aspect_ratio_for": "Recommended aspect ratio for", "record": "Record Audio", "record_delete_confirm": "Are you sure you want to delete this record?", @@ -1229,6 +1237,9 @@ "redirected_to_create_consultation": "Note: You will be redirected to create consultation form. Please complete the form to finish the transfer process", "referral_letter": "Referral Letter", "referred_to": "Referred to", + "referring_facility_contact_name_error": "Name of the contact at the referring facility is required.", + "referring_facility_contact_number_error": "Phone number of the contact at the referring facility is required.", + "referring_facility_contact_number_invalid": "Please enter a valid phone number.", "refresh": "Refresh", "refresh_list": "Refresh List", "refuted": "Refuted", @@ -1254,6 +1265,7 @@ "request_sample_test": "Request Sample Test", "request_title": "Request Title", "request_title_placeholder": "Type your title here", + "requested_quantity_error": "Value can't be smaller than 1", "required": "Required", "required_quantity": "Required Quantity", "resend_otp": "Resend OTP", @@ -1313,6 +1325,7 @@ "select_date": "Select date", "select_eligible_policy": "Select an Eligible Insurance Policy", "select_facility_for_discharged_patients_warning": "Facility needs to be selected to view discharged patients.", + "select_facility_type_error": "Please select a facility type.", "select_for_administration": "Select for Administration", "select_groups": "Select Groups", "select_investigation": "Select Investigations (all investigations will be selected by default)", @@ -1404,6 +1417,8 @@ "test_type": "Type of test done", "tested_on": "Tested on", "third_party_software_licenses": "Third Party Software Licenses", + "title_error": "Title for the resource request is mandatory.", + "title_invalid": "Please enter a title for the resource request.", "titrate_dosage": "Titrate Dosage", "to_be_conducted": "To be conducted", "total_amount": "Total Amount", @@ -1475,6 +1490,7 @@ "update_preset_position_to_current": "Update preset's position to camera's current position", "update_record": "Update Record", "update_record_for_asset": "Update record for asset", + "update_resource_request": "Update Resource Request", "update_shift_request": "Update Shift Request", "update_status_details": "Update Status/Details", "update_volunteer": "Reassign Volunteer", diff --git a/src/Routers/routes/ResourceRoutes.tsx b/src/Routers/routes/ResourceRoutes.tsx index 547aeb53610..60e7a94ea00 100644 --- a/src/Routers/routes/ResourceRoutes.tsx +++ b/src/Routers/routes/ResourceRoutes.tsx @@ -1,8 +1,8 @@ import { Redirect } from "raviger"; import BoardView from "@/components/Resource/ResourceBoard"; +import ResourceCreate from "@/components/Resource/ResourceCreate"; import ResourceDetails from "@/components/Resource/ResourceDetails"; -import { ResourceDetailsUpdate } from "@/components/Resource/ResourceDetailsUpdate"; import ListView from "@/components/Resource/ResourceList"; import { AppRoutes } from "@/Routers/AppRouter"; @@ -15,7 +15,7 @@ const ResourceRoutes: AppRoutes = { "/resource/board": () => , "/resource/list": () => , "/resource/:id": ({ id }) => , - "/resource/:id/update": ({ id }) => , + "/resource/:id/update": ({ id }) => , }; export default ResourceRoutes; diff --git a/src/components/Resource/ResourceCreate.tsx b/src/components/Resource/ResourceCreate.tsx index 7c3e79cc4cb..89057f20503 100644 --- a/src/components/Resource/ResourceCreate.tsx +++ b/src/components/Resource/ResourceCreate.tsx @@ -1,27 +1,27 @@ -import { navigate } from "raviger"; +import { navigate, useQueryParams } from "raviger"; import { useReducer, useState } from "react"; import { useTranslation } from "react-i18next"; -import Card from "@/CAREUI/display/Card"; - -import { Cancel, Submit } from "@/components/Common/ButtonV2"; import { FacilitySelect } from "@/components/Common/FacilitySelect"; import Loading from "@/components/Common/Loading"; import Page from "@/components/Common/Page"; -import { PhoneNumberValidator } from "@/components/Form/FieldValidators"; +import { + PhoneNumberValidator, + RequiredFieldValidator, +} from "@/components/Form/FieldValidators"; import { FieldLabel } from "@/components/Form/FormFields/FormField"; import PhoneNumberFormField from "@/components/Form/FormFields/PhoneNumberFormField"; import RadioFormField from "@/components/Form/FormFields/RadioFormField"; import { SelectFormField } from "@/components/Form/FormFields/SelectFormField"; import TextAreaFormField from "@/components/Form/FormFields/TextAreaFormField"; import TextFormField from "@/components/Form/FormFields/TextFormField"; -import { FieldChangeEvent } from "@/components/Form/FormFields/Utils"; import useAppHistory from "@/hooks/useAppHistory"; import { OptionsType, RESOURCE_CATEGORY_CHOICES, + RESOURCE_CHOICES, RESOURCE_SUBCATEGORIES, } from "@/common/constants"; import { phonePreg } from "@/common/validation"; @@ -32,48 +32,47 @@ import request from "@/Utils/request/request"; import useTanStackQueryInstead from "@/Utils/request/useQuery"; import { parsePhoneNumber } from "@/Utils/utils"; +import CircularProgress from "../Common/CircularProgress"; +import UserAutocomplete from "../Common/UserAutocompleteFormField"; +import { ResourceModel } from "../Facility/models"; +import Form from "../Form/Form"; + interface resourceProps { - facilityId: number; + facilityId?: number; + resourceId?: string; } -const initForm: any = { +type ResourceData = Partial< + Omit< + ResourceModel, + | "status" + | "requested_quantity" + | "assigned_quantity" + | "emergency" + | "sub_category" + > +> & { + sub_category: number; + status: string; + requested_quantity: string; + assigned_quantity: string; + emergency: string; +}; + +const initForm: ResourceData = { + status: "PENDING", category: "OXYGEN", sub_category: 1000, - approving_facility: null, - assigned_facility: null, + approving_facility_object: null, + assigned_facility_object: null, emergency: "false", title: "", reason: "", refering_facility_contact_name: "", refering_facility_contact_number: "+91", - required_quantity: null, -}; - -const requiredFields: any = { - category: { - errorText: "Category", - }, - sub_category: { - errorText: "Subcategory", - }, - approving_facility: { - errorText: "Name of the referring facility", - }, - refering_facility_contact_name: { - errorText: "Name of contact of the referring facility", - }, - refering_facility_contact_number: { - errorText: "Phone number of contact of the referring facility", - invalidText: "Please enter valid phone number", - }, - title: { - errorText: "Title for resource request is mandatory", - invalidText: "Please enter title for resource request", - }, - reason: { - errorText: "Description of resource request is mandatory", - invalidText: "Please enter Description of resource request", - }, + assigned_to_object: null, + requested_quantity: "", + assigned_quantity: "", }; const initError = Object.assign( @@ -88,9 +87,46 @@ const initialState = { export default function ResourceCreate(props: resourceProps) { const { goBack } = useAppHistory(); - const { facilityId } = props; + const { facilityId, resourceId } = props; const { t } = useTranslation(); + const [qParams, _] = useQueryParams(); const [isLoading, setIsLoading] = useState(false); + const [initialResourceData, setInitialResouceData] = + useState(initForm); + const resourceStatusOptions = RESOURCE_CHOICES.map((obj) => obj.text); + + const requiredFields: any = { + category: { + errorText: t("category"), + }, + sub_category: { + errorText: t("sub_category"), + }, + approving_facility_object: { + errorText: t("approving_facility_error"), + }, + refering_facility_contact_name: { + errorText: t("referring_facility_contact_name_error"), + }, + refering_facility_contact_number: { + errorText: t("referring_facility_contact_number_error"), + invalidText: t("referring_facility_contact_number_invalid"), + }, + title: { + errorText: t("title_error"), + invalidText: t("title_invalid"), + }, + reason: { + errorText: t("reason_error"), + invalidText: t("reason_invalid"), + }, + requested_quantity: { + errorText: t("requested_quantity_error"), + }, + assigned_quantity: { + errorText: t("assigned_quantity_error"), + }, + }; const resourceFormReducer = (state = initialState, action: any) => { switch (action.type) { @@ -121,82 +157,129 @@ export default function ResourceCreate(props: resourceProps) { }, ); - const validateForm = () => { - const errors = { ...initError }; - let isInvalidForm = false; - Object.keys(requiredFields).forEach((field) => { + const resourceQuery = useTanStackQueryInstead(routes.getResourceDetails, { + pathParams: { + id: resourceId!, + }, + prefetch: !!resourceId, + onResponse: ({ data: resource }) => { + if (!resource) return; + + setInitialResouceData({ + ...resource, + sub_category: + Number( + RESOURCE_SUBCATEGORIES.find( + (item) => item.text === resource.sub_category, + )?.id, + ) ?? 1000, + emergency: resource.emergency ? "true" : "false", + requested_quantity: resource.requested_quantity.toString(), + assigned_quantity: resource.assigned_quantity.toString(), + status: qParams.status || resource.status, + }); + dispatch({ type: "set_form", form: resource }); + + setIsLoading(false); + }, + }); + + const { loading: assignedUserLoading } = useTanStackQueryInstead( + routes.userList, + { + prefetch: !!resourceId, + }, + ); + + const ResourceFormValidator = ( + form: ResourceData, + ): Partial> => { + const errors: Partial> = {}; + + Object.entries(requiredFields).forEach(([field, config]) => { + const { errorText, invalidText }: any = config; + switch (field) { case "refering_facility_contact_number": { - const phoneNumber = parsePhoneNumber(state.form[field]); - if (!state.form[field]) { - errors[field] = requiredFields[field].errorText; - isInvalidForm = true; + if (resourceId) break; + const phoneNumber = parsePhoneNumber(form[field] ?? ""); + if (!form[field as keyof ResourceData]) { + errors[field as keyof ResourceData] = errorText; } else if ( !phoneNumber || !PhoneNumberValidator()(phoneNumber) === undefined || !phonePreg(String(phoneNumber)) ) { - errors[field] = requiredFields[field].invalidText; - isInvalidForm = true; + errors[field as keyof ResourceData] = invalidText; } - return; + break; } + case "requested_quantity": + case "assigned_quantity": { + if (!resourceId && field === "assigned_quantity") break; + const value = form[field as keyof ResourceData]; + const minVal = field === "assigned_quantity" ? 0 : 1; + if (!value || parseFloat(String(value)) < minVal) { + errors[field as keyof ResourceData] = errorText; + } + break; + } + case "approving_facility_object": + if (!form[field]?.name) { + errors[field as keyof ResourceData] = errorText; + } + break; default: - if (!state.form[field]) { - errors[field] = requiredFields[field].errorText; - isInvalidForm = true; + if (!form[field as keyof ResourceData]) { + errors[field as keyof ResourceData] = errorText; } + break; } }); - dispatch({ type: "set_error", errors }); - return !isInvalidForm; - }; - - const handleChange = (e: FieldChangeEvent) => { - const form = { ...state.form }; - const { name, value } = e; - form[name] = value; - dispatch({ type: "set_form", form }); + return errors; }; - const handleValueChange = (value: any, name: string) => { - const form = { ...state.form }; - form[name] = value; - dispatch({ type: "set_form", form }); - }; - - const handleFormFieldChange = (event: FieldChangeEvent) => { - dispatch({ - type: "set_form", - form: { ...state.form, [event.name]: event.value }, - }); - }; + const handleSubmit = async (form: ResourceData) => { + setIsLoading(true); + + const resourceData = { + status: form.status || "PENDING", + category: form.category, + sub_category: form.sub_category?.toString(), + origin_facility: + form.origin_facility_object?.id || String(props.facilityId), + approving_facility: (form.approving_facility_object || {}).id, + assigned_facility: (form.assigned_facility_object || {}).id, + emergency: form.emergency === "true", + title: form.title, + reason: form.reason, + refering_facility_contact_name: form.refering_facility_contact_name, + refering_facility_contact_number: parsePhoneNumber( + form.refering_facility_contact_number ?? "", + ), + requested_quantity: parseFloat(form.requested_quantity || "1"), + assigned_quantity: parseFloat(form.assigned_quantity || "0"), + assigned_to_object: form.assigned_to_object, + assigned_to: form.assigned_to_object?.id.toString() ?? undefined, + }; + + if (resourceId) { + const { res, data } = await request(routes.updateResource, { + pathParams: { id: resourceId }, + body: resourceData, + }); - const handleSubmit = async () => { - const validForm = validateForm(); - - if (validForm) { - setIsLoading(true); - - const resourceData = { - status: "PENDING", - category: state.form.category, - sub_category: state.form.sub_category, - origin_facility: String(props.facilityId), - approving_facility: (state.form.approving_facility || {}).id, - assigned_facility: (state.form.assigned_facility || {}).id, - emergency: state.form.emergency === "true", - title: state.form.title, - reason: state.form.reason, - refering_facility_contact_name: - state.form.refering_facility_contact_name, - refering_facility_contact_number: parsePhoneNumber( - state.form.refering_facility_contact_number, - ), - requested_quantity: state.form.requested_quantity || 0, - }; + if (res && res.status == 200 && data) { + dispatch({ type: "set_form", form: data }); + Notification.Success({ + msg: "Resource request updated successfully", + }); + navigate(`/resource/${resourceId}`); + } + setIsLoading(false); + } else { const { res, data } = await request(routes.createResource, { body: resourceData, }); @@ -213,118 +296,241 @@ export default function ResourceCreate(props: resourceProps) { } }; - if (isLoading) { + if (isLoading || resourceQuery.loading) { return ; } return ( - - - - -
- {t("approving_facility")} - - handleValueChange(value, "approving_facility") - } - errors={state.errors.approving_facility} - /> -
- - (o ? t("yes") : t("no"))} - optionValue={(o) => String(o)} - value={state.form.emergency} - onChange={handleChange} - /> - - option} - optionValue={(option: string) => option} - onChange={({ value }) => handleValueChange(value, "category")} - /> - option.text} - optionValue={(option: OptionsType) => option.id} - onChange={({ value }) => handleValueChange(value, "sub_category")} - /> - - - - - -
- -
- -
- goBack()} /> - -
-
+ + disabled={isLoading} + defaults={initialResourceData} + onCancel={goBack} + className="rounded transition-all sm:rounded-xl bg-white mt-2" + onSubmit={handleSubmit} + validate={ResourceFormValidator} + > + {(field) => ( +
+ {/* Create Flow */} + {!resourceId && ( + <> + + + +
+ {t("approving_facility")} + { + field("approving_facility_object").onChange({ + name: "approving_facility_object", + value: selected, + }); + }} + {...field( + "approving_facility_object", + RequiredFieldValidator(t("approving_facility_error")), + )} + errors={state.errors.approving_facility_object} + /> +
+ (o ? t("yes") : t("no"))} + optionValue={(o) => String(o)} + {...field("emergency")} + /> + + option} + optionValue={(option: string) => option} + required + /> + option.text} + optionValue={(option: OptionsType) => option.id} + /> + + + +
+ +
+ + )} + + {/* Update Flow */} + {resourceId && ( + <> + option} + {...field("status")} + /> + {assignedUserLoading ? ( + + ) : ( + + )} + +
+ {t("approving_facility")} + { + field("approving_facility_object").onChange({ + name: "approving_facility_object", + value: selected, + }); + }} + {...field( + "approving_facility_object", + RequiredFieldValidator(t("approving_facility_error")), + )} + errors={state.errors.approving_facility_object} + /> +
+
+ {t("assign_facility_label")} + { + field("assigned_facility_object").onChange({ + name: "assigned_facility_object", + value: selected, + }); + }} + errors={state.errors.assigned_facility_object} + /> +
+ + + + (o ? t("yes") : t("no"))} + optionValue={(o) => String(o)} + {...field("emergency")} + /> +
+ +
+ + )} +
+ )} +
); }