user management ui

This commit is contained in:
yaschalew
2026-07-10 10:41:48 +03:00
parent dcb2d98503
commit 28a20923ff
595 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { useTranslation } from "react-i18next";
import { useErrorHandler } from "@/shared/hooks/useErrorHandler";
import {
applicationService,
CreateApplicationDto,
UpdateApplicationDto,
} from "@/user-management/services/api/applicationService";
import {
ApplicationDto,
ApplicationListResponse,
} from "@/user-management/dto/applications/applicationDto";
export const useApplications = () => {
const queryClient = useQueryClient();
const { t } = useTranslation();
const { handleError } = useErrorHandler(t);
const { data, isLoading, isError, refetch } =
useQuery<ApplicationListResponse>({
queryKey: ["applications"],
queryFn: () => applicationService.getAll().then((res) => res.data),
staleTime: 1000 * 60 * 5,
});
const createApplication = useMutation({
mutationFn: (payload: CreateApplicationDto) =>
applicationService.create(payload),
onSuccess: () => {
toast.success("Application created");
queryClient.invalidateQueries({ queryKey: ["applications"] });
},
onError: (error) => {
handleError(error);
},
});
const updateApplication = useMutation({
mutationFn: ({ id, data }: { id: string; data: UpdateApplicationDto }) =>
applicationService.update(id, data),
onSuccess: () => {
toast.success("Application updated");
queryClient.invalidateQueries({ queryKey: ["applications"] });
},
onError: (error) => {
handleError(error);
},
});
const deleteApplication = useMutation({
mutationFn: (id: string) => applicationService.delete(id),
onSuccess: () => {
toast.success("Application deleted");
queryClient.invalidateQueries({ queryKey: ["applications"] });
},
onError: (error) => {
handleError(error);
},
});
return {
applications: data?.items ?? [],
applicationsResponse: data,
isLoading,
isError,
refetch,
createApplication,
updateApplication,
deleteApplication,
};
};

View File

@@ -0,0 +1,155 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { useErrorHandler } from "@/shared/hooks/useErrorHandler";
import {
getArchivedUnits,
restoreUnit,
softDeleteUnit,
} from "@/user-management/services/api/unitService";
import {
getArchivedPositions,
restorePosition,
softDeletePosition,
} from "@/user-management/services/api/positionService";
import {
getArchivedOrganizations,
restoreOrganization,
softDeleteOrganization,
} from "@/shared/services/organizationsService";
export const useArchivedUnits = (parentId: string | undefined) => {
return useQuery({
queryKey: ["archived-units", parentId],
queryFn: async () => {
if (!parentId) return null;
const { data } = await getArchivedUnits(parentId);
return data;
},
staleTime: 0,
refetchOnMount: "always",
refetchOnWindowFocus: false,
});
};
export const useArchivedOrganizations = () => {
return useQuery({
queryKey: ["archived-organizations"],
queryFn: async () => {
const { data } = await getArchivedOrganizations();
return data;
},
staleTime: 0,
refetchOnMount: "always",
refetchOnWindowFocus: false,
});
};
export const useArchivedPositions = (parentId: string | undefined) => {
return useQuery({
queryKey: ["archived-positions", parentId],
queryFn: async () => {
if (!parentId) return null;
const { data } = await getArchivedPositions(parentId);
return data;
},
staleTime: 0,
refetchOnMount: "always",
refetchOnWindowFocus: false,
});
};
export const useArchiveActions = () => {
const queryClient = useQueryClient();
const { t } = useTranslation();
const { handleError } = useErrorHandler(t);
const invalidateArchivedUnits = () => {
queryClient.invalidateQueries({ queryKey: ["archived-units"] });
queryClient.invalidateQueries({ queryKey: ["unitList"] });
queryClient.invalidateQueries({ queryKey: ["unitChildren"] });
};
const invalidateArchivedPositions = () => {
queryClient.invalidateQueries({ queryKey: ["archived-positions"] });
queryClient.invalidateQueries({ queryKey: ["positions"] });
};
const invalidateArchivedOrganizations = () => {
queryClient.invalidateQueries({ queryKey: ["archived-organizations"] });
queryClient.invalidateQueries({ queryKey: ["organizations"] });
queryClient.invalidateQueries({ queryKey: ["organizationsResponse"] });
};
const softDeleteUnitMutation = useMutation({
mutationFn: (id: string) => softDeleteUnit(id),
onSuccess: () => {
toast.success(t("archive.unitArchived", "Unit archived"));
invalidateArchivedUnits();
},
onError: handleError,
});
const restoreUnitMutation = useMutation({
mutationFn: (id: string) => restoreUnit(id),
onSuccess: () => {
toast.success(t("archive.unitRestored", "Unit restored"));
invalidateArchivedUnits();
},
onError: handleError,
});
const softDeletePositionMutation = useMutation({
mutationFn: (id: string) => softDeletePosition(id),
onSuccess: () => {
toast.success(t("archive.positionArchived", "Position archived"));
invalidateArchivedPositions();
},
onError: handleError,
});
const restorePositionMutation = useMutation({
mutationFn: (id: string) => restorePosition(id),
onSuccess: () => {
toast.success(t("archive.positionRestored", "Position restored"));
invalidateArchivedPositions();
},
onError: handleError,
});
const softDeleteOrganizationMutation = useMutation({
mutationFn: (id: string) => softDeleteOrganization(id),
onSuccess: () => {
toast.success(t("archive.organizationArchived", "Organization archived"));
invalidateArchivedOrganizations();
},
onError: handleError,
});
const restoreOrganizationMutation = useMutation({
mutationFn: (id: string) => restoreOrganization(id),
onSuccess: () => {
toast.success(
t("archive.organizationRestored", "Organization restored"),
);
invalidateArchivedOrganizations();
},
onError: handleError,
});
return {
softDeleteUnit: softDeleteUnitMutation.mutate,
isArchivingUnit: softDeleteUnitMutation.isPending,
restoreUnit: restoreUnitMutation.mutate,
isRestoringUnit: restoreUnitMutation.isPending,
softDeletePosition: softDeletePositionMutation.mutate,
isArchivingPosition: softDeletePositionMutation.isPending,
restorePosition: restorePositionMutation.mutate,
isRestoringPosition: restorePositionMutation.isPending,
softDeleteOrganization: softDeleteOrganizationMutation.mutate,
isArchivingOrganization: softDeleteOrganizationMutation.isPending,
restoreOrganization: restoreOrganizationMutation.mutate,
isRestoringOrganization: restoreOrganizationMutation.isPending,
};
};

View File

@@ -0,0 +1,103 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { useTranslation } from "react-i18next";
import { useErrorHandler } from "@/shared/hooks/useErrorHandler";
import {
employeePositionChangeRequestService,
type BulkApproveEmployeePositionChangePayload,
type EmployeePositionChangeDecisionPayload,
type EmployeePositionChangeRequestParams,
type TransferEmployeePositionPayload,
} from "@/user-management/services/api/employeePositionChangeRequestService";
export const EMPLOYEE_POSITION_CHANGE_REQUESTS_QUERY_KEY =
"employee-position-change-requests";
interface DecisionVariables extends EmployeePositionChangeDecisionPayload {
id: string;
}
export const useEmployeePositionChangeRequests = (
params: EmployeePositionChangeRequestParams = {},
) => {
const queryClient = useQueryClient();
const { t } = useTranslation();
const { handleError } = useErrorHandler(t);
const requestsQuery = useQuery({
queryKey: [EMPLOYEE_POSITION_CHANGE_REQUESTS_QUERY_KEY, params],
queryFn: () => employeePositionChangeRequestService.list(params),
});
const getRequestByIdQuery = (id?: string | null) =>
useQuery({
queryKey: [EMPLOYEE_POSITION_CHANGE_REQUESTS_QUERY_KEY, id],
queryFn: () => employeePositionChangeRequestService.getById(id!),
enabled: !!id,
});
const invalidateRequests = () =>
queryClient.invalidateQueries({
queryKey: [EMPLOYEE_POSITION_CHANGE_REQUESTS_QUERY_KEY],
});
const transferMutation = useMutation({
mutationFn: (payload: TransferEmployeePositionPayload) =>
employeePositionChangeRequestService.transfer(payload),
onSuccess: () => {
invalidateRequests();
},
onError: (error) => {
handleError(error);
},
});
const bulkApproveMutation = useMutation({
mutationFn: (payload: BulkApproveEmployeePositionChangePayload) =>
employeePositionChangeRequestService.bulkApprove(payload),
onSuccess: () => {
toast.success("Employee position change requests approved successfully");
invalidateRequests();
},
onError: (error) => {
handleError(error);
},
});
const approveMutation = useMutation({
mutationFn: ({ id, comment }: DecisionVariables) =>
employeePositionChangeRequestService.approve(id, { comment }),
onSuccess: () => {
toast.success("Employee position change request approved successfully");
invalidateRequests();
},
onError: (error) => {
handleError(error);
},
});
const rejectMutation = useMutation({
mutationFn: ({ id, comment }: DecisionVariables) =>
employeePositionChangeRequestService.reject(id, { comment }),
onSuccess: () => {
toast.success("Employee position change request rejected successfully");
invalidateRequests();
},
onError: (error) => {
handleError(error);
},
});
return {
requestsQuery,
getRequestByIdQuery,
transfer: transferMutation.mutateAsync,
isTransferring: transferMutation.isPending,
bulkApprove: bulkApproveMutation.mutateAsync,
isBulkApproving: bulkApproveMutation.isPending,
approve: approveMutation.mutateAsync,
isApproving: approveMutation.isPending,
reject: rejectMutation.mutateAsync,
isRejecting: rejectMutation.isPending,
};
};

