This commit is contained in:
natib21
2026-07-10 11:25:59 +00:00
parent 2696ca7498
commit e6e44e773b
1146 changed files with 200266 additions and 90527 deletions

View File

@@ -0,0 +1,67 @@
import { useQuery } from "@tanstack/react-query";
import {
getEmployeePlanByPlanId,
getEmployeeSubPlanByPlanId,
} from "../services/api/employeePlanService";
interface CombinedEmployeePlanResponse {
items: any[];
total: number;
}
export const useCombinedEmployeePlanByPlanId = (planId: string | undefined) => {
// Main query that intelligently fetches data
const query = useQuery<CombinedEmployeePlanResponse>({
queryKey: ["combined-employee-plan", planId],
queryFn: async () => {
if (!planId) return { items: [], total: 0 };
try {
// First, try to get data from the sub endpoint (this has actual plan data)
const subResponse = await getEmployeeSubPlanByPlanId(planId);
if (subResponse.data?.items && subResponse.data.items.length > 0) {
return {
items: subResponse.data.items,
total: subResponse.data.total || subResponse.data.items.length,
};
}
// If sub endpoint returns empty, try the initial endpoint
const initialResponse = await getEmployeePlanByPlanId(planId);
if (initialResponse.data?.items) {
// Transform initial data to match the expected format
const transformedItems = initialResponse.data.items.map(
(item: any) => ({
...item,
weeks: {}, // Initialize empty weeks object
no_of_employee_plans: "0", // No plans exist yet
})
);
return {
items: transformedItems,
total: transformedItems.length,
};
}
// Both endpoints returned empty
return { items: [], total: 0 };
} catch (error) {
console.error("Error fetching employee plans:", error);
// Return empty but don't throw - we'll show empty state in UI
return { items: [], total: 0 };
}
},
enabled: !!planId,
staleTime: 5 * 60 * 1000, // 5 minutes
gcTime: 10 * 60 * 1000, // 10 minutes
});
return {
data: query.data,
isLoading: query.isLoading,
error: query.error,
refetch: query.refetch,
};
};

View File

