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,19 @@
import * as React from "react"
const MOBILE_BREAKPOINT = 768
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
}
mql.addEventListener("change", onChange)
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
return () => mql.removeEventListener("change", onChange)
}, [])
return !!isMobile
}

View File

@@ -0,0 +1,15 @@
import { ItemDTO, User } from "@/shared/dto/user/usersDto";
import { getArchivedUserId, OrgQueryParams } from "@/shared/services/organizationsService";
import { useQuery } from "@tanstack/react-query";
export const useArchivedUsers = (UnitId: string,params?:OrgQueryParams) => {
return useQuery({
queryKey: ["archived-users", UnitId,params],
queryFn: async () => {
if (!UnitId) return { items: [], count: 0 };
const { data } = await getArchivedUserId(UnitId,params);
return data as { items: ItemDTO[]; count: number };
},
enabled: !!UnitId,
});
};

View File

@@ -0,0 +1,167 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import type { AxiosResponse } from "axios";
import {
createGlobalOrgConfig,
updateGlobalOrgConfig,
getGlobalOrgConfig,
getListOfGlobalOrgConfig,
createGlobalUnitConfig,
updateGlobalUnitConfig,
deleteGlobalUnitConfig,
getGlobalUnitConfig,
getListOfGlobalUnitConfig,
GlobalOrgConfig,
GlobalUnitConfig,
createOrganizationConfig,
updateOrganizationConfig,
deleteOrganizationConfig,
getOrganizationConfig,
OrganizationConfigPayload,
} from "@/shared/services/organizationConfigService";
// 🔹 Global Org Config Hooks
export const useGetGlobalOrgConfig = (id: string) => {
return useQuery<AxiosResponse>({
queryKey: ["globalOrgConfig", id],
queryFn: () => getGlobalOrgConfig(id),
enabled: !!id,
});
};
export const useGetListOfGlobalOrgConfig = (id: string) => {
return useQuery<AxiosResponse>({
queryKey: ["listGlobalOrgConfig", id],
queryFn: () => getListOfGlobalOrgConfig(id),
enabled: !!id,
});
};
export const useCreateGlobalOrgConfig = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: GlobalOrgConfig) => createGlobalOrgConfig(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["listGlobalOrgConfig"] });
},
});
};
export const useUpdateGlobalOrgConfig = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string; data: GlobalOrgConfig }) =>
updateGlobalOrgConfig(id, data),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({
queryKey: ["globalOrgConfig", variables.id],
});
queryClient.invalidateQueries({
queryKey: ["listGlobalOrgConfig"],
});
},
});
};
// 🔹 Global Unit Config Hooks
export const useGetGlobalUnitConfig = (id: string) => {
return useQuery<AxiosResponse>({
queryKey: ["globalUnitConfig", id],
queryFn: () => getGlobalUnitConfig(id),
enabled: !!id,
});
};
export const useGetListOfGlobalUnitConfig = (id: string) => {
return useQuery<AxiosResponse>({
queryKey: ["listGlobalUnitConfig", id],
queryFn: () => getListOfGlobalUnitConfig(id),
enabled: !!id,
});
};
export const useCreateGlobalUnitConfig = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: any) => createGlobalUnitConfig(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["listGlobalUnitConfig"] });
},
});
};
export const useUpdateGlobalUnitConfig = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string; data: any }) =>
updateGlobalUnitConfig(id, data),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({
queryKey: ["globalUnitConfig", variables.id],
});
queryClient.invalidateQueries({
queryKey: ["listGlobalUnitConfig"],
});
},
});
};
export const useDeleteGlobalUnitConfig = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => deleteGlobalUnitConfig(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["listGlobalUnitConfig"] });
queryClient.invalidateQueries({ queryKey: ["globalUnitConfig"] });
},
});
};
// 🔹 Organization Configuration Hooks (/organization-configurations)
export const useGetOrganizationConfig = (organizationId: string) => {
return useQuery<AxiosResponse>({
queryKey: ["organizationConfig", organizationId],
queryFn: () => getOrganizationConfig(organizationId),
enabled: !!organizationId,
});
};
export const useCreateOrganizationConfig = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: OrganizationConfigPayload) =>
createOrganizationConfig(data),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({
queryKey: ["organizationConfig", variables.organizationId],
});
},
});
};
export const useUpdateOrganizationConfig = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
id,
data,
}: {
id: string;
data: OrganizationConfigPayload;
}) => updateOrganizationConfig(id, data),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({
queryKey: ["organizationConfig", variables.data.organizationId],
});
},
});
};
export const useDeleteOrganizationConfig = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => deleteOrganizationConfig(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["organizationConfig"] });
},
});
};

View File

@@ -0,0 +1,194 @@
import { OrganizationDto } from "@/shared/dto/organization/organizationDto";
import { getOrganizations } from "@/shared/services/organizationsService";
import { useQuery } from "@tanstack/react-query";
import Cookies from "js-cookie";
import { toast } from "sonner";
import { useOrganizations } from "./useOrganizations";
// Types for dashboard data
export interface DashboardStats {
totalOrganizations: number;
activeOrganizations: number;
inactiveOrganizations: number;
pendingRequests: number;
orgAdmins: number;
recentActivities: Activity[];
percentChanges: {
orgs: number;
active: number;
inactive: number;
pending: number;
admins: number;
};
}
export interface Activity {
id: string;
type: string;
description: string;
user: string;
timestamp: string;
entity: string;
entityId: string;
}
// Mock data
const mockDashboardData: DashboardStats = {
totalOrganizations: 48,
activeOrganizations: 35,
inactiveOrganizations: 13,
pendingRequests: 7,
orgAdmins: 21,
recentActivities: [
{
id: "1",
type: "CREATE",
description: "Created new organization",
user: "John Doe",
timestamp: "2023-07-15T10:30:00Z",
entity: "Woreda 01",
entityId: "org-1",
},
{
id: "2",
type: "UPDATE",
description: "Updated organization status",
user: "Jane Smith",
timestamp: "2023-07-14T14:45:00Z",
entity: "Woreda 02",
entityId: "org-2",
},
{
id: "3",
type: "DELETE",
description: "Deleted organization",
user: "Admin User",
timestamp: "2023-07-13T09:15:00Z",
entity: "Test Org",
entityId: "org-3",
},
{
id: "4",
type: "CREATE",
description: "Added admin user",
user: "System",
timestamp: "2023-07-12T16:20:00Z",
entity: "Samuel Bekele",
entityId: "user-1",
},
{
id: "5",
type: "UPDATE",
description: "Changed organization settings",
user: "Jane Smith",
timestamp: "2023-07-11T11:05:00Z",
entity: "Yeka Subcity",
entityId: "org-4",
},
],
percentChanges: {
orgs: 12.5,
active: 8.2,
inactive: -3.7,
pending: -15.4,
admins: 5.6,
},
};
const fetchDashboardStats = async (
totalOrganizations: number
): Promise<DashboardStats> => {
// Use mock data for now, but replace totalOrganizations with the actual count
const dashboardData = { ...mockDashboardData };
dashboardData.totalOrganizations =
totalOrganizations || dashboardData.totalOrganizations;
return dashboardData;
};
const fetchRecentOrganizations = async (): Promise<OrganizationDto[]> => {
try {
const tenantKey = Cookies.get("tenant-key");
if (!tenantKey) {
console.error("No tenant key found");
return [];
}
const headers = {
tenantKey,
unitId: Cookies.get("unit-id") || undefined,
};
const { data } = await getOrganizations({
orderBy: "createdAt",
take: 5,
});
return data.items;
} catch (error) {
console.error("Error fetching recent organizations:", error);
return [];
}
};
export const useDashboardData = () => {
const { organizationsResponse } = useOrganizations("Org");
const {
data: stats,
isLoading: isStatsLoading,
isError: isStatsError,
refetch: refetchStats,
} = useQuery({
queryKey: ["dashboardStats"],
queryFn: async () => {
try {
const totalOrganizations = organizationsResponse?.count || 0;
return await fetchDashboardStats(totalOrganizations);
} catch (error) {
console.error("Failed to fetch dashboard statistics:", error);
toast.error("Failed to fetch dashboard statistics");
return mockDashboardData;
}
},
staleTime: 5 * 60 * 1000, // 5 minutes
retry: 1,
});
const {
data: recentOrgs,
isLoading: isOrgsLoading,
isError: isOrgsError,
refetch: refetchOrgs,
} = useQuery({
queryKey: ["recentOrganizations"],
queryFn: async () => {
try {
return await fetchRecentOrganizations();
} catch (error) {
console.error("Failed to fetch recent organizations:", error);
toast.error("Failed to fetch recent organizations");
return [];
}
},
staleTime: 2 * 60 * 1000, // 2 minutes
retry: 1,
});
const isLoading = isStatsLoading || isOrgsLoading;
const isError = isStatsError || isOrgsError;
const refetch = () => {
refetchStats();
refetchOrgs();
};
const orgCount = organizationsResponse?.count;
return {
stats: stats || orgCount,
recentOrgs: recentOrgs || [],
isLoading,
isError,
refetch,
};
};