View File

@@ -0,0 +1,185 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import {
activateUser,
AssignEmployeePayload,
assignEmployees,
assignFirstsForSecond,
assignSecondsForFirst,
deactivateUser,
deleteEmployeePosition,
EmployeePositionQueryParams,
getGivenFirst,
getGivenSecond,
InactiveEPPayload,
inviteEmployeePosition,
removeFirstsForSecond,
removeSecondsForFirst,
setInactive,
} from "../services/api/employeePositionsService";
import { toast } from "sonner";
import { useTranslation } from "react-i18next";
import { useErrorHandler } from "@/shared/hooks/useErrorHandler";
export const useEmployeePositions = (params?: EmployeePositionQueryParams) => {
const queryClient = useQueryClient();
const { t } = useTranslation();
const { handleError } = useErrorHandler(t);
// --- Queries ---
const getGivenFirstQuery = (id: string) =>
useQuery({
queryKey: ["positionEmployees", "given-first", id],
queryFn: () => getGivenFirst(id, params).then((res) => res.data),
enabled: !!id,
});
const getGivenSecondQuery = (id: string) =>
useQuery({
queryKey: ["positionEmployees", "given-second", id],
queryFn: () => getGivenSecond(id, params).then((res: any) => res.data),
enabled: !!id,
});
// --- Mutations ---
const invite = useMutation({
mutationFn: ({
payload,
successCallback,
}: {
payload: any;
successCallback: () => void;
}) => inviteEmployeePosition(payload),
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({ queryKey: ["positionEmployees"] });
variables.successCallback();
},
onError: (error) => {
handleError(error);
},
});
const deactivate = useMutation({
mutationFn: ({
payload,
successCallback,
}: {
payload: string;
successCallback: () => void;
}) => deactivateUser(payload),
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({ queryKey: ["deleteEmployees"] });
queryClient.invalidateQueries({ queryKey: ["positionEmployees"] });
queryClient.invalidateQueries({ queryKey: ["employees"] });
variables.successCallback();
},
onError: (error) => {
handleError(error);
},
});
const activate = useMutation({
mutationFn: ({
payload,
successCallback,
}: {
payload: string;
successCallback: () => void;
}) => activateUser(payload),
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({ queryKey: ["archived-users"] });
queryClient.invalidateQueries({ queryKey: ["employees"] });
variables.successCallback();
},
onError: (error) => {
handleError(error);
},
});
const setInactiveMutation = useMutation({
mutationFn: ({
payload,
successCallback,
}: {
payload: InactiveEPPayload;
successCallback: () => void;
}) => setInactive(payload),
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({ queryKey: ["positionEmployees"] });
toast.success("Team member removed successfully.");
variables.successCallback();
},
onError: (error) => {
handleError(error);
},
});
const assignFirstsForSecondMutation = useMutation({
mutationFn: (payload: any) => assignFirstsForSecond(payload),
onSuccess: () =>
queryClient.invalidateQueries({ queryKey: ["positionEmployees"] }),
});
const assignSecondsForFirstMutation = useMutation({
mutationFn: (payload: any) => assignSecondsForFirst(payload),
onSuccess: () =>
queryClient.invalidateQueries({ queryKey: ["positionEmployees"] }),
});
const assignEmployeeForPosition = useMutation({
mutationFn: (payload: AssignEmployeePayload) => assignEmployees(payload),
onSuccess: () =>{
queryClient.invalidateQueries({ queryKey: ["positionEmployees"] });
queryClient.invalidateQueries({ queryKey: ["employees"] });
},
onError: (error) => {
handleError(error);
},
});
const removeFirstsForSecondMutation = useMutation({
mutationFn: (payload: any) => removeFirstsForSecond(payload),
onSuccess: () =>
queryClient.invalidateQueries({ queryKey: ["positionEmployees"] }),
});
const removeSecondsForFirstMutation = useMutation({
mutationFn: (payload: any) => removeSecondsForFirst(payload),
onSuccess: () =>
queryClient.invalidateQueries({ queryKey: ["positionEmployees"] }),
});
const remove = useMutation({
mutationFn: (id: string) => deleteEmployeePosition(id),
onSuccess: () =>
queryClient.invalidateQueries({ queryKey: ["positionEmployees"] }),
});
// --- Return exposed mutateAsync and query helpers ---
return {
getGivenFirstQuery,
getGivenSecondQuery,
invite: invite.mutateAsync,
isInviting: invite.isPending,
setInactive: setInactiveMutation.mutateAsync,
isDeActivatingUser: setInactiveMutation.isPending,
assignFirstsForSecond: assignFirstsForSecondMutation.mutateAsync,
assignSecondsForFirst: assignSecondsForFirstMutation.mutateAsync,
removeFirstsForSecond: removeFirstsForSecondMutation.mutateAsync,
assignEmployee: assignEmployeeForPosition.mutateAsync,
isAssigning: assignEmployeeForPosition.isPending,
removeSecondsForFirst: removeSecondsForFirstMutation.mutateAsync,
remove: remove.mutateAsync,
deactivateUser: deactivate.mutateAsync,
isDeactivating: deactivate.isPending,
activateUser: activate.mutateAsync,
isActivatingUser: activate.isPending,
};
};

View File

@@ -0,0 +1,98 @@
import { useMutation, useQuery } from "@tanstack/react-query";
import { EmployeeWithUnitListResponse, UserWithUnitListResponse } from "../dto/employees/employees";
import { EmployeeQueryParams, employeeService } from "../services/api/employeesService";
import { getEmployeesUnderOrg } from "@/shared/services/organizationsService";
export const useEmployees = ({
unitId,
organizationId,
params,
}: {
unitId?: string;
organizationId?: string;
params?:EmployeeQueryParams;
}) => {
const {
data: employeesResponse,
isLoading,
isError,
refetch,
} = useQuery<UserWithUnitListResponse>({
queryKey: ["employees", unitId],
queryFn: async () => {
const { data } = await employeeService.getUsersWithUnitById(unitId!, {
take: 3000,
});
return data;
},
enabled: !!unitId,
staleTime: 5 * 60 * 1000,
retry: false,
});
const {
data: employeesResponseByOrg,
isLoading: isLoadingEmployeesByOrg,
isError: isErrorEmployeesByOrg,
refetch: refetchEmployeesByOrg,
} = useQuery<EmployeeWithUnitListResponse | UserWithUnitListResponse>({
queryKey: ["employees", organizationId, unitId, params],
queryFn: async () => {
// If unitId is selected, use the active-with-unit endpoint
if (unitId) {
const { data } = await employeeService.getUsersWithUnitById(unitId, params || {});
return data;
}
// Otherwise use the by-organization endpoint
const { data } = await getEmployeesUnderOrg(
organizationId!,
params
);
return data;
},
enabled: !!(organizationId || unitId),
staleTime: 5 * 60 * 1000,
retry: false,
});
const { mutate: getEmployeeDetails, isPending: isFetchingEmployee } =
useMutation({
mutationFn: async (id: string) => {
const { data } = await employeeService.getEmployee(id);
return data;
},
});
// Normalize the response to always have a consistent structure
const normalizedEmployeesResponseByOrg = employeesResponseByOrg
? {
count: employeesResponseByOrg.count,
items: employeesResponseByOrg.items.map((item: any) => {
// If the item already has a user property, it's EmployeeWithUnitDto
if ('user' in item && item.user) {
return item;
}
// If not, wrap it as UserDTO in the user property for consistency
return {
id: item.id,
user: item,
employeePositions: item.employeePositions || [],
};
}),
}
: undefined;
return {
employeesResponse,
isLoading,
isError,
refetch,
getEmployeeByDetails: getEmployeeDetails,
isFetchingEmployee,
refetchEmployeesByOrg,
isErrorEmployeesByOrg,
isLoadingEmployeesByOrg,
employeesResponseByOrg: normalizedEmployeesResponseByOrg,
};
};