@@ -0,0 +1,182 @@
// hooks/useEmployeePlanMutations.ts
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import { useErrorHandler } from "@/shared/hooks/useErrorHandler";
import { toast } from "sonner";
import { useAuth } from "@/shared/context/AuthContext";
import {
createEmployeePlan,
getEmployeePlan,
getEmployeePlans,
updateEmployeePlan,
deleteEmployeePlan,
getPlansByUnit,
getPositionsByUnit,
fetchImmediateChild,
getServices,
getParentEmployeePlanDetail,
type CreateEmployeePlanDto,
getEmployeePlanByPlanId,
getEmployeeSubPlanByPlanId,
getMyEmployeePlan,
planParam,
} from "../services/api/employeePlanService";
// Query Keys
export const EMPLOYEE_PLAN_KEYS = {
all: ["employee-plans"] as const,
list: (planId: string) => ["employee-plans", "list", planId] as const,
detail: (id: string) => ["employee-plans", id] as const,
plansByUnit: (unitId: string) => ["employee-plans", "plans", unitId] as const,
positionsByUnit: (unitId: string) =>
["employee-plans", "positions", unitId] as const,
employeePositions: () => ["employee-plans", "employee-positions"] as const,
services: ["employee-plans", "services"] as const,
parentDetail: (id: string) => ["employee-plans", "parent", id] as const,
planId: (planId: string) => ["employee-plans", "plan", planId] as const,
subPlanId: (subPlanId: string) =>
["sub-employee-plans", "plan", subPlanId] as const,
};
// GET SINGLE Employee Plan
export function useEmployeePlan(id: string) {
return useQuery({
queryKey: EMPLOYEE_PLAN_KEYS.detail(id),
queryFn: () => getEmployeePlan(id),
enabled: !!id,
});
}
// GET Employee Plans LIST by planId
export function useEmployeePlanList(planId: string) {
return useQuery({
queryKey: EMPLOYEE_PLAN_KEYS.list(planId),
queryFn: () => getEmployeePlans(planId),
enabled: !!planId,
});
}
// GET Plans by Unit
export function usePlansByUnit(unitId: string) {
return useQuery({
queryKey: EMPLOYEE_PLAN_KEYS.plansByUnit(unitId),
queryFn: () => getPlansByUnit(unitId),
enabled: !!unitId,
});
}
// GET Positions by Unit
export function usePositionsByUnit(unitId: string) {
return useQuery({
queryKey: EMPLOYEE_PLAN_KEYS.positionsByUnit(unitId),
queryFn: () => getPositionsByUnit(unitId),
enabled: !!unitId,
});
}
// GET Employee Positions by Position ID
export function useEmployeePositions() {
const { selectedPositionId } = useAuth();
return useQuery({
queryKey: [...EMPLOYEE_PLAN_KEYS.employeePositions(), selectedPositionId],
queryFn: () => fetchImmediateChild(),
enabled: true,
});
}
// GET Services
export function useServices() {
return useQuery({
queryKey: EMPLOYEE_PLAN_KEYS.services,
queryFn: () => getServices(),
});
}
// GET MY Employee Plans
export function useMineEmployeePlan(params: planParam) {
const { selectedPositionId } = useAuth();
return useQuery({
queryKey: ["my-employee-plans", selectedPositionId, params],
queryFn: () => getMyEmployeePlan(params),
});
}
// GET Employee Plans by Plan ID
export function useEmployeePlanByPlanId(planId: string) {
const { selectedPositionId } = useAuth();
return useQuery({
queryKey: [...EMPLOYEE_PLAN_KEYS.planId(planId), selectedPositionId],
queryFn: () => getEmployeePlanByPlanId(planId),
enabled: !!planId,
});
}
// GET Sub Employee Plans by Plan ID
export function useSubEmployeePlanByPlanId(planId: string) {
const { selectedPositionId } = useAuth();
return useQuery({
queryKey: [...EMPLOYEE_PLAN_KEYS.subPlanId(planId), selectedPositionId],
queryFn: () => getEmployeeSubPlanByPlanId(planId),
enabled: !!planId,
});
}
// GET Parent Employee Plan Detail
export function useParentEmployeePlanDetail(id: string) {
return useQuery({
queryKey: EMPLOYEE_PLAN_KEYS.parentDetail(id),
queryFn: () => getParentEmployeePlanDetail(id),
enabled: !!id,
});
}
// Extended DTO interface to include all fields from your form
export interface ExtendedEmployeePlanDto extends CreateEmployeePlanDto {
id?: string;
}
// UNIFIED HOOK - Returns all Employee Plan operations in one object
export function useEmployeePlanMutations() {
const queryClient = useQueryClient();
const { t } = useTranslation();
const { handleError } = useErrorHandler(t);
const createMutation = useMutation({
mutationFn: (data: ExtendedEmployeePlanDto) => {
const { id, ...createData } = data;
return createEmployeePlan(createData);
},
onError: (exception) => {
handleError(exception);
},
});
const updateMutation = useMutation({
mutationFn: (data: ExtendedEmployeePlanDto) => {
if (!data.id) {
throw new Error("Employee plan ID is required for update");
}
const { id, ...updateData } = data;
return updateEmployeePlan(id, updateData);
},
onError: (exception) => {
handleError(exception);
},
});
const deleteMutation = useMutation({
mutationFn: (id: string) => deleteEmployeePlan(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: EMPLOYEE_PLAN_KEYS.all });
toast.success("Employee plan deleted successfully");
},
onError: (exception) => {
queryClient.invalidateQueries({ queryKey: EMPLOYEE_PLAN_KEYS.all });
handleError(exception);
},
});
return {
create: createMutation,
update: updateMutation,
delete: deleteMutation,
};
}

View File