View File

@@ -0,0 +1,253 @@
import { useState, useEffect } from "react";
import axios from "axios";
import { Position } from "./usePositions";
interface ApiResponse<T> {
success: boolean;
data: T;
message?: string;
}
export interface Department {
id: string;
name: string;
positions: Position[];
isExpanded?: boolean;
parentDepartmentId?: string;
}
export const useDepartments = (unitId: string) => {
const [departments, setDepartments] = useState<Department[]>([]);
const [selectedDepartmentId, setSelectedDepartmentId] = useState<string>("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
// Fetch departments for a unit
const fetchDepartments = async () => {
if (!unitId) {
setDepartments([]);
setSelectedDepartmentId("");
return;
}
setLoading(true);
try {
// In a real app, this would be an API call
setTimeout(() => {
// Mock departments for this unit
const mockDepartments: Department[] = [
{
id: "dept1",
name: "Directorate",
positions: [],
isExpanded: true,
},
{
id: "dept2",
name: "Development",
positions: [],
isExpanded: false,
},
{
id: "dept3",
name: "Operations",
positions: [],
isExpanded: false,
},
];
setDepartments(mockDepartments);
if (mockDepartments.length > 0) {
setSelectedDepartmentId(mockDepartments[0].id);
} else {
setSelectedDepartmentId("");
}
setLoading(false);
}, 500);
} catch (err) {
setError(
err instanceof Error ? err : new Error("Failed to fetch departments")
);
setLoading(false);
}
};
// Create a new department
const createDepartment = async (
departmentData: Partial<Department>
): Promise<Department> => {
if (!unitId) {
throw new Error("No unit selected");
}
setLoading(true);
try {
// In a real app, this would be an API call
// const response = await axios.post<ApiResponse<Department>>(
// `/api/units/${unitId}/departments`,
// departmentData
// );
// const newDepartment = response.data.data;
// For now, simulate API response
const newDepartment: Department = {
id: `dept-${Date.now()}`,
name: departmentData.name || "New Department",
positions: [],
isExpanded: false,
parentDepartmentId: departmentData.parentDepartmentId,
};
setDepartments((prev) => [...prev, newDepartment]);
setLoading(false);
return newDepartment;
} catch (err) {
setError(
err instanceof Error ? err : new Error("Failed to create department")
);
setLoading(false);
throw err;
}
};
// Update an existing department
const updateDepartment = async (
departmentId: string,
updates: Partial<Department>
): Promise<Department> => {
setLoading(true);
try {
// In a real app, this would be an API call
// const response = await axios.put<ApiResponse<Department>>(
// `/api/departments/${departmentId}`,
// updates
// );
// const updatedDepartment = response.data.data;
// For now, simulate API response
const updatedDepartments = departments.map((department) =>
department.id === departmentId
? { ...department, ...updates }
: department
);
setDepartments(updatedDepartments);
setLoading(false);
const updatedDepartment = updatedDepartments.find(
(d) => d.id === departmentId
);
if (!updatedDepartment) {
throw new Error("Department not found after update");
}
return updatedDepartment;
} catch (err) {
setError(
err instanceof Error ? err : new Error("Failed to update department")
);
setLoading(false);
throw err;
}
};
// Delete a department
const deleteDepartment = async (departmentId: string): Promise<void> => {
setLoading(true);
try {
// In a real app, this would be an API call
// await axios.delete<ApiResponse<void>>(`/api/departments/${departmentId}`);
// For now, simulate API response
setDepartments((prev) =>
prev.filter((department) => department.id !== departmentId)
);
// If the deleted department was selected, select another department
if (selectedDepartmentId === departmentId) {
const remaining = departments.filter(
(department) => department.id !== departmentId
);
if (remaining.length > 0) {
setSelectedDepartmentId(remaining[0].id);
} else {
setSelectedDepartmentId("");
}
}
setLoading(false);
} catch (err) {
setError(
err instanceof Error ? err : new Error("Failed to delete department")
);
setLoading(false);
throw err;
}
};
// Add a sub-department
const addSubDepartment = async (
parentDepartmentId: string,
name: string
): Promise<Department> => {
const subdepartment: Partial<Department> = {
name,
parentDepartmentId,
};
return createDepartment(subdepartment);
};
// Toggle expand/collapse for a department
const toggleDepartmentExpand = (departmentId: string) => {
setDepartments((prev) =>
prev.map((department) =>
department.id === departmentId
? { ...department, isExpanded: !department.isExpanded }
: department
)
);
};
// Get root-level departments (no parent)
const getRootDepartments = () => {
return departments.filter((department) => !department.parentDepartmentId);
};
// Get sub-departments of a department
const getSubDepartments = (parentDepartmentId: string) => {
return departments.filter(
(department) => department.parentDepartmentId === parentDepartmentId
);
};
// Select a department
const selectDepartment = (departmentId: string) => {
setSelectedDepartmentId(departmentId);
};
// Load departments when unitId changes
useEffect(() => {
fetchDepartments();
}, [unitId]);
return {
departments,
selectedDepartmentId,
selectedDepartment: departments.find(
(department) => department.id === selectedDepartmentId
),
loading,
error,
fetchDepartments,
createDepartment,
updateDepartment,
deleteDepartment,
addSubDepartment,
toggleDepartmentExpand,
getRootDepartments,
getSubDepartments,
selectDepartment,
};
};

View File

@@ -0,0 +1,341 @@
import { useState, useEffect } from "react";
import axios from "axios";
export type InviteStatus = "Pending" | "Accepted" | "Not Invited";
export type EmployeeRole =
| "TeamLeader"
| "TeamMember"
| "Director"
| "Manager"
| "Regular";
interface ApiResponse<T> {
success: boolean;
data: T;
message?: string;
}
export interface Employee {
id: string;
name: string;
email?: string;
role?: EmployeeRole;
inviteStatus?: InviteStatus;
title?: string;
phone?: string;
dateAdded?: string;
}
export const useEmployees = (positionId: string, departmentId?: string) => {
const [employees, setEmployees] = useState<Employee[]>([]);
const [selectedEmployeeId, setSelectedEmployeeId] = useState<string>("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
// Fetch employees for a position or department
const fetchEmployees = async () => {
if (!positionId && !departmentId) {
setEmployees([]);
setSelectedEmployeeId("");
return;
}
setLoading(true);
try {
// In a real app, this would be an API call
// if (positionId) {
// const response = await axios.get<ApiResponse<Employee[]>>(`/api/positions/${positionId}/employees`);
// setEmployees(response.data.data);
// } else if (departmentId) {
// const response = await axios.get<ApiResponse<Employee[]>>(`/api/departments/${departmentId}/employees`);
// setEmployees(response.data.data);
// }
// For now, simulate API response with mock data
setTimeout(() => {
const mockEmployees: Employee[] = [
{
id: "emp1",
name: "Abebe Kebede",
email: "abebe@example.com",
role: "Director",
title: "Department Director",
phone: "+251911123456",
dateAdded: "2023-05-15",
inviteStatus: "Accepted",
},
{
id: "emp2",
name: "Kebede Abebe",
email: "kebede@example.com",
role: "TeamLeader",
title: "Team Lead - Development",
phone: "+251922123456",
dateAdded: "2023-06-20",
inviteStatus: "Pending",
},
{
id: "emp3",
name: "Chala Demeke",
email: "chala@example.com",
role: "TeamMember",
title: "Senior Developer",
phone: "+251933123456",
dateAdded: "2023-07-10",
inviteStatus: "Not Invited",
},
{
id: "emp4",
name: "Tigist Alemu",
email: "tigist@example.com",
role: "Manager",
title: "Project Manager",
phone: "+251944123456",
dateAdded: "2023-08-05",
inviteStatus: "Accepted",
},
];
setEmployees(mockEmployees);
if (mockEmployees.length > 0) {
setSelectedEmployeeId(mockEmployees[0].id);
} else {
setSelectedEmployeeId("");
}
setLoading(false);
}, 500);
} catch (err) {
setError(
err instanceof Error ? err : new Error("Failed to fetch employees")
);
setLoading(false);
}
};
// Create a new employee
const createEmployee = async (
employeeData: Partial<Employee>
): Promise<Employee> => {
if (!positionId && !departmentId) {
throw new Error("No position or department selected");
}
setLoading(true);
try {
// In a real app, this would be an API call
// let response;
// if (positionId) {
// response = await axios.post<ApiResponse<Employee>>(
// `/api/positions/${positionId}/employees`,
// employeeData
// );
// } else if (departmentId) {
// response = await axios.post<ApiResponse<Employee>>(
// `/api/departments/${departmentId}/employees`,
// employeeData
// );
// } else {
// throw new Error('No position or department ID provided');
// }
// const newEmployee = response.data.data;
// For now, simulate API response
const newEmployee: Employee = {
id: `emp-${Date.now()}`,
name: employeeData.name || "New Employee",
email: employeeData.email,
role: employeeData.role || "Regular",
title: employeeData.title,
phone: employeeData.phone,
dateAdded: new Date().toISOString().split("T")[0],
inviteStatus: employeeData.inviteStatus || "Not Invited",
};
setEmployees((prev) => [...prev, newEmployee]);
setLoading(false);
return newEmployee;
} catch (err) {
setError(
err instanceof Error ? err : new Error("Failed to create employee")
);
setLoading(false);
throw err;
}
};
// Update an existing employee
const updateEmployee = async (
employeeId: string,
updates: Partial<Employee>
): Promise<Employee> => {
setLoading(true);
try {
// In a real app, this would be an API call
// const response = await axios.put<ApiResponse<Employee>>(
// `/api/employees/${employeeId}`,
// updates
// );
// const updatedEmployee = response.data.data;
// For now, simulate API response
const updatedEmployees = employees.map((employee) =>
employee.id === employeeId ? { ...employee, ...updates } : employee
);
setEmployees(updatedEmployees);
setLoading(false);
const updatedEmployee = updatedEmployees.find((e) => e.id === employeeId);
if (!updatedEmployee) {
throw new Error("Employee not found after update");
}
return updatedEmployee;
} catch (err) {
setError(
err instanceof Error ? err : new Error("Failed to update employee")
);
setLoading(false);
throw err;
}
};
// Delete an employee
const deleteEmployee = async (employeeId: string): Promise<void> => {
setLoading(true);
try {
// In a real app, this would be an API call
// await axios.delete<ApiResponse<void>>(`/api/employees/${employeeId}`);
// For now, simulate API response
setEmployees((prev) =>
prev.filter((employee) => employee.id !== employeeId)
);
// If the deleted employee was selected, select another employee
if (selectedEmployeeId === employeeId) {
const remaining = employees.filter(
(employee) => employee.id !== employeeId
);
if (remaining.length > 0) {
setSelectedEmployeeId(remaining[0].id);
} else {
setSelectedEmployeeId("");
}
}
setLoading(false);
} catch (err) {
setError(
err instanceof Error ? err : new Error("Failed to delete employee")
);
setLoading(false);
throw err;
}
};
// Send invitation to an employee
const inviteEmployee = async (employeeId: string): Promise<string> => {
setLoading(true);
try {
// In a real app, this would be an API call
// const response = await axios.post<ApiResponse<{ inviteLink: string }>>(
// `/api/employees/${employeeId}/invite`
// );
// return response.data.data.inviteLink;
// For now, simulate API response
return new Promise((resolve) => {
setTimeout(() => {
const inviteLink = `https://example.com/invite/${employeeId}-${Date.now()}`;
// Update employee status in state
setEmployees((prev) =>
prev.map((employee) =>
employee.id === employeeId
? { ...employee, inviteStatus: "Pending" as InviteStatus }
: employee
)
);
setLoading(false);
resolve(inviteLink);
}, 1000);
});
} catch (err) {
setError(
err instanceof Error ? err : new Error("Failed to send invitation")
);
setLoading(false);
throw err;
}
};
// Get invite status for an employee
const getInviteStatus = async (employeeId: string): Promise<InviteStatus> => {
setLoading(true);
try {
// In a real app, this would be an API call
// const response = await axios.get<ApiResponse<{ status: InviteStatus }>>(
// `/api/employees/${employeeId}/invite-status`
// );
// return response.data.data.status;
// For now, simulate API response
return new Promise((resolve) => {
setTimeout(() => {
const employee = employees.find((e) => e.id === employeeId);
setLoading(false);
resolve(employee?.inviteStatus || "Not Invited");
}, 500);
});
} catch (err) {
setError(
err instanceof Error
? err
: new Error("Failed to get invitation status")
);
setLoading(false);
throw err;
}
};
// Select an employee
const selectEmployee = (employeeId: string) => {
setSelectedEmployeeId(employeeId);
};
// Filter employees by role
const getEmployeesByRole = (role: EmployeeRole) => {
return employees.filter((employee) => employee.role === role);
};
// Filter employees by invite status
const getEmployeesByInviteStatus = (status: InviteStatus) => {
return employees.filter((employee) => employee.inviteStatus === status);
};
// Load employees when positionId or departmentId changes
useEffect(() => {
fetchEmployees();
}, [positionId, departmentId]);
return {
employees,
selectedEmployeeId,
selectedEmployee: employees.find(
(employee) => employee.id === selectedEmployeeId
),
loading,
error,
fetchEmployees,
createEmployee,
updateEmployee,
deleteEmployee,
inviteEmployee,
getInviteStatus,
selectEmployee,
getEmployeesByRole,
getEmployeesByInviteStatus,
};
};

View File

@@ -0,0 +1,106 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import {
activateUser,
approveUser,
deactivateUser,
deleteUser,
getAllExternalUsers,
getPendingUsers,
} from "../services/api/userService";
// 📦 Query Key
const queryKeys = {
pendingUsers: ["external-users", "pending"],
allExternalUsers: ["external-users", "all"],
};
export enum userTypeEnum {
external = "external_organization",
individual = "individual",
externalUsers = "external_organization,individual",
}
export interface ExternalQueryParams {
orderBy?: string;
take?: number;
skip?: number;
order?: string;
userType?: userTypeEnum;
name?: string;
email?: string;
username?: string;
phoneNumber?: string;
}
// ✅ Hook: Get pending external users
export const usePendingExternalUsers = (params?: ExternalQueryParams) => {
return useQuery({
queryKey: queryKeys.pendingUsers,
queryFn: async () => {
const users = await getPendingUsers(params);
return users.data;
},
});
};
// ✅ Hook: Get all external users
export const useAllExternalUsers = (params?: ExternalQueryParams) => {
return useQuery({
queryKey: [...queryKeys.allExternalUsers, params],
queryFn: async () => {
const users = await getAllExternalUsers(params);
return users.data;
},
});
};
// ✅ Mutation: Activate user
export const useActivateExternalUser = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => activateUser(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.pendingUsers });
queryClient.invalidateQueries({ queryKey: queryKeys.allExternalUsers });
},
});
};
// Mutatae Approve user
export const useApproveExternalUser = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, status }: { id: string; status: string }) =>
approveUser(id, status),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.pendingUsers });
queryClient.invalidateQueries({ queryKey: queryKeys.allExternalUsers });
},
});
};
// ✅ Mutation: Deactivate user
export const useDeactivateExternalUser = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => deactivateUser(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.pendingUsers });
queryClient.invalidateQueries({ queryKey: queryKeys.allExternalUsers });
},
});
};
// ✅ Mutation: Delete user
export const useDeleteExternalUser = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => deleteUser(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: queryKeys.pendingUsers });
queryClient.invalidateQueries({ queryKey: queryKeys.allExternalUsers });
},
});
};

