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 (
+
+ );
+};
+
+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 (
+
+ );
+};
+
+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 (
+
+ );
+};
+
+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 (
+
+ );
+};
+
+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 = () => {
>
-
- {" "}
-
-
-
+
-
>
);
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}`),