@@ -0,0 +1,177 @@
import { useState, useEffect, useRef, useCallback } from "react";
import { useTranslation } from "react-i18next";
import {
metabaseService,
MetabaseDashboard,
GetIframeUrlRequest,
} from "../services/api/metabaseService";
export const useMetabaseDashboard = () => {
const { i18n } = useTranslation();
const [dashboards, setDashboards] = useState<MetabaseDashboard[]>([]);
const [selectedDashboard, setSelectedDashboard] =
useState<MetabaseDashboard | null>(null);
const [iframeUrl, setIframeUrl] = useState<string>("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const currentRequestRef = useRef<string | null>(null);
const fetchDashboards = useCallback(async () => {
try {
setLoading(true);
setError(null);
const response = await metabaseService.getPermittedDashboards(
i18n.language === "am" ? "am" : "en",
);
setDashboards(response.items);
// Auto-select the first dashboard if available
if (response.items.length > 0 && !selectedDashboard) {
setSelectedDashboard(response.items[0]);
}
} catch (err) {
// Handle different error types
const error = err as { response?: { status?: number } };
if (error?.response?.status === 403) {
setError("You don't have permission to access any dashboards");
} else if (error?.response?.status === 401) {
setError("Please log in to access dashboards");
} else {
setError("Failed to load dashboards");
}
console.error("Error fetching permitted dashboards:", err);
} finally {
setLoading(false);
}
}, [selectedDashboard, i18n.language]);
const getIframeUrl = useCallback(
async (
dashboardId: string,
params: GetIframeUrlRequest = { params: {} },
) => {
// Prevent multiple simultaneous requests for the same dashboard
if (currentRequestRef.current === dashboardId) {
return null;
}
try {
currentRequestRef.current = dashboardId;
setLoading(true);
setError(null);
const response = await metabaseService.getIframeUrl(
dashboardId,
params,
);
const secureUrl = response.iframeUrl.replace(/^http:\/\//i, "https://");
// Only update if this is still the current request
if (currentRequestRef.current === dashboardId) {
setIframeUrl(secureUrl);
}
return secureUrl;
} catch (err) {
if (currentRequestRef.current === dashboardId) {
// Handle permission errors specifically
const error = err as { response?: { status?: number } };
if (error?.response?.status === 403) {
setError("You don't have permission to access this dashboard");
} else {
setError("Failed to load dashboard");
}
console.error("Error getting iframe URL:", err);
}
return null;
} finally {
if (currentRequestRef.current === dashboardId) {
setLoading(false);
currentRequestRef.current = null;
}
}
},
[],
);
const getFilteredDashboardUrl = useCallback(
async (dashboardId: string, planYearId: string) => {
// Prevent multiple simultaneous requests for the same dashboard
const requestKey = `${dashboardId}-${planYearId}`;
if (currentRequestRef.current === requestKey) {
return null;
}
try {
currentRequestRef.current = requestKey;
setLoading(true);
setError(null);
const response = await metabaseService.getFilteredDashboardUrl(
dashboardId,
planYearId,
);
const secureUrl = response.iframeUrl.replace(/^http:\/\//i, "https://");
// Only update if this is still the current request
if (currentRequestRef.current === requestKey) {
setIframeUrl(secureUrl);
}
return secureUrl;
} catch (err) {
if (currentRequestRef.current === requestKey) {
// Handle permission errors specifically
const error = err as { response?: { status?: number } };
if (error?.response?.status === 403) {
setError("You don't have permission to access this dashboard");
} else {
setError("Failed to load filtered dashboard");
}
console.error("Error getting filtered dashboard URL:", err);
}
return null;
} finally {
if (currentRequestRef.current === requestKey) {
setLoading(false);
currentRequestRef.current = null;
}
}
},
[],
);
const selectDashboard = useCallback((dashboard: MetabaseDashboard) => {
setSelectedDashboard(dashboard);
setIframeUrl(""); // Clear previous iframe URL
// The useEffect will handle calling getIframeUrl
}, []);
const getDashboardByNumber = useCallback(
(dashboardNumber: number) => {
return dashboards.find(
(d) => d.metabaseDashboardNumber === dashboardNumber,
);
},
[dashboards],
);
useEffect(() => {
fetchDashboards();
}, [fetchDashboards]);
useEffect(() => {
if (selectedDashboard) {
getIframeUrl(selectedDashboard.id);
}
}, [selectedDashboard, getIframeUrl]);
return {
dashboards,
selectedDashboard,
iframeUrl,
loading,
error,
fetchDashboards,
getIframeUrl,
getFilteredDashboardUrl,
selectDashboard,
getDashboardByNumber,
};
};

View File

@@ -0,0 +1,184 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { AxiosError, AxiosResponse } from "axios";
import {
getPlanYears,
getPlanYear,
createPlanYear,
updatePlanYear,
deletePlanYear,
PlanYearParams,
} from "../services/api/planYearService";
import { CreatePlanYearDto, PlanYear } from "../types/planYearTypes";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { useErrorHandler } from "@/shared/hooks/useErrorHandler";
// Query keys
const PLAN_YEAR_KEYS = {
all: ["planYears"] as const,
lists: () => [...PLAN_YEAR_KEYS.all, "list"] as const,
list: (unitId: string, params?: PlanYearParams) =>
[...PLAN_YEAR_KEYS.lists(), unitId, params] as const,
details: () => [...PLAN_YEAR_KEYS.all, "detail"] as const,
detail: (id: string) => [...PLAN_YEAR_KEYS.details(), id] as const,
active: (unitId: string) =>
[...PLAN_YEAR_KEYS.all, "active", unitId] as const,
};
// Fetch all plan years
export const usePlanYears = (unitId: string, params?: PlanYearParams) => {
return useQuery({
queryKey: PLAN_YEAR_KEYS.list(unitId, params),
queryFn: () => getPlanYears(unitId, params),
enabled: !!unitId,
staleTime: 300_000,
refetchOnWindowFocus: false,
});
};
// Fetch plan year by ID or year number
export const usePlanYearById = (id: string) => {
return useQuery({
queryKey: PLAN_YEAR_KEYS.list(id),
queryFn: () => getPlanYear(id),
enabled: !!id,
staleTime: 300_000,
refetchOnWindowFocus: false,
});
};
// Fetch plan year by year number (e.g., "2024")
export const usePlanYearByNumber = (unitId: string, year: string) => {
return useQuery({
queryKey: [...PLAN_YEAR_KEYS.all, "byYear", unitId, year],
queryFn: async () => {
const response = await getPlanYears(unitId, { take: 100 });
const planYear = response.data?.items?.find(
(py: any) => py.year.toString() === year
);
if (!planYear) {
throw new Error(`Plan year ${year} not found`);
}
return { data: planYear };
},
enabled: !!unitId && !!year && /^\d{4}$/.test(year),
staleTime: 300_000,
refetchOnWindowFocus: false,
});
};
// Fetch single plan year
export const usePlanYear = (id: string) => {
return useQuery({
queryKey: PLAN_YEAR_KEYS.detail(id),
queryFn: () => getPlanYear(id),
enabled: !!id,
staleTime: 300_000,
refetchOnWindowFocus: false,
});
};
// Hook to resolve year parameter (supports both year number and UUID)
export const useResolvedPlanYear = (
yearParam: string,
unitId: string
): {
data: any;
isLoading: boolean;
error: any;
yearId: string | null;
} => {
const isYearNumber = /^\d{4}$/.test(yearParam);
const byNumber = usePlanYearByNumber(
unitId,
isYearNumber ? yearParam : ""
);
const byId = usePlanYearById(!isYearNumber ? yearParam : "");
if (isYearNumber) {
return {
data: byNumber.data,
isLoading: byNumber.isLoading,
error: byNumber.error,
yearId: byNumber.data?.data?.id || null,
};
}
return {
data: byId.data,
isLoading: byId.isLoading,
error: byId.error,
yearId: yearParam,
};
};
// Mutations
export const usePlanYearMutations = () => {
const queryClient = useQueryClient();
const { t } = useTranslation();
const { handleError } = useErrorHandler(t);
const create = useMutation<
AxiosResponse,
AxiosError,
CreatePlanYearDto,
unknown
>({
mutationFn: createPlanYear,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: PLAN_YEAR_KEYS.all });
toast.success(
t("planYear.createdSuccessfully", "Plan year created successfully")
);
},
onError: (error: AxiosError) => {
handleError(error);
},
});
const update = useMutation<
AxiosResponse,
AxiosError,
CreatePlanYearDto,
unknown
>({
mutationFn: updatePlanYear,
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: PLAN_YEAR_KEYS.all });
if (variables.id) {
queryClient.invalidateQueries({
queryKey: PLAN_YEAR_KEYS.detail(variables.id),
});
}
toast.success(
t("planYear.updatedSuccessfully", "Plan year updated successfully")
);
},
onError: (error: AxiosError) => {
handleError(error);
},
});
const deleteMutation = useMutation<
AxiosResponse,
AxiosError,
string,
unknown
>({
mutationFn: deletePlanYear,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: PLAN_YEAR_KEYS.all });
toast.success(
t("planYear.deletedSuccessfully", "Plan year deleted successfully")
);
},
onError: (error: AxiosError) => {
handleError(error);
},
});
return {
create,
update,
delete: deleteMutation,
};
};