View File

@@ -0,0 +1,14 @@
import { useQuery } from "@tanstack/react-query";
import { MainDtoResponse } from "../dto/SuperAdminDto";
import { getAllRecords } from "../services/api/userService";
import { ExternalQueryParams } from "./useExternalUsers";
export const useMigratedData = (unitId: string,params?:ExternalQueryParams) => {
return useQuery({
queryKey: ["migratedData",unitId,params],
queryFn: async () => {
const { data } = await getAllRecords(unitId,params);
return data as MainDtoResponse;
},
});
};

View File

@@ -0,0 +1,186 @@
import { resendVerificationCode } from "@/shared/services/authService";
import {
assignOrganizationAdmin,
assignUnitAdmin,
fetchAllUnitAdminById,
fetchUnitAdminById,
getOrganizationAdminById,
getOrganizationAdmins,
OrgAdminQueryParams,
OrganizationAdminPayload,
UnitAdminPayload,
updateOrganizationAdmin,
} from "@/super-admin/services/api/organizationAdminService";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { AxiosError } from "axios";
import { toast } from "sonner";
import { useTranslation } from "react-i18next";
import { useErrorHandler } from "@/shared/hooks/useErrorHandler";
// Resend verification code API call
const resendVerificationCodeToAdmin = async ({
organizationId,
email,
phoneNumber,
}: {
organizationId: string;
email: string;
phoneNumber: string;
}) => {
const response = await fetch("/api/resend-verification-code", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ organizationId, email, phoneNumber }),
});
if (!response.ok) {
throw new Error("Failed to resend verification code");
}
return response.json();
};
export const useOrganizationAdmins = (params?: OrgAdminQueryParams) => {
const queryClient = useQueryClient();
const { t } = useTranslation();
const { handleError } = useErrorHandler(t);
// Get all admins
const {
data: adminsResponse,
isLoading,
isError,
refetch,
} = useQuery({
queryKey: ["organizationAdmins", params],
queryFn: async () => {
const { data } = await getOrganizationAdmins(params);
return {
count: data,
items: data,
};
},
staleTime: 5 * 60 * 1000,
retry: false,
enabled: false,
});
// Create admin
const { mutate: createAdminMutation, isPending: isCreating } = useMutation({
mutationFn: async ({
payload,
successCallback,
}: {
payload: OrganizationAdminPayload;
successCallback: () => void;
}) => assignOrganizationAdmin(payload),
onSuccess: (_data, variables) => {
toast.success("Admin assigned successfully");
queryClient.invalidateQueries({ queryKey: ["organizationAdmins"] });
variables.successCallback();
},
onError: (error) => {
handleError(error);
},
});
const { mutate: createUnitAdminMutation, isPending: isUnitCreating } = useMutation({
mutationFn: async ({
payload,
successCallback,
}: {
payload: UnitAdminPayload;
successCallback: () => void;
}) => assignUnitAdmin(payload),
onSuccess: (_data, variables) => {
toast.success("Admin assigned successfully");
queryClient.invalidateQueries({ queryKey: ["organizationAdmins"] });
variables.successCallback();
},
onError: (error) => {
handleError(error);
},
});
// Update admin
const { mutate: updateAdminMutation, isPending: isUpdating } = useMutation({
mutationFn: ({
id,
data,
}: {
id: string;
data: OrganizationAdminPayload;
}) => updateOrganizationAdmin(id, data),
onSuccess: () => {
toast.success("Admin updated successfully");
queryClient.invalidateQueries({ queryKey: ["organizationAdmins"] });
},
onError: (error) => {
handleError(error);
},
});
// Get admin by ID
const getAdminById = async (id: string) => {
try {
return await getOrganizationAdminById(id);
} catch (error) {
toast.error("Failed to fetch admin details");
return undefined;
}
};
const getUnitAdminById = async (id: string) => {
try {
return await fetchUnitAdminById(id);
} catch (error) {
toast.error("Failed to fetch unit admin details");
return undefined;
}
};
const getAllAdminById = async (id: string, params?: OrgAdminQueryParams) => {
try {
return await fetchAllUnitAdminById(id, params);
} catch (error) {
toast.error("Failed to fetch all admin details");
return undefined;
}
};
// Resend invitation (verification code)
const resendInvitation = async ({
email,
phoneNumber,
}: {
email: string;
phoneNumber: string;
}) => {
try {
await resendVerificationCode({
email,
phoneNumber,
});
} catch (error) {
console.error("Failed to resend verification code:", error);
throw error;
}
};
return {
adminsResponse,
createAdmin: createAdminMutation,
createUnitAdmin:createUnitAdminMutation,
updateAdmin: updateAdminMutation,
getAdminById,
getUnitAdminById,
resendInvitation,
getAllAdminById,// 👈 important to expose this
isCreating,
isUpdating,
isLoading,
isError,
refetch,
};
};

