diff --git a/src/components/master/CreateActivities.jsx b/src/components/master/CreateActivities.jsx new file mode 100644 index 00000000..19627663 --- /dev/null +++ b/src/components/master/CreateActivities.jsx @@ -0,0 +1,223 @@ +import React, { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { + useCreateActivities, + useGetServices, + useGetActivityGroups, +} from "../../hooks/masterHook/useMaster"; + +// ✅ Schema validation +const schema = z.object({ + serviceId: z.string().min(1, { message: "Service selection is required" }), + activityGroupId: z + .string() + .min(1, { message: "Activity Group selection is required" }), + activityName: z.string().min(1, { message: "Activity Name is required" }), + unitOfMeasurement: z + .string() + .min(1, { message: "Unit of Measurement is required" }), + description: z + .string() + .min(1, { message: "Description is required" }) + .max(255, { message: "Description cannot exceed 255 characters" }), +}); + +const CreateActivities = ({ onClose }) => { + const { + register, + handleSubmit, + formState: { errors }, + reset, + watch, + } = useForm({ + resolver: zodResolver(schema), + defaultValues: { + serviceId: "", + activityGroupId: "", + activityName: "", + unitOfMeasurement: "", + description: "", + }, + }); + + const [descriptionLength, setDescriptionLength] = useState(0); + const maxDescriptionLength = 255; + + // ✅ Watch dropdown values + const selectedService = watch("serviceId"); + const selectedActivityGroup = watch("activityGroupId"); + + // ✅ Mutation Hook + const createActivityMutation = useCreateActivities(() => { + resetForm(); + onClose(); + }); + + // ✅ Fetch services + const { data: services = [], isLoading: servicesLoading } = useGetServices(); + + // ✅ Fetch activity groups based on selected service + const { data: activityGroups = [], isLoading: groupsLoading } = + useGetActivityGroups(selectedService, { + enabled: !!selectedService, + }); + + const onSubmit = (data) => { + createActivityMutation.mutate({ + activityName: data.activityName, + description: data.description, + serviceId: data.serviceId, + activityGroupId: data.activityGroupId, + unitOfMeasurement: data.unitOfMeasurement, + }); + }; + + const resetForm = () => { + reset({ + serviceId: "", + activityGroupId: "", + activityName: "", + unitOfMeasurement: "", + description: "", + }); + setDescriptionLength(0); + }; + + useEffect(() => { + return () => resetForm(); + }, []); + + return ( +
+ {/* Service Dropdown */} +
+ + + {errors.serviceId && ( +

{errors.serviceId.message}

+ )} +
+ + {/* Activity Group Dropdown */} + {selectedService && ( +
+ + + {errors.activityGroupId && ( +

{errors.activityGroupId.message}

+ )} +
+ )} + + {/* Activity Name + Unit of Measurement + Description */} + {selectedActivityGroup && ( + <> + {/* Activity Name */} +
+ + + {errors.activityName && ( +

{errors.activityName.message}

+ )} +
+ + {/* Unit of Measurement */} +
+ + + {errors.unitOfMeasurement && ( +

{errors.unitOfMeasurement.message}

+ )} +
+ + {/* Description */} +
+ + +
+ {maxDescriptionLength - descriptionLength} characters left +
+ {errors.description && ( +

{errors.description.message}

+ )} +
+ + {/* Buttons */} +
+ + +
+ + )} +
+ ); +}; + +export default CreateActivities; diff --git a/src/components/master/CreateActivityGroup.jsx b/src/components/master/CreateActivityGroup.jsx new file mode 100644 index 00000000..afe3690c --- /dev/null +++ b/src/components/master/CreateActivityGroup.jsx @@ -0,0 +1,159 @@ +import React, { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useCreateActivityGroup, useGetServices } from "../../hooks/masterHook/useMaster"; + +// ✅ Schema validation +const schema = z.object({ + name: z.string().min(1, { message: "Activity Group Name is required" }), + description: z + .string() + .min(1, { message: "Description is required" }) + .max(255, { message: "Description cannot exceed 255 characters" }), + serviceId: z.string().min(1, { message: "Service selection is required" }), +}); + +const CreateActivityGroup = ({ onClose }) => { + const { + register, + handleSubmit, + formState: { errors }, + reset, + watch, + } = useForm({ + resolver: zodResolver(schema), + defaultValues: { + name: "", + description: "", + serviceId: "", + }, + }); + + const [descriptionLength, setDescriptionLength] = useState(0); + const maxDescriptionLength = 255; + + // ✅ Watch serviceId value + const selectedService = watch("serviceId"); + + // ✅ Mutation Hook + const createActivityGroupMutation = useCreateActivityGroup(() => { + resetForm(); + onClose(); + }); + + // ✅ Fetch services for dropdown + const { data: services = [], isLoading: servicesLoading } = useGetServices(); + + const onSubmit = (data) => { + createActivityGroupMutation.mutate({ + name: data.name, + description: data.description, + serviceId: data.serviceId, // ✅ attach serviceId + }); + }; + + const resetForm = () => { + reset({ + name: "", + description: "", + serviceId: "", + }); + setDescriptionLength(0); + }; + + useEffect(() => { + return () => resetForm(); + }, []); + + return ( +
+ {/* Service Dropdown */} +
+ + + {errors.serviceId && ( +

{errors.serviceId.message}

+ )} +
+ + {/* Render rest only when service is selected */} + {selectedService && ( + <> + {/* Activity Group Name */} +
+ + + {errors.name && ( +

{errors.name.message}

+ )} +
+ + {/* Description */} +
+ + +
+ {maxDescriptionLength - descriptionLength} characters left +
+ {errors.description && ( +

{errors.description.message}

+ )} +
+ + {/* Buttons */} +
+ + +
+ + )} +
+ ); +}; + +export default CreateActivityGroup; diff --git a/src/components/master/CreateServices.jsx b/src/components/master/CreateServices.jsx new file mode 100644 index 00000000..44231498 --- /dev/null +++ b/src/components/master/CreateServices.jsx @@ -0,0 +1,113 @@ +import React, { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useCreateService } from "../../hooks/masterHook/useMaster"; + +const schema = z.object({ + name: z.string().min(1, { message: "Service Name is required" }), + description: z + .string() + .min(1, { message: "Description is required" }) + .max(255, { message: "Description cannot exceed 255 characters" }), +}); + +const CreateServices = ({ onClose }) => { + const { + register, + handleSubmit, + formState: { errors }, + reset, + } = useForm({ + resolver: zodResolver(schema), + defaultValues: { + name: "", + description: "", + }, + }); + + const [descriptionLength, setDescriptionLength] = useState(0); + const maxDescriptionLength = 255; + + // ✅ Use mutation hook + const createServiceMutation = useCreateService(() => { + resetForm(); + onClose(); + }); + + const onSubmit = (data) => { + createServiceMutation.mutate({ + name: data.name, + description: data.description, + }); + }; + + const resetForm = () => { + reset({ + name: "", + description: "", + }); + setDescriptionLength(0); + }; + + useEffect(() => { + return () => resetForm(); + }, []); + + return ( +
+
+ + + {errors.name &&

{errors.name.message}

} +
+ +
+ + +
+ {maxDescriptionLength - descriptionLength} characters left +
+ {errors.description && ( +

{errors.description.message}

+ )} +
+ +
+ + +
+
+ ); +}; + +export default CreateServices; diff --git a/src/components/master/EditServices.jsx b/src/components/master/EditServices.jsx new file mode 100644 index 00000000..f18f4847 --- /dev/null +++ b/src/components/master/EditServices.jsx @@ -0,0 +1,123 @@ +import React, { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useUpdateService } from "../../hooks/masterHook/useMaster"; // ✅ your custom hook + +// Validation schema +const schema = z.object({ + name: z.string().min(1, { message: "Service Name is required" }), + description: z + .string() + .min(1, { message: "Description is required" }) + .max(255, { message: "Description cannot exceed 255 characters" }), +}); + +const EditServices = ({ data, onClose }) => { + const { + register, + handleSubmit, + formState: { errors }, + reset, + watch, + } = useForm({ + resolver: zodResolver(schema), + defaultValues: { + name: data?.name || "", + description: data?.description || "", + }, + }); + + const [descriptionLength, setDescriptionLength] = useState( + data?.description?.length || 0 + ); + const maxDescriptionLength = 255; + + // ✅ hook for update + const { mutate: updateService, isPending } = useUpdateService(() => { + onClose(); // close modal on success + }); + + const onSubmit = (formData) => { + + const payload= { + id: data?.id, + name: formData.name, + description: formData.description, + }; + updateService({ id: data?.id, payload }); + // }); + }; + + // Reset form when modal opens with fresh data + useEffect(() => { + reset({ + name: data?.name, + description: data?.description, + }); + setDescriptionLength(data?.description?.length || 0); + }, [data, reset]); + + return ( +
+ {/* Service Name */} +
+ + + {errors.name &&

{errors.name.message}

} +
+ + {/* Description */} +
+ + +
+ {maxDescriptionLength - descriptionLength} characters left +
+ {errors.description && ( +

{errors.description.message}

+ )} +
+ + {/* Buttons */} +
+ + +
+
+ ); +}; + +export default EditServices; diff --git a/src/components/master/MasterModal.jsx b/src/components/master/MasterModal.jsx index 0a472727..43909f44 100644 --- a/src/components/master/MasterModal.jsx +++ b/src/components/master/MasterModal.jsx @@ -4,8 +4,8 @@ import DeleteMaster from "./DeleteMaster"; import EditRole from "./EditRole"; import CreateJobRole from "./CreateJobRole"; import EditJobRole from "./EditJobRole"; -import CreateActivity from "./CreateActivity"; -import EditActivity from "./EditActivity"; +import CreateActivity from "./CreateServices"; +import EditActivity from "./EditServices"; import ConfirmModal from "../common/ConfirmModal"; import { MasterRespository } from "../../repositories/MastersRepository"; import { cacheData, getCachedData } from "../../slices/apiDataManager"; @@ -20,6 +20,10 @@ import { useDeleteMasterItem } from "../../hooks/masterHook/useMaster"; import ManageExpenseType from "./ManageExpenseType"; import ManagePaymentMode from "./ManagePaymentMode"; import ManageExpenseStatus from "./ManageExpenseStatus"; +import CreateServices from "./CreateServices"; +import EditServices from "./EditServices"; +import CreateActivityGroup from "./CreateActivityGroup"; +import CreateActivities from "./CreateActivities"; const MasterModal = ({ modaldata, closeModal }) => { @@ -83,8 +87,10 @@ const MasterModal = ({ modaldata, closeModal }) => { "Edit-Application Role": , "Job Role": , "Edit-Job Role": , - "Activity": , - "Edit-Activity": , + "Services": , + "Edit-Services": , + "Activity-Group": , + "Activities":, "Work Category": , "Edit-Work Category": , "Contact Category": , diff --git a/src/data/masters.js b/src/data/masters.js index 67044587..5568eaa5 100644 --- a/src/data/masters.js +++ b/src/data/masters.js @@ -2,7 +2,7 @@ export const mastersList = [ { id: 1, name: "Application Role" }, { id: 2, name: "Job Role" }, - { id: 3, name: "Activity" }, + { id: 3, name: "Services" }, { id: 4, name: "Work Category" }, { id: 5, name: "Contact Category" }, { id: 6, name: "Contact Tag" }, diff --git a/src/hooks/masterHook/useMaster.js b/src/hooks/masterHook/useMaster.js index 15dc3305..f505d97e 100644 --- a/src/hooks/masterHook/useMaster.js +++ b/src/hooks/masterHook/useMaster.js @@ -171,8 +171,12 @@ const fetchMasterData = async (masterType) => { return (await MasterRespository.getRoles()).data; case "Job Role": return (await MasterRespository.getJobRole()).data; - case "Activity": - return (await MasterRespository.getActivites()).data; + case "Services": + return (await MasterRespository.getServices()).data; + case "Activity-Group": + return (await MasterRespository.getActivityGroup()).data; + case "Activities": + return (await MasterRespository.getActivities()).data; case "Work Category": return (await MasterRespository.getWorkCategory()).data; case "Contact Category": @@ -332,6 +336,126 @@ export const useUpdateApplicationRole = (onSuccessCallback) => }); } +// Services------------------------------- + +export const useCreateService = (onSuccessCallback) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (payload) => { + const resp = await MasterRespository.createService(payload); + return resp.data; // full API response + }, + onSuccess: (data) => { + // Invalidate & refetch service list + queryClient.invalidateQueries({ queryKey: ["masterData", "Services"] }); + + showToast(data?.message || "Service added successfully", "success"); + + if (onSuccessCallback) onSuccessCallback(data?.data); // pass back new service object + }, + onError: (error) => { + showToast(error.message || "Something went wrong", "error"); + }, + }); +}; + +export const useUpdateService = (onSuccessCallback) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ id, payload }) => { + const response = await MasterRespository.updateService(id, payload); + return response; // full response since it already has { success, message, data } + }, + onSuccess: (data, variables) => { + queryClient.invalidateQueries({ + queryKey: ["masterData", "Services"], + }); + + showToast(data.message || "Service updated successfully.", "success"); + + if (onSuccessCallback) onSuccessCallback(data); + }, + onError: (error) => { + showToast(error?.message || "Something went wrong", "error"); + }, + }); +}; + +//Activity-group-------------------------------- +export const useGetServices = () => { + return useQuery({ + queryKey: ["masterData", "Services"], + queryFn: async () => { + const resp = await MasterRepository.getServices(); + return resp.data?.data || []; // only return array of services + }, + }); +}; + + +export const useCreateActivityGroup = (onSuccessCallback) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (payload) => { + const resp = await MasterRespository.createActivityGroup(payload); + return resp.data; // full API response + }, + onSuccess: (data) => { + // Invalidate & refetch activity groups + queryClient.invalidateQueries({ + queryKey: ["masterData", "Activity-Group"], + }); + + showToast(data?.message || "Activity Group added successfully", "success"); + + if (onSuccessCallback) onSuccessCallback(data?.data); + }, + onError: (error) => { + showToast(error.message || "Something went wrong", "error"); + }, + }); +}; + +//Activities-------------------------------------- + +export const useGetActivityGroups = () => { + return useQuery({ + queryKey: ["masterData", "Activity-Group"], + queryFn: async () => { + const resp = await MasterRepository.getActivityGroup(); + return resp.data?.data || []; // only return array of services + }, + }); +}; + +export const useCreateActivities = (onSuccessCallback) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (payload) => { + const resp = await MasterRespository.createActivities(payload); + return resp.data; // return full API response + }, + onSuccess: (data) => { + // Invalidate & refetch activities list + queryClient.invalidateQueries({ + queryKey: ["masterData", "Activities"], + }); + + showToast(data?.message || "Activity added successfully", "success"); + + if (onSuccessCallback) onSuccessCallback(data?.data); + }, + onError: (error) => { + showToast(error.message || "Something went wrong", "error"); + }, + }); +}; + + // Activity------------------------------ export const useCreateActivity = (onSuccessCallback) => { diff --git a/src/pages/master/MasterPage.jsx b/src/pages/master/MasterPage.jsx index 7904e6af..79c47729 100644 --- a/src/pages/master/MasterPage.jsx +++ b/src/pages/master/MasterPage.jsx @@ -23,7 +23,16 @@ const MasterPage = () => { const selectedMaster = useSelector((store) => store.localVariables.selectedMaster); const queryClient = useQueryClient(); - const { data: masterData = [], loading, error, RecallApi } = useMaster(); + const { data: masterData = [], loading } = useMaster(); + + // Define button configs for Services + const masterButtonsConfig = { + Services: [ + { label: "Add Service", modalType: "Services" }, + { label: "Add Activity-Group", modalType: "serviceActivity" }, + { label: "Add Activities", modalType: "serviceUnit" }, + ], + }; const openModal = () => setIsCreateModalOpen(true); @@ -61,7 +70,9 @@ const MasterPage = () => { }; const displayData = useMemo(() => { if (searchTerm) return filteredResults; - return queryClient.getQueryData(["masterData", selectedMaster]) || masterData; + return ( + queryClient.getQueryData(["masterData", selectedMaster]) || masterData + ); }, [searchTerm, filteredResults, selectedMaster, masterData]); const columns = useMemo(() => { @@ -148,43 +159,86 @@ const MasterPage = () => { > -
- {" "} -
- - }} - > - + + + + + ) : ( + {" "} + Add {selectedMaster} + + )}
+ - +
- ); diff --git a/src/repositories/MastersRepository.jsx b/src/repositories/MastersRepository.jsx index 6f3fb89b..55c2186d 100644 --- a/src/repositories/MastersRepository.jsx +++ b/src/repositories/MastersRepository.jsx @@ -27,7 +27,19 @@ export const MasterRespository = { getJobRole: () => api.get("/api/roles/jobrole"), updateJobRole: (id, data) => api.put(`/api/roles/jobrole/${id}`, data), - getActivites: () => api.get("api/master/activities"), + getServices: () => api.get("api/master/services"), + createService: (data) => api.post("api/master/service", data), + updateService: (id, data) => api.put(`api/master/service/${id}`, data), + + getActivityGroup: () => api.get("api/master/activity-group"), + createActivityGroup: (data) => api.post("api/master/activity-group", data), + updateActivityGroup:(id,data) => api.put(`api/master/activity-group/${id}`, data), + + getActivities: () => api.get("api/master/activities"), + createActivities: (data) => api.post("api/master/activity", data), + updateActivities: (id, data) => api.put(`api/master/activity/${id}`, data), + + // getActivites: () => api.get("api/master/activities"), createActivity: (data) => api.post("api/master/activity", data), updateActivity: (id, data) => api.post(`api/master/activity/edit/${id}`, data), @@ -36,6 +48,7 @@ export const MasterRespository = { // delete "Job Role": (id) => api.delete(`/api/roles/jobrole/${id}`), Activity: (id) => api.delete(`/api/master/activity/delete/${id}`), + "Services": (id) => api.delete(`/api/master/service/${id}`), "Application Role": (id) => api.delete(`/api/roles/${id}`), "Work Category": (id) => api.delete(`api/master/work-category/${id}`), "Contact Category": (id) => api.delete(`/api/master/contact-category/${id}`),