mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 14:50:57 +00:00
1171 lines
47 KiB
TypeScript
1171 lines
47 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { Input } from "@/shared/common/ui/input";
|
|
import {
|
|
Search,
|
|
ChevronLeft,
|
|
ChevronRight,
|
|
Download,
|
|
RefreshCw,
|
|
Mail,
|
|
X,
|
|
} from "lucide-react";
|
|
import { useToast } from "@/shared/common/ui/use-toast";
|
|
import axiosInstance from "@/shared/services/axiosInstance";
|
|
import { toast } from "sonner";
|
|
import { Button } from "@/shared/common/ui/button";
|
|
import { MultiSelect } from "@/shared/common/ui/multi-select";
|
|
import { withHeaders } from "@/record-management/services/api/withHeaders";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/shared/common/ui/select";
|
|
import { SingleSelect } from "@/shared/common/ui/single-select";
|
|
|
|
import { OrganizationList } from "./lists/OrganizationList";
|
|
import { UnitList } from "./lists/UnitList";
|
|
import { DepartmentList } from "./lists/DepartmentList";
|
|
import { TeamMembers } from "./TeamMembers";
|
|
|
|
import { AddDepartmentDialog } from "./dialogs/AddDepartmentDialog";
|
|
import { AddUserDialog } from "./dialogs/AddUserDialog";
|
|
import { ViewUsersDialog } from "./dialogs/ViewUsersDialog";
|
|
import { AddSubDepartmentDialog } from "./dialogs/AddSubDepartmentDialog";
|
|
import { AddAssignUserDialog } from "./dialogs/AddAssignUserDialog";
|
|
|
|
import { useOrganizationsData } from "./hooks/useOrganizationsData";
|
|
import { useUnitsData } from "./hooks/useUnitsData";
|
|
import { useDepartmentsData } from "./hooks/useDepartmentsData";
|
|
import { useEmployeesData } from "./hooks/useEmployeesData";
|
|
|
|
import { useSelectionHandlers } from "./handlers/useSelectionHandlers";
|
|
import { useActionHandlers } from "./handlers/useActionHandlers";
|
|
import { Breadcrumb } from "./components/Breadcrumb";
|
|
import { AddUserUnderDepartmentDialog } from "./dialogs/AddUserUnderDepartmentDialog";
|
|
import { useAuth } from "@/shared/context/AuthContext";
|
|
import { EditDepartmentDialog } from "./dialogs/EditDepartmentDialog";
|
|
import { EditUnitDialog } from "./dialogs/EditUnitDialog";
|
|
import { DeleteUnitDialog } from "./dialogs/DeleteUnitDialog";
|
|
import { PermanentDeleteUnitDialog } from "./dialogs/PermanentDeleteUnitDialog";
|
|
import { AddFromSubCityDialog } from "./dialogs/AddFromSubCityDialog";
|
|
import { UnitSelectionModal } from "./components/UnitSelectionModal";
|
|
import { AddUnitDialog } from "./dialogs/AddUnitDialog";
|
|
import { Plus } from "lucide-react";
|
|
import { DeleteDepartmentDialog } from "./dialogs/DeleteDepartmentDialog";
|
|
import { DeleteTeamMemberDialog } from "./dialogs/DeleteTeamMemberDialog";
|
|
import { TeamMemberDto } from "../dto/teamMember/teamMember";
|
|
import { useRef } from "react";
|
|
import { UserCircle } from "lucide-react";
|
|
import { resendVerificationCode } from "@/shared/services/authService";
|
|
import { useLocalizedName } from "@/shared/common/localizedName";
|
|
import { MoveDepartmentDialog } from "./dialogs/MoveDepartmentDialog";
|
|
import i18n from "@/i18n";
|
|
import { t } from "i18next";
|
|
import { useExportUserData } from "@/shared/hooks/useExportUserData";
|
|
import { DeactivateTeamMemberDialog } from "./dialogs/DeactivateTeamMemberDialog";
|
|
import { OrganizationEmployeeSearch } from "./components/OrganizationEmployeeSearch";
|
|
import { useGetOrganizationConfig } from "@/super-admin/hooks/useConfig";
|
|
import { useUnit } from "@/user-management/hooks/useUnit";
|
|
import { useArchiveActions } from "@/user-management/hooks/useArchived";
|
|
import { useErrorHandler } from "@/shared/hooks/useErrorHandler";
|
|
import { ManageUnitAdminDialog } from "./dialogs/ManageUnitAdminDialog";
|
|
|
|
const UserManagementTree = () => {
|
|
const { toast: uiToast } = useToast();
|
|
const { softDeletePosition } = useArchiveActions();
|
|
const handleArchiveDepartment = (departmentId: string) => {
|
|
softDeletePosition(departmentId);
|
|
};
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
const [unitSearchQuery, setUnitSearchQuery] = useState("");
|
|
const [departmentSearchQuery, setDepartmentSearchQuery] = useState("");
|
|
const [employeeSearchQuery, setEmployeeSearchQuery] = useState("");
|
|
const { user } = useAuth();
|
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
|
const localizedName = useLocalizedName();
|
|
const lang = i18n.language;
|
|
|
|
// Resend invitation states
|
|
const [showResendModal, setShowResendModal] = useState(false);
|
|
const [showConfirmAllModal, setShowConfirmAllModal] = useState(false);
|
|
const [pendingUsers, setPendingUsers] = useState<any[]>([]);
|
|
const [selectedUserIds, setSelectedUserIds] = useState<string[]>([]);
|
|
const [isSendingInvitations, setIsSendingInvitations] = useState(false);
|
|
const [isLoadingPending, setIsLoadingPending] = useState(false);
|
|
const isOrganizationAdmin =
|
|
user?.roles?.some((role) => role.key === "organization_admin") ?? false;
|
|
// Export functionality
|
|
const {
|
|
exportUserData,
|
|
getUnits,
|
|
isExporting,
|
|
isLoadingUnits,
|
|
units: exportUnits,
|
|
} = useExportUserData();
|
|
const [selectedExportUnitId, setSelectedExportUnitId] = useState<string>("");
|
|
const [showUnitSelection, setShowUnitSelection] = useState(false);
|
|
const [exportFormat, setExportFormat] = useState<"xlsx" | "csv">("xlsx");
|
|
const [showEmployeeSearch, setShowEmployeeSearch] = useState(false);
|
|
const [showSendToUnitModal, setShowSendToUnitModal] = useState(false);
|
|
const [selectedSendUnitId, setSelectedSendUnitId] = useState<string>("");
|
|
const { handleError } = useErrorHandler(t);
|
|
// Hooks for data loading
|
|
const organizationId =
|
|
user?.employee && user.employee.length > 0
|
|
? user.employee[0].organizationId
|
|
: undefined;
|
|
const { organizations, isLoading: orgsLoading } = useOrganizationsData({
|
|
currentOrgId: organizationId,
|
|
});
|
|
const {
|
|
selectedOrgId,
|
|
selectedUnitId,
|
|
selectedDepartmentId,
|
|
selectedDepartmentName,
|
|
breadcrumb,
|
|
handleSelectOrganization,
|
|
handleSelectUnit,
|
|
handleSelectDepartment,
|
|
} = useSelectionHandlers(organizations, localizedName);
|
|
const [take] = useState(100);
|
|
const [skip, setSkip] = useState(0);
|
|
|
|
const {
|
|
units,
|
|
isLoading: unitsLoading,
|
|
setUnits,
|
|
fetchedUnits,
|
|
} = useUnitsData(selectedOrgId, { take, skip });
|
|
|
|
const { data: orgConfigResp } = useGetOrganizationConfig(selectedOrgId || "");
|
|
const unitCapacity: number | null = (() => {
|
|
const items = orgConfigResp?.data?.items ?? [];
|
|
if (!Array.isArray(items) || items.length === 0) return null;
|
|
const match =
|
|
items.find((it: any) => it?.organizationId === selectedOrgId) ?? items[0];
|
|
const value = Number(match?.maximumNumberOfUnits);
|
|
return Number.isFinite(value) ? value : null;
|
|
})();
|
|
|
|
// Pull child units (units related under one of this org's units) so they
|
|
// show up alongside the top-level units in the column. Track the expanded
|
|
// parent separately so clicking on a child (or its action menu) doesn't
|
|
// collapse the sibling list.
|
|
const { getChildren } = useUnit();
|
|
const [expandedParentId, setExpandedParentId] = useState<string>("");
|
|
useEffect(() => {
|
|
if (!selectedUnitId) return;
|
|
const isTopLevel = units.some((u) => u.id === selectedUnitId);
|
|
if (isTopLevel) setExpandedParentId(selectedUnitId);
|
|
}, [selectedUnitId, units]);
|
|
useEffect(() => {
|
|
setExpandedParentId("");
|
|
}, [selectedOrgId]);
|
|
const { data: childUnitsResp } = getChildren(expandedParentId || "");
|
|
const childUnits = (() => {
|
|
const data = childUnitsResp?.data;
|
|
if (!data) return [] as Array<{ id: string; name: any }>;
|
|
const items = Array.isArray(data) ? data : (data.items ?? []);
|
|
return Array.isArray(items)
|
|
? items.map((u: any) => ({ id: u.id, name: u.name }))
|
|
: [];
|
|
})();
|
|
const childUnitIds = new Set(childUnits.map((u) => u.id));
|
|
console.log(units, childUnitIds);
|
|
const mergedUnits = (() => {
|
|
const seen = new Set<string>();
|
|
const out: Array<{ id: string; name: any; isRelated: boolean }> = [];
|
|
[...units, ...childUnits].forEach((u) => {
|
|
if (u?.id && !seen.has(u.id)) {
|
|
seen.add(u.id);
|
|
out.push({ ...u, isRelated: childUnitIds.has(u.id) });
|
|
}
|
|
});
|
|
return out;
|
|
})();
|
|
|
|
const [addUnitDialogOpen, setAddUnitDialogOpen] = useState(false);
|
|
const canCreateUnit =
|
|
unitCapacity !== null && units.length < unitCapacity && !!selectedOrgId;
|
|
|
|
const {
|
|
departments,
|
|
isLoading: deptsLoading,
|
|
setDepartments,
|
|
positions,
|
|
} = useDepartmentsData(selectedUnitId);
|
|
|
|
const {
|
|
employees,
|
|
isLoading: employeesLoading,
|
|
setEmployees,
|
|
refetch: refetchEmployees,
|
|
} = useEmployeesData(selectedDepartmentId);
|
|
|
|
// All dialog states & handlers bundled
|
|
const actionHandlers = useActionHandlers({
|
|
selectedOrgId,
|
|
selectedUnitId,
|
|
toast: uiToast,
|
|
});
|
|
// Handle inviting or resending invitation to employees
|
|
const handleInviteEmployee = async (employee: TeamMemberDto) => {
|
|
try {
|
|
// Show immediate feedback that action is being processed
|
|
if (employee.status === "pending") {
|
|
toast.loading("Resending verification code...");
|
|
} else {
|
|
toast.loading("Sending invitation...");
|
|
}
|
|
await resendVerificationCode({
|
|
email: employee.user.email,
|
|
phoneNumber: employee.user.phoneNumber,
|
|
});
|
|
// Show success notification with more specific message for resend
|
|
const userName =
|
|
typeof employee.user.name === "string"
|
|
? localizedName(employee.user.name)
|
|
: localizedName(employee.user.name) || "User";
|
|
|
|
if (employee.user.status === "pending") {
|
|
toast.success("Verification Code Resent", {
|
|
description: `A new verification code has been sent to ${userName}`,
|
|
duration: 4000,
|
|
});
|
|
} else {
|
|
toast.success("Invitation Sent", {
|
|
description: `Invitation sent to ${userName}`,
|
|
duration: 4000,
|
|
});
|
|
}
|
|
|
|
// Refresh the employee list to update statuses
|
|
if (refetchEmployees) {
|
|
await refetchEmployees();
|
|
}
|
|
} catch (error: unknown) {
|
|
// Show error notification
|
|
const errorMessage =
|
|
error instanceof Error ? error.message : "Unknown error";
|
|
toast.error("Error", {
|
|
description: `Failed to ${
|
|
employee.status === "pending"
|
|
? "resend verification code"
|
|
: "send invitation"
|
|
}: ${errorMessage}`,
|
|
duration: 4000,
|
|
});
|
|
}
|
|
setTimeout(() => {
|
|
toast.dismiss();
|
|
}, 4000);
|
|
};
|
|
|
|
const fetchPendingUsers = async () => {
|
|
try {
|
|
setIsLoadingPending(true);
|
|
// First fetch to get the total count
|
|
const countResponse = await axiosInstance.get(
|
|
"/employees/my-unit/pending",
|
|
{
|
|
params: { take: 10 },
|
|
headers: withHeaders(),
|
|
},
|
|
);
|
|
const totalCount = countResponse.data?.count ?? 0;
|
|
|
|
// Fetch all pending users using the total count
|
|
const response = await axiosInstance.get("/employees/my-unit/pending", {
|
|
params: { take: totalCount || 3000 },
|
|
headers: withHeaders(),
|
|
});
|
|
const data = response.data;
|
|
const usersList = Array.isArray(data)
|
|
? data
|
|
: Array.isArray(data?.items)
|
|
? data.items
|
|
: [];
|
|
setPendingUsers(usersList);
|
|
} catch (err: any) {
|
|
handleError(err);
|
|
} finally {
|
|
setIsLoadingPending(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (showResendModal) {
|
|
fetchPendingUsers();
|
|
}
|
|
}, [showResendModal]);
|
|
|
|
const handleResendToAll = async () => {
|
|
try {
|
|
setIsSendingInvitations(true);
|
|
toast.loading("Resending verification to all pending users...");
|
|
|
|
await axiosInstance.post(
|
|
"/employees/my-unit/send-verification-to-all-pending",
|
|
{},
|
|
{ headers: withHeaders() },
|
|
);
|
|
|
|
toast.dismiss();
|
|
toast.success("Successfully resent verification to all pending users.");
|
|
|
|
if (refetchEmployees) {
|
|
await refetchEmployees();
|
|
}
|
|
} catch (err: any) {
|
|
toast.dismiss();
|
|
handleError(err);
|
|
} finally {
|
|
setIsSendingInvitations(false);
|
|
setShowConfirmAllModal(false);
|
|
}
|
|
};
|
|
|
|
const handleSendSelectedInvitations = async () => {
|
|
if (selectedUserIds.length === 0) return;
|
|
|
|
try {
|
|
setIsSendingInvitations(true);
|
|
toast.loading(
|
|
`Sending verification to ${selectedUserIds.length} user(s)...`,
|
|
);
|
|
|
|
await axiosInstance.post(
|
|
"/employees/my-unit/generate-verification-codes",
|
|
{ employeeIds: selectedUserIds },
|
|
{ headers: withHeaders() },
|
|
);
|
|
|
|
toast.dismiss();
|
|
toast.success("Successfully sent verification codes.");
|
|
|
|
setShowResendModal(false);
|
|
setSelectedUserIds([]);
|
|
|
|
if (refetchEmployees) {
|
|
await refetchEmployees();
|
|
}
|
|
} catch (err: any) {
|
|
toast.dismiss();
|
|
handleError(err);
|
|
} finally {
|
|
setIsSendingInvitations(false);
|
|
}
|
|
};
|
|
|
|
const handleSendToSelectedUnits = async () => {
|
|
if (!selectedSendUnitId) return;
|
|
|
|
try {
|
|
toast.loading("Sending verification to unit...");
|
|
|
|
await axiosInstance.post(
|
|
`/employees/unit/${selectedSendUnitId}/send-verification-to-all-pending`,
|
|
{},
|
|
{ headers: withHeaders() },
|
|
);
|
|
|
|
toast.dismiss();
|
|
toast.success("Successfully sent verification to selected unit.");
|
|
|
|
setShowSendToUnitModal(false);
|
|
setSelectedSendUnitId("");
|
|
|
|
if (refetchEmployees) {
|
|
await refetchEmployees();
|
|
}
|
|
} catch (err: any) {
|
|
toast.dismiss();
|
|
handleError(err);
|
|
}
|
|
};
|
|
|
|
const scrollLeft = () => {
|
|
if (scrollContainerRef.current) {
|
|
scrollContainerRef.current.scrollBy({ left: -200, behavior: "smooth" });
|
|
}
|
|
};
|
|
|
|
const scrollRight = () => {
|
|
if (scrollContainerRef.current) {
|
|
scrollContainerRef.current.scrollBy({ left: 200, behavior: "smooth" });
|
|
}
|
|
};
|
|
|
|
// Export handlers
|
|
const handleExportUserData = async () => {
|
|
if (!organizationId) {
|
|
toast.error(t("organizationIdRequired"));
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const unitsList = await getUnits(organizationId);
|
|
if (unitsList && unitsList.length > 0) {
|
|
setShowUnitSelection(true);
|
|
} else {
|
|
toast.error(t("noUnitsFound"));
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to load units:", error);
|
|
toast.error(t("exportError"));
|
|
}
|
|
};
|
|
|
|
const handleUnitSelect = async (unitId: string) => {
|
|
setSelectedExportUnitId(unitId);
|
|
setShowUnitSelection(false);
|
|
|
|
// Export data for selected unit
|
|
const result = await exportUserData(unitId, exportFormat);
|
|
|
|
// Reset selectedExportUnitId after export (successful or failed)
|
|
setSelectedExportUnitId("");
|
|
};
|
|
|
|
return (
|
|
<div className="h-full flex flex-col">
|
|
<div className="p-4 border-b dark:border-gray-700">
|
|
<div className="flex justify-between items-center mb-4">
|
|
<h1 className="text-xl font-semibold text-gray-800 dark:text-gray-200">
|
|
{t("organization.userManagement")}
|
|
</h1>
|
|
|
|
<div className="flex gap-2">
|
|
{/* Format selection */}
|
|
<Select
|
|
value={exportFormat}
|
|
onValueChange={(value: "xlsx" | "csv") => setExportFormat(value)}
|
|
>
|
|
<SelectTrigger className="w-32 dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent className="dark:bg-gray-700 dark:border-gray-600">
|
|
<SelectItem
|
|
value="xlsx"
|
|
className="dark:text-gray-200 dark:hover:bg-gray-600"
|
|
>
|
|
XLSX
|
|
</SelectItem>
|
|
<SelectItem
|
|
value="csv"
|
|
className="dark:text-gray-200 dark:hover:bg-gray-600"
|
|
>
|
|
CSV
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
{/* Export button */}
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={handleExportUserData}
|
|
disabled={isExporting || !organizationId}
|
|
className="bg-primary-50 text-primary-700 border-primary-200 hover:bg-primary-100 dark:bg-primary-900/50 dark:text-primary-300 dark:border-primary-700 dark:hover:bg-primary-900/70"
|
|
>
|
|
<Download
|
|
className={`h-4 w-4 mr-2 ${isExporting ? "animate-pulse" : ""}`}
|
|
/>
|
|
{isExporting ? t("exporting") : t("exportUserData")}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex gap-2">
|
|
<div className="relative flex-1">
|
|
<Search className="absolute left-2 top-2.5 h-4 w-4 text-gray-500 dark:text-gray-400" />
|
|
<Input
|
|
placeholder="Search organizations, units, departments..."
|
|
className="pl-8 dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600"
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
/>
|
|
</div>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => setShowEmployeeSearch(true)}
|
|
disabled={!organizationId}
|
|
className="bg-blue-50 text-blue-700 border-blue-200 hover:bg-blue-100 whitespace-nowrap dark:bg-blue-900/30 dark:text-blue-400 dark:border-blue-800 dark:hover:bg-blue-900/50"
|
|
>
|
|
<UserCircle className="h-4 w-4 mr-2" />
|
|
{t("search.searchAllEmployees")}
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="mt-4">
|
|
<Breadcrumb
|
|
breadcrumb={breadcrumb}
|
|
hasTeamMembers={selectedDepartmentId != "" && employees.length > 0}
|
|
/>
|
|
</div>
|
|
|
|
{selectedOrgId && (
|
|
<div className="mt-3 flex gap-2 flex-wrap">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => setShowConfirmAllModal(true)}
|
|
disabled={isSendingInvitations}
|
|
className="bg-orange-50 text-orange-700 border-orange-200 hover:bg-orange-100 dark:bg-orange-900/30 dark:text-orange-400 dark:border-orange-800 dark:hover:bg-orange-900/50"
|
|
>
|
|
<RefreshCw
|
|
className={`h-4 w-4 mr-2 ${isSendingInvitations ? "animate-spin" : ""}`}
|
|
/>
|
|
{t("userManagement.resendToAll", "Resend Invitation to All")}
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => setShowResendModal(true)}
|
|
disabled={isSendingInvitations}
|
|
className="border border-blue-200 bg-blue-50 text-blue-700 hover:border-blue-300 hover:bg-blue-100 dark:border-blue-900 dark:bg-blue-950 dark:text-blue-200 dark:hover:border-blue-700 dark:hover:bg-blue-900 dark:hover:text-white"
|
|
>
|
|
<Mail className="h-4 w-4 mr-2" />
|
|
{t("userManagement.resendInvitation", "Resend Invitation")}
|
|
</Button>
|
|
{isOrganizationAdmin && (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => setShowSendToUnitModal(true)}
|
|
disabled={isSendingInvitations}
|
|
className="border border-green-200 bg-green-50 text-green-700 hover:border-green-300 hover:bg-green-100 dark:border-green-900 dark:bg-green-950 dark:text-green-200 dark:hover:border-green-700 dark:hover:bg-green-900 dark:hover:text-white"
|
|
>
|
|
<Mail className="h-4 w-4 mr-2" />
|
|
{t(
|
|
"userManagement.sendToSelectedUnit",
|
|
"Send to Selected Unit",
|
|
)}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Mobile view - horizontal scrollable cards */}
|
|
<div className="flex-1 p-2 md:hidden">
|
|
<div className="relative h-[calc(100vh-180px)]">
|
|
<button
|
|
onClick={scrollLeft}
|
|
className="absolute left-0 top-1/2 -translate-y-1/2 z-10 bg-white dark:bg-gray-800 rounded-full shadow-md p-1"
|
|
aria-label="Scroll left"
|
|
>
|
|
<ChevronLeft className="h-5 w-5 dark:text-gray-300" />
|
|
</button>
|
|
|
|
<div
|
|
ref={scrollContainerRef}
|
|
className="flex overflow-x-auto gap-3 pb-4 snap-x snap-mandatory h-full scrollbar-hide"
|
|
style={{ scrollbarWidth: "none", msOverflowStyle: "none" }}
|
|
>
|
|
<div className="border rounded-md p-3 bg-white dark:bg-gray-800 shadow-sm min-w-[90%] max-w-[90%] flex-shrink-0 snap-center h-full flex flex-col dark:border-gray-700">
|
|
<h3 className="font-medium text-gray-700 dark:text-gray-200 mb-3 border-b pb-2 dark:border-gray-700">
|
|
{t("organization.organizations")}
|
|
</h3>
|
|
<div className="overflow-y-auto flex-1">
|
|
<OrganizationList
|
|
selectedOrgId={selectedOrgId}
|
|
organizations={organizations}
|
|
isLoading={orgsLoading}
|
|
searchQuery={searchQuery}
|
|
onSelectOrganization={(id: string) => {
|
|
setUnits([]);
|
|
setDepartments([]);
|
|
setEmployees([]);
|
|
handleSelectOrganization(id);
|
|
}}
|
|
canCreateUnitForOrg={(id) =>
|
|
id === selectedOrgId && canCreateUnit
|
|
}
|
|
onCreateUnit={() => setAddUnitDialogOpen(true)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="border rounded-md p-3 bg-white dark:bg-gray-800 shadow-sm min-w-[90%] max-w-[90%] flex-shrink-0 snap-center h-full flex flex-col dark:border-gray-700">
|
|
<h3 className="font-medium text-gray-700 dark:text-gray-200 mb-3 border-b pb-2 dark:border-gray-700 flex items-center justify-between gap-2">
|
|
<span>{t("contentManagement.Units")}</span>
|
|
<div className="flex items-center gap-2">
|
|
{unitCapacity !== null && (
|
|
<span className="text-xs font-normal px-2 py-0.5 rounded-full bg-primary-50 text-primary-700 dark:bg-primary-900/30 dark:text-primary-300 border border-primary-200 dark:border-primary-800">
|
|
{mergedUnits.length}/{unitCapacity}
|
|
</span>
|
|
)}
|
|
{canCreateUnit && (
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => setAddUnitDialogOpen(true)}
|
|
className="h-7 px-2 text-xs border-primary-500 text-primary-700 hover:bg-primary-50 dark:border-primary-700 dark:text-primary-300 dark:hover:bg-primary-900/30"
|
|
>
|
|
<Plus className="h-3.5 w-3.5 mr-1" />
|
|
{t("contentManagement.createUnit", "Create Unit")}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</h3>
|
|
<div className="mb-3">
|
|
<div className="relative">
|
|
<Search className="absolute left-2 top-2.5 h-4 w-4 text-gray-500 dark:text-gray-400" />
|
|
<Input
|
|
placeholder={t("search.searchUnits", "Search units...")}
|
|
className="pl-8 text-sm dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600"
|
|
value={unitSearchQuery}
|
|
onChange={(e) => setUnitSearchQuery(e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="overflow-y-auto flex-1">
|
|
<UnitList
|
|
organizationId={selectedOrgId}
|
|
selectedUnitId={selectedUnitId}
|
|
onSelectUnit={(id: string) => {
|
|
setDepartments([]);
|
|
setEmployees([]);
|
|
handleSelectUnit(id, mergedUnits);
|
|
}}
|
|
units={mergedUnits}
|
|
isLoading={unitsLoading}
|
|
searchQuery={unitSearchQuery}
|
|
onAddDepartment={actionHandlers.unit.onAddDepartment}
|
|
onManageUnitAdmin={actionHandlers.unit.onManageUnitAdmin}
|
|
onEditUnit={actionHandlers.unit.onEditUnit}
|
|
onArchiveUnit={actionHandlers.unit.onArchiveUnit}
|
|
onDeleteUnit={actionHandlers.unit.onDeleteUnit}
|
|
onAddFromSubCity={actionHandlers.unit.onAddFromSubCity}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="border rounded-md p-3 bg-white dark:bg-gray-800 shadow-sm min-w-[90%] max-w-[90%] flex-shrink-0 snap-center h-full flex flex-col dark:border-gray-700">
|
|
<h3 className="font-medium text-gray-700 dark:text-gray-200 mb-3 border-b pb-2 dark:border-gray-700">
|
|
{t("userIncoming.Departments")}
|
|
</h3>
|
|
<div className="mb-3">
|
|
<div className="relative">
|
|
<Search className="absolute left-2 top-2.5 h-4 w-4 text-gray-500 dark:text-gray-400" />
|
|
<Input
|
|
placeholder={t(
|
|
"search.searchDepartments",
|
|
"Search departments...",
|
|
)}
|
|
className="pl-8 text-sm dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600"
|
|
value={departmentSearchQuery}
|
|
onChange={(e) => setDepartmentSearchQuery(e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="overflow-y-auto flex-1">
|
|
<DepartmentList
|
|
unitId={selectedUnitId}
|
|
selectedDepartmentId={selectedDepartmentId}
|
|
onSelectDepartment={(id: string) => {
|
|
if (id !== selectedDepartmentId) {
|
|
setEmployees([]);
|
|
handleSelectDepartment(id, departments);
|
|
}
|
|
}}
|
|
departments={departments}
|
|
positions={positions}
|
|
isLoading={deptsLoading}
|
|
searchQuery={departmentSearchQuery}
|
|
onAddUser={actionHandlers.department.onAddUserUnderDepartment}
|
|
onViewUsers={actionHandlers.department.onViewUsers}
|
|
onAddSubDepartment={
|
|
actionHandlers.department.onAddSubDepartment
|
|
}
|
|
onEditDepartment={actionHandlers.department.onEditDepartment}
|
|
onDeleteDepartment={
|
|
actionHandlers.department.onDeleteDepartment
|
|
}
|
|
onArchiveDepartment={handleArchiveDepartment}
|
|
onDelegate={actionHandlers.department.onDelegate}
|
|
onAssignUser={actionHandlers.department.onAssignUser}
|
|
onMoveDepartment={actionHandlers.department.onMoveDepartment}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="border rounded-md p-3 bg-white dark:bg-gray-800 shadow-sm min-w-[90%] max-w-[90%] flex-shrink-0 snap-center h-full flex flex-col dark:border-gray-700">
|
|
<h3 className="font-medium text-gray-700 dark:text-gray-200 mb-3 border-b pb-2 dark:border-gray-700">
|
|
{t("contentManagement.teamMember")}
|
|
</h3>
|
|
<div className="mb-3">
|
|
<div className="relative">
|
|
<Search className="absolute left-2 top-2.5 h-4 w-4 text-gray-500 dark:text-gray-400" />
|
|
<Input
|
|
placeholder={t(
|
|
"search.searchEmployees",
|
|
"Search employees...",
|
|
)}
|
|
className="pl-8 text-sm dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600"
|
|
value={employeeSearchQuery}
|
|
onChange={(e) => setEmployeeSearchQuery(e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="overflow-y-auto flex-1">
|
|
<TeamMembers
|
|
departmentId={selectedDepartmentId}
|
|
departmentName={selectedDepartmentName}
|
|
employees={employees}
|
|
isLoading={employeesLoading}
|
|
searchQuery={employeeSearchQuery}
|
|
onDeleteEmployee={
|
|
actionHandlers.teamMember.onDeleteTeamMember
|
|
}
|
|
onInviteEmployee={handleInviteEmployee}
|
|
onDeactivateTeamMember={
|
|
actionHandlers.teamMember.onDeactivateTeamMember
|
|
}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
onClick={scrollRight}
|
|
className="absolute right-0 top-1/2 -translate-y-1/2 z-10 bg-white dark:bg-gray-800 rounded-full shadow-md p-1"
|
|
aria-label="Scroll right"
|
|
>
|
|
<ChevronRight className="h-5 w-5 dark:text-gray-300" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Desktop view - grid layout */}
|
|
<div className="hidden md:flex flex-1 p-4 flex-col lg:grid lg:grid-cols-4 lg:gap-4">
|
|
<div className="lg:col-span-3 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
<div className="border rounded-md p-3 bg-white dark:bg-gray-800 shadow-sm dark:border-gray-700">
|
|
<h3 className="font-medium text-gray-700 dark:text-gray-200 mb-3 border-b pb-2 dark:border-gray-700">
|
|
{t("organization.organizations")}
|
|
</h3>
|
|
<OrganizationList
|
|
selectedOrgId={selectedOrgId}
|
|
organizations={organizations}
|
|
isLoading={orgsLoading}
|
|
searchQuery={searchQuery}
|
|
onSelectOrganization={(id: string) => {
|
|
setUnits([]);
|
|
setDepartments([]);
|
|
setEmployees([]);
|
|
handleSelectOrganization(id);
|
|
}}
|
|
canCreateUnitForOrg={(id) =>
|
|
id === selectedOrgId && canCreateUnit
|
|
}
|
|
onCreateUnit={() => setAddUnitDialogOpen(true)}
|
|
/>
|
|
</div>
|
|
<div className="border rounded-md p-3 bg-white dark:bg-gray-800 shadow-sm dark:border-gray-700">
|
|
<h3 className="font-medium text-gray-700 dark:text-gray-200 mb-3 border-b pb-2 dark:border-gray-700 flex items-center justify-between gap-2">
|
|
<span>{t("contentManagement.Units")}</span>
|
|
{unitCapacity !== null && (
|
|
<span className="text-xs font-normal px-2 py-0.5 rounded-full bg-primary-50 text-primary-700 dark:bg-primary-900/30 dark:text-primary-300 border border-primary-200 dark:border-primary-800">
|
|
{mergedUnits.length}/{unitCapacity}
|
|
</span>
|
|
)}
|
|
</h3>
|
|
<div className="mb-3">
|
|
<div className="relative">
|
|
<Search className="absolute left-2 top-2.5 h-4 w-4 text-gray-500 dark:text-gray-400" />
|
|
<Input
|
|
placeholder={t("search.searchUnits", "Search units...")}
|
|
className="pl-8 text-sm dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600"
|
|
value={unitSearchQuery}
|
|
onChange={(e) => setUnitSearchQuery(e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<UnitList
|
|
organizationId={selectedOrgId}
|
|
selectedUnitId={selectedUnitId}
|
|
onSelectUnit={(id: string) => {
|
|
setDepartments([]);
|
|
setEmployees([]);
|
|
handleSelectUnit(id, mergedUnits);
|
|
}}
|
|
units={mergedUnits}
|
|
isLoading={unitsLoading}
|
|
searchQuery={unitSearchQuery}
|
|
onAddDepartment={actionHandlers.unit.onAddDepartment}
|
|
onManageUnitAdmin={actionHandlers.unit.onManageUnitAdmin}
|
|
onEditUnit={actionHandlers.unit.onEditUnit}
|
|
onArchiveUnit={actionHandlers.unit.onArchiveUnit}
|
|
onDeleteUnit={actionHandlers.unit.onDeleteUnit}
|
|
onAddFromSubCity={actionHandlers.unit.onAddFromSubCity}
|
|
/>
|
|
</div>
|
|
<div className="border rounded-md p-3 bg-white dark:bg-gray-800 shadow-sm dark:border-gray-700">
|
|
<h3 className="font-medium text-gray-700 dark:text-gray-200 mb-3 border-b pb-2 dark:border-gray-700">
|
|
{t("userIncoming.Departments")}
|
|
</h3>
|
|
<div className="mb-3">
|
|
<div className="relative">
|
|
<Search className="absolute left-2 top-2.5 h-4 w-4 text-gray-500 dark:text-gray-400" />
|
|
<Input
|
|
placeholder={t(
|
|
"search.searchDepartments",
|
|
"Search departments...",
|
|
)}
|
|
className="pl-8 text-sm dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600"
|
|
value={departmentSearchQuery}
|
|
onChange={(e) => setDepartmentSearchQuery(e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<DepartmentList
|
|
unitId={selectedUnitId}
|
|
selectedDepartmentId={selectedDepartmentId}
|
|
onSelectDepartment={(id: string) => {
|
|
if (id !== selectedDepartmentId) {
|
|
setEmployees([]);
|
|
handleSelectDepartment(id, departments);
|
|
}
|
|
}}
|
|
departments={departments}
|
|
positions={positions}
|
|
isLoading={deptsLoading}
|
|
searchQuery={departmentSearchQuery}
|
|
onAddUser={actionHandlers.department.onAddUserUnderDepartment}
|
|
onViewUsers={actionHandlers.department.onViewUsers}
|
|
onAddSubDepartment={actionHandlers.department.onAddSubDepartment}
|
|
onEditDepartment={actionHandlers.department.onEditDepartment}
|
|
onDeleteDepartment={actionHandlers.department.onDeleteDepartment}
|
|
onArchiveDepartment={handleArchiveDepartment}
|
|
onDelegate={actionHandlers.department.onDelegate}
|
|
onAssignUser={actionHandlers.department.onAssignUser}
|
|
onMoveDepartment={actionHandlers.department.onMoveDepartment}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-4 lg:mt-0 border rounded-md p-4 bg-white dark:bg-gray-800 shadow-sm dark:border-gray-700">
|
|
<h3 className="font-medium text-gray-700 dark:text-gray-200 mb-4 border-b pb-2 flex items-center dark:border-gray-700">
|
|
<UserCircle className="mr-2 h-4 w-4 text-gray-500 dark:text-gray-400" />
|
|
{t("contentManagement.teamMember")}
|
|
</h3>
|
|
<div className="mb-4">
|
|
<div className="relative">
|
|
<Search className="absolute left-2 top-2.5 h-4 w-4 text-gray-500 dark:text-gray-400" />
|
|
<Input
|
|
placeholder={t("search.searchEmployees", "Search employees...")}
|
|
className="pl-8 text-sm dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600"
|
|
value={employeeSearchQuery}
|
|
onChange={(e) => setEmployeeSearchQuery(e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<TeamMembers
|
|
departmentId={selectedDepartmentId}
|
|
departmentName={selectedDepartmentName}
|
|
employees={employees}
|
|
isLoading={employeesLoading}
|
|
searchQuery={employeeSearchQuery}
|
|
onDeleteEmployee={actionHandlers.teamMember.onDeleteTeamMember}
|
|
onInviteEmployee={handleInviteEmployee}
|
|
onDeactivateTeamMember={
|
|
actionHandlers.teamMember.onDeactivateTeamMember
|
|
}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Dialogs */}
|
|
{actionHandlers.dialogs.manageUnitAdmin.isOpen && (
|
|
<ManageUnitAdminDialog {...actionHandlers.dialogs.manageUnitAdmin} />
|
|
)}
|
|
{actionHandlers.dialogs.userUnderDepartment.isOpen && (
|
|
<AddUserUnderDepartmentDialog
|
|
{...actionHandlers.dialogs.userUnderDepartment}
|
|
/>
|
|
)}
|
|
{actionHandlers.dialogs.editUnit.isOpen && (
|
|
<EditUnitDialog
|
|
{...actionHandlers.dialogs.editUnit}
|
|
units={fetchedUnits}
|
|
/>
|
|
)}
|
|
{actionHandlers.dialogs.deleteUnit.isOpen && (
|
|
<DeleteUnitDialog {...actionHandlers.dialogs.deleteUnit} />
|
|
)}
|
|
{actionHandlers.dialogs.permanentDeleteUnit.isOpen && (
|
|
<PermanentDeleteUnitDialog
|
|
{...actionHandlers.dialogs.permanentDeleteUnit}
|
|
/>
|
|
)}
|
|
{actionHandlers.dialogs.addFromSubCity.isOpen && (
|
|
<AddFromSubCityDialog {...actionHandlers.dialogs.addFromSubCity} />
|
|
)}
|
|
{addUnitDialogOpen && (
|
|
<AddUnitDialog
|
|
isOpen={addUnitDialogOpen}
|
|
onClose={() => setAddUnitDialogOpen(false)}
|
|
organizationId={selectedOrgId}
|
|
/>
|
|
)}
|
|
{actionHandlers.dialogs.department.isOpen && (
|
|
<AddDepartmentDialog
|
|
{...actionHandlers.dialogs.department}
|
|
organizationId={selectedOrgId}
|
|
/>
|
|
)}
|
|
{actionHandlers.dialogs.editDepartment.isOpen && (
|
|
<EditDepartmentDialog
|
|
{...actionHandlers.dialogs.editDepartment}
|
|
postions={positions}
|
|
/>
|
|
)}
|
|
{actionHandlers.dialogs.deleteDepartment.isOpen && (
|
|
<DeleteDepartmentDialog {...actionHandlers.dialogs.deleteDepartment} />
|
|
)}
|
|
{actionHandlers.dialogs.subDepartment.isOpen && (
|
|
<AddSubDepartmentDialog {...actionHandlers.dialogs.subDepartment} />
|
|
)}
|
|
{actionHandlers.dialogs.moveDepartment.isOpen && (
|
|
<MoveDepartmentDialog
|
|
isOpen={actionHandlers.dialogs.moveDepartment.isOpen}
|
|
onClose={actionHandlers.dialogs.moveDepartment.onClose}
|
|
departmentId={actionHandlers.dialogs.moveDepartment.departmentId}
|
|
organizationId={selectedOrgId}
|
|
unitId={selectedUnitId}
|
|
/>
|
|
)}
|
|
|
|
{actionHandlers.dialogs.user.isOpen && (
|
|
<AddUserDialog {...actionHandlers.dialogs.user} />
|
|
)}
|
|
{actionHandlers.dialogs.viewUsers.isOpen && (
|
|
<ViewUsersDialog {...actionHandlers.dialogs.viewUsers} />
|
|
)}
|
|
{actionHandlers.dialogs.assignUser.isOpen && (
|
|
<AddAssignUserDialog {...actionHandlers.dialogs.assignUser} />
|
|
)}
|
|
{actionHandlers.dialogs.deleteTeamMember.isOpen && (
|
|
<DeleteTeamMemberDialog {...actionHandlers.dialogs.deleteTeamMember} />
|
|
)}
|
|
{actionHandlers.dialogs.deactivateTeamMember.isOpen && (
|
|
<DeactivateTeamMemberDialog
|
|
isOpen={actionHandlers.dialogs.deactivateTeamMember.isOpen}
|
|
onClose={actionHandlers.dialogs.deactivateTeamMember.onClose}
|
|
userId={actionHandlers.dialogs.deactivateTeamMember.userId}
|
|
teamMemberName={
|
|
actionHandlers.dialogs.deactivateTeamMember.teamMemberName
|
|
}
|
|
/>
|
|
)}
|
|
|
|
{/* Unit Selection Modal */}
|
|
{showUnitSelection && (
|
|
<div className="fixed inset-0 bg-black/40 dark:bg-black/60 flex items-center justify-center z-50">
|
|
<UnitSelectionModal
|
|
units={exportUnits}
|
|
isLoading={isLoadingUnits}
|
|
onSelectUnit={handleUnitSelect}
|
|
onClose={() => setShowUnitSelection(false)}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Confirm Resend to All Modal */}
|
|
{showConfirmAllModal && (
|
|
<div className="fixed inset-0 bg-black/40 dark:bg-black/60 flex items-center justify-center z-50">
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4 shadow-xl border dark:border-gray-700 animate-in fade-in zoom-in-95 duration-150">
|
|
<h3 className="text-lg font-semibold mb-2 dark:text-white">
|
|
{t(
|
|
"userManagement.confirmResendTitle",
|
|
"Resend Verification Code",
|
|
)}
|
|
</h3>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400 mb-6">
|
|
{t(
|
|
"userManagement.confirmResendDescription",
|
|
"Are you sure you want to resend verification to all pending users?",
|
|
)}
|
|
</p>
|
|
<div className="flex justify-end gap-2">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setShowConfirmAllModal(false)}
|
|
disabled={isSendingInvitations}
|
|
className="dark:border-gray-600 dark:text-gray-200 dark:hover:bg-gray-700"
|
|
>
|
|
{t("common.cancel")}
|
|
</Button>
|
|
<Button
|
|
onClick={handleResendToAll}
|
|
disabled={isSendingInvitations}
|
|
className="bg-primary hover:bg-primary-700 text-white font-medium shadow-md"
|
|
>
|
|
{isSendingInvitations ? (
|
|
<span className="flex items-center">
|
|
<RefreshCw className="animate-spin h-4 w-4 mr-2" />
|
|
{t("common.sending", "Sending...")}
|
|
</span>
|
|
) : (
|
|
t("common.send", "Send")
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Resend Invitation (MultiSelect) Modal */}
|
|
{showResendModal && (
|
|
<div className="fixed inset-0 bg-black/40 dark:bg-black/60 flex items-center justify-center z-50">
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4 flex flex-col max-h-[85vh] shadow-xl border dark:border-gray-700 animate-in fade-in zoom-in-95 duration-150">
|
|
<h3 className="text-lg font-semibold mb-2 dark:text-white">
|
|
{t("userManagement.resendInvitation", "Resend Invitation")}
|
|
</h3>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4">
|
|
{t(
|
|
"userManagement.selectUsersDescription",
|
|
"Select the users you want to resend invitations to:",
|
|
)}
|
|
</p>
|
|
|
|
{/* Scrollable container for the dropdown selection and items */}
|
|
<div className="flex-1 overflow-y-auto my-2 pr-1 space-y-4 min-h-[200px]">
|
|
{isLoadingPending ? (
|
|
<div className="flex justify-center items-center py-12">
|
|
<RefreshCw className="animate-spin h-8 w-8 text-primary" />
|
|
</div>
|
|
) : pendingUsers.length === 0 ? (
|
|
<div className="text-center py-12 text-gray-500 dark:text-gray-400">
|
|
{t(
|
|
"userManagement.noPendingUsers",
|
|
"No pending users found in your unit.",
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div className="w-full">
|
|
<MultiSelect
|
|
options={pendingUsers.map((emp) => {
|
|
const empName =
|
|
typeof emp.name === "string"
|
|
? emp.name
|
|
: localizedName(emp.name) ||
|
|
(emp.user?.name
|
|
? localizedName(emp.user.name)
|
|
: "Unnamed");
|
|
return {
|
|
label: empName,
|
|
value: emp.id,
|
|
};
|
|
})}
|
|
value={selectedUserIds}
|
|
onValueChange={setSelectedUserIds}
|
|
placeholder={t(
|
|
"userManagement.selectUsersPlaceholder",
|
|
"Select employees...",
|
|
)}
|
|
maxCount={5}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex justify-end gap-2 mt-6 border-t pt-4 dark:border-gray-700">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => {
|
|
setShowResendModal(false);
|
|
setSelectedUserIds([]);
|
|
}}
|
|
disabled={isSendingInvitations}
|
|
className="dark:border-gray-600 dark:text-gray-200 dark:hover:bg-gray-700"
|
|
>
|
|
{t("common.cancel")}
|
|
</Button>
|
|
<Button
|
|
onClick={handleSendSelectedInvitations}
|
|
disabled={selectedUserIds.length === 0 || isSendingInvitations}
|
|
className="bg-primary hover:bg-primary-700 text-white font-medium shadow-md"
|
|
>
|
|
{isSendingInvitations ? (
|
|
<span className="flex items-center">
|
|
<RefreshCw className="animate-spin h-4 w-4 mr-2" />
|
|
{t("common.sending", "Sending...")}
|
|
</span>
|
|
) : (
|
|
t("common.send", "Send")
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Organization Employee Search Modal */}
|
|
<OrganizationEmployeeSearch
|
|
organizationId={organizationId}
|
|
isOpen={showEmployeeSearch}
|
|
onClose={() => setShowEmployeeSearch(false)}
|
|
/>
|
|
|
|
{showSendToUnitModal && (
|
|
<div className="fixed inset-0 bg-black/40 dark:bg-black/60 flex items-center justify-center z-50">
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4 flex flex-col max-h-[85vh] shadow-xl border dark:border-gray-700 animate-in fade-in zoom-in-95 duration-150">
|
|
<h3 className="text-lg font-semibold mb-2 dark:text-white">
|
|
{t("userManagement.sendToSelectedUnit", "Send to Selected Unit")}
|
|
</h3>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4">
|
|
Select the unit you want to send verification to:
|
|
</p>
|
|
<div className="flex-1 overflow-y-auto my-2 pr-1 space-y-4 min-h-[200px]">
|
|
<SingleSelect
|
|
options={mergedUnits.map((unit) => {
|
|
const unitName =
|
|
typeof unit.name === "string"
|
|
? unit.name
|
|
: localizedName(unit.name) ||
|
|
(typeof unit.name === "object" && unit.name?.en) ||
|
|
unit.id ||
|
|
"Unnamed Unit";
|
|
return {
|
|
label: unitName,
|
|
value: unit.id,
|
|
};
|
|
})}
|
|
value={selectedSendUnitId}
|
|
onValueChange={setSelectedSendUnitId}
|
|
placeholder={t(
|
|
"search.selectUnitPlaceholder",
|
|
"Select unit...",
|
|
)}
|
|
/>
|
|
</div>
|
|
<div className="flex justify-end gap-2 mt-6 border-t pt-4 dark:border-gray-700">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => {
|
|
setShowSendToUnitModal(false);
|
|
setSelectedSendUnitId("");
|
|
}}
|
|
className="dark:border-gray-600 dark:text-gray-200 dark:hover:bg-gray-700"
|
|
>
|
|
{t("common.cancel")}
|
|
</Button>
|
|
<Button
|
|
onClick={handleSendToSelectedUnits}
|
|
disabled={!selectedSendUnitId}
|
|
className="bg-primary hover:bg-primary-700 text-white font-medium shadow-md"
|
|
>
|
|
{t("common.send", "Send")}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default UserManagementTree;
|