View File

@@ -0,0 +1,138 @@
import {
keepPreviousData,
useMutation,
useQuery,
useQueryClient,
} from "@tanstack/react-query";
import {
getTemplatesByUnitId,
getTemplateById,
createTemplate,
updateTemplate,
deleteTemplate,
LetterTemplatePayload,
LetterTemplate,
} from "@/user-management/services/api/letterTemplateService";
export interface UseLetterTemplatesOptions {
skip?: number;
take?: number;
// BE expects "field:DIRECTION" form, e.g. "createdAt:DESC".
orderBy?: string;
order?: string;
enabled?: boolean;
}
export const useLetterTemplates = (
unitId: string,
options: UseLetterTemplatesOptions = {},
) => {
const queryClient = useQueryClient();
const {
skip = 0,
take = 20,
// Default to newest-first so freshly-created templates appear at the top.
orderBy = "createdAt:DESC",
order,
enabled = true,
} = options;
const params = {
skip,
take,
orderBy,
...(order ? { order } : {}),
};
// GET all templates by unitId
const {
data: letterTemplatesResponse,
isLoading,
isFetching,
isError,
refetch,
} = useQuery({
queryKey: ["letter-templates", unitId, params],
queryFn: async () => {
const { data } = await getTemplatesByUnitId(unitId, params);
return data;
},
enabled: !!unitId && enabled,
staleTime: 5 * 60 * 1000,
retry: false,
placeholderData: keepPreviousData,
});
// GET single template
const { mutate: getTemplateByDetails, isPending: isFetchingTemplate } =
useMutation({
mutationFn: async (id: string) => {
const { data } = await getTemplateById(id);
return data as LetterTemplate;
},
});
// CREATE
const { mutate: createLetterTemplate, isPending: isCreating } = useMutation({
mutationFn: async (data: LetterTemplatePayload) => {
const { data: created } = await createTemplate(data);
return created;
},
onSuccess: (_, variables) => {
queryClient.invalidateQueries({
queryKey: ["letter-templates", variables.unitId],
});
},
});
// UPDATE
const { mutate: updateLetterTemplate, isPending: isUpdating } = useMutation({
mutationFn: async (payload: {
id: string;
data: LetterTemplatePayload;
}) => {
const { data: updated } = await updateTemplate(payload.id, payload.data);
return updated;
},
onSuccess: (_, variables) => {
queryClient.invalidateQueries({
queryKey: ["letter-templates", variables.data.unitId],
});
queryClient.invalidateQueries({
queryKey: ["letter-template", variables.id],
});
},
});
// DELETE
const { mutate: deleteLetterTemplate, isPending: isDeleting } = useMutation({
mutationFn: async ({ id }: { id: string }) => {
await deleteTemplate(id);
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ["letter-templates", unitId],
});
},
});
return {
letterTemplatesResponse,
items: letterTemplatesResponse?.items ?? [],
count: letterTemplatesResponse?.count ?? 0,
isLoading,
isFetching,
isError,
refetch,
getTemplateByDetails,
isFetchingTemplate,
createLetterTemplate,
isCreating,
updateLetterTemplate,
isUpdating,
deleteLetterTemplate,
isDeleting,
};
};

View File

@@ -0,0 +1,84 @@
import { useQuery } from "@tanstack/react-query";
import { getMyAdminOrganizations } from "@/shared/services/organizationsService";
import { getMyAdminUnits } from "@/user-management/services/api/unitService";
import { Name } from "../userManagement/types";
export interface OrgAdminOrganization {
id: string;
name: Name;
[key: string]: any;
}
export interface OrgAdminUnit {
id: string;
name: Name;
[key: string]: any;
}
// Stable empty arrays — reusing the same reference prevents useEffect dependency loops
const EMPTY_ORGS: OrgAdminOrganization[] = [];
const EMPTY_UNITS: OrgAdminUnit[] = [];
/**
* Fetches the list of organizations this org-admin administers.
* Calls GET /organizations/my-admin-organizations
*/
export const useMyAdminOrganizations = () => {
const { data, isLoading, isError, refetch } = useQuery({
queryKey: ["my-admin-organizations"],
queryFn: async () => {
const { data } = await getMyAdminOrganizations();
if (Array.isArray(data)) {
return { count: data.length, items: data as OrgAdminOrganization[] };
}
return {
count: (data.count ?? data.items?.length ?? 0) as number,
items: (data.items ?? data) as OrgAdminOrganization[],
};
},
staleTime: 5 * 60 * 1000,
retry: false,
});
return {
organizations: data?.items ?? EMPTY_ORGS,
count: data?.count ?? 0,
isLoading,
isError,
refetch,
};
};
/**
* Fetches the units this org-admin administers inside a given organization.
* Calls GET /units/my-admin-units/organization/{organizationId}
*/
export const useMyAdminUnits = (
organizationId: string,
enabled: boolean = true,
) => {
const { data, isLoading, isError, refetch } = useQuery({
queryKey: ["my-admin-units", organizationId],
queryFn: async () => {
const { data } = await getMyAdminUnits(organizationId);
if (Array.isArray(data)) {
return { count: data.length, items: data as OrgAdminUnit[] };
}
return {
count: (data.count ?? data.items?.length ?? 0) as number,
items: (data.items ?? data) as OrgAdminUnit[],
};
},
enabled: enabled && !!organizationId,
staleTime: 5 * 60 * 1000,
retry: false,
});
return {
units: data?.items ?? EMPTY_UNITS,
count: data?.count ?? 0,
isLoading,
isError,
refetch,
};
};

View File

@@ -0,0 +1,68 @@
import { useQuery, useQueries } from "@tanstack/react-query";
import {
getUnitList,
getChildUnits,
} from "@/user-management/services/api/unitService";
const extractItems = (data: any): any[] => {
if (!data) return [];
if (Array.isArray(data)) return data;
if (Array.isArray(data.items)) return data.items;
return [];
};
/**
* Total count of every unit in an organization's hierarchy:
* top-level units from `/units/list/{orgId}` plus every descendant
* reachable via `/units/child-units/{parentId}`.
*
* Both create-unit and relate-from-sub-city flows count against this
* same total so the configured cap is enforced consistently.
*/
export const useOrgUnitTotalCount = (organizationId: string) => {
const topLevelQuery = useQuery({
queryKey: ["unitList", organizationId, "capacity-total"],
queryFn: () => getUnitList(organizationId, { take: 1000, skip: 0 }),
enabled: !!organizationId,
staleTime: 0,
});
const topLevelItems = extractItems(topLevelQuery.data?.data);
const topLevelIds: string[] = topLevelItems
.map((u: any) => u?.id)
.filter(Boolean);
const childQueries = useQueries({
queries: topLevelIds.map((id) => ({
queryKey: ["unitChildren", id, "capacity-total"],
queryFn: () => getChildUnits(id),
enabled: !!id,
staleTime: 0,
})),
});
// De-dupe units by `id` because some backends include the parent in
// `/units/child-units/{parentId}` responses, and some `/units/list/{orgId}`
// implementations already include non-top-level units.
const uniqueIds = new Set<string>();
for (const unit of topLevelItems) {
const id = unit?.id;
if (id) uniqueIds.add(String(id));
}
for (const q of childQueries) {
if (!q.data) continue;
const items = extractItems((q.data as any).data);
for (const unit of items) {
const id = unit?.id;
if (id) uniqueIds.add(String(id));
}
}
const total = uniqueIds.size;
const isLoading =
topLevelQuery.isLoading ||
topLevelQuery.isFetching ||
childQueries.some((q) => q.isLoading || q.isFetching);
return { total, isLoading };
};