View File

@@ -0,0 +1,201 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useAuth } from "@/shared/context/AuthContext";
import {
approvePlan,
createPlan,
CreatePlanDto,
deletePlan,
getMinePlan,
getMySubPlans,
getPlan,
getPlanList,
getSectorList,
getSubPlans,
getTeamSubPlans,
PlanParams,
rejectPlan,
requestApproval,
updatePlan,
} from "../services/api/planService";
import { useTranslation } from "react-i18next";
import { useErrorHandler } from "@/shared/hooks/useErrorHandler";
import { useNavigate } from "react-router-dom";
import { toast } from "sonner";
import { MeDto } from "@/shared/dto/user/meDto";
import { SectorListParam } from "../types/planTypes";
// Query Keys
const Plan_KEYS = {
all: ["plans"] as const,
list: (id: string, params?: PlanParams) =>
["plans", "list", id, params] as const,
detail: (id: string) => ["plans", id] as const,
};
// Helper function to check if user is unit admin
export const checkIsUnitAdmin = (user: MeDto | null): boolean => {
if (!user || !user.roles) return false;
return user.roles.some((role) => role.key === "unit_admin");
};
export const checkIsPlanInitiator = (
user: MeDto | null,
positionId?: string
): boolean => {
if (!user || !user.employee) return false;
if (positionId) {
// Check specific position
return user.employee.some((emp) =>
emp.positions.some(
(pos) =>
pos.employeePositionId === positionId &&
pos.permissions.some((perm) => perm.key === "can:initiatePlan")
)
);
}
// Check all positions (fallback)
return user.employee.some((emp) =>
emp.positions.some((pos) =>
pos.permissions.some((perm) => perm.key === "can:initiatePlan")
)
);
};
// GET SINGLE Plan
export function usePlan(id: string) {
return useQuery({
queryKey: Plan_KEYS.detail(id),
queryFn: () => getPlan(id),
enabled: !!id,
});
}
export function useSectorList(id?: string) {
return useQuery({
queryKey: ["plans-sector", "sector", id],
queryFn: () => getSectorList(id),
enabled: !!id,
});
}
export function useMinePlan(params?: SectorListParam) {
const { selectedPositionId } = useAuth();
return useQuery({
// Include selectedPositionId in the key so switching positions yields a
// distinct cache entry and refetches with the up-to-date header.
queryKey: ["my-plans", selectedPositionId, params],
queryFn: () => getMinePlan(params),
});
}
// GET Plan LIST with optional params filtering
export function usePlanList(unitId: string, params?: PlanParams) {
return useQuery({
queryKey: Plan_KEYS.list(unitId, params),
queryFn: () => getPlanList(unitId, params),
enabled: !!unitId,
});
}
export function useSubPlans(id: string) {
return useQuery({
queryKey: ["plans", "sub-plans", id],
queryFn: () => getSubPlans(id),
enabled: !!id,
});
}
export function useMySubPlans(id: string) {
return useQuery({
queryKey: ["plans", "my-sub-plans", id],
queryFn: () => getMySubPlans(id),
enabled: !!id,
});
}
export function useTeamSubPlans(id: string) {
return useQuery({
queryKey: ["plans", "team-sub-plans", id],
queryFn: () => getTeamSubPlans(id),
enabled: !!id,
});
}
//UNIFIED HOOK - Returns all Plan operations in one object
export function usePlanMutations() {
const queryClient = useQueryClient();
const { t } = useTranslation();
const { handleError } = useErrorHandler(t);
const createMutation = useMutation({
mutationFn: (data: CreatePlanDto) => createPlan(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: Plan_KEYS.all });
toast("Plan created successfully");
},
onError: (exception) => {
queryClient.invalidateQueries({ queryKey: Plan_KEYS.all });
handleError(exception);
},
});
const updateMutation = useMutation({
mutationFn: (data: CreatePlanDto) => updatePlan(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: Plan_KEYS.all });
},
onError: (exception) => {
queryClient.invalidateQueries({ queryKey: Plan_KEYS.all });
handleError(exception);
},
});
const requestApprovalMutation = useMutation({
mutationFn: (id: string) => requestApproval(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: Plan_KEYS.all });
toast("Plan Approval Requested successfully");
},
onError: (exception) => {
queryClient.invalidateQueries({ queryKey: Plan_KEYS.all });
handleError(exception);
},
});
const approveMutation = useMutation({
mutationFn: (id: string) => approvePlan(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: Plan_KEYS.all });
toast("Plan Approved successfully");
},
onError: (exception) => {
queryClient.invalidateQueries({ queryKey: Plan_KEYS.all });
handleError(exception);
},
});
const rejectMutation = useMutation({
mutationFn: (id: string) => rejectPlan(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: Plan_KEYS.all });
toast("Plan Rejected successfully");
},
onError: (exception) => {
queryClient.invalidateQueries({ queryKey: Plan_KEYS.all });
handleError(exception);
},
});
const deleteMutation = useMutation({
mutationFn: (id: string) => deletePlan(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: Plan_KEYS.all });
toast("Plan deleted successfully");
},
onError: (exception) => {
queryClient.invalidateQueries({ queryKey: Plan_KEYS.all });
handleError(exception);
},
});
return {
create: createMutation,
update: updateMutation,
delete: deleteMutation,
requestApproval: requestApprovalMutation,
approve: approveMutation,
reject: rejectMutation,
};
}