View File

@@ -0,0 +1,83 @@
import { useQuery } from "@tanstack/react-query";
import {
getOrganizationTypes,
OrgTypeResponse,
OrgType,
} from "@/super-admin/services/api/organizationTypesService";
import Cookies from "js-cookie";
// Mock data to use when the API fails
const MOCK_ORGANIZATION_TYPES: OrgType[] = [
{
id: "1",
name: {
en: "Government",
am: "መንግስታዊ",
},
key: "government",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
{
id: "2",
name: {
en: "Private",
am: "የግል",
},
key: "private",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
{
id: "3",
name: {
en: "Non-Governmental",
am: "መንግስታዊ ያልሆነ",
},
key: "ngo",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
];
export const useOrganizationTypes = () => {
const {
data: organizationTypesResponse,
isLoading,
isError,
refetch,
} = useQuery<OrgTypeResponse>({
queryKey: ["organizationTypes"],
queryFn: async () => {
try {
// Use a default tenant key if none is found
const tenantKey = Cookies.get("tenant-key") || "default-tenant-key";
const unitId = Cookies.get("unit-id");
const response = await getOrganizationTypes();
return response.data;
} catch (error) {
console.error("Error fetching organization types:", error);
// Return mock data so the UI doesn't break
return {
count: MOCK_ORGANIZATION_TYPES.length,
items: MOCK_ORGANIZATION_TYPES,
};
}
},
staleTime: 5 * 60 * 1000,
// Always enable the query, regardless of tenant key
enabled: true,
});
return {
// Ensure we always return at least the mock data if nothing is returned
organizationTypesResponse: organizationTypesResponse || {
count: MOCK_ORGANIZATION_TYPES.length,
items: MOCK_ORGANIZATION_TYPES,
},
isLoading,
isError,
refetch,
};
};

View File

@@ -0,0 +1,251 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
createOrganization,
OrgQueryParams,
OrganizationPayload,
getOrganizationById,
updateOrganization,
activateOrganization,
deActivateOrganization,
getOrganizationsWithAdminFlag,
softDeleteOrganization,
} from "@/shared/services/organizationsService";
import {
OrganizationAdminsDto,
OrganizationDetailDTO,
OrganizationDto,
} from "@/shared/dto/organization/organizationDto";
import { useNavigate } from "react-router-dom";
import { toast } from "sonner";
import { useTranslation } from "react-i18next";
import { useErrorHandler } from "@/shared/hooks/useErrorHandler";
interface ApiResponse<T> {
success: boolean;
data: T;
message?: string;
}
interface Unit {
id: string;
name: string;
departments: any[];
isExpanded?: boolean;
}
export interface Organization {
id: string;
name: string;
units: Unit[];
isExpanded?: boolean;
}
export const useOrganizations = (
type: "Admin" | "Org",
params?: OrgQueryParams,
) => {
const queryClient = useQueryClient();
const navigate = useNavigate();
const { t } = useTranslation();
const { handleError } = useErrorHandler(t);
const {
data: organizationsResponse,
isLoading,
isError,
refetch,
} = useQuery({
queryKey: ["organizations", params],
queryFn: async () => {
const { data } = await getOrganizationsWithAdminFlag(params);
return {
count: data.count as number,
items: data.items as OrganizationDto[],
};
},
staleTime: 5 * 60 * 1000,
retry: false,
enabled: !!(type === "Org"),
});
const {
data: organizationsAdminsResponse,
isLoading: isLoadingAdmins,
isError: isErrorAdmins,
refetch: refetchAdmins,
} = useQuery({
queryKey: ["organizationAdmins", params?.skip, params?.take, params?.name, params?.orderBy],
queryFn: async () => {
const { data } = await getOrganizationsWithAdminFlag(params);
return {
count: data.count as number,
items: data.items as OrganizationAdminsDto[],
};
},
staleTime: 5 * 60 * 1000,
retry: false,
enabled: !!(type === "Admin"),
});
const {
mutate: createMutation,
isPending: isOrganizationCreating,
isError: organizationCreationError,
status: organizationCreationStatus,
} = useMutation({
mutationFn: async (payload: OrganizationPayload) => {
const { data } = await createOrganization(payload);
return data;
},
onSuccess: () => {
toast.success("Organization created successfully!", {
description: "Redirecting...",
});
navigate("/user-management/organizations");
queryClient.invalidateQueries({ queryKey: ["organizations"] });
},
onError: (error) => {
handleError(error);
},
});
const {
mutate: editMutation,
isPending: isOrganizationEditing,
isError: organizationEditError,
status: organizationEditStatus,
} = useMutation({
mutationFn: async ({
id,
payload,
}: {
id: string;
payload: OrganizationPayload;
}) => {
const { data } = await updateOrganization(id, payload);
return data;
},
onSuccess: () => {
toast.success("Organization created successfully!", {
description: "Redirecting...",
});
navigate("/user-management/organizations");
queryClient.invalidateQueries({ queryKey: ["organizations"] });
},
onError: (error) => {
handleError(error);
},
});
const { mutate: activateMutation, isPending: isOrganizationActivating } =
useMutation({
mutationFn: async ({ id }: { id: string }) => {
const { data } = await activateOrganization(id);
return data;
},
onSuccess: () => {
toast.success("Organization Activated successfully!", {
description: "Redirecting...",
});
navigate("/user-management/organizations");
queryClient.invalidateQueries({ queryKey: ["organizations"] });
},
onError: (error) => {
handleError(error);
},
});
const { mutate: deActivateMutation, isPending: isOrganizationDeactivating } =
useMutation({
mutationFn: async ({ id }: { id: string }) => {
const { data } = await deActivateOrganization(id);
return data;
},
onSuccess: () => {
toast.success("Organization De-Activated successfully!", {
description: "Redirecting...",
});
navigate("/user-management/organizations");
queryClient.invalidateQueries({ queryKey: ["organizations"] });
},
onError: (error) => {
handleError(error);
},
});
const { mutate: deleteMutation, isPending: isOrganizationDeleting } =
useMutation({
mutationFn: async ({ id }: { id: string }) => {
const { data } = await softDeleteOrganization(id);
return data;
},
onSuccess: () => {
toast.success("Organization Deleted successfully!", {
description: "Redirecting...",
});
navigate("/user-management/organizations");
queryClient.invalidateQueries({ queryKey: ["organizations"] });
},
onError: (error) => {
handleError(error);
},
});
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,
createOrganization: createMutation,
editOrganization: editMutation,
isCreating: isOrganizationCreating,
isEditing: isOrganizationEditing,
editStatus: organizationEditStatus,
organizationCreationError,
organizationEditError,
getOrganizationByDetails: getOrganization,
isFetchingOrganization,
activateOrganization: activateMutation,
isOrganizationActivating,
deactivateOrganization: deActivateMutation,
isOrganizationDeactivating,
deleteOrganization: deleteMutation,
isOrganizationDeleting,
organizationsAdminsResponse,
isLoadingAdmins,
isErrorAdmins,
refetchAdmins,
};
};
export function useOrganizationDetail(type: string, id: string) {
const query = useQuery<{ items: OrganizationDetailDTO }, Error>({
queryKey: ["organization-detail", id],
queryFn: async () => {
const { data } = await getOrganizationById(id);
return { items: data };
},
staleTime: 5 * 60 * 1000,
retry: false,
enabled: type === "Org" && !!id,
});
return {
organizationsDetailResponse: query.data,
isDetailLoading: query.isLoading,
isDetailError: query.isError,
};
}

View File

@@ -0,0 +1,68 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import {
createPasswordSettings,
getPasswordSettings,
updatePasswordSettings,
deletePasswordSettings,
PasswordSettings,
} from "../services/api/passwordSettingsService";
import { useErrorHandler } from "@/shared/hooks/useErrorHandler";
export const usePasswordSettingsQuery = () =>
useQuery({
queryKey: ["passwordSettings"],
queryFn: () => getPasswordSettings(),
staleTime: 30_000,
refetchOnWindowFocus: false,
retry: 1,
});
export const usePasswordSettingsMutations = () => {
const queryClient = useQueryClient();
const { t } = useTranslation();
const { handleError } = useErrorHandler(t);
const update = useMutation({
mutationFn: (payload: PasswordSettings) => updatePasswordSettings(payload),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["passwordSettings"] });
toast.success(
t("passwordSettings.updated", "Password settings updated successfully")
);
},
onError: (error: any) => {
handleError(error);
},
});
const create = useMutation({
mutationFn: (payload: PasswordSettings) => createPasswordSettings(payload),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["passwordSettings"] });
toast.success(
t("passwordSettings.created", "Password settings created successfully")
);
},
onError: (error: any) => {
handleError(error);
},
});
const delete_mutation = useMutation({
mutationFn: (id: string) => deletePasswordSettings(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["passwordSettings"] });
toast.success(
t("passwordSettings.deleted", "Password settings deleted successfully")
);
},
onError: (error: any) => {
console.error("Delete mutation error:", error);
handleError(error);
},
});
return { update, create, delete: delete_mutation };
};