View File

@@ -0,0 +1,141 @@
import { useState, useEffect, useMemo } from "react";
import { useQuery } from "@tanstack/react-query";
import {
EmployeeQueryParams,
employeeService,
} from "../services/api/employeesService";
import { useLocalizedName } from "@/shared/common/localizedName";
import {
EmployeeWithUnitDto,
EmployeeWithUnitListResponse,
} from "../dto/employees/employees";
// Use the existing EmployeeWithUnitDto interface
export type OrganizationEmployee = EmployeeWithUnitDto;
export const useOrganizationEmployeeSearch = (
organizationId?: string,
params?: EmployeeQueryParams,
) => {
const [searchQuery, setSearchQuery] = useState("");
const [dynamicParams, setDynamicParams] = useState<
EmployeeQueryParams | undefined
>({
...params,
take: params?.take || 3000, // Initial take value of 3000
});
const [hasSetCount, setHasSetCount] = useState(false);
const localizedName = useLocalizedName();
const {
data: employeesData,
isLoading,
error,
refetch,
} = useQuery<EmployeeWithUnitListResponse>({
queryKey: ["organizationEmployees", organizationId, dynamicParams],
queryFn: async () => {
if (!organizationId) throw new Error("Organization ID is required");
const response = await employeeService.getEmployeesWithOrganizationById(
organizationId,
dynamicParams,
);
return response.data;
},
enabled: !!organizationId,
});
// Update params with count from response using useEffect
useEffect(() => {
if (employeesData?.count && !hasSetCount) {
setDynamicParams((prev) => ({
...prev,
take: employeesData.count,
}));
setHasSetCount(true);
}
}, [employeesData?.count, hasSetCount]);
// Deduplicate employees by ID and filter based on search query
const filteredEmployees = useMemo(() => {
if (!employeesData?.items) {
return [];
}
// First, deduplicate employees by their ID
const uniqueEmployees = employeesData.items.reduce((acc, employee) => {
if (!acc.find((e) => e.id === employee.id)) {
acc.push(employee);
}
return acc;
}, [] as EmployeeWithUnitDto[]);
// Then filter based on search query
if (!searchQuery.trim()) {
return uniqueEmployees;
}
const query = searchQuery.toLowerCase().trim();
return uniqueEmployees.filter((employee) => {
// Search by user name (both am and en)
const userNameAm = employee.user.name.am?.toLowerCase() || "";
const userNameEn = employee.user.name.en?.toLowerCase() || "";
const localizedUserName =
localizedName(employee.user.name)?.toLowerCase() || "";
// Search by employee name (both am and en)
const empNameAm = employee.name.am?.toLowerCase() || "";
const empNameEn = employee.name.en?.toLowerCase() || "";
const localizedEmpName =
localizedName(employee.name)?.toLowerCase() || "";
// Search by email
const email = employee.user.email?.toLowerCase() || "";
// Search by phone number
const phoneNumber = employee.user.phoneNumber?.toLowerCase() || "";
// Search by position name
const positionNames = (employee.employeePositions ?? [])
.filter((ep) => ep?.position?.name)
.map((ep) => localizedName(ep.position.name)?.toLowerCase() || "")
.join(" ");
return (
userNameAm.includes(query) ||
userNameEn.includes(query) ||
localizedUserName.includes(query) ||
empNameAm.includes(query) ||
empNameEn.includes(query) ||
localizedEmpName.includes(query) ||
email.includes(query) ||
phoneNumber.includes(query) ||
positionNames.includes(query)
);
});
}, [employeesData?.items, searchQuery, localizedName]);
// Get unique employees for total count
const uniqueEmployees = useMemo(() => {
if (!employeesData?.items) return [];
return employeesData.items.reduce((acc, employee) => {
if (!acc.find((e) => e.id === employee.id)) {
acc.push(employee);
}
return acc;
}, [] as EmployeeWithUnitDto[]);
}, [employeesData?.items]);
return {
employees: uniqueEmployees,
filteredEmployees,
totalCount: uniqueEmployees.length,
filteredCount: filteredEmployees.length,
searchQuery,
setSearchQuery,
isLoading,
error,
refetch,
};
};

View File

@@ -0,0 +1,61 @@
import { useMutation, useQuery } from "@tanstack/react-query";
import {
getOrganizations,
OrgQueryParams,
getOrganizationById,
} from "@/shared/services/organizationsService";
import { OrganizationDto } from "@/shared/dto/organization/organizationDto";
interface Unit {
id: string;
name: string;
departments: any[];
isExpanded?: boolean;
}
export interface Organization {
id: string;
name: string;
units: Unit[];
isExpanded?: boolean;
}
export const useOrganizations = (params?: OrgQueryParams) => {
const {
data: organizationsResponse,
isLoading,
isError,
refetch,
} = useQuery({
queryKey: ["organizations", params],
queryFn: async () => {
const { data } = await getOrganizations(params);
return {
count: data.count as number,
items: data.items as OrganizationDto[],
};
},
staleTime: 5 * 60 * 1000,
retry: false,
});
const { mutate: getOrganization, isPending: isFetchingOrganization } =
useMutation({
mutationFn: async (id: string) => {
const { data } = await getOrganizationById(id);
return data;
},
onSuccess: (data) => {
return data;
},
});
return {
organizationsResponse,
isLoading,
isError,
refetch,
getOrganizationByDetails: getOrganization,
};
};

View File

@@ -0,0 +1,101 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import {
permissionService,
CreatePermissionDto,
PermissionQueryParams,
} from "../services/api/permissionService";
import { toast } from "sonner";
import { useTranslation } from "react-i18next";
import { useErrorHandler } from "@/shared/hooks/useErrorHandler";
import {
PermissionDto,
PermissionListResponse,
} from "../dto/permissions/permissonDto";
interface UsePermissionManagerProps {
params?: PermissionQueryParams;
permissionId?: string;
}
export const usePermissionManager = ({
params,
permissionId,
}: UsePermissionManagerProps = {}) => {
const queryClient = useQueryClient();
const { t } = useTranslation();
const { handleError } = useErrorHandler(t);
// Fetch all permissions
const {
data: permissions,
isLoading: isPermissionsLoading,
refetch: refetchPermissions,
} = useQuery<PermissionListResponse>({
queryKey: ["permissions", params],
queryFn: () => permissionService.getAll(params).then((res) => res.data),
enabled: !!params,
staleTime: 1000 * 60 * 5,
});
// Fetch single permission (for edit)
const {
data: selectedPermission,
isLoading: isPermissionLoading,
refetch: refetchPermission,
} = useQuery<PermissionDto>({
queryKey: ["permission", permissionId],
queryFn: () =>
permissionService.getById(permissionId!).then((res) => res.data),
enabled: !!permissionId,
});
// Create
const createPermission = useMutation({
mutationFn: (payload: CreatePermissionDto) =>
permissionService.create(payload),
onSuccess: () => {
toast.success("Permission created");
queryClient.invalidateQueries({ queryKey: ["permissions"] });
},
onError: (error) => {
handleError(error);
},
});
// Update
const updatePermission = useMutation({
mutationFn: ({ id, data }: { id: string; data: CreatePermissionDto }) =>
permissionService.update(id, data),
onSuccess: () => {
toast.success("Permission updated");
queryClient.invalidateQueries({ queryKey: ["permissions"] });
},
onError: (error) => {
handleError(error);
},
});
// Delete
const deletePermission = useMutation({
mutationFn: (id: string) => permissionService.delete(id),
onSuccess: () => {
toast.success("Permission deleted");
queryClient.invalidateQueries({ queryKey: ["permissions"] });
},
onError: (error) => {
handleError(error);
},
});
return {
permissions,
selectedPermission,
isPermissionsLoading,
isPermissionLoading,
refetchPermissions,
refetchPermission,
createPermission,
updatePermission,
deletePermission,
};
};

View File

