Update done

This commit is contained in:
F Shrabon 2024-09-30 21:01:01 +06:00
parent 7c1ec18262
commit 153cb984df
8 changed files with 628 additions and 4 deletions

View File

@ -31,7 +31,7 @@ const RadioInput: React.FC<Props> = ({ options = [], name, onChange }) => {
<label
id={name}
className={classNames(
"flex font-semibold items-center space-x-2",
"flex items-center space-x-2",
option?.disabled ? "text-[#8A8A8A]" : ""
)}
>

View File

@ -0,0 +1,284 @@
import { ErrorMessage, useField } from "formik";
import React, { useEffect, useRef, useState } from "react";
interface Option {
label: {
name: string;
mobile: string;
nid: string;
brn: string;
};
value: string;
}
interface DropdownProps {
options: Option[];
onChange?: (selectedOptions: any) => void;
isArray?: boolean;
disabled?: boolean;
hidePills?: boolean;
disableSelectAll?: boolean;
name: string;
}
const ReactDoesMultiSelectInput: React.FC<DropdownProps> = ({
name,
options,
onChange,
hidePills = false,
disableSelectAll = false,
}) => {
const showPillsSection = !hidePills;
const [isOpen, setIsOpen] = useState<boolean>(false);
const [searchTerm, setSearchTerm] = useState<string>("");
const dropdownRef = useRef<HTMLDivElement>(null);
const [field, , helpers] = useField(name);
const fieldValues = field.value ?? "";
const isArray = Array.isArray(fieldValues);
const processedFieldValue = isArray ? fieldValues : fieldValues.split(",");
const selectedOptions = options.filter((item) =>
processedFieldValue.includes(item.value)
);
const handleOnChange = (selectedData: any) => {
const value = isArray ? selectedData : selectedData.join(",");
helpers.setValue(value);
if (typeof onChange === "function") {
onChange(value);
}
};
const toggleOption = (option: Option) => {
const index = selectedOptions.findIndex(
(selectedOption) => selectedOption.value === option.value
);
if (index === -1) {
handleOnChange([...processedFieldValue, option.value].filter((i) => i));
} else {
handleOnChange(
processedFieldValue.filter(
(selectedOption: any) => selectedOption !== option.value
)
);
}
};
const handleOptionClick = (
event: React.MouseEvent<HTMLDivElement>,
option: Option
) => {
event.stopPropagation(); // Prevents event from propagating to parent elements
toggleOption(option);
// setIsOpen(false);
};
const handleSelectAll = () => {
let selectedData;
if (searchTerm) {
selectedData = filteredOptions.map((item) => item.value);
} else {
selectedData = options.map((item) => item.value);
}
handleOnChange(selectedData);
};
const handleClearAll = () => {
handleOnChange([]);
};
const filteredOptions = options.filter((option) =>
option.label.name.toLowerCase().includes(searchTerm.toLowerCase())
);
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
setIsOpen(false);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
return (
<fieldset>
<div className="space-y-1" ref={dropdownRef}>
<div className="relative block text-left w-full">
<div className="rounded-md shadow-sm flex items-center">
<button
type="button"
className={`flex w-full rounded-md border z-30 border-gray-300 bg-white px-2 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:border-blue-300 focus:ring focus:ring-blue-200 ${
showPillsSection ? "justify-start" : "justify-start"
}`}
onClick={() => setIsOpen((prev) => !prev)}
>
{showPillsSection ? (
<>
<span className="flex gap-1 flex-wrap w-full max-h-24 overflow-auto">
{selectedOptions.map((option, index) => (
<div className="flex" key={index}>
<div className=" bg-slate-200 py-[1px] px-1 text-[12px] rounded rounded-tr-none rounded-br-none">
{option.label.name}
</div>
<div
className="group flex items-center bg-slate-300 hover:bg-red-200 rounded-tr rounded-br px-[2px]"
onClick={(event) => handleOptionClick(event, option)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={2}
stroke="currentColor"
className="w-4 h-4 group-hover:text-red-800"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6 18 18 6M6 6l12 12"
/>
</svg>
</div>
</div>
))}
{selectedOptions.length === 0 && (
<p className="text-start">Select options</p>
)}
</span>
</>
) : (
<span>
{selectedOptions.length > 0 ? (
<span>
{selectedOptions.length > 0
? `Selected ${selectedOptions.length} of ${options.length}`
: "Select"}
</span>
) : (
<p className="text-start">Select options</p>
)}
</span>
)}
</button>
</div>
{isOpen && (
<>
<div
className={`origin-top-right z-50 absolute right-0 mt-2 w-full rounded-md shadow-lg border-blue-300 bg-slate-50 ring-1 ring-black ring-opacity-5 focus:outline-none`}
role="menu"
aria-orientation="vertical"
>
<div className="p-2">
<input
type="text"
placeholder="Search..."
className="border rounded-md px-2 py-1 text-sm w-full focus:outline-none focus:border-blue-300 focus:ring focus:ring-blue-200"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
<div className="pb-1 max-h-60 overflow-auto" role="none">
{!disableSelectAll && (
<div className="block w-full text-center border-b px-4 py-2 text-sm hover:bg-gray-100">
{filteredOptions.length === 0 ? (
"No options"
) : selectedOptions.length > 0 ? (
<div className="text-left" onClick={handleClearAll}>
Clear All
</div>
) : (
<div className="text-left" onClick={handleSelectAll}>
Select All
</div>
)}
</div>
)}
{filteredOptions.map((option) => (
<div
key={option.value}
className={`block w-full text-left px-4 py-2 text-sm border-b last:border-b-0 hover:cursor-pointer ${
selectedOptions.some(
(selectedOption) =>
selectedOption.value === option.value
)
? "bg-blue-100 border-blue-400"
: "hover:bg-gray-100"
}`}
role="menuitem"
onClick={(event) => handleOptionClick(event, option)}
>
<div className="flex justify-between items-center">
<div className="flex items-center gap-4">
<input
type="checkbox"
checked={selectedOptions.some(
(selectedOption) =>
selectedOption.value === option.value
)}
/>
<div className="space-y-1">
<p> {option.label.name}</p>
{option.label.mobile && (
<p> Mobile: {option.label.mobile}</p>
)}
{option.label.nid && (
<p> NID: {option.label.nid}</p>
)}
{option.label.brn && (
<p> BRN: {option.label.brn}</p>
)}
</div>
</div>
{selectedOptions.some(
(selectedOption) =>
selectedOption.value === option.value
) && (
<div
className="group"
onClick={(event) =>
handleOptionClick(event, option)
}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={2}
stroke="currentColor"
className="w-4 h-4 group-hover:text-red-800"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6 18 18 6M6 6l12 12"
/>
</svg>
</div>
)}
</div>
</div>
))}
</div>
</div>
</>
)}
</div>
<small className="text-red-500">
<ErrorMessage name={name} />
</small>
</div>
</fieldset>
);
};
export default ReactDoesMultiSelectInput;

View File

@ -0,0 +1,56 @@
import InputWrapper from "../atoms/InputWrapper";
import ReactDoesMultiSelectInput from "../atoms/ReactDoesMultiSelectInput";
type Props = {
label: string;
isRequired?: boolean;
horizontal?: boolean;
name: string;
disabled?: boolean;
isArray?: boolean;
hidePills?: boolean;
disableSelectAll?: boolean;
options: {
label: {
name: string;
mobile: string;
nid: string;
brn: string;
};
value: string;
disabled?: boolean;
}[];
onChange?: (selectedOptions: any) => void;
};
const FormikDoseMultiReactSelect: React.FC<Props> = ({
label,
options,
isRequired = false,
disabled = false,
horizontal = false,
isArray = false,
hidePills = false,
disableSelectAll = false,
name,
onChange,
}) => {
return (
<InputWrapper
isRequired={isRequired}
label={label}
disabled={disabled}
horizontal={horizontal}
>
<ReactDoesMultiSelectInput
isArray={isArray}
name={name}
options={options}
disabled={disabled}
onChange={onChange}
disableSelectAll={disableSelectAll}
hidePills={hidePills}
/>
</InputWrapper>
);
};
export default FormikDoseMultiReactSelect;

View File

@ -1,2 +1,2 @@
import * as yup from "yup";
export const YUP = yup;
export const YUP: any = yup;

View File

@ -55,8 +55,8 @@ const OCVDoseList = ({
<ScrollToTop />
<div className="space-y-6">
<div className="flex justify-end">
<Link to="/ocv-management/dose-1-form">
<SubmitBtn title="Dose 1 Form" />
<Link to={`/ocv-management/dose-${numOfDose}-form`}>
<SubmitBtn title={`Dose ${numOfDose} Form`} />
</Link>
</div>
<div className="bg-white px-3 py-6 rounded-2xl">

View File

@ -0,0 +1,269 @@
import { useMutation } from "@tanstack/react-query";
import { Form, Formik } from "formik";
import { useState } from "react";
import toast from "react-hot-toast";
import useOcvManagement from "../../../apis/useOcvManagement";
import DangerAlert from "../../../components/atoms/DangerAlert";
import ScrollToTop from "../../../components/atoms/ScrollToTop";
import SubmitBtn from "../../../components/atoms/SubmitBtn";
import FormikDoseMultiReactSelect from "../../../components/molecules/FormikDoseMultiReactSelect";
import FormikRadio from "../../../components/molecules/FormikRadio";
import FormikText from "../../../components/molecules/FormikText";
import { YUP as Yup } from "../../../constant/yup";
import Sidebar from "../../../layouts/Sidebar";
import OcvLocationManagement from "../components/OcvLocationManagement";
const initialFormValues = {
type: "location", // can be 'location' or 'identity'
division: "",
district: "",
upazila: "",
paurasava: "",
union_name: "",
ward: "",
id: "",
};
const Dose2Form = () => {
const [isFormVisible, setFormVisible] = useState(true); // State to toggle form visibility
const [formKey, setFormKey] = useState(0); // State to force form reset
const { updateSecondOcvDoses, ocvDosesList } = useOcvManagement();
// Mutation for fetching doses list
const {
isLoading: isLoadingDoses,
error: dosesError,
mutateAsync: fetchOcvDoses,
data: dosesData = [],
} = useMutation(ocvDosesList, {
onSuccess: () => setFormVisible(false), // Hide form when data is successfully fetched
});
// Mutation for updating the second OCV doses
const {
isLoading: isLoadingUpdate,
error: updateError,
mutateAsync: updateSecondDose,
} = useMutation(updateSecondOcvDoses, {
onSuccess: () => {
toast.success("Dose updated successfully");
},
});
// Helper function to extract ID from string (split by "___")
const extractID = (data: string) => data?.split("___")[0] ?? "";
// Handle form submission for registering second dose
const handleSecondDoseSubmit = async (values: any) => {
const payload = {
divison_id: extractID(values.division),
district_id: extractID(values.district),
paurasava_id: extractID(values.paurasava),
union_id: extractID(values.union_name),
upazila_id: extractID(values.upazila),
ward_id: extractID(values.ward),
dose: 2,
id: values.id,
};
await fetchOcvDoses(payload);
};
// Handle form submission for updating dose details
const handleUpdateDoseSubmit = async (values: any) => {
const { beneficiary, date } = values;
const ids = beneficiary.split(",").map((id: string) => ({
id: Number(id),
date,
}));
await updateSecondDose(ids);
};
// Function to reset form and display initial form again
const handleAddNew = () => {
setFormKey((prevKey) => prevKey + 1); // Force reset Formik form
setFormVisible(true); // Show the initial form again
};
return (
<Sidebar title="Dose 2 Form">
<ScrollToTop />
<div className="space-y-4">
{isFormVisible ? (
// Render the form if no data has been fetched
<div className="bg-white px-8 py-8 rounded-2xl shadow-sm space-y-4 max-w-3xl mx-auto">
<p className="text-center font-semibold">
Register 2nd Dose Beneficiary
</p>
<Formik
key={formKey} // Use key to force a full form reset when necessary
initialValues={initialFormValues}
onSubmit={handleSecondDoseSubmit}
// validationSchema={Yup.object({
// type: Yup.string()
// .oneOf(
// ["location", "identity"],
// "Type must be either location or identity"
// )
// .required("Type is required"),
// id: Yup.string()
// .required("ID is required")
// .test(
// "id-required",
// "ID is required when type is identity",
// function (value: any) {
// const { type } = this.parent as FormData; // Ensure type safety
// return type === "identity" ? !!value : true; // Required if type is 'identity'
// }
// ),
// division: Yup.string().test(
// "division-required",
// "Division is required when type is location",
// function (value: any) {
// const { type } = this.parent as FormData; // Ensure type safety
// return type === "location" ? !!value : true; // Required if type is 'location'
// }
// ),
// district: Yup.string().test(
// "district-required",
// "District is required when type is location",
// function (value: any) {
// const { type } = this.parent as FormData; // Ensure type safety
// return type === "location" ? !!value : true; // Required if type is 'location'
// }
// ),
// upazila: Yup.string().test(
// "upazila-required",
// "Upazila is required when type is location",
// function (value: any) {
// const { type } = this.parent as FormData; // Ensure type safety
// return type === "location" ? !!value : true; // Required if type is 'location'
// }
// ),
// paurasava: Yup.string().test(
// "paurasava-required",
// "Paurasava is required when type is location",
// function (value: any) {
// const { type } = this.parent as FormData; // Ensure type safety
// return type === "location" ? !!value : true; // Required if type is 'location'
// }
// ),
// union_name: Yup.string().test(
// "union-name-required",
// "Union name is required when type is location",
// function (value: any) {
// const { type } = this.parent as FormData; // Ensure type safety
// return type === "location" ? !!value : true; // Required if type is 'location'
// }
// ),
// ward: Yup.string().test(
// "ward-required",
// "Ward is required when type is location",
// function (value: any) {
// const { type } = this.parent as FormData; // Ensure type safety
// return type === "location" ? !!value : true; // Required if type is 'location'
// }
// ),
// })}
enableReinitialize
>
{({ values }) => (
<fieldset disabled={isLoadingDoses}>
<Form className="space-y-8">
<DangerAlert error={dosesError} />
<FormikRadio
label="Identification Type"
isRequired
name="type"
options={[
{ label: "By Area", value: "location" },
{
label: "By NID/BRN/Mobile Number",
value: "identity",
},
]}
/>
{values.type === "location" ? (
<>
<p className="font-medium underline">Location</p>
<OcvLocationManagement />
</>
) : (
<FormikText label="NID/BRN/Mobile Number" name="id" />
)}
<div className="flex justify-around items-center gap-4">
<SubmitBtn title="Searchss" loading={isLoadingDoses} />
</div>
</Form>
</fieldset>
)}
</Formik>
</div>
) : (
// Render this section if data has been fetched
dosesData.length > 0 && (
<div className="bg-white px-8 py-8 rounded-2xl shadow-sm space-y-4 max-w-3xl mx-auto">
<Formik
initialValues={{ beneficiary: "", date: "" }}
onSubmit={handleUpdateDoseSubmit}
enableReinitialize
validationSchema={Yup.object({
beneficiary: Yup.string().required("Beneficiary is required"),
date: Yup.string().required("Date is required"),
})}
>
{() => (
<fieldset disabled={isLoadingUpdate}>
<Form className="space-y-8">
<DangerAlert error={updateError} />
<FormikDoseMultiReactSelect
label="Choose Beneficiary"
isRequired
name="beneficiary"
options={dosesData.map(
(item: {
id: number;
firstName: string;
mobile: string;
nid: string;
brn: string;
}) => ({
value: String(item.id),
label: {
name: item.firstName,
mobile: item.mobile ?? "",
nid: item.nid,
brn: item.brn,
},
})
)}
/>
<FormikText label="Date" name="date" type="date" />
<div className="flex justify-around items-center gap-4">
<SubmitBtn title="Submit" loading={isLoadingUpdate} />
<SubmitBtn
title="Add New"
loading={isLoadingUpdate}
secondary
click={handleAddNew}
/>
</div>
</Form>
</fieldset>
)}
</Formik>
</div>
)
)}
</div>
</Sidebar>
);
};
export default Dose2Form;

View File

@ -30,6 +30,7 @@ import DoseDetailsPage from "../pages/ocvManagement/DoseDetailsPage.tsx";
import OCVDose1List from "../pages/ocvManagement/OCVDose1List.tsx";
import OCVDose2List from "../pages/ocvManagement/OCVDose2List.tsx";
import Dose1Form from "../pages/ocvManagement/dose1Fom/Dose1Form.tsx";
import Dose2Form from "../pages/ocvManagement/dose2Fom/Dose2Form.tsx";
import Dashboard from "../pages/stages/Dashboard.tsx";
import Login from "../pages/stages/Login";
import { ROLES } from "./authRoutes";
@ -161,6 +162,18 @@ const routes: RouteInterface[] = [
ROLES.UPAZILA_MANAGER,
],
},
{
path: "/ocv-management/dose-2-form",
element: <Dose2Form />,
isPublic: false,
isProtected: true,
roles: [
ROLES.ADMIN,
ROLES.MANAGER,
ROLES.DATA_MANAGER,
ROLES.UPAZILA_MANAGER,
],
},
{
path: "/ocv-management/dose-1-list",
element: <OCVDose1List />,

View File

@ -1,6 +1,7 @@
{
"compilerOptions": {
"target": "ES2020",
"noImplicitAny": false,
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
@ -20,6 +21,7 @@
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}