View File

@@ -0,0 +1,284 @@
import { useState, useEffect } from "react";
import axios from "axios";
interface ApiResponse<T> {
success: boolean;
data: T;
message?: string;
}
export type PositionType = "TeamLeader" | "TeamMember" | "Director" | "Other";
export interface Position {
id: string;
title: string;
type?: PositionType;
employeeCount: number;
employees: any[];
isExpanded?: boolean;
}
export const usePositions = (departmentId: string, unitId: string) => {
const [positions, setPositions] = useState<Position[]>([]);
const [selectedPositionId, setSelectedPositionId] = useState<string>("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
// Fetch positions for a department
const fetchPositions = async () => {
if (!departmentId) {
setPositions([]);
setSelectedPositionId("");
return;
}
setLoading(true);
try {
// In a real app, this would be an API call
// const response = await axios.get<ApiResponse<Position[]>>(`/api/departments/${departmentId}/positions`);
// const fetchedPositions = response.data.data;
// For now, simulate API response with mock data
setTimeout(() => {
// Mock positions for this department
const mockPositions: Position[] = [
{
id: "pos1",
title: "Team Leader",
type: "TeamLeader",
employeeCount: 1,
employees: [
{
id: "emp1",
name: "Abebe Kebede",
email: "abebe@example.com",
role: "Manager",
inviteStatus: "Accepted",
},
],
isExpanded: true,
},
{
id: "pos2",
title: "Team Member",
type: "TeamMember",
employeeCount: 3,
employees: [
{
id: "emp2",
name: "Kebede Abebe",
email: "kebede@example.com",
inviteStatus: "Pending",
},
{
id: "emp3",
name: "Chala Demeke",
email: "chala@example.com",
inviteStatus: "Not Invited",
},
],
},
{
id: "pos3",
title: "Director",
type: "Director",
employeeCount: 1,
employees: [
{
id: "emp4",
name: "Tigist Alemu",
email: "tigist@example.com",
role: "Director",
inviteStatus: "Accepted",
},
],
},
];
setPositions(mockPositions);
if (mockPositions.length > 0) {
setSelectedPositionId(mockPositions[0].id);
} else {
setSelectedPositionId("");
}
setLoading(false);
}, 500);
} catch (err) {
setError(
err instanceof Error ? err : new Error("Failed to fetch positions")
);
setLoading(false);
}
};
// Create a new position
const createPosition = async (
positionData: Partial<Position>
): Promise<Position> => {
if (!departmentId) {
throw new Error("No department selected");
}
setLoading(true);
try {
// In a real app, this would be an API call
// const response = await axios.post<ApiResponse<Position>>(
// `/api/departments/${departmentId}/positions`,
// positionData
// );
// const newPosition = response.data.data;
// For now, simulate API response
const newPosition: Position = {
id: `pos-${Date.now()}`,
title: positionData.title || "New Position",
type: positionData.type || "Other",
employeeCount: 0,
employees: [],
isExpanded: false,
};
setPositions((prev) => [...prev, newPosition]);
setLoading(false);
return newPosition;
} catch (err) {
setError(
err instanceof Error ? err : new Error("Failed to create position")
);
setLoading(false);
throw err;
}
};
// Update an existing position
const updatePosition = async (
positionId: string,
updates: Partial<Position>
): Promise<Position> => {
setLoading(true);
try {
// In a real app, this would be an API call
// const response = await axios.put<ApiResponse<Position>>(
// `/api/positions/${positionId}`,
// updates
// );
// const updatedPosition = response.data.data;
// For now, simulate API response
const updatedPositions = positions.map((position) =>
position.id === positionId ? { ...position, ...updates } : position
);
setPositions(updatedPositions);
setLoading(false);
const updatedPosition = updatedPositions.find((p) => p.id === positionId);
if (!updatedPosition) {
throw new Error("Position not found after update");
}
return updatedPosition;
} catch (err) {
setError(
err instanceof Error ? err : new Error("Only a super admin can update this position type")
);
setLoading(false);
throw err;
}
};
// Delete a position
const deletePosition = async (positionId: string): Promise<void> => {
setLoading(true);
try {
// In a real app, this would be an API call
// await axios.delete<ApiResponse<void>>(`/api/positions/${positionId}`);
// For now, simulate API response
setPositions((prev) =>
prev.filter((position) => position.id !== positionId)
);
// If the deleted position was selected, select another position
if (selectedPositionId === positionId) {
const remaining = positions.filter(
(position) => position.id !== positionId
);
if (remaining.length > 0) {
setSelectedPositionId(remaining[0].id);
} else {
setSelectedPositionId("");
}
}
setLoading(false);
} catch (err) {
setError(
err instanceof Error ? err : new Error("Failed to delete position")
);
setLoading(false);
throw err;
}
};
// Toggle expand/collapse for a position
const togglePositionExpand = (positionId: string) => {
setPositions((prev) =>
prev.map((position) =>
position.id === positionId
? { ...position, isExpanded: !position.isExpanded }
: position
)
);
};
// Select a position
const selectPosition = (positionId: string) => {
setSelectedPositionId(positionId);
};
// Filter positions by type
const getPositionsByType = (type: PositionType) => {
return positions.filter((position) => position.type === type);
};
// Get team leaders
const getTeamLeaders = () => {
return positions.filter((position) => position.type === "TeamLeader");
};
// Get team members
const getTeamMembers = () => {
return positions.filter((position) => position.type === "TeamMember");
};
// Get directors
const getDirectors = () => {
return positions.filter((position) => position.type === "Director");
};
// Load positions when departmentId changes
useEffect(() => {
fetchPositions();
}, [departmentId, unitId]);
return {
positions,
selectedPositionId,
selectedPosition: positions.find(
(position) => position.id === selectedPositionId
),
loading,
error,
fetchPositions,
createPosition,
updatePosition,
deletePosition,
togglePositionExpand,
selectPosition,
getPositionsByType,
getTeamLeaders,
getTeamMembers,
getDirectors,
};
};

View File

@@ -0,0 +1,19 @@
import { prefixSuffixService } from "@/user-management/services/api/prefixSuffixService";
import { useQuery } from "@tanstack/react-query";
export interface RemarkParams{
skip:number,
take:number,
orderBy?:string;
}
export const useRemarkByUnitId = (unitId: string, params:RemarkParams) => {
return useQuery({
queryKey: ["remark", unitId,params],
queryFn: async () => {
const response = await prefixSuffixService.getRemarkList(unitId,params);
return response.data ;
},
});
};

View File

@@ -0,0 +1,185 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
getSites,
getSiteById,
createSite,
updateSite,
softDeleteSite,
deleteSite,
restoreSite,
} from "@/super-admin/services/api/sitesService";
import { SiteDto, SitePayloadDto, SiteQueryParams } from "@/super-admin/dto/SitesDto";
import { toast } from "sonner";
import { useErrorHandler } from "@/shared/hooks/useErrorHandler";
import { useTranslation } from "react-i18next";
// Helper function to transform English name into snake_case format
export function toSnakeCase(str: string): string {
if (!str) return "";
return str
.toLowerCase()
.trim()
.replace(/[^a-z0-9\s-_]/g, "") // remove special characters
.replace(/[\s-]+/g, "_"); // replace spaces or hyphens with underscores
}
export const useSites = (params?: SiteQueryParams) => {
const queryClient = useQueryClient();
const { t } = useTranslation();
const { handleError } = useErrorHandler(t);
// 🔵 Query: Fetch sites list
const {
data: sitesResponse,
isLoading,
isError,
refetch,
} = useQuery({
queryKey: ["sites", params],
queryFn: async () => {
const { data } = await getSites(params);
// Defensively support both [{...}] and { items: [...], count: X } formats
const items = Array.isArray(data) ? data : (data?.items || []);
const count = Array.isArray(data) ? data.length : (data?.count || items.length || 0);
return {
items: items as SiteDto[],
count: count as number,
};
},
staleTime: 5 * 60 * 1000,
retry: false,
});
// 🟢 Mutation: Create Site
const { mutate: createMutation, isPending: isCreating } = useMutation({
mutationFn: async (payload: SitePayloadDto) => {
const transformedPayload: SitePayloadDto = {
...payload,
name: {
...payload.name,
en: toSnakeCase(payload.name.en),
},
};
const { data } = await createSite(transformedPayload);
return data;
},
onSuccess: () => {
toast.success("Site created successfully!");
queryClient.invalidateQueries({ queryKey: ["sites"] });
},
onError: (error) => {
handleError(error);
},
});
// 🟠 Mutation: Update Site
const { mutate: updateMutation, isPending: isUpdating } = useMutation({
mutationFn: async ({ id, payload }: { id: string; payload: SitePayloadDto }) => {
const transformedPayload: SitePayloadDto = {
...payload,
name: {
...payload.name,
en: toSnakeCase(payload.name.en),
},
};
const { data } = await updateSite(id, transformedPayload);
return data;
},
onSuccess: () => {
toast.success("Site updated successfully!");
queryClient.invalidateQueries({ queryKey: ["sites"] });
},
onError: (error) => {
handleError(error);
},
});
// 🟡 Mutation: Archive Site (Soft Delete)
const { mutate: archiveMutation, isPending: isArchiving } = useMutation({
mutationFn: async ({ id }: { id: string }) => {
const { data } = await softDeleteSite(id);
return data;
},
onSuccess: () => {
toast.success("Site archived successfully!");
queryClient.invalidateQueries({ queryKey: ["sites"] });
},
onError: (error) => {
handleError(error);
},
});
// 🔴 Mutation: Permanent Delete Site
const { mutate: deleteMutation, isPending: isDeleting } = useMutation({
mutationFn: async ({ id }: { id: string }) => {
const { data } = await deleteSite(id);
return data;
},
onSuccess: () => {
toast.success("Site permanently deleted!");
queryClient.invalidateQueries({ queryKey: ["sites"] });
},
onError: (error) => {
handleError(error);
},
});
// 🟣 Mutation: Restore Archived Site
const { mutate: restoreMutation, isPending: isRestoring } = useMutation({
mutationFn: async ({ id }: { id: string }) => {
const { data } = await restoreSite(id);
return data;
},
onSuccess: () => {
toast.success("Site restored successfully!");
queryClient.invalidateQueries({ queryKey: ["sites"] });
},
onError: (error) => {
handleError(error);
},
});
return {
sitesResponse,
isLoading,
isError,
refetch,
createSite: createMutation,
isCreating,
updateSite: updateMutation,
isUpdating,
archiveSite: archiveMutation,
isArchiving,
deleteSite: deleteMutation,
isDeleting,
restoreSite: restoreMutation,
isRestoring,
};
};
export const useSiteDetail = (id: string, enabled: boolean = true) => {
const {
data: site,
isLoading,
isError,
refetch,
} = useQuery({
queryKey: ["site", id],
queryFn: async () => {
const { data } = await getSiteById(id);
return data;
},
enabled: enabled && !!id,
staleTime: 5 * 60 * 1000,
retry: false,
});
return {
site,
isLoading,
isError,
refetch,
};
};