View File

@@ -0,0 +1,77 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
serviceCategoryService,
CreateServiceCategoryRequest,
} from "../services/api/serviceCategoryService";
import { useErrorHandler } from "@/shared/hooks/useErrorHandler";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
// Query Keys
const SERVICE_CATEGORY_KEYS = {
all: ["service-categories"] as const,
list: (organizationId: string) =>
["service-categories", "list", organizationId] as const,
};
// GET SERVICE CATEGORY LIST
export function useServiceCategoryList(organizationId: string) {
return useQuery({
queryKey: SERVICE_CATEGORY_KEYS.list(organizationId),
queryFn: () => serviceCategoryService.getList(organizationId),
enabled: !!organizationId,
});
}
// 🎯 UNIFIED HOOK - Returns all service category operations in one object
export function useServiceCategoryMutations() {
const queryClient = useQueryClient();
const { t } = useTranslation();
const { handleError } = useErrorHandler(t);
const createMutation = useMutation({
mutationFn: (data: CreateServiceCategoryRequest) =>
serviceCategoryService.create(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: SERVICE_CATEGORY_KEYS.all });
toast("Service Category created successfully");
},
onError: (exception) => {
handleError(exception);
},
});
const updateMutation = useMutation({
mutationFn: ({
id,
data,
}: {
id: string;
data: CreateServiceCategoryRequest;
}) => serviceCategoryService.update(id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: SERVICE_CATEGORY_KEYS.all });
toast("Service Category updated successfully");
},
onError: (exception) => {
handleError(exception);
},
});
const deleteMutation = useMutation({
mutationFn: (id: string) => serviceCategoryService.delete(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: SERVICE_CATEGORY_KEYS.all });
toast("Service Category deleted successfully");
},
onError: (exception) => {
handleError(exception);
},
});
return {
create: createMutation,
update: updateMutation,
delete: deleteMutation,
};
}

