Files
RasadDam_Frontend/src/Pages/Dashboard.tsx
2026-01-19 13:08:58 +03:30

448 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import moment from "jalali-moment";
import { useEffect, useRef, useState } from "react";
import Typography from "../components/Typography/Typography";
import { useUserProfileStore } from "../context/zustand-store/userStore";
import { ItemWithSubItems } from "../types/userPermissions";
import { getUserPermissions } from "../utils/getUserAvalableItems";
import { getFaPermissions } from "../utils/getFaPermissions";
import { motion, AnimatePresence } from "framer-motion";
import {
MagnifyingGlassIcon,
Squares2X2Icon,
XMarkIcon,
ClockIcon,
CalendarIcon,
ArrowRightCircleIcon,
} from "@heroicons/react/24/outline";
import { checkIsMobile } from "../utils/checkIsMobile";
import { useNavigate } from "@tanstack/react-router";
import { useDashboardTabStore } from "../context/zustand-store/dashboardTabStore";
interface Tab {
id: string;
title: string;
component: React.ComponentType;
path: string;
icon?: React.ComponentType<{ className?: string }>;
}
export default function Dashboard() {
const { profile } = useUserProfileStore();
const { dashboarTabs, setDashboardTabs, activeTabId, setActiveTabId } =
useDashboardTabStore();
const menuItems: ItemWithSubItems[] = getUserPermissions(
profile?.permissions
);
const [tabs, setTabs] = useState<Tab[]>(dashboarTabs || []);
const [search, setSearch] = useState("");
const navigate = useNavigate();
useEffect(() => {
setTabs(dashboarTabs || []);
}, [dashboarTabs]);
useEffect(() => {
setDashboardTabs(tabs);
}, [tabs, setDashboardTabs]);
const persianDate = moment().locale("fa").format("dddd D MMMM YYYY");
const [time, setTime] = useState(
new Date().toLocaleTimeString("fa-IR", {
hour: "2-digit",
minute: "2-digit",
hour12: false,
})
);
useEffect(() => {
const interval = setInterval(() => {
setTime(
new Date().toLocaleTimeString("fa-IR", {
hour: "2-digit",
minute: "2-digit",
hour12: false,
})
);
}, 60000);
return () => clearInterval(interval);
}, []);
const openTab = (subItem: ItemWithSubItems["subItems"][0]) => {
const existingTab = tabs.find((tab) => tab.path === subItem.path);
if (existingTab) {
setActiveTabId(existingTab.id);
} else {
const newTab = {
id: `tab-${Date.now()}`,
title: getFaPermissions(subItem.name),
component: subItem.component,
path: subItem.path,
};
setTabs((prev) => [...prev, newTab]);
setActiveTabId(newTab.id);
}
};
const closeTab = (id: string, e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
const newTabs = tabs.filter((tab) => tab.id !== id);
setTabs(newTabs);
if (activeTabId === id) {
setActiveTabId(
newTabs.length > 0 ? newTabs[newTabs.length - 1].id : null
);
}
};
const closeAllTabs = () => {
setTabs([]);
setActiveTabId(null);
};
const filteredMenuItems = menuItems
.map((item) => ({
...item,
subItems: item.subItems.filter(
(subItem) =>
!subItem.path.includes("$") &&
(search.trim() === "" ||
getFaPermissions(subItem.name).includes(search.trim()))
),
}))
.filter((item) => item.subItems.length > 0);
function findSubItemByPath(
items: ItemWithSubItems[],
path: string
): ItemWithSubItems["subItems"][0] | null {
for (const item of items) {
for (const subItem of item.subItems) {
if (subItem.path === path) return subItem;
}
}
return null;
}
const activeTabObj = tabs.find((tab) => tab.id === activeTabId);
const activeComponentItem =
activeTabObj && findSubItemByPath(menuItems, activeTabObj.path);
const ActiveComponent = activeComponentItem?.component || null;
const draggedTabIndex = useRef<number | null>(null);
const onDragStart = (e: React.DragEvent<HTMLDivElement>, index: number) => {
draggedTabIndex.current = index;
e.dataTransfer.effectAllowed = "move";
};
const onDragOver = (e: React.DragEvent) => {
e.preventDefault();
};
const onDrop = (e: React.DragEvent, dropIndex: number) => {
e.preventDefault();
if (
draggedTabIndex.current === null ||
draggedTabIndex.current === dropIndex
)
return;
const newTabs = [...tabs];
const draggedItem = newTabs[draggedTabIndex.current];
newTabs.splice(draggedTabIndex.current, 1);
newTabs.splice(dropIndex, 0, draggedItem);
draggedTabIndex.current = null;
setTabs(newTabs);
};
const onDragEnd = () => {
draggedTabIndex.current = null;
};
return (
<div className="w-full px-3 py-2 min-h-screen">
<header className="backdrop-blur-xl bg-white/20 dark:bg-dark-800/80 rounded-2xl shadow-lg border border-white/30 dark:border-dark-700/30 p-4 mb-4">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3">
<div className="flex items-center gap-2">
<div className="p-2 rounded-lg bg-primary-600 dark:bg-primary-800 backdrop-blur-sm shadow-lg">
<Squares2X2Icon className="w-4 h-4 text-white" />
</div>
<Typography
variant="h6"
className="text-dark-800 dark:text-dark-100 font-semibold"
>
داشبورد
</Typography>
</div>
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3 w-full sm:w-auto">
<div className="relative w-full sm:w-48 md:w-64">
<div className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<MagnifyingGlassIcon className="w-4 h-4 text-dark-400 dark:text-dark-100" />
</div>
<input
type="text"
placeholder="جستجو..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-full pr-8 pl-3 py-2 text-xs rounded-lg backdrop-blur-sm bg-white/30 dark:bg-dark-800/30 border border-white/40 dark:border-dark-600/40 text-dark-700 dark:text-dark-200 placeholder:text-dark-400 focus:outline-none focus:ring-1 focus:ring-primary-500/50 focus:bg-white/50 dark:focus:bg-dark-700/50 transition-all duration-200"
/>
</div>
<div className="flex items-center justify-center sm:justify-start gap-1.5 px-2 py-1.5 rounded-lg backdrop-blur-sm bg-white/20 dark:bg-dark-800/80 border border-white/30 dark:border-dark-600/30">
<CalendarIcon className="w-3 h-3 text-primary-500" />
<span className="text-xs text-dark-600 dark:text-dark-300">
{persianDate}
</span>
<span className="mx-1 h-3 w-px bg-dark-300 dark:bg-dark-600"></span>
<ClockIcon className="w-3 h-3 text-primary-500" />
<span className="text-xs text-dark-600 dark:text-dark-300">
{time}
</span>
</div>
</div>
</div>
</header>
<div className="space-y-3">
{filteredMenuItems.length === 0 ? (
<div className="backdrop-blur-xl bg-white/20 dark:bg-dark-800/80 rounded-xl shadow-lg border border-white/30 dark:border-dark-700/30 p-8">
<div className="flex flex-col items-center justify-center text-center space-y-3">
<div className="p-3 rounded-full backdrop-blur-sm bg-white/30 dark:bg-dark-700/30">
<MagnifyingGlassIcon className="w-6 h-6 text-dark-400" />
</div>
<Typography
variant="body1"
className="text-dark-600 dark:text-dark-300 font-medium"
>
موردی یافت نشد
</Typography>
<Typography
variant="body2"
className="text-dark-400 dark:text-dark-500"
>
هیچ آیتمی با عبارت &quot;{search}&quot; مطابقت ندارد
</Typography>
</div>
</div>
) : (
<div
className={`${
checkIsMobile()
? "space-y-3 pb-20"
: "flex overflow-x-auto pb-2 gap-3 scrollbar-thin scrollbar-thumb-dark-300 dark:scrollbar-thumb-dark-600"
}`}
>
{checkIsMobile()
? filteredMenuItems.map(({ fa, icon: Icon, subItems }, index) => {
const filteredSubItems = subItems.filter(
(item) =>
!item.path.includes("$") &&
getFaPermissions(item.name).includes(search.trim())
);
if (filteredSubItems.length === 0) return null;
return (
<section
key={index}
className="w-full space-y-5 border border-gray-200 dark:border-dark-600 bg-white dark:bg-dark-800 rounded-2xl p-6 shadow-sm"
>
<div className="flex items-center gap-3">
<Icon className="w-6 h-6 text-primary-600 dark:text-primary-400" />
<h2 className="text-xl font-bold text-dark-900 dark:text-white">
{fa}
</h2>
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 xl:grid-cols-5 gap-4">
{filteredSubItems.map((sub, subIndex) => (
<motion.button
key={subIndex}
onClick={() => navigate({ to: sub.path })}
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.97 }}
className="flex items-center gap-2 cursor-pointer bg-gray-50 dark:bg-dark-700 text-right border border-gray-200 dark:border-dark-600 hover:border-primary-500 dark:hover:border-primary-400 shadow-sm rounded-xl p-3 transition-all duration-200"
>
<ArrowRightCircleIcon className="w-5 h-5 text-primary-500 dark:text-primary-400" />
<span className="text-sm font-medium text-dark-800 dark:text-white">
{getFaPermissions(sub.name)}
</span>
</motion.button>
))}
</div>
</section>
);
})
: filteredMenuItems.map(({ fa, icon: Icon, subItems }, index) => (
<motion.div
key={index}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.05 }}
className="flex-none w-48 backdrop-blur-xl bg-white/20 dark:bg-dark-800/80 rounded-xl shadow-lg border border-white/30 dark:border-dark-700/30 overflow-hidden"
>
<div className="backdrop-blur-sm bg-white/30 dark:bg-dark-700/30 px-3 py-2 border-b border-white/20 dark:border-dark-600/20">
<div className="flex items-center gap-2">
<div className="p-1.5 rounded-md backdrop-blur-sm bg-primary-500/20 dark:bg-primary-400/20">
<Icon className="w-3.5 h-3.5 text-primary-600 dark:text-primary-400" />
</div>
<span className="text-xs font-semibold text-dark-800 dark:text-dark-100 truncate">
{fa}
</span>
</div>
</div>
<div className="p-1.5 space-y-0.5">
{subItems.map((sub, subIndex) => {
const isActive = tabs.some(
(tab) =>
tab.path === sub.path && activeTabId === tab.id
);
return (
<motion.div
key={subIndex}
initial={{ opacity: 0, x: -5 }}
animate={{ opacity: 1, x: 0 }}
transition={{
delay: index * 0.05 + subIndex * 0.02,
}}
whileHover={{ x: 1 }}
whileTap={{ scale: 0.98 }}
onClick={() => openTab(sub)}
className={`flex items-center gap-1.5 px-2 py-1.5 text-xs rounded-md cursor-pointer transition-all duration-200 focus:outline-none ${
isActive
? "backdrop-blur-sm bg-primary-500/20 dark:bg-primary-400/20 text-primary-700 dark:text-primary-300 "
: "hover:backdrop-blur-sm hover:bg-white/30 dark:hover:bg-dark-600/30 border-none"
}`}
>
<div
className={`w-1.5 h-1.5 rounded-full ${
isActive
? "bg-primary-600 dark:bg-primary-400"
: "bg-dark-400 dark:bg-dark-500"
}`}
/>
<span
className={`truncate ${
isActive
? "text-primary-700 dark:text-white font-medium"
: "text-dark-600 dark:text-dark-200/80"
}`}
>
{getFaPermissions(sub.name)}
</span>
</motion.div>
);
})}
</div>
</motion.div>
))}
</div>
)}
</div>
{tabs.length > 0 && !checkIsMobile() && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2 }}
className="backdrop-blur-xl bg-white/20 dark:bg-dark-800/80 rounded-xl shadow-lg border border-white/30 dark:border-dark-700/30 overflow-hidden mt-4"
>
<div className="backdrop-blur-sm bg-white/30 dark:bg-dark-700/30 border-b border-white/20 dark:border-dark-600/20">
<div className="flex items-center justify-between px-3 py-2">
<div className="flex items-center gap-1.5">
<div className="w-1.5 h-1.5 rounded-full bg-green-500"></div>
<span className="text-xs font-medium text-dark-600 dark:text-dark-300">
صفحات باز
</span>
<span className="text-xs text-dark-400 dark:text-dark-500 backdrop-blur-sm bg-white/40 dark:bg-dark-600/40 px-1.5 py-0.5 rounded-full">
{tabs.length}
</span>
</div>
{tabs.length > 1 && (
<button
onClick={closeAllTabs}
className="flex items-center gap-1 text-xs text-dark-500 dark:text-dark-400 hover:text-red-500 dark:hover:text-red-400 px-2 py-1 rounded-md hover:backdrop-blur-sm hover:bg-red-500/20 dark:hover:bg-red-400/20 transition-all duration-200 focus:outline-none"
>
<XMarkIcon className="w-3 h-3" />
بستن همه
</button>
)}
</div>
</div>
<div className="flex items-center overflow-x-auto scrollbar-hide backdrop-blur-sm bg-white/20 dark:bg-dark-700/20 border-b border-white/20 dark:border-dark-600/20">
<AnimatePresence initial={false}>
{tabs.map((tab, index) => (
<motion.div
draggable
key={tab.id}
onDragStart={(e: any) => onDragStart(e, index)}
onDragOver={onDragOver}
onDrop={(e) => onDrop(e, index)}
onDragEnd={onDragEnd}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 10 }}
transition={{ duration: 0.15 }}
onClick={() => setActiveTabId(tab.id)}
className={`group flex items-center gap-1.5 px-3 py-1 cursor-pointer transition-all duration-200 border-b-2 focus:outline-none ${
activeTabId === tab.id
? "backdrop-blur-sm bg-primary-500/20 dark:bg-primary-400/20 text-primary-700 dark:text-primary-300 border-primary-500 dark:border-primary-400"
: "text-dark-600 dark:text-dark-300 hover:backdrop-blur-sm hover:bg-white/30 dark:hover:bg-dark-600/30 border-transparent hover:border-dark-300 dark:hover:border-dark-500"
}`}
>
{tab.icon && (
<tab.icon
className={`w-3.5 h-3.5 flex-shrink-0 ${
activeTabId === tab.id
? "text-primary-600 dark:text-primary-400"
: "text-dark-400 dark:text-dark-500"
}`}
/>
)}
<span className="text-xs font-medium whitespace-nowrap">
{tab.title}
</span>
<button
onClick={(e) => closeTab(tab.id, e)}
className="opacity-0 group-hover:opacity-100 p-0.5 rounded-full hover:backdrop-blur-sm hover:bg-white/40 dark:hover:bg-dark-500/40 transition-all duration-200 focus:outline-none"
>
<XMarkIcon className="w-2.5 h-2.5" />
</button>
</motion.div>
))}
</AnimatePresence>
</div>
<AnimatePresence mode="wait">
{activeTabId && (
<motion.div
key={activeTabId}
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -5 }}
transition={{ duration: 0.2, ease: "easeInOut" }}
className="p-4 backdrop-blur-sm bg-white/10 dark:bg-dark-800/10"
>
{ActiveComponent && <ActiveComponent />}
</motion.div>
)}
</AnimatePresence>
</motion.div>
)}
</div>
);
}