mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-01 01:57:37 +00:00
509 lines
18 KiB
TypeScript
509 lines
18 KiB
TypeScript
import SealCard from "./SealCard";
|
|
import LetterTemplatesTable from "./LetterTemplates/LetterTemplatesTable";
|
|
import RecentActivitiesCard from "./RecentActivitiesCard";
|
|
import { CommonRemarks } from "./CommonRemarks";
|
|
import { useAuth } from "@/shared/context/AuthContext";
|
|
import { useUnit } from "@/user-management/hooks/useUnit";
|
|
import HeaderAndFooter from "./HeaderAndFooter";
|
|
import { useState, useEffect, useCallback, useMemo } from "react";
|
|
import { useSearchParams } from "react-router-dom";
|
|
import { UnitDto } from "@/user-management/dto/unit/unitDto";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/shared/common/ui/select";
|
|
import { PrefixAndSuffix } from "./PrefixAndSuffixCard";
|
|
import { t } from "i18next";
|
|
import i18n from "@/i18n";
|
|
import AdminSetupAlert from "@/layout/components/AlertShow";
|
|
import RecordTagsManagement from "./record-tags/RecordTags";
|
|
import { TemplateSamplePage } from "@/user-management/TemplateSample/TemplateSamplePage";
|
|
import {
|
|
BookImage,
|
|
ClipboardType,
|
|
ChevronRight,
|
|
Menu,
|
|
PencilRuler,
|
|
SquareActivity,
|
|
Stamp,
|
|
Tag,
|
|
X,
|
|
Building,
|
|
ClipboardList,
|
|
FileText,
|
|
} from "lucide-react";
|
|
import { cn } from "@/shared/lib/utils";
|
|
import { useDebounce } from "../content/useDebounce";
|
|
import { SingleSelect } from "@/shared/common/ui/single-select";
|
|
|
|
export interface FilePreview {
|
|
type: "header" | "footer" | "seal";
|
|
url: string;
|
|
file: File;
|
|
uploadedAt: Date;
|
|
id: string | null;
|
|
}
|
|
|
|
export interface Activity {
|
|
id: string;
|
|
type: string;
|
|
description: string;
|
|
timestamp: Date;
|
|
}
|
|
|
|
interface MenuItem {
|
|
label: string;
|
|
icon: React.ReactNode;
|
|
element?: React.ReactNode;
|
|
}
|
|
|
|
const ContentManagement = () => {
|
|
const { user } = useAuth();
|
|
const lang = i18n.language;
|
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
|
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
|
|
const [selectedUnitId, setSelectedUnitId] = useState<string>("");
|
|
const debouncedUnitId = useDebounce(selectedUnitId, 300);
|
|
|
|
const organizationId =
|
|
user?.employee && user.employee.length > 0
|
|
? user.employee[0].organizationId
|
|
: undefined;
|
|
|
|
const { getAccessibleList } = useUnit();
|
|
const { data: unitsResponse, isLoading: unitsLoading } = organizationId
|
|
? getAccessibleList(organizationId, { take: 300, skip: 0 })
|
|
: { data: undefined, isLoading: false };
|
|
|
|
// Notify listeners (e.g. AdminSetupAlert) after the unit selection debounces.
|
|
useEffect(() => {
|
|
if (!debouncedUnitId) return;
|
|
|
|
window.dispatchEvent(
|
|
new CustomEvent("unitChanged", { detail: debouncedUnitId }),
|
|
);
|
|
}, [debouncedUnitId]);
|
|
|
|
const handleUnitChange = useCallback(
|
|
(value: string) => {
|
|
setSelectedUnitId(value);
|
|
setSearchParams(
|
|
(prev) => {
|
|
const next = new URLSearchParams(prev);
|
|
next.set("unit", value);
|
|
return next;
|
|
},
|
|
{ replace: true },
|
|
);
|
|
},
|
|
[setSearchParams],
|
|
);
|
|
|
|
const selectMenuItem = useCallback(
|
|
(index: number, label: string) => {
|
|
setSearchParams(
|
|
(prev) => {
|
|
const next = new URLSearchParams(prev);
|
|
next.set("tab", label);
|
|
if (selectedUnitId) {
|
|
next.set("unit", selectedUnitId);
|
|
}
|
|
return next;
|
|
},
|
|
{ replace: true },
|
|
);
|
|
setIsMobileMenuOpen(false);
|
|
},
|
|
[selectedUnitId, setSearchParams],
|
|
);
|
|
|
|
// Close mobile menu when resizing to desktop
|
|
useEffect(() => {
|
|
const handleResize = () => {
|
|
if (window.innerWidth >= 768) {
|
|
setIsMobileMenuOpen(false);
|
|
}
|
|
};
|
|
|
|
window.addEventListener("resize", handleResize);
|
|
return () => window.removeEventListener("resize", handleResize);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const unitFromUrl = searchParams.get("unit");
|
|
const items = unitsResponse?.data?.items;
|
|
if (
|
|
unitFromUrl &&
|
|
items?.some((unit: UnitDto) => unit.id === unitFromUrl) &&
|
|
unitFromUrl !== selectedUnitId
|
|
) {
|
|
setSelectedUnitId(unitFromUrl);
|
|
return;
|
|
}
|
|
|
|
if (
|
|
!unitsLoading &&
|
|
items?.length &&
|
|
!selectedUnitId &&
|
|
!unitFromUrl
|
|
) {
|
|
const firstUnitId = items[0].id;
|
|
setSelectedUnitId(firstUnitId);
|
|
setSearchParams(
|
|
(prev) => {
|
|
const next = new URLSearchParams(prev);
|
|
next.set("unit", firstUnitId);
|
|
if (!next.get("tab")) {
|
|
next.set("tab", "contentManagement.seal");
|
|
}
|
|
return next;
|
|
},
|
|
{ replace: true },
|
|
);
|
|
}
|
|
}, [
|
|
unitsLoading,
|
|
unitsResponse,
|
|
selectedUnitId,
|
|
searchParams,
|
|
setSearchParams,
|
|
]);
|
|
|
|
// Sidebar menu items with dynamic loading states
|
|
const menuItems: MenuItem[] = [
|
|
{
|
|
label: "contentManagement.seal",
|
|
icon: <Stamp className="h-5 w-5 flex-shrink-0" />,
|
|
element: <SealCard unitId={selectedUnitId} />,
|
|
},
|
|
{
|
|
label: "contentManagement.letterTemplate",
|
|
icon: <ClipboardList className="h-5 w-5 flex-shrink-0" />,
|
|
element: <LetterTemplatesTable unitId={selectedUnitId} />,
|
|
},
|
|
{
|
|
label: "recordTag.title",
|
|
icon: <Tag className="h-5 w-5 flex-shrink-0" />,
|
|
element: <RecordTagsManagement unitId={selectedUnitId} />,
|
|
},
|
|
{
|
|
label: "contentManagement.prefix",
|
|
icon: <PencilRuler className="h-5 w-5 flex-shrink-0" />,
|
|
element: (
|
|
<PrefixAndSuffix unitId={selectedUnitId} initialTab="internal" />
|
|
),
|
|
},
|
|
{
|
|
label: "contentManagement.commonRemarks",
|
|
icon: <ClipboardType className="h-5 w-5 flex-shrink-0" />,
|
|
element: <CommonRemarks unitId={selectedUnitId} />,
|
|
},
|
|
{
|
|
label: "contentManagement.headerAndFooter",
|
|
icon: <BookImage className="h-5 w-5 flex-shrink-0" />,
|
|
element: <HeaderAndFooter unitId={selectedUnitId} />,
|
|
},
|
|
{
|
|
label: "template.sample",
|
|
icon: <FileText className="h-5 w-5 flex-shrink-0" />,
|
|
element: <TemplateSamplePage unitId={selectedUnitId} />,
|
|
},
|
|
{
|
|
label: "dashboard.recentActivities",
|
|
icon: <SquareActivity className="h-5 w-5 flex-shrink-0" />,
|
|
element: <RecentActivitiesCard />,
|
|
},
|
|
];
|
|
|
|
const activeMenuIndex = useMemo(() => {
|
|
const tab = searchParams.get("tab");
|
|
if (!tab) return 0;
|
|
const index = menuItems.findIndex((item) => item.label === tab);
|
|
return index >= 0 ? index : 0;
|
|
}, [menuItems, searchParams]);
|
|
|
|
const activeMenuItem = menuItems[activeMenuIndex];
|
|
const isTemplateSampleActive = activeMenuItem?.label === "template.sample";
|
|
|
|
useEffect(() => {
|
|
if (!searchParams.get("tab") && menuItems.length > 0) {
|
|
setSearchParams(
|
|
(prev) => {
|
|
const next = new URLSearchParams(prev);
|
|
next.set("tab", menuItems[0].label);
|
|
return next;
|
|
},
|
|
{ replace: true },
|
|
);
|
|
}
|
|
}, [menuItems, searchParams, setSearchParams]);
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100/30 dark:from-gray-900 dark:to-gray-800">
|
|
<div className="w-full h-full p-4 lg:p-6 space-y-6">
|
|
{/* Header Section */}
|
|
<div className="flex flex-col lg:flex-row justify-between items-start lg:items-center gap-4">
|
|
<div className="flex-1 min-w-0">
|
|
<h1 className="text-2xl lg:text-3xl font-bold text-gray-900 dark:text-white tracking-tight">
|
|
{t("organization.contentManagement")}
|
|
</h1>
|
|
<p className="text-gray-600 dark:text-gray-400 mt-2 text-sm lg:text-base">
|
|
{t("organization.contentMsg")}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Admin Alert */}
|
|
<div className="w-full lg:w-auto">
|
|
<AdminSetupAlert />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Unit Selection Card */}
|
|
{unitsResponse?.data?.items?.length > 0 && (
|
|
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-sm border border-gray-200 dark:border-gray-700 p-4 lg:p-6 transition-all duration-200 hover:shadow-md">
|
|
<div className="flex flex-col sm:flex-row sm:items-center gap-4">
|
|
<div className="flex items-center gap-3 flex-shrink-0">
|
|
<div className="p-2 bg-purple-50 dark:bg-purple-900/30 rounded-lg">
|
|
<Building className="h-5 w-5 text-purple-600 dark:text-purple-400" />
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-semibold text-gray-900 dark:text-white">
|
|
{t("organization.selectUnit")}
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex-1 min-w-0">
|
|
<SingleSelect
|
|
options={unitsResponse?.data.items.map((u: any) => ({
|
|
value: u.id,
|
|
label: lang === "en" ? u.name.en : u.name.am,
|
|
}))}
|
|
onValueChange={handleUnitChange}
|
|
value={selectedUnitId ?? ""}
|
|
placeholder={t("delegation.selectDelegatedPosition")}
|
|
/>
|
|
|
|
{/* <Select
|
|
value={selectedUnitId}
|
|
onValueChange={handleUnitChange}
|
|
disabled={unitsLoading}
|
|
>
|
|
<SelectTrigger
|
|
className={cn(
|
|
"w-full border-gray-300 dark:border-gray-600 rounded-xl shadow-sm transition-all duration-200 dark:bg-gray-700 dark:text-white",
|
|
"focus:ring-2 focus:ring-purple-500 focus:border-purple-500",
|
|
"hover:border-gray-400 dark:hover:border-gray-500",
|
|
unitsLoading && "opacity-50 cursor-not-allowed",
|
|
)}
|
|
>
|
|
<SelectValue
|
|
placeholder={
|
|
unitsLoading
|
|
? t("common.loading")
|
|
: t("organization.selectUnit")
|
|
}
|
|
/>
|
|
</SelectTrigger>
|
|
<SelectContent className="rounded-xl border border-gray-200 dark:border-gray-600 shadow-lg dark:bg-gray-800">
|
|
{unitsResponse?.data.items.map((unit: UnitDto) => (
|
|
<SelectItem
|
|
key={unit.id}
|
|
value={unit.id}
|
|
className="rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors dark:text-gray-200"
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<span className="flex-1 truncate">
|
|
{lang === "en" ? unit.name.en : unit.name.am}
|
|
</span>
|
|
{unit.id === selectedUnitId && (
|
|
<div className="w-2 h-2 bg-purple-600 dark:bg-purple-400 rounded-full" />
|
|
)}
|
|
</div>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select> */}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Main Content Grid */}
|
|
<div
|
|
className={cn(
|
|
"flex w-full min-h-[calc(100vh-12rem)] bg-transparent rounded-2xl",
|
|
isTemplateSampleActive ? "overflow-visible" : "overflow-hidden",
|
|
)}
|
|
>
|
|
{/* Mobile Overlay */}
|
|
{isMobileMenuOpen && (
|
|
<div
|
|
className="fixed inset-0 bg-black/50 dark:bg-black/70 z-40 md:hidden backdrop-blur-sm transition-opacity duration-300"
|
|
onClick={() => setIsMobileMenuOpen(false)}
|
|
/>
|
|
)}
|
|
|
|
{/* Sidebar */}
|
|
<div
|
|
className={cn(
|
|
// Remove `fixed` and use `sticky` instead for proper scroll behavior
|
|
"md:sticky top-0 flex flex-col bg-white dark:bg-gray-800 border-r border-gray-200/60 dark:border-gray-700 transition-all duration-300 ease-in-out",
|
|
"backdrop-blur-sm bg-white/95 md:bg-white dark:bg-gray-800/95",
|
|
isMobileMenuOpen
|
|
? "absolute left-0 top-0 z-40 w-72 lg:w-80"
|
|
: "hidden md:flex",
|
|
isSidebarCollapsed ? "md:w-16 lg:w-20" : "md:w-72 lg:w-80",
|
|
"h-[calc(100vh-2rem)] md:h-screen rounded-2xl md:rounded-none shadow-xl md:shadow-none",
|
|
)}
|
|
>
|
|
{/* Sidebar Header */}
|
|
<div
|
|
className={cn(
|
|
"flex items-center p-4 border-b border-gray-200/60 dark:border-gray-700 transition-all duration-300",
|
|
isSidebarCollapsed ? "justify-center" : "justify-between",
|
|
)}
|
|
>
|
|
{!isSidebarCollapsed && (
|
|
<div className="flex items-center gap-3 min-w-0">
|
|
<h2 className="text-lg font-bold text-gray-900 dark:text-white truncate">
|
|
{t("organization.contentManagement")}
|
|
</h2>
|
|
</div>
|
|
)}
|
|
<div className="flex items-center gap-1">
|
|
{/* Collapse Toggle - Desktop */}
|
|
<button
|
|
onClick={() => setIsSidebarCollapsed(!isSidebarCollapsed)}
|
|
className={cn(
|
|
"hidden md:flex p-2 rounded-xl hover:bg-purple-200 dark:hover:bg-purple-900/50 transition-all duration-200",
|
|
"hover:shadow-sm border border-transparent hover:border-purple-200 dark:hover:border-purple-800",
|
|
)}
|
|
title={
|
|
isSidebarCollapsed
|
|
? t("expandSidebar")
|
|
: t("collapseSidebar")
|
|
}
|
|
>
|
|
{isSidebarCollapsed ? (
|
|
<Menu className="h-4 w-4 text-gray-600 dark:text-gray-300" />
|
|
) : (
|
|
<Menu className="h-4 w-4 text-gray-600 dark:text-gray-300" />
|
|
)}
|
|
</button>
|
|
|
|
{/* Close Button - Mobile */}
|
|
<button
|
|
onClick={() => setIsMobileMenuOpen(false)}
|
|
className="md:hidden p-2 rounded-xl hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
|
>
|
|
<X className="h-4 w-4 text-gray-600 dark:text-gray-300" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Navigation Menu */}
|
|
<nav className="flex-1 p-3 space-y-1 overflow-y-auto">
|
|
{menuItems.map((item, index) => (
|
|
<button
|
|
key={index}
|
|
onClick={() => selectMenuItem(index, item.label)}
|
|
className={cn(
|
|
"w-full flex items-center gap-3 p-3 rounded-xl text-sm font-medium cursor-pointer transition-all duration-200",
|
|
"border border-transparent hover:border-gray-200 dark:hover:border-gray-600 hover:shadow-sm",
|
|
isSidebarCollapsed ? "justify-center" : "",
|
|
activeMenuIndex === index
|
|
? "bg-gradient-to-r from-purple-50 to-purple-50 dark:from-purple-900/30 dark:to-purple-900/20 text-purple-700 dark:text-purple-300 border-purple-200 dark:border-purple-700 shadow-sm"
|
|
: "text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-white",
|
|
)}
|
|
>
|
|
<span
|
|
className={cn(
|
|
"transition-transform duration-200",
|
|
activeMenuIndex === index && "scale-110",
|
|
)}
|
|
>
|
|
{item.icon}
|
|
</span>
|
|
{!isSidebarCollapsed && (
|
|
<span className="flex-1 text-left truncate font-semibold">
|
|
{t(item.label)}
|
|
</span>
|
|
)}
|
|
</button>
|
|
))}
|
|
</nav>
|
|
</div>
|
|
|
|
{/* Main Content Area */}
|
|
<div
|
|
className={cn(
|
|
"flex-1 flex flex-col min-w-0 transition-all duration-300",
|
|
isSidebarCollapsed ? "md:ml-0" : "md:ml-0",
|
|
)}
|
|
>
|
|
{/* Mobile Header */}
|
|
<div className="md:hidden flex items-center justify-between p-4 bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm border-b border-gray-200/60 dark:border-gray-700 rounded-t-2xl">
|
|
<button
|
|
onClick={() => setIsMobileMenuOpen(true)}
|
|
className="p-2 rounded-xl hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors shadow-sm border border-gray-200 dark:border-gray-600"
|
|
>
|
|
<Menu className="h-5 w-5 text-gray-600 dark:text-gray-300" />
|
|
</button>
|
|
<div className="flex-1 text-center">
|
|
<h1 className="text-lg font-semibold text-gray-900 dark:text-white truncate">
|
|
{t(activeMenuItem?.label || "contentmanagement")}
|
|
</h1>
|
|
</div>
|
|
<div className="w-9"></div>
|
|
</div>
|
|
|
|
{/* Page Content */}
|
|
<div
|
|
className={cn(
|
|
"flex-1 p-4 md:p-6",
|
|
isTemplateSampleActive ? "overflow-visible" : "overflow-auto",
|
|
)}
|
|
>
|
|
{/* Breadcrumb Navigation */}
|
|
<div className="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400 mb-6 flex-wrap">
|
|
<span className="text-gray-400 dark:text-gray-500">
|
|
{t("content")}
|
|
</span>
|
|
{activeMenuItem && (
|
|
<>
|
|
<ChevronRight className="h-4 w-4 text-gray-400 dark:text-gray-500" />
|
|
<span className="text-gray-900 dark:text-white font-semibold bg-gray-100 dark:bg-gray-700 px-3 py-1 rounded-full text-sm">
|
|
{t(activeMenuItem.label)}
|
|
</span>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
{/* Render the active element with loading state */}
|
|
<div className={cn("w-full transition-opacity duration-300")}>
|
|
<div
|
|
className={cn(
|
|
"rounded-2xl shadow-sm border border-gray-200/60 dark:border-gray-700",
|
|
isTemplateSampleActive
|
|
? "overflow-visible"
|
|
: "overflow-hidden",
|
|
)}
|
|
>
|
|
{activeMenuItem?.element}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ContentManagement;
|