Files
edr-platform/apps/edr-freight-web/backoffice/src/pages/OrgAdminDashboard.tsx
natib21 1b9b4ebb77 fix ui
2026-07-14 09:12:22 +00:00

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;