mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 17:38:12 +00:00
user management ui
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
@@ -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"] });
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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 });
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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 ;
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user