View File

@@ -0,0 +1,23 @@
import { useQuery } from "@tanstack/react-query";
import { getUnitInfo, getUnitInfoByUnitID } from "../services/api/userService";
export const useUnitInfo = () => {
return useQuery({
queryKey: ["unitInfo"],
queryFn: async () => {
const { data } = await getUnitInfo();
return data ;
},
});
};
export const useUnitInfoByUnitID = (unitID:string) => {
return useQuery({
queryKey: ["unitInfobyID"],
queryFn: async () => {
const { data } = await getUnitInfoByUnitID(unitID);
return data;
},
});
};

View File

@@ -0,0 +1,230 @@
import { useState, useEffect } from "react";
import axios from "axios";
interface ApiResponse<T> {
success: boolean;
data: T;
message?: string;
}
export interface Department {
id: string;
name: string;
positions: any[];
isExpanded?: boolean;
}
export interface Unit {
id: string;
name: string;
departments: Department[];
isExpanded?: boolean;
}
export const useUnits = (organizationId: string) => {
const [units, setUnits] = useState<Unit[]>([]);
const [selectedUnitId, setSelectedUnitId] = useState<string>("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
// Fetch units for an organization
const fetchUnits = async () => {
if (!organizationId) {
setUnits([]);
setSelectedUnitId("");
return;
}
setLoading(true);
};
// Create a new unit
const createUnit = async (unitData: Partial<Unit>): Promise<Unit> => {
if (!organizationId) {
throw new Error("No organization selected");
}
setLoading(true);
try {
// In a real app, this would be an API call
// const response = await axios.post<ApiResponse<Unit>>(
// `/api/organizations/${organizationId}/units`,
// unitData
// );
// const newUnit = response.data.data;
// For now, simulate API response
const newUnit: Unit = {
id: `unit-${Date.now()}`,
name: unitData.name || "New Unit",
departments: [],
isExpanded: false,
};
setUnits((prev) => [...prev, newUnit]);
setLoading(false);
return newUnit;
} catch (err) {
setError(err instanceof Error ? err : new Error("Failed to create unit"));
setLoading(false);
throw err;
}
};
// Update an existing unit
const updateUnit = async (
unitId: string,
updates: Partial<Unit>
): Promise<Unit> => {
setLoading(true);
try {
// In a real app, this would be an API call
// const response = await axios.put<ApiResponse<Unit>>(
// `/api/units/${unitId}`,
// updates
// );
// const updatedUnit = response.data.data;
// For now, simulate API response
const updatedUnits = units.map((unit) =>
unit.id === unitId ? { ...unit, ...updates } : unit
);
setUnits(updatedUnits);
setLoading(false);
const updatedUnit = updatedUnits.find((u) => u.id === unitId);
if (!updatedUnit) {
throw new Error("Unit not found after update");
}
return updatedUnit;
} catch (err) {
setError(err instanceof Error ? err : new Error("Failed to update unit"));
setLoading(false);
throw err;
}
};
// Delete a unit
const deleteUnit = async (unitId: string): Promise<void> => {
setLoading(true);
try {
// In a real app, this would be an API call
// await axios.delete<ApiResponse<void>>(`/api/units/${unitId}`);
// For now, simulate API response
setUnits((prev) => prev.filter((unit) => unit.id !== unitId));
// If the deleted unit was selected, select another unit
if (selectedUnitId === unitId) {
const remaining = units.filter((unit) => unit.id !== unitId);
if (remaining.length > 0) {
setSelectedUnitId(remaining[0].id);
} else {
setSelectedUnitId("");
}
}
setLoading(false);
} catch (err) {
setError(err instanceof Error ? err : new Error("Failed to delete unit"));
setLoading(false);
throw err;
}
};
// Add department to a unit
const addDepartment = async (
unitId: string,
departmentName: string
): Promise<Department> => {
setLoading(true);
try {
// In a real app, this would be an API call
// const response = await axios.post<ApiResponse<Department>>(
// `/api/units/${unitId}/departments`,
// { name: departmentName }
// );
// const newDepartment = response.data.data;
// For now, simulate API response
const newDepartment: Department = {
id: `dept-${Date.now()}`,
name: departmentName,
positions: [],
isExpanded: false,
};
setUnits((prev) =>
prev.map((unit) =>
unit.id === unitId
? { ...unit, departments: [...unit.departments, newDepartment] }
: unit
)
);
setLoading(false);
return newDepartment;
} catch (err) {
setError(
err instanceof Error ? err : new Error("Failed to add department")
);
setLoading(false);
throw err;
}
};
// Toggle expand/collapse for a unit
const toggleUnitExpand = (unitId: string) => {
setUnits((prev) =>
prev.map((unit) =>
unit.id === unitId ? { ...unit, isExpanded: !unit.isExpanded } : unit
)
);
};
// Toggle expand/collapse for a department
const toggleDepartmentExpand = (unitId: string, deptId: string) => {
setUnits((prev) =>
prev.map((unit) => {
if (unit.id !== unitId) return unit;
return {
...unit,
departments: unit.departments.map((dept) =>
dept.id === deptId
? { ...dept, isExpanded: !dept.isExpanded }
: dept
),
};
})
);
};
// Select a unit
const selectUnit = (unitId: string) => {
setSelectedUnitId(unitId);
};
// Load units when organizationId changes
useEffect(() => {
fetchUnits();
}, [organizationId]);
return {
units,
selectedUnitId,
selectedUnit: units.find((unit) => unit.id === selectedUnitId),
loading,
error,
fetchUnits,
createUnit,
updateUnit,
deleteUnit,
addDepartment,
toggleUnitExpand,
toggleDepartmentExpand,
selectUnit,
};
};

View File

@@ -0,0 +1,121 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { useTranslation } from "react-i18next";
import { useErrorHandler } from "@/shared/hooks/useErrorHandler";
import {
removeUnitAdminRole,
assignFirstsForSecond,
assignSecondsForFirst,
deleteUserRole,
removeFirstsForSecond,
removeSecondsForFirst,
RemoveOrAssignUnitAdminPayload,
removeOrgAdminRole,
RemoveOrAssignOrgAdminPayload,
} from "@/super-admin/services/api/userRoleService";
export const useUserRoles = () => {
const queryClient = useQueryClient();
const { t } = useTranslation();
const { handleError } = useErrorHandler(t);
// Remove Unit Admin Role
const { mutateAsync: removeAdminRole, isPending: isRemoving } = useMutation({
mutationFn: (payload: RemoveOrAssignUnitAdminPayload) =>
removeUnitAdminRole(payload),
onSuccess: () => {
toast.success("Unit admin role removed successfully");
queryClient.invalidateQueries({ queryKey: ["organizationAdmins"] });
},
onError: (error) => {
handleError(error);
},
});
const { mutateAsync: removeUnitAdmin, isPending: isRemovingUnitAdmin } =
useMutation({
mutationFn: (payload: RemoveOrAssignOrgAdminPayload) =>
removeOrgAdminRole(payload),
onSuccess: () => {
toast.success("Unit admin role removed successfully");
queryClient.invalidateQueries({ queryKey: ["organizationAdmins"] });
},
onError: (error) => {
handleError(error);
},
});
// Assign Firsts for Second
const { mutate: assignFirsts, isPending: isAssigningFirsts } = useMutation({
mutationFn: (payload: { secondId: string; firstIds: string[] }) =>
assignFirstsForSecond(payload),
onSuccess: () => {
toast.success("Firsts assigned successfully");
},
onError: (error) => {
handleError(error);
},
});
// Assign Seconds for First
const { mutate: assignSeconds, isPending: isAssigningSeconds } = useMutation({
mutationFn: (payload: { firstId: string; secondIds: string[] }) =>
assignSecondsForFirst(payload),
onSuccess: () => {
toast.success("Seconds assigned successfully");
},
onError: (error) => {
handleError(error);
},
});
// Remove Firsts for Second
const { mutate: removeFirsts, isPending: isRemovingFirsts } = useMutation({
mutationFn: (payload: { secondId: string; firstIds: string[] }) =>
removeFirstsForSecond(payload),
onSuccess: () => {
toast.success("Firsts removed successfully");
},
onError: (error) => {
handleError(error);
},
});
// Remove Seconds for First
const { mutate: removeSeconds, isPending: isRemovingSeconds } = useMutation({
mutationFn: (payload: { firstId: string; secondIds: string[] }) =>
removeSecondsForFirst(payload),
onSuccess: () => {
toast.success("Seconds removed successfully");
},
onError: (error) => {
handleError(error);
},
});
// Delete User Role by ID
const { mutate: deleteRole, isPending: isDeleting } = useMutation({
mutationFn: (id: string) => deleteUserRole(id),
onSuccess: () => {
toast.success("Role deleted successfully");
},
onError: (error) => {
handleError(error);
},
});
return {
removeAdminRole,
removeUnitAdmin,
assignFirsts,
assignSeconds,
removeFirsts,
removeSeconds,
deleteRole,
isRemoving,
isRemovingUnitAdmin,
isAssigningFirsts,
isAssigningSeconds,
isRemovingFirsts,
isRemovingSeconds,
isDeleting,
};
};

View File

@@ -0,0 +1,25 @@
import { OrganizationUserDto, User } from "@/shared/dto/user/usersDto";
import { getEmployeesUnderOrg } from "@/shared/services/organizationsService";
import { useQuery } from "@tanstack/react-query";
import { getUserById } from "../services/api/userService";
export const useUsers = (organizationId: string) => {
return useQuery({
queryKey: ["users"],
queryFn: async () => {
const { data } = await getEmployeesUnderOrg(organizationId);
return data as { items: OrganizationUserDto[]; count: number };
},
});
};
export const useUsersByID = (id: string) => {
return useQuery({
queryKey: ["user",id],
queryFn: async () => {
const response = await getUserById(id);
return response.data as User;
},
});
};