View File

@@ -0,0 +1,109 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
createService,
updateService,
deleteService,
getService,
getServiceList,
CreateServiceDto,
} from "../services/api/performanceService";
import { useAuth } from "@/shared/context/AuthContext";
import { useUnit } from "@/user-management/hooks/useUnit";
import { useErrorHandler } from "@/shared/hooks/useErrorHandler";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { useNavigate, useParams } from "react-router-dom";
// Query Keys
const SERVICE_KEYS = {
all: ["services"] as const,
list: (id: string) => ["services", "list", id] as const,
detail: (id: string) => ["services", id] as const,
};
// GET SINGLE SERVICE
export function useService(id: string) {
return useQuery({
queryKey: SERVICE_KEYS.detail(id),
queryFn: () => getService(id),
enabled: !!id,
});
}
// GET SERVICE LIST
export function useServiceList(
organizationId: string,
serviceCategoryId?: string,
parentServiceId?: string
) {
// react-query
const queryKey = [
...SERVICE_KEYS.list(organizationId as string),
...(serviceCategoryId ? [serviceCategoryId] : []),
...(parentServiceId ? [parentServiceId] : []),
];
return useQuery({
queryKey,
queryFn: () =>
getServiceList(
organizationId as string,
serviceCategoryId,
parentServiceId
),
enabled: !!organizationId, // only fetch when organizationId exists
});
}
// 🎯 UNIFIED HOOK - Returns all service operations in one object
export function useServiceMutations() {
const queryClient = useQueryClient();
const { t } = useTranslation();
const { handleError } = useErrorHandler(t);
const navigate = useNavigate();
const createMutation = useMutation({
mutationFn: (data: CreateServiceDto) => createService(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: SERVICE_KEYS.all });
toast("Service created successfully");
navigate(`/performance-management/services`);
},
onError: (exception) => {
queryClient.invalidateQueries({ queryKey: SERVICE_KEYS.all });
handleError(exception);
},
});
const updateMutation = useMutation({
mutationFn: (data: CreateServiceDto) => updateService(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: SERVICE_KEYS.all });
toast("Service updated successfully");
navigate(`/performance-management/services`);
},
onError: (exception) => {
queryClient.invalidateQueries({ queryKey: SERVICE_KEYS.all });
handleError(exception);
},
});
const deleteMutation = useMutation({
mutationFn: (id: string) => deleteService(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: SERVICE_KEYS.all });
toast("Service deleted successfully");
navigate(`/performance-management/services`);
},
onError: (exception) => {
queryClient.invalidateQueries({ queryKey: SERVICE_KEYS.all });
handleError(exception);
},
});
return {
create: createMutation,
update: updateMutation,
delete: deleteMutation,
};
}

