Files
edr-platform/apps/edr-freight-web/backoffice/src/user-management/components/content/ContentManagement.tsx
2026-07-10 10:41:48 +03:00

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;