mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 23:00:57 +00:00
352 lines
12 KiB
TypeScript
352 lines
12 KiB
TypeScript
import {
|
|
Card,
|
|
CardContent,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "@/shared/common/ui/card";
|
|
import { Button } from "@/shared/common/ui/button";
|
|
import {
|
|
Users,
|
|
FileText,
|
|
Building2,
|
|
Files,
|
|
Upload,
|
|
AlertCircle,
|
|
RefreshCw,
|
|
} from "lucide-react";
|
|
import { useNavigate } from "react-router-dom";
|
|
import { Navigate } from "react-router-dom";
|
|
import { useOrganizationReport } from "@/shared/hooks/useOrganizationReport";
|
|
import { Alert, AlertDescription } from "@/shared/common/ui/alert";
|
|
import { Skeleton } from "@/shared/common/ui/skeleton";
|
|
import { useAuth } from "@/shared/context/AuthContext";
|
|
import { t } from "i18next";
|
|
import SmartOfficeAuditPage from "@/record-management/pages/AuditLog/SmartOfficeAuditPage";
|
|
import { useUnit } from "@/user-management/hooks/useUnit";
|
|
import type { UnitDto } from "@/user-management/dto/unit/unitDto";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/shared/common/ui/select";
|
|
import { useEffect, useState } from "react";
|
|
import { useLocalizedName } from "@/shared/common/localizedName";
|
|
import { useUnitReport } from "@/shared/hooks/useUnitReport";
|
|
import {
|
|
useTenantConfig,
|
|
resolveModuleConfig,
|
|
} from "@/layout/components/TenantConfig";
|
|
import { hasComplaintVerification } from "@/complaints/utils/complaintVerificationStorage";
|
|
import { COMPLAINT_RECORDS_PATH } from "@/complaints/utils/complaintRoutes";
|
|
|
|
const MODULE_PATHS: Record<string, string> = {
|
|
recordManagement: "/record-management/dashboard",
|
|
performance: "/performance-management/plan-years",
|
|
objective: "/objective-management/plan-years",
|
|
dms: "/dms/dashboard",
|
|
};
|
|
|
|
const OrgAdminDashboard = () => {
|
|
const navigate = useNavigate();
|
|
const { user } = useAuth();
|
|
const { config: tenantConfig } = useTenantConfig();
|
|
const localizedName = useLocalizedName();
|
|
const organizationId = user?.employee?.[0]?.organizationId ?? "";
|
|
const isOrganizationAdmin =
|
|
user?.roles?.some((role) => role.key === "organization_admin") ?? false;
|
|
const hasUnitAdminRole =
|
|
user?.roles?.some((role) => role.key === "unit_admin") ?? false;
|
|
const isUnitAdmin = !isOrganizationAdmin && hasUnitAdminRole;
|
|
const [selectedUnitId, setSelectedUnitId] = useState("");
|
|
const { getAccessibleList } = useUnit();
|
|
const { data: unitsResponse, isLoading: isUnitsLoading } = getAccessibleList(
|
|
organizationId,
|
|
{ take: 1000, skip: 0 },
|
|
isUnitAdmin && !!organizationId,
|
|
);
|
|
const units = (unitsResponse?.data?.items ?? []) as UnitDto[];
|
|
|
|
useEffect(() => {
|
|
if (!units.some((unit) => unit.id === selectedUnitId)) {
|
|
setSelectedUnitId(units[0]?.id ?? "");
|
|
}
|
|
}, [selectedUnitId, units]);
|
|
|
|
const { report, isLoading, isError, error, refetch } =
|
|
useOrganizationReport(organizationId, { enabled: !isUnitAdmin });
|
|
const unitReportQuery = useUnitReport(organizationId, selectedUnitId, {
|
|
enabled: isUnitAdmin,
|
|
});
|
|
const activeReport = isUnitAdmin ? unitReportQuery.data : report;
|
|
const isDashboardLoading = isUnitAdmin
|
|
? isUnitsLoading || (!!selectedUnitId && unitReportQuery.isLoading)
|
|
: isLoading;
|
|
const isDashboardError = isUnitAdmin
|
|
? unitReportQuery.isError
|
|
: isError;
|
|
const dashboardError = isUnitAdmin ? unitReportQuery.error : error;
|
|
const refetchDashboard = () =>
|
|
isUnitAdmin ? unitReportQuery.refetch() : refetch();
|
|
|
|
const statusCode: number | undefined = (dashboardError as any)?.response
|
|
?.status;
|
|
const isUnauthorized =
|
|
isDashboardError && (statusCode === 401 || statusCode === 403);
|
|
|
|
const fallbackPath = (() => {
|
|
if (hasComplaintVerification()) return COMPLAINT_RECORDS_PATH;
|
|
|
|
const modules = resolveModuleConfig(tenantConfig);
|
|
const enabledModulePaths = Object.entries(MODULE_PATHS)
|
|
.filter(([key]) => modules[key as keyof typeof modules])
|
|
.map(([, path]) => path);
|
|
|
|
if (enabledModulePaths.length === 1) return enabledModulePaths[0];
|
|
return "/homepage";
|
|
})();
|
|
|
|
if (isUnauthorized) {
|
|
return <Navigate to={fallbackPath} replace />;
|
|
}
|
|
|
|
// Define stats structure for the dashboard
|
|
const getStats = () => [
|
|
// One brand hue, stepped depth per tile — identity comes from the icon
|
|
// and title, so the chips stay cohesive instead of competing hues.
|
|
{
|
|
id: "employees",
|
|
title: t("dashboard.totalEmployees"),
|
|
value: activeReport?.employeesCount?.toLocaleString() || "0",
|
|
icon: Users,
|
|
color: "from-primary-500 to-primary-600",
|
|
},
|
|
...(!isUnitAdmin
|
|
? [{
|
|
id: "units",
|
|
title: t("dashboard.totalUnits"),
|
|
value: report?.unitsCount?.toLocaleString() || "0",
|
|
icon: Building2,
|
|
color: "from-primary-600 to-primary-700",
|
|
}]
|
|
: []),
|
|
{
|
|
id: "positions",
|
|
title: t("dashboard.totalPositions"),
|
|
value: activeReport?.positionsCount?.toLocaleString() || "0",
|
|
icon: FileText,
|
|
color: "from-primary-700 to-primary-800",
|
|
},
|
|
];
|
|
|
|
// Use report activities if available, otherwise use static data
|
|
|
|
// Brand gradient for all navigation actions; archive alone stays neutral
|
|
// gray to read as the "dormant" destination. Icons carry the identity.
|
|
const quickActions = [
|
|
{
|
|
id: "user-mgmt",
|
|
title: t("dashboard.userManagement"),
|
|
description: t("dashboard.userManagementDesc"),
|
|
icon: Users,
|
|
action: () => navigate("/user-management/user_management"),
|
|
color: "bg-gradient-to-r from-primary-500 to-primary-700",
|
|
},
|
|
{
|
|
id: "content-mgmt",
|
|
title: t("dashboard.contentManagement"),
|
|
description: t("dashboard.contentManagementDesc"),
|
|
icon: Files,
|
|
action: () => navigate("/user-management/content-management"),
|
|
color: "bg-gradient-to-r from-primary-500 to-primary-700",
|
|
},
|
|
{
|
|
id: "excel-upload",
|
|
title: t("dashboard.excelUploader"),
|
|
description: t("dashboard.excelUploaderDesc"),
|
|
icon: Upload,
|
|
action: () => navigate("/user-management/bulk-upload"),
|
|
color: "bg-gradient-to-r from-primary-500 to-primary-700",
|
|
},
|
|
{
|
|
id: "position-settings",
|
|
title: t("dashboard.positionSettings"),
|
|
description: t("dashboard.positionSettingsDesc"),
|
|
icon: FileText,
|
|
action: () => navigate("/user-management/position-management"),
|
|
color: "bg-gradient-to-r from-primary-500 to-primary-700",
|
|
},
|
|
{
|
|
id: "archive-users",
|
|
title: t("dashboard.archiveUsers"),
|
|
description: t("dashboard.archiveUsersDesc"),
|
|
icon: AlertCircle,
|
|
action: () => navigate("/user-management/archives"),
|
|
color: "bg-gradient-to-r from-gray-500 to-slate-600",
|
|
},
|
|
{
|
|
id: "web-management",
|
|
title: t("dashboard.webManagement"),
|
|
description: t("dashboard.webManagementDesc"),
|
|
icon: FileText,
|
|
action: () => navigate("/user-management/web-management"),
|
|
color: "bg-gradient-to-r from-primary-500 to-primary-700",
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div className="mx-auto p-6 space-y-6">
|
|
<div className="flex justify-between items-center">
|
|
<div>
|
|
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">
|
|
{isUnitAdmin
|
|
? t("dashboard.unitDashboard", "Unit Dashboard")
|
|
: t("dashboard.organizationDashboard")}
|
|
</h1>
|
|
<p className="text-muted-foreground dark:text-gray-400">
|
|
{t("dashboard.orgMsg")}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3">
|
|
{isUnitAdmin && (
|
|
<Select
|
|
value={selectedUnitId}
|
|
onValueChange={setSelectedUnitId}
|
|
disabled={isUnitsLoading || units.length === 0}
|
|
>
|
|
<SelectTrigger className="w-[260px]">
|
|
<SelectValue
|
|
placeholder={t("dashboard.selectUnit", "Select unit")}
|
|
/>
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{units.map((unit) => (
|
|
<SelectItem key={unit.id} value={unit.id}>
|
|
{localizedName(unit.name)}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
)}
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={refetchDashboard}
|
|
disabled={isDashboardLoading || (isUnitAdmin && !selectedUnitId)}
|
|
>
|
|
<RefreshCw
|
|
className={`h-4 w-4 mr-2 ${
|
|
isDashboardLoading ? "animate-spin" : ""
|
|
}`}
|
|
/>
|
|
{t("dashboard.refresh")}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Error display */}
|
|
{isDashboardError && (
|
|
<Alert variant="destructive">
|
|
<AlertCircle className="h-4 w-4" />
|
|
<AlertDescription>
|
|
{t("dashboard.errorMsg")}
|
|
{dashboardError instanceof Error && `: ${dashboardError.message}`}
|
|
</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
{/* Stats Grid */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
|
{isDashboardLoading
|
|
? // Loading skeletons for stats
|
|
Array(isUnitAdmin ? 2 : 3)
|
|
.fill(0)
|
|
.map((_, index) => (
|
|
<Card
|
|
key={`skeleton-stat-${index}`}
|
|
className="hover:shadow-lg transition-shadow duration-200 dark:bg-gray-800 dark:border-gray-700"
|
|
>
|
|
<CardContent className="p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div className="w-full">
|
|
<Skeleton className="h-4 w-24 mb-2" />
|
|
<Skeleton className="h-8 w-16" />
|
|
</div>
|
|
<Skeleton className="h-12 w-12 rounded-full" />
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))
|
|
: getStats().map((stat) => (
|
|
<Card
|
|
key={`stat-${stat.id}`}
|
|
className="hover:shadow-lg transition-shadow duration-200 border-l-4 border-l-primary-500 dark:bg-gray-800 dark:border-gray-700 dark:border-l-primary-500"
|
|
>
|
|
<CardContent className="p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-muted-foreground dark:text-gray-400">
|
|
{stat.title}
|
|
</p>
|
|
<h3 className="text-3xl font-bold mt-2 text-gray-900 dark:text-gray-100">
|
|
{stat.value}
|
|
</h3>
|
|
</div>
|
|
<div
|
|
className={`p-4 rounded-full bg-gradient-to-r ${stat.color} shadow-lg`}
|
|
>
|
|
<stat.icon className="h-6 w-6 text-white" />
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
|
|
{/* Quick Actions */}
|
|
<Card className="dark:bg-gray-800 dark:border-gray-700">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center dark:text-gray-100">
|
|
<RefreshCw className="h-5 w-5 mr-2" />
|
|
{t("landingPage.quickActions")}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
{quickActions.map((action) => (
|
|
<Button
|
|
key={`action-${action.id}`}
|
|
onClick={action.action}
|
|
className={`h-auto p-6 ${action.color} text-white hover:opacity-90 hover:scale-105 transition-all duration-200`}
|
|
>
|
|
<div className="flex flex-col items-center space-y-3 text-center">
|
|
<action.icon className="h-8 w-8" />
|
|
<div>
|
|
<div className="font-semibold text-base">
|
|
{action.title}
|
|
</div>
|
|
<div className="text-sm opacity-90 mt-1">
|
|
{action.description}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Button>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
{isUnitAdmin ? (
|
|
selectedUnitId && <SmartOfficeAuditPage unitId={selectedUnitId} />
|
|
) : (
|
|
organizationId && (
|
|
<SmartOfficeAuditPage organizationId={organizationId} />
|
|
)
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default OrgAdminDashboard;
|