View File

@@ -0,0 +1,152 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
createPlanBulkTeamMember,
CreatePlanBulkTeamMemberDto,
createPlanTeam,
CreatePlanTeamDto,
createPlanTeamMember,
CreatePlanTeamMemberDto,
deletePlanTeam,
deletePlanTeamMember,
getPlanTeamList,
getPlanTeamMemberList,
updatePlanTeam,
updatePlanTeamMember,
} from "../services/api/planTeamServices";
import { toast } from "sonner";
import { useTranslation } from "react-i18next";
import { useErrorHandler } from "@/shared/hooks/useErrorHandler";
import { getEnvUrl } from "@/shared/config/env";
import { id } from "date-fns/locale";
// Fetch teams
export const usePlanTeamList = (id: string, options?: { enabled?: boolean }) =>
useQuery({
queryKey: ["plan-team-list", id],
queryFn: () => getPlanTeamList(id),
enabled: options?.enabled !== false && !!id, // Only fetch when enabled and id exists
});
export const usePlanTeamMemberList = (
id: string,
options?: { enabled?: boolean },
) =>
useQuery({
queryKey: ["plan-team-member-list", id],
queryFn: () => getPlanTeamMemberList(id),
enabled: options?.enabled !== false && !!id, // Only fetch when enabled and id exists
});
export const usePlanTeamMutation = () => {
const { t } = useTranslation();
const { handleError } = useErrorHandler(t);
const queryClient = useQueryClient();
const createPlanTeamMutation = useMutation({
mutationFn: ({ data }: { data: CreatePlanTeamDto }) => createPlanTeam(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["plan-team-list"] });
},
onError: (error) => {
handleError(error);
},
});
const updatePlanTeamMutation = useMutation({
mutationFn: ({ id, data }: { id: string; data: CreatePlanTeamDto }) =>
updatePlanTeam(id, data),
onSuccess: () => {},
onError: (error) => {
handleError(error);
},
});
const deletePlanTeamMutation = useMutation({
mutationFn: ({ id }: { id: string }) => deletePlanTeam(id),
onSuccess: () => {
toast.success("Plan team deleted successfully");
},
onError: (error) => {
handleError(error);
},
});
const createPlanTeamMemberMutation = useMutation({
mutationFn: ({ data }: { data: CreatePlanTeamMemberDto }) =>
createPlanTeamMember(data),
onSuccess: () => {
toast.success("Plan team member created successfully");
},
onError: (error) => {
handleError(error);
},
});
const createPlanBulkTeamMemberMutation = useMutation({
mutationFn: ({ data }: { data: CreatePlanBulkTeamMemberDto }) =>
createPlanBulkTeamMember(data),
onSuccess: () => {
toast.success("Plan team member created successfully");
},
onError: (error) => {
handleError(error);
},
});
const updatePlanTeamMemberMutation = useMutation({
mutationFn: ({ id, data }: { id: string; data: CreatePlanTeamMemberDto }) =>
updatePlanTeamMember(id, data),
onSuccess: () => {
toast.success("Plan team member updated successfully");
},
onError: (error) => {
handleError(error);
},
});
const deletePlanTeamMemberMutation = useMutation({
mutationFn: deletePlanTeamMember,
onSuccess: () => {
toast.success("Plan team member deleted successfully");
},
onError: (error) => {
handleError(error);
},
});
return {
createPlanTeam: createPlanTeamMutation.mutate,
isLoading: createPlanTeamMutation.isPending,
isError: createPlanTeamMutation.isError,
error: createPlanTeamMutation.error,
updatePlanTeam: updatePlanTeamMutation.mutate,
deletePlanTeam: deletePlanTeamMutation.mutate,
createPlanTeamMember: createPlanTeamMemberMutation.mutate,
updatePlanTeamMember: updatePlanTeamMemberMutation.mutate,
deletePlanTeamMember: deletePlanTeamMemberMutation.mutate,
isPlanTeamLoading: updatePlanTeamMutation.isPending,
isPlanTeamError: updatePlanTeamMutation.isError,
planTeamError: updatePlanTeamMutation.error,
isPlanTeamMemberLoading: deletePlanTeamMutation.isPending,
isPlanTeamMemberError: deletePlanTeamMutation.isError,
planTeamMemberError: deletePlanTeamMutation.error,
isPlanTeamMemberCreateLoading: createPlanTeamMemberMutation.isPending,
isPlanTeamMemberCreateError: createPlanTeamMemberMutation.isError,
planTeamMemberCreateError: createPlanTeamMemberMutation.error,
isPlanTeamMemberUpdateLoading: updatePlanTeamMemberMutation.isPending,
isPlanTeamMemberUpdateError: updatePlanTeamMemberMutation.isError,
planTeamMemberUpdateError: updatePlanTeamMemberMutation.error,
isPlanTeamMemberDeleteLoading: deletePlanTeamMemberMutation.isPending,
isPlanTeamMemberDeleteError: deletePlanTeamMemberMutation.isError,
planTeamMemberDeleteError: deletePlanTeamMemberMutation.error,
createPlanBulkTeamMember: createPlanBulkTeamMemberMutation.mutate,
isPlanBulkTeamMemberCreateLoading:
createPlanBulkTeamMemberMutation.isPending,
};
};
// Fetch users for team member selection
export const useUsers = () =>
useQuery({
queryKey: ["users"],
queryFn: async () => {
const apiBaseUrl = getEnvUrl("VITE_API_URL");
const response = await fetch(`${apiBaseUrl}/users/active`);
return response.json();
},
});