@@ -0,0 +1,199 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
getPositions,
PositionQueryParams,
getPositionById,
getPositionHierarchy,
getPositionList,
getEmployeesUnderPosition,
PositionPayload,
createPosition,
updatePosition,
deletePosition,
MovePostionPayload,
movePosition,
} from "@/user-management/services/api/positionService";
import { PositionDto } from "../dto/positions/positionDto";
import { toast } from "sonner";
import { useTranslation } from "react-i18next";
import { useErrorHandler } from "@/shared/hooks/useErrorHandler";
export interface Position {
id: string;
name: string;
code?: string;
parentId?: string;
organizationId?: string;
isExpanded?: boolean;
children?: PositionDto[];
}
export interface PositionWithEmployees extends Position {
employees?: any[];
}
export const usePositions = (params?: PositionQueryParams) => {
const queryClient = useQueryClient();
const { t } = useTranslation();
const { handleError } = useErrorHandler(t);
// Main positions query
const usePositionsQuery = () => {
return useQuery({
queryKey: ["positions", params],
queryFn: () => getPositions(params),
select: (data) => ({
count: data.data.count as number,
items: data.data.items as PositionDto[],
}),
staleTime: 5 * 60 * 1000,
enabled: params ? true : false,
});
};
// Single position by ID
const usePositionById = (id: string) => {
return useQuery({
queryKey: ["position", id],
queryFn: () => getPositionById(id),
enabled: !!id,
select: (data) => data.data,
});
};
// Position hierarchy
const usePositionHierarchy = (id: string = "root") => {
return useQuery({
queryKey: ["positionHierarchy", id],
queryFn: () => getPositionHierarchy(id),
enabled: !!id,
select: (data) => data.data,
});
};
// Employees under position
const useEmployeesUnderPosition = (positionId: string) => {
return useQuery({
queryKey: ["positionEmployees", positionId],
queryFn: () => getEmployeesUnderPosition(positionId),
enabled: !!positionId || params ? true : false,
});
};
// Position list by unit/organization
const usePositionListByUnitId = (
organizationId: string,
params: PositionQueryParams
) => {
return useQuery({
queryKey: ["positionList", organizationId, params],
queryFn: () => getPositionList(organizationId, params),
enabled: !!organizationId,
select: (data) => data.data,
});
};
const createPositionMutation = useMutation({
mutationFn: ({
payload,
successCallback,
}: {
payload: PositionPayload;
successCallback: () => void;
}) => createPosition(payload),
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({ queryKey: ["positionHierarchy"] });
variables.successCallback();
},
onError: (error) => {
handleError(error);
},
});
const updatePositionMutation = useMutation({
mutationFn: ({
id,
payload,
successCallback,
}: {
id: string;
payload: PositionPayload;
successCallback: () => void;
}) => updatePosition(id, payload),
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({ queryKey: ["positionHierarchy"] });
variables.successCallback();
},
onError: (error) => {
handleError(error);
},
});
const deletePositionMutation = useMutation({
mutationFn: ({
id,
successCallback,
}: {
id: string;
successCallback: () => void;
}) => deletePosition(id),
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({ queryKey: ["positionHierarchy"] });
toast.success("Department removed successfully.");
variables.successCallback();
},
onError: (error: any) => {
// Check if it's a "Referenced Entity" error (meaning department has related employees)
if (error?.response?.status === 400 && error?.response?.data?.message) {
const errorMessage = error?.response?.data?.message;
if (errorMessage.includes("Referenced Entity") || errorMessage.includes("referenced")) {
toast.error(
t("department.cannotDeleteWithEmployees", "Cannot delete department with assigned employees"),
{
description: t("department.reassignEmployeesFirst", "Please reassign or remove all employees from this department first."),
duration: 5000
}
);
} else {
handleError(error);
}
} else {
handleError(error);
}
},
});
return {
usePositionsQuery,
usePositionById,
usePositionHierarchy,
useEmployeesUnderPosition,
usePositionListByUnitId,
createPosition: createPositionMutation.mutateAsync,
isCreating: createPositionMutation.isPending,
updatePosition: updatePositionMutation.mutateAsync,
isUpdating: updatePositionMutation.isPending,
deletePosition: deletePositionMutation.mutateAsync,
isDeleting: deletePositionMutation.isPending,
};
};
export const useMovePosition = () => {
const queryClient = useQueryClient();
const { t } = useTranslation();
const { handleError } = useErrorHandler(t);
return useMutation({
mutationFn: (data: MovePostionPayload) => movePosition(data),
onSuccess: (_data, variables) => {
// ✅ Invalidate anything related to positions so UI updates
queryClient.invalidateQueries({ queryKey: ["positions"] });
queryClient.invalidateQueries({ queryKey: ["positionHierarchy"] });
},
onError: (error) => {
handleError(error);
},
});
};

View File

@@ -0,0 +1,224 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
CreatePositionTypePayload,
PositionRequest,
positionTypeService,
UpdatePositionTypePayload,
} from "../services/api/positionTypesService";
import { toast } from "sonner";
import { useTranslation } from "react-i18next";
import { useErrorHandler } from "@/shared/hooks/useErrorHandler";
import {
PositionTypeDto,
PositionTypesListResponse,
} from "../dto/positions/positionType";
interface positionParams {
take: number;
skip: number;
orderBy: string;
order?: string;
}
interface UsePositionTypeManagerProps {
id?: string;
unitId?: string;
organizationId?: string;
params?: positionParams; // 👈 we expected query params to be passed like this
}
export const usePositionTypes = ({
id,
params = {
take: 1000,
skip: 0,
orderBy: "createdAt:Desc",
},
unitId,
organizationId,
}: UsePositionTypeManagerProps = {}) => {
const queryClient = useQueryClient();
const { t } = useTranslation();
const { handleError } = useErrorHandler(t);
const { data, isLoading, isError, refetch } = useQuery({
queryKey: ["position-types", params],
queryFn: () => positionTypeService.getAll(params).then((res) => res.data),
});
// Single Position Type (for edit)
const {
data: positionType,
isLoading: isLoadingSingle,
isError: isErrorSingle,
refetch: refetchSingle,
} = useQuery<PositionTypeDto>({
queryKey: ["position-type", id],
queryFn: () => positionTypeService.getById(id!).then((res) => res.data),
enabled: !!id,
});
const {
data: positionTypeByUnitId,
isLoading: isLoadingPosition,
isError: isErrorPosition,
refetch: refetchPosition,
} = useQuery<PositionTypesListResponse | undefined>({
queryKey: ["position-type", unitId, params],
queryFn: async () => {
if (!unitId) return undefined;
const res = await positionTypeService.getByUnitId(unitId, params);
return res.data as PositionTypesListResponse | undefined;
},
enabled: !!unitId,
});
// Position types by organization ID
const {
data: positionTypeByOrgId,
isLoading: isLoadingOrgPosition,
isError: isErrorOrgPosition,
refetch: refetchOrgPosition,
} = useQuery<PositionTypesListResponse | undefined>({
queryKey: ["position-type-org", organizationId, params],
queryFn: async () => {
if (!organizationId) return undefined;
const res = await positionTypeService.getByOrganizationId(organizationId, params);
return res.data as PositionTypesListResponse | undefined;
},
enabled: !!organizationId,
});
// Common types with organization ID (includes both org-specific and common types)
const {
data: commonPositionTypesByOrgId,
isLoading: isLoadingCommonOrgTypes,
isError: isErrorCommonOrgTypes,
refetch: refetchCommonOrgTypes,
} = useQuery<PositionTypesListResponse | undefined>({
queryKey: ["position-types-common-org", organizationId, params],
queryFn: async () => {
if (!organizationId) return undefined;
const res = await positionTypeService.getCommonTypesByOrganizationId(organizationId, params);
return res.data as PositionTypesListResponse | undefined;
},
enabled: !!organizationId,
});
// Common types with unit ID (includes both unit-specific and common types)
const {
data: commonPositionTypes,
isLoading: isLoadingCommonTypes,
isError: isErrorCommonTypes,
refetch: refetchCommonTypes,
} = useQuery<PositionTypesListResponse | undefined>({
queryKey: ["position-types-common", unitId, params],
queryFn: async () => {
if (!unitId) return undefined;
const res = await positionTypeService.getCommonTypesById(unitId, params);
return res.data as PositionTypesListResponse | undefined;
},
enabled: !!unitId,
});
// Create
const createPositionType = useMutation({
mutationFn: (payload: CreatePositionTypePayload) =>
positionTypeService.create(payload),
onSuccess: () => {
toast.success("Position type created");
queryClient.invalidateQueries({ queryKey: ["position-types"] });
},
onError: (error) => {
handleError(error);
},
});
// Update
const updatePositionType = useMutation({
mutationFn: ({
id,
data,
}: {
id: string;
data: UpdatePositionTypePayload;
}) => positionTypeService.update(id, data),
onSuccess: () => {
toast.success("Position type updated");
queryClient.invalidateQueries({ queryKey: ["position-types"] });
queryClient.invalidateQueries({ queryKey: ["position-type", id] });
},
onError: () => {},
});
//update positon from to
const updatePositionTypeFromTo = useMutation({
mutationFn: ({ toId, fromId }: { toId: string; fromId: string }) =>
positionTypeService.updateFromto(toId, fromId),
onSuccess: () => {
toast.success("Position type migration updated");
queryClient.invalidateQueries({ queryKey: ["position-types-to"] });
queryClient.invalidateQueries({ queryKey: ["position-type", id] });
},
onError: () => {},
});
//update all postions
const migratePositionsByPositions = useMutation({
mutationFn: ({ id, data }: { id: string; data: PositionRequest }) =>
positionTypeService.updateByPostion(id, data),
onSuccess: () => {
toast.success("Position type migration updated");
queryClient.invalidateQueries({ queryKey: ["position-types-migration"] });
queryClient.invalidateQueries({ queryKey: ["position-type", id] });
},
onError: () => {},
});
// Delete
const deletePositionType = useMutation({
mutationFn: (id: string) => positionTypeService.delete(id),
onSuccess: () => {
toast.success("Position type deleted");
queryClient.invalidateQueries({ queryKey: ["position-types"] });
},
onError: (error) => {
handleError(error);
},
});
return {
positionTypes: data?.items ?? [],
positionTypeResponse: data,
isLoading,
isError,
refetch,
// single
positionType,
isLoadingSingle,
isErrorSingle,
refetchSingle,
// mutations
createPositionType,
updatePositionType,
deletePositionType,
positionTypeByUnitId,
migratePositionsByPositions,
updatePositionTypeFromTo,
refetchPosition,
isErrorPosition,
isLoadingPosition,
// organization-based position types
positionTypeByOrgId,
refetchOrgPosition,
isErrorOrgPosition,
isLoadingOrgPosition,
// common types with organization ID
commonPositionTypesByOrgId,
refetchCommonOrgTypes,
isErrorCommonOrgTypes,
isLoadingCommonOrgTypes,
// common types with unit ID
commonPositionTypes: commonPositionTypes?.items ?? [],
isLoadingCommonTypes,
isErrorCommonTypes,
refetchCommonTypes,
};
};