View File

@@ -0,0 +1,75 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import {
createWorkingHours,
getWorkingHoursList,
getWorkingHoursByPositionServiceId,
updateWorkingHours,
deleteWorkingHours,
} from "../services/api/workingHoursService";
import { CreateWorkingHoursDto } from "../types/workingHoursTypes";
import { useErrorHandler } from "@/shared/hooks/useErrorHandler";
export const useWorkingHoursList = () => {
return useQuery({
queryKey: ["workingHours"],
queryFn: getWorkingHoursList,
});
};
export const useWorkingHoursByPositionService = (positionServiceId: string) => {
return useQuery({
queryKey: ["workingHours", positionServiceId],
queryFn: () => getWorkingHoursByPositionServiceId(positionServiceId),
enabled: !!positionServiceId,
});
};
export const useWorkingHoursMutations = () => {
const queryClient = useQueryClient();
const { t } = useTranslation();
const { handleError } = useErrorHandler(t);
const create = useMutation({
mutationFn: (data: CreateWorkingHoursDto) => createWorkingHours(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["workingHours"] });
toast.success(
t("workingHours.createdSuccessfully", "Working hours created successfully")
);
},
onError: (error: any) => {
handleError(error);
},
});
const update = useMutation({
mutationFn: ({ id, data }: { id: string; data: CreateWorkingHoursDto }) =>
updateWorkingHours(id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["workingHours"] });
toast.success(
t("workingHours.updatedSuccessfully", "Working hours updated successfully")
);
},
onError: (error: any) => {
handleError(error);
},
});
const deleteHours = useMutation({
mutationFn: (id: string) => deleteWorkingHours(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["workingHours"] });
toast.success(
t("workingHours.deletedSuccessfully", "Working hours deleted successfully")
);
},
onError: (error: any) => {
handleError(error);
},
});
return { create, update, delete: deleteHours };
};