View File

@@ -0,0 +1,99 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import { useErrorHandler } from "@/shared/hooks/useErrorHandler";
import {
getPositionTypeConfigurationsByUnitId,
getPositionTypeConfigurationById,
createPositionTypeConfiguration,
updatePositionTypeConfiguration,
CreatePositionTypeConfigurationPayload,
UpdatePositionTypeConfigurationPayload,
} from "../services/api/positionTypeConfigurationService";
export const usePositionTypeConfiguration = (unitId?: string | null) => {
const queryClient = useQueryClient();
const { t } = useTranslation();
const { handleError } = useErrorHandler(t);
// GET: List by unitId
const {
data: configurationsData,
isLoading: isLoadingConfigurations,
isError: isConfigurationsError,
refetch: refetchConfigurations,
} = useQuery({
queryKey: ["positionTypeConfigurations", unitId],
queryFn: async () => {
const response = await getPositionTypeConfigurationsByUnitId(unitId!);
return response.data;
},
enabled: !!unitId,
});
// GET: Single by id
const getConfigurationById = (id: string | null) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
return useQuery({
queryKey: ["positionTypeConfiguration", id],
queryFn: async () => {
const response = await getPositionTypeConfigurationById(id!);
return response.data;
},
enabled: !!id,
});
};
// POST: Create
const createConfigurationMutation = useMutation({
mutationFn: async (payload: CreatePositionTypeConfigurationPayload) => {
const response = await createPositionTypeConfiguration(payload);
return response.data;
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ["positionTypeConfigurations", unitId],
});
},
onError: (error) => {
handleError(error);
},
});
// PUT: Update
const updateConfigurationMutation = useMutation({
mutationFn: async ({
id,
payload,
}: {
id: string;
payload: UpdatePositionTypeConfigurationPayload;
}) => {
const response = await updatePositionTypeConfiguration(id, payload);
return response.data;
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ["positionTypeConfigurations", unitId],
});
},
onError: (error) => {
handleError(error);
},
});
return {
// Queries
configurations: configurationsData?.items || [],
totalConfigurations: configurationsData?.count || 0,
isLoadingConfigurations,
isConfigurationsError,
refetchConfigurations,
getConfigurationById,
// Mutations
createConfiguration: createConfigurationMutation.mutateAsync,
isCreatingConfiguration: createConfigurationMutation.isPending,
updateConfiguration: updateConfigurationMutation.mutateAsync,
isUpdatingConfiguration: updateConfigurationMutation.isPending,
};
};

View File

@@ -0,0 +1,207 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
CreatePrefixPayload,
CreateSuffixPayload,
prefixSuffixService,
} from "@/user-management/services/api/prefixSuffixService";
export const usePrefixesList = (
unitId: string,
recordTypeKey: string,
skip = 0,
take = 10
) =>
useQuery({
queryKey: ["prefixes", unitId, recordTypeKey, { cc: false }],
queryFn: () => prefixSuffixService.getPrefix(unitId, recordTypeKey, skip, take),
enabled: !!unitId && !!recordTypeKey,
});
export const useCCPrefixesList = (
unitId: string,
recordTypeKey: string,
skip = 0,
take = 10
) =>
useQuery({
queryKey: ["prefixes", unitId, recordTypeKey, { cc: true }],
queryFn: () =>
prefixSuffixService.getPrefixCC(unitId, recordTypeKey, skip, take),
enabled: !!unitId && !!recordTypeKey,
});
export const usePositionPrefixesList = (
unitId: string,
recordTypeKey: string,
skip = 0,
take = 10
) =>
useQuery({
queryKey: ["prefixes-by-position", unitId, recordTypeKey],
queryFn: () =>
prefixSuffixService.getPrefixByPosition(unitId, recordTypeKey, skip, take),
enabled: !!unitId && !!recordTypeKey,
});
export const usePrefixDetail = (id: string) =>
useQuery({
queryKey: ["prefix", id],
queryFn: () => prefixSuffixService.getPrefixById(id),
enabled: !!id,
});
export const useCreatePrefix = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (payload: CreatePrefixPayload) =>
prefixSuffixService.createPrefix(payload),
onSuccess: (_, payload) => {
queryClient.invalidateQueries({
queryKey: [
"prefixes",
payload.unitId,
payload.recordTypeKey,
{ cc: payload.isForCC },
],
});
},
});
};
export const useUpdatePrefix = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, payload }: { id: string; payload: CreatePrefixPayload }) =>
prefixSuffixService.editPrefix(id, payload),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({
queryKey: [
"prefixes",
variables.payload.unitId,
variables.payload.recordTypeKey,
{ cc: variables.payload.isForCC },
],
});
queryClient.invalidateQueries({ queryKey: ["prefix", variables.id] });
},
});
};
export const useDeletePrefix = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
id,
}: {
id: string;
unitId: string;
recordTypeKey: string;
isForCC: boolean;
}) => prefixSuffixService.deletePrefix(id),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({
queryKey: [
"prefixes",
variables.unitId,
variables.recordTypeKey,
{ cc: variables.isForCC },
],
});
queryClient.invalidateQueries({ queryKey: ["prefix", variables.id] });
},
});
};
export const useSuffixesList = (
unitId: string,
recordTypeKey: string,
skip = 0,
take = 10
) =>
useQuery({
queryKey: ["suffixes", unitId, recordTypeKey, { cc: false }],
queryFn: () => prefixSuffixService.getSuffix(unitId, recordTypeKey, skip, take),
enabled: !!unitId && !!recordTypeKey,
});
export const useCCSuffixesList = (
unitId: string,
recordTypeKey: string,
skip = 0,
take = 10
) =>
useQuery({
queryKey: ["suffixes", unitId, recordTypeKey, { cc: true }],
queryFn: () =>
prefixSuffixService.getSuffixCC(unitId, recordTypeKey, skip, take),
enabled: !!unitId && !!recordTypeKey,
});
export const useSuffixDetail = (id: string) =>
useQuery({
queryKey: ["suffix", id],
queryFn: () => prefixSuffixService.getSuffixById(id),
enabled: !!id,
});
export const useCreateSuffix = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (payload: CreateSuffixPayload) =>
prefixSuffixService.createSuffix(payload),
onSuccess: (_, payload) => {
queryClient.invalidateQueries({
queryKey: [
"suffixes",
payload.unitId,
payload.recordTypeKey,
{ cc: payload.isForCC },
],
});
},
});
};
export const useUpdateSuffix = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, payload }: { id: string; payload: CreateSuffixPayload }) =>
prefixSuffixService.editSuffix(id, payload),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({
queryKey: [
"suffixes",
variables.payload.unitId,
variables.payload.recordTypeKey,
{ cc: variables.payload.isForCC },
],
});
queryClient.invalidateQueries({ queryKey: ["suffix", variables.id] });
},
});
};
export const useDeleteSuffix = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
id,
}: {
id: string;
unitId: string;
recordTypeKey: string;
isForCC: boolean;
}) => prefixSuffixService.deleteSuffix(id),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({
queryKey: [
"suffixes",
variables.unitId,
variables.recordTypeKey,
{ cc: variables.isForCC },
],
});
queryClient.invalidateQueries({ queryKey: ["suffix", variables.id] });
},
});
};

View File

@@ -0,0 +1,106 @@
// hooks/useRecordTagPrefixes.ts
"use client";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { useTranslation } from "react-i18next";
import { useErrorHandler } from "@/shared/hooks/useErrorHandler";
import {
CreatePrefixPayload,
PrefixSuffixListResponseDto,
prefixSuffixService,
} from "../services/api/prefixSuffixService";
export interface UseRecordTagPrefixesParams {
unitId: string;
recordTagId?: string;
skip?: number;
take?: number;
}
export const useRecordTagPrefixes = ({
unitId,
recordTagId,
skip = 0,
take = 10,
}: UseRecordTagPrefixesParams) => {
const { t } = useTranslation();
const { handleError } = useErrorHandler(t);
const queryClient = useQueryClient();
// --- Get Prefixes by Record Tag ---
const {
data: prefixesResponse,
isLoading: isLoadingPrefixes,
isError: isErrorPrefixes,
refetch: refetchPrefixes,
} = useQuery<PrefixSuffixListResponseDto>({
queryKey: ["recordTagPrefixes", unitId, recordTagId, skip, take],
queryFn: async () => {
const { data } = await prefixSuffixService.getPrefixByRecordTagId(
unitId,
recordTagId!,
skip,
take,
);
return data;
},
enabled: !!unitId && !!recordTagId,
staleTime: 5 * 60 * 1000,
retry: false,
});
// --- Create Prefix for Tag ---
const { mutate: createPrefix, isPending: isCreatingPrefix } = useMutation({
mutationFn: async (payload: CreatePrefixPayload) => {
const { data } = await prefixSuffixService.createPrefix(payload);
return data;
},
onSuccess: () => {
toast.success(
t("prefixes.createdSuccess") || "Prefix created successfully ✅",
);
queryClient.invalidateQueries({
queryKey: ["recordTagPrefixes", unitId, recordTagId],
});
},
onError: (error) => {
handleError(error);
},
});
// --- Delete Prefix ---
const { mutate: deletePrefix, isPending: isDeletingPrefix } = useMutation({
mutationFn: async (prefixId: string) => {
const { data } = await prefixSuffixService.deletePrefix(prefixId);
return data;
},
onSuccess: () => {
toast.success(
t("prefixes.deletedSuccess") || "Prefix deleted successfully 🗑️",
);
queryClient.invalidateQueries({
queryKey: ["recordTagPrefixes", unitId, recordTagId],
});
},
onError: (error) => {
handleError(error);
},
});
return {
// queries
prefixes: prefixesResponse?.items ?? [],
total: prefixesResponse?.count ?? 0,
isLoadingPrefixes,
isErrorPrefixes,
refetchPrefixes,
// mutations
createPrefix,
isCreatingPrefix,
deletePrefix,
isDeletingPrefix,
};
};

View File

@@ -0,0 +1,134 @@
import { useQuery, useMutation } from "@tanstack/react-query";
import { toast } from "sonner"; // ✅ shadcn sonner toast
import { useTranslation } from "react-i18next";
import { useErrorHandler } from "@/shared/hooks/useErrorHandler";
import {
CreateRecordTagPayload,
RecordTag,
RecordTagResponse,
UpdateRecordTagPayload,
} from "@/user-management/dto/recordTags/recordTags.type";
import { recordTagService } from "../services/api/recordTagsService";
export const useRecordTags = ({
unitId,
recordTagId,
}: {
unitId?: string | null;
recordTagId?: string;
}) => {
const { t } = useTranslation();
const { handleError } = useErrorHandler(t);
// --- Get Record Tags List by Unit ---
const {
data: recordTagsList,
isLoading: isLoadingRecordTagsList,
isError: isErrorRecordTagsList,
refetch: refetchRecordTagsList,
} = useQuery<RecordTagResponse>({
queryKey: ["recordTagsList", unitId],
queryFn: async () => {
const { data } = await recordTagService.getRecordTagsListWithUnitById(
unitId!
);
return data;
},
enabled: !!unitId,
staleTime: 5 * 60 * 1000,
retry: false,
});
// --- Get Record Tag by Id ---
const {
data: recordTag,
isLoading: isLoadingRecordTag,
isError: isErrorRecordTag,
refetch: refetchRecordTag,
} = useQuery<RecordTag>({
queryKey: ["recordTag", recordTagId],
queryFn: async () => {
const { data } = await recordTagService.getRecordTags(recordTagId!);
return data;
},
enabled: !!recordTagId,
staleTime: 5 * 60 * 1000,
retry: false,
});
// --- Create Record Tag ---
const { mutate: createRecordTag, isPending: isCreatingRecordTag } =
useMutation({
mutationFn: async (payload: CreateRecordTagPayload) => {
const { data } = await recordTagService.createRecordTags(payload);
return data;
},
onSuccess: () => {
toast.success("Record Tag created successfully ✅");
refetchRecordTagsList();
},
onError: (error) => {
handleError(error);
},
});
// --- Update Record Tag ---
const { mutate: updateRecordTag, isPending: isUpdatingRecordTag } =
useMutation({
mutationFn: async ({
id,
payload,
}: {
id: string;
payload: UpdateRecordTagPayload;
}) => {
const { data } = await recordTagService.updateEmployee(id, payload);
return data;
},
onSuccess: () => {
toast.success("Record Tag updated successfully ✨");
refetchRecordTagsList();
},
onError: (error) => {
handleError(error);
},
});
// --- Delete Record Tag ---
const { mutate: deleteRecordTag, isPending: isDeletingRecordTag } =
useMutation({
mutationFn: async (id: string) => {
const { data } = await recordTagService.deleteEmployee(id);
return data;
},
onSuccess: () => {
toast.success("Record Tag deleted successfully 🗑️");
refetchRecordTagsList();
},
onError: (error) => {
handleError(error);
},
});
return {
// list
recordTagsList,
isLoadingRecordTagsList,
isErrorRecordTagsList,
refetchRecordTagsList,
// single
recordTag,
isLoadingRecordTag,
isErrorRecordTag,
refetchRecordTag,
// mutations
createRecordTag,
isCreatingRecordTag,
updateRecordTag,
isUpdatingRecordTag,
deleteRecordTag,
isDeletingRecordTag,
};
};

View File

@@ -0,0 +1,153 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import { useErrorHandler } from "@/shared/hooks/useErrorHandler";
import {
sealService,
CreateSealPayload,
SealListResponse,
SealResponseDto,
SealStatusUpdatePayload,
} from "../services/api/sealService";
export const useSeal = () => {
const queryClient = useQueryClient();
const { t } = useTranslation();
const { handleError } = useErrorHandler(t);
// Query for fetching all seals
const {
data: sealsData,
isLoading: isLoadingSeals,
isError: isSealsError,
refetch: refetchSeals,
} = useQuery<SealListResponse>({
queryKey: ["seals"],
queryFn: async () => {
const response = await sealService.getSeals();
return response.data;
},
enabled: false,
});
// Query for fetching a single seal
const getSealsByUnitId = (id: string | null) => {
return useQuery({
queryKey: ["seal", id],
queryFn: async () => {
if (id) {
const response = await sealService.getSealListsByUnitId(id);
return response.data;
}
},
enabled: !!id,
});
};
// Query for fetching a single seal
const getSealsById = (id: string | null) => {
return useQuery({
queryKey: ["seal", id],
queryFn: async () => {
if (id) {
const response = await sealService.getSeal(id);
return response.data;
}
},
enabled: !!id,
});
};
// Mutation for creating a seal
const createSealMutation = useMutation<
SealResponseDto,
Error,
CreateSealPayload
>({
mutationFn: async (payload) => {
const response = await sealService.createSeal(payload);
return response.data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["seals"] });
},
onError: (error) => {
handleError(error);
},
});
// Mutation for updating a seal
const updateSealMutation = useMutation({
mutationFn: async ({
id,
payload,
}: {
id: string;
payload: Partial<CreateSealPayload>;
}) => {
const response = await sealService.updateSeal(id, payload);
return response.data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["seal"] });
},
onError: (error) => {
handleError(error);
},
});
// Mutation for deleting a seal (actually deactivates via change-status)
const deleteSealMutation = useMutation({
mutationFn: async (id: string) => {
await sealService.changeSealStatus(id, { isCurrent: false });
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["seals"] });
},
onError: (error) => {
handleError(error);
},
});
// Mutation for updating seal upload status
const updateSealStatusMutation = useMutation({
mutationFn: async ({
id,
updateSealStatusPayload,
}: {
id: string;
updateSealStatusPayload: SealStatusUpdatePayload;
}) => {
const response = await sealService.updateSealUploadStatus(
id,
updateSealStatusPayload
);
return response.data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["seals"] });
},
onError: (error) => {
handleError(error);
},
});
return {
// Queries
seals: sealsData?.items || [],
totalSeals: sealsData?.count || 0,
isLoadingSeals,
isSealsError,
refetchSeals,
getSealsByUnitId,
getSealsById,
// Mutations
createSeal: createSealMutation.mutateAsync,
isCreatingSeal: createSealMutation.isPending,
updateSeal: updateSealMutation.mutate,
isUpdatingSeal: updateSealMutation.isPending,
deleteSeal: deleteSealMutation.mutate,
isDeletingSeal: deleteSealMutation.isPending,
updateSealStatus: updateSealStatusMutation.mutateAsync,
isUpdatingStatus: updateSealStatusMutation.isPending,
};
};

View File

@@ -0,0 +1,198 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import { useErrorHandler } from "@/shared/hooks/useErrorHandler";
import { useAuth } from "@/shared/context/AuthContext";
import {
getUnitList,
getMyAdminUnits,
getUnitById,
getUnitHierarchy,
getCurrentUnitEmployees,
createUnit,
updateUnit,
softDeleteUnit,
relateUnitToUnit,
getChildUnits,
RelateUnitToUnitPayload,
UnitPayload,
UnitQueryParams,
} from "@/user-management/services/api/unitService";
export const useUnit = () => {
const queryClient = useQueryClient();
const { t } = useTranslation();
const { handleError } = useErrorHandler(t);
const { user } = useAuth();
const isOrganizationAdmin =
user?.roles?.some((role) => role.key === "organization_admin") ?? false;
const getList = (
organizationId: string,
params?: UnitQueryParams,
enabled: boolean = true,
) => {
return useQuery({
queryKey: ["unitList", organizationId, params ?? {}],
queryFn: () => getUnitList(organizationId, params ?? {}),
enabled: enabled && !!organizationId,
staleTime: 0,
refetchOnMount: "always",
refetchOnWindowFocus: false,
});
};
const getAccessibleList = (
organizationId: string,
params?: UnitQueryParams,
enabled: boolean = true,
) => {
return useQuery({
queryKey: [
"accessibleUnitList",
organizationId,
params ?? {},
isOrganizationAdmin ? "all" : "my-admin-units",
],
queryFn: async () => {
const response = isOrganizationAdmin
? await getUnitList(organizationId, params ?? {})
: await getMyAdminUnits(organizationId);
const responseData = response.data;
const items = Array.isArray(responseData)
? responseData
: (responseData?.items ?? []);
return {
...response,
data: {
...(Array.isArray(responseData) ? {} : responseData),
count: responseData?.count ?? items.length,
items,
},
};
},
enabled: enabled && !!organizationId,
staleTime: 0,
refetchOnMount: "always",
refetchOnWindowFocus: false,
});
};
const getById = (id: string) => {
return useQuery({
queryKey: ["unit", id],
queryFn: () => getUnitById(id),
enabled: !!id,
});
};
const getHierarchy = (id: string) => {
return useQuery({
queryKey: ["unitHierarchy", id],
queryFn: () => getUnitHierarchy(id),
enabled: !!id,
});
};
const getEmployees = (id: string) => {
return useQuery({
queryKey: ["unitEmployees", id],
queryFn: () => getCurrentUnitEmployees(id),
enabled: !!id,
});
};
const getChildren = (parentUnitId: string) => {
return useQuery({
queryKey: ["unitChildren", parentUnitId],
queryFn: () => getChildUnits(parentUnitId),
enabled: !!parentUnitId,
staleTime: 0,
refetchOnMount: "always",
refetchOnWindowFocus: false,
});
};
const create = useMutation({
mutationFn: ({
payload,
successCallback,
}: {
payload: UnitPayload;
successCallback: () => void;
}) => createUnit(payload),
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({ queryKey: ["unitList"] });
queryClient.invalidateQueries({ queryKey: ["unitChildren"] });
variables.successCallback();
},
onError: (error) => {
handleError(error);
},
});
const update = useMutation({
mutationFn: ({
id,
payload,
successCallback,
}: {
id: string;
payload: UnitPayload;
successCallback: () => void;
}) => updateUnit(id, payload),
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({ queryKey: ["unitList"] });
queryClient.invalidateQueries({ queryKey: ["unitChildren"] });
variables.successCallback();
},
onError: (error) => {
handleError(error);
},
});
const remove = useMutation({
mutationFn: (id: string) => softDeleteUnit(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["unitList"] });
queryClient.invalidateQueries({ queryKey: ["unitChildren"] });
},
onError: (error) => {
handleError(error);
},
});
const relate = useMutation({
mutationFn: ({
payload,
}: {
payload: RelateUnitToUnitPayload;
successCallback?: () => void;
}) => relateUnitToUnit(payload),
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({ queryKey: ["unitList"] });
queryClient.invalidateQueries({ queryKey: ["unitChildren"] });
variables.successCallback?.();
},
onError: (error) => {
handleError(error);
},
});
return {
getList,
getAccessibleList,
getById,
getHierarchy,
getEmployees,
getChildren,
createUnit: create.mutateAsync,
isCreating: create.isPending,
updateUnit: update.mutateAsync,
isUpdating: update.isPending,
removeUnit: remove.mutateAsync,
isDeleting: remove.isPending,
relateUnit: relate.mutateAsync,
isRelating: relate.isPending,
};
};

View File

@@ -0,0 +1,51 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import {
approveUserPosition,
fetchUserPositionApprovals,
rejectUserPosition,
type UserPositionApprovalParams,
type UserPositionDecisionPayload,
} from "@/user-management/services/api/userPositionApprovalService";
interface DecisionVariables extends UserPositionDecisionPayload {
id: string;
}
const QUERY_KEY = "user-position-approvals";
export const useUserPositionApprovals = (
params: UserPositionApprovalParams = {},
) => {
const queryClient = useQueryClient();
const approvalsQuery = useQuery({
queryKey: [QUERY_KEY, params],
queryFn: () => fetchUserPositionApprovals(params),
});
const invalidateList = () =>
queryClient.invalidateQueries({ queryKey: [QUERY_KEY] });
const approveMutation = useMutation({
mutationFn: ({ id, comment }: DecisionVariables) =>
approveUserPosition(id, { comment }),
onSuccess: () => {
toast.success("User position approved successfully");
invalidateList();
},
onError: () => toast.error("Failed to approve user position"),
});
const rejectMutation = useMutation({
mutationFn: ({ id, comment }: DecisionVariables) =>
rejectUserPosition(id, { comment }),
onSuccess: () => {
toast.success("User position rejected successfully");
invalidateList();
},
onError: () => toast.error("Failed to reject user position"),
});
return { approvalsQuery, approveMutation, rejectMutation };
};