Compare commits

...

4 Commits

Author SHA1 Message Date
c20f083115 feat: switch between provinces 2026-02-01 16:27:24 +03:30
9ac1e0fc4f chore: remove comments 2026-02-01 15:50:25 +03:30
1f456f4d17 update: combine menu and sidebar paths 2026-02-01 15:49:32 +03:30
adf180bc5f update: combine menu and sidebar paths 2026-02-01 15:47:45 +03:30
6 changed files with 178 additions and 196 deletions

View File

@@ -11,7 +11,7 @@ import { makeRouter } from "./routes/routes";
import { useDarkMode } from "./hooks/useDarkMode"; import { useDarkMode } from "./hooks/useDarkMode";
import { ItemWithSubItems } from "./types/userPermissions"; import { ItemWithSubItems } from "./types/userPermissions";
import { useFetchProfile } from "./hooks/useFetchProfile"; import { useFetchProfile } from "./hooks/useFetchProfile";
import { getInspectionMenuItems } from "./screen/SideBar"; import { getInspectionMenuItems } from "./config/menuItems";
const versionNumber = "/src/version.txt"; const versionNumber = "/src/version.txt";
import "./index.css"; import "./index.css";
@@ -127,7 +127,7 @@ export default function App() {
window.history.replaceState( window.history.replaceState(
{}, {},
document.title, document.title,
url.pathname + url.search url.pathname + url.search,
); );
} }
}, []); }, []);

View File

@@ -4,99 +4,26 @@ import { useUserProfileStore } from "../context/zustand-store/userStore";
import { getFaPermissions } from "../utils/getFaPermissions"; import { getFaPermissions } from "../utils/getFaPermissions";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import { import {
UsersIcon,
DocumentTextIcon,
ChartBarIcon,
ChevronDownIcon, ChevronDownIcon,
UserIcon,
GlobeAsiaAustraliaIcon, GlobeAsiaAustraliaIcon,
MapPinIcon,
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
import { ItemWithSubItems } from "../types/userPermissions"; import { getInspectionMenuItems } from "../config/menuItems";
import { checkMenuPermission } from "../utils/checkMenuPermission";
const getInspectionMenuItems = ( const ADMIN_PROVINCES = [
permissions: string[] = [] { value: "hamedan", label: "همدان" },
): ItemWithSubItems[] => { { value: "markazi", label: "مرکزی" },
const items: ItemWithSubItems[] = []; ] as const;
if (checkMenuPermission("nationalinfo", permissions)) {
items.push({
en: "nationalinfo",
fa: "اطلاعات",
icon: () => <UserIcon className="w-6 h-6" />,
subItems: [
{
name: "nationalinfo",
path: "/nationalinfo",
component: () => import("./NationalInfo"),
},
{
name: "ladinginfo",
path: "/ladinginfo",
component: () => import("./LadingInfo"),
},
{
name: "veterinarytransfer",
path: "/veterinarytransfer",
component: () => import("./VeterinaryTransfer"),
},
],
});
}
if (checkMenuPermission("users", permissions)) {
items.push({
en: "users",
fa: "کاربران",
icon: () => <UsersIcon className="w-6 h-6" />,
subItems: [
{
name: "users",
path: "/users",
component: () => import("./Users"),
},
],
});
}
if (checkMenuPermission("inspections", permissions)) {
items.push({
en: "inspections",
fa: "سوابق بازرسی",
icon: () => <DocumentTextIcon className="w-6 h-6" />,
subItems: [
{
name: "inspections",
path: "/inspections",
component: () => import("./UserInspections"),
},
],
});
}
if (checkMenuPermission("statics", permissions)) {
items.push({
en: "statics",
fa: "آمار",
icon: () => <ChartBarIcon className="w-6 h-6" />,
subItems: [
{
name: "statics",
path: "/statics",
component: () => import("./Statics"),
},
],
});
}
return items;
};
export const Menu = () => { export const Menu = () => {
const { profile } = useUserProfileStore(); const { profile, updateProfile } = useUserProfileStore();
const navigate = useNavigate(); const navigate = useNavigate();
const menuItems = getInspectionMenuItems(profile?.permissions || []); const menuItems = getInspectionMenuItems(profile?.permissions || []);
const [openIndex, setOpenIndex] = useState<number | null>(null); const [openIndex, setOpenIndex] = useState<number | null>(null);
const hasAdmin =
Array.isArray(profile?.permissions) &&
profile.permissions.includes("admin");
const currentProvince = profile?.province || "hamedan";
const toggleSubmenu = (index: number) => { const toggleSubmenu = (index: number) => {
setOpenIndex((prev) => (prev === index ? null : index)); setOpenIndex((prev) => (prev === index ? null : index));
@@ -110,7 +37,7 @@ export const Menu = () => {
منو منو
</h1> </h1>
<div className="mb-4"> <div className="mb-4 space-y-3">
<button <button
onClick={() => navigate({ to: "/" })} onClick={() => navigate({ to: "/" })}
className="w-full flex items-center justify-center gap-2 px-4 py-3 rounded-xl bg-primary-500 hover:bg-primary-600 cursor-pointer dark:bg-primary-800 dark:hover:bg-primary-700 text-white font-semibold text-sm transition-all duration-200 shadow-md hover:shadow-lg" className="w-full flex items-center justify-center gap-2 px-4 py-3 rounded-xl bg-primary-500 hover:bg-primary-600 cursor-pointer dark:bg-primary-800 dark:hover:bg-primary-700 text-white font-semibold text-sm transition-all duration-200 shadow-md hover:shadow-lg"
@@ -118,6 +45,30 @@ export const Menu = () => {
<GlobeAsiaAustraliaIcon className="w-5 h-5" /> <GlobeAsiaAustraliaIcon className="w-5 h-5" />
<span>مشاهده نقشه</span> <span>مشاهده نقشه</span>
</button> </button>
{hasAdmin && (
<div className="flex flex-col gap-2 p-3 rounded-xl bg-white dark:bg-dark-700 border border-gray-200 dark:border-dark-600">
<div className="flex items-center gap-2 text-sm font-medium text-gray-600 dark:text-gray-400">
<MapPinIcon className="w-4 h-4 shrink-0" />
<span>استان</span>
</div>
<div className="grid grid-cols-2 gap-2">
{ADMIN_PROVINCES.map(({ value, label }) => (
<button
key={value}
type="button"
onClick={() => updateProfile({ province: value })}
className={`px-4 py-2.5 rounded-lg text-sm font-medium transition-all ${
currentProvince === value
? "bg-primary-500 text-white dark:bg-primary-600 shadow-md"
: "bg-gray-100 dark:bg-dark-600 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-dark-500"
}`}
>
{label}
</button>
))}
</div>
</div>
)}
</div> </div>
<div className="space-y-2"> <div className="space-y-2">

86
src/config/menuItems.tsx Normal file
View File

@@ -0,0 +1,86 @@
import {
UsersIcon,
DocumentTextIcon,
ChartBarIcon,
UserIcon,
} from "@heroicons/react/24/outline";
import { ItemWithSubItems } from "../types/userPermissions";
import { checkMenuPermission } from "../utils/checkMenuPermission";
export const getInspectionMenuItems = (
permissions: string[] = [],
): ItemWithSubItems[] => {
const items: ItemWithSubItems[] = [];
if (checkMenuPermission("nationalinfo", permissions)) {
items.push({
en: "nationalinfo",
fa: "اطلاعات",
icon: () => <UserIcon className="w-6 h-6" />,
subItems: [
{
name: "nationalinfo",
path: "/nationalinfo",
component: () => import("../Pages/NationalInfo"),
},
{
name: "ladinginfo",
path: "/ladinginfo",
component: () => import("../Pages/LadingInfo"),
},
{
name: "veterinarytransfer",
path: "/veterinarytransfer",
component: () => import("../Pages/VeterinaryTransfer"),
},
],
});
}
if (checkMenuPermission("users", permissions)) {
items.push({
en: "users",
fa: "کاربران",
icon: () => <UsersIcon className="w-6 h-6" />,
subItems: [
{
name: "users",
path: "/users",
component: () => import("../Pages/Users"),
},
],
});
}
if (checkMenuPermission("inspections", permissions)) {
items.push({
en: "inspections",
fa: "سوابق بازرسی",
icon: () => <DocumentTextIcon className="w-6 h-6" />,
subItems: [
{
name: "inspections",
path: "/inspections",
component: () => import("../Pages/UserInspections"),
},
],
});
}
if (checkMenuPermission("statics", permissions)) {
items.push({
en: "statics",
fa: "آمار",
icon: () => <ChartBarIcon className="w-6 h-6" />,
subItems: [
{
name: "statics",
path: "/statics",
component: () => import("../Pages/Statics"),
},
],
});
}
return items;
};

View File

@@ -5,12 +5,13 @@ import { useDashboardTabStore } from "./dashboardTabStore";
interface UseUserProfileStore { interface UseUserProfileStore {
profile?: Record<string, any> | null; profile?: Record<string, any> | null;
setUserProfile: (profile: Record<string, any>) => void; setUserProfile: (profile: Record<string, any>) => void;
updateProfile: (updates: Partial<Record<string, any>>) => void;
clearProfile: () => void; clearProfile: () => void;
} }
const arePermissionsEqual = ( const arePermissionsEqual = (
permissions1?: string[], permissions1?: string[],
permissions2?: string[] permissions2?: string[],
): boolean => { ): boolean => {
if (!permissions1 && !permissions2) return true; if (!permissions1 && !permissions2) return true;
if (!permissions1 || !permissions2) return false; if (!permissions1 || !permissions2) return false;
@@ -24,7 +25,7 @@ const arePermissionsEqual = (
const areProfilesEqual = ( const areProfilesEqual = (
currentProfile: Record<string, any> | null | undefined, currentProfile: Record<string, any> | null | undefined,
newProfile: Record<string, any> newProfile: Record<string, any>,
): boolean => { ): boolean => {
if (!currentProfile) return false; if (!currentProfile) return false;
@@ -35,10 +36,10 @@ const areProfilesEqual = (
} }
const currentKeys = Object.keys(currentProfile).filter( const currentKeys = Object.keys(currentProfile).filter(
(key) => key !== "permissions" (key) => key !== "permissions",
); );
const newKeys = Object.keys(newProfile).filter( const newKeys = Object.keys(newProfile).filter(
(key) => key !== "permissions" (key) => key !== "permissions",
); );
if (currentKeys.length !== newKeys.length) return false; if (currentKeys.length !== newKeys.length) return false;
@@ -72,10 +73,15 @@ export const useUserProfileStore = create<UseUserProfileStore>()(
console.log("profile", profile); console.log("profile", profile);
} }
}, },
updateProfile: (updates) => {
const currentProfile = get().profile;
if (!currentProfile) return;
set({ profile: { ...currentProfile, ...updates } });
},
clearProfile: () => set({ profile: null }), clearProfile: () => set({ profile: null }),
}), }),
{ name: "userprofile" } { name: "userprofile" },
) ),
); );
interface UseUserStore { interface UseUserStore {
@@ -97,6 +103,6 @@ export const useUserStore = create<UseUserStore>()(
}), }),
{ {
name: "user", name: "user",
} },
) ),
); );

View File

@@ -17,15 +17,7 @@ export function makeRouter(auth: string | null) {
try { try {
const routeConfigs = getRoutes(auth); const routeConfigs = getRoutes(auth);
console.log(
"Creating router with routes:",
routeConfigs.length,
"auth:",
!!auth
);
if (routeConfigs.length === 0) { if (routeConfigs.length === 0) {
console.log("No routes found, adding default Auth route");
routeConfigs.push({ path: "/", component: Auth }); routeConfigs.push({ path: "/", component: Auth });
} }
@@ -57,7 +49,6 @@ export function makeRouter(auth: string | null) {
defaultPreload: "intent", defaultPreload: "intent",
}); });
console.log("Router created successfully");
return router; return router;
} catch (error) { } catch (error) {
console.error("Error creating router:", error); console.error("Error creating router:", error);

View File

@@ -4,24 +4,25 @@ import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import { ChevronDownIcon } from "@heroicons/react/24/solid"; import { ChevronDownIcon } from "@heroicons/react/24/solid";
import { useUserProfileStore } from "../context/zustand-store/userStore"; import { useUserProfileStore } from "../context/zustand-store/userStore";
import { ItemWithSubItems } from "../types/userPermissions";
import { useNavigate } from "@tanstack/react-router"; import { useNavigate } from "@tanstack/react-router";
import { getFaPermissions } from "../utils/getFaPermissions"; import { getFaPermissions } from "../utils/getFaPermissions";
import { useSideBarStore } from "../context/zustand-store/appStore"; import { useSideBarStore } from "../context/zustand-store/appStore";
import SVGImage from "../components/SvgImage/SvgImage"; import SVGImage from "../components/SvgImage/SvgImage";
import { checkMenuPermission } from "../utils/checkMenuPermission"; import { getInspectionMenuItems } from "../config/menuItems";
import { import {
ChevronLeftIcon, ChevronLeftIcon,
ChevronRightIcon, ChevronRightIcon,
MagnifyingGlassIcon, MagnifyingGlassIcon,
BuildingOfficeIcon, BuildingOfficeIcon,
UsersIcon,
DocumentTextIcon,
ChartBarIcon,
UserIcon,
GlobeAsiaAustraliaIcon, GlobeAsiaAustraliaIcon,
MapPinIcon,
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
const ADMIN_PROVINCES = [
{ value: "hamedan", label: "همدان" },
{ value: "markazi", label: "مرکزی" },
] as const;
const containerVariants = { const containerVariants = {
hidden: {}, hidden: {},
visible: { transition: { staggerChildren: 0.03 } }, visible: { transition: { staggerChildren: 0.03 } },
@@ -69,91 +70,14 @@ const sidebarVariants = {
}, },
}; };
export const getInspectionMenuItems = (
permissions: string[] = []
): ItemWithSubItems[] => {
const items: ItemWithSubItems[] = [];
if (checkMenuPermission("nationalinfo", permissions)) {
items.push({
en: "nationalinfo",
fa: "اطلاعات",
icon: () => <UserIcon className="w-6 h-6" />,
subItems: [
{
name: "nationalinfo",
path: "/nationalinfo",
component: () => import("../Pages/NationalInfo"),
},
{
name: "ladinginfo",
path: "/ladinginfo",
component: () => import("../Pages/LadingInfo"),
},
{
name: "veterinarytransfer",
path: "/veterinarytransfer",
component: () => import("../Pages/VeterinaryTransfer"),
},
],
});
}
if (checkMenuPermission("users", permissions)) {
items.push({
en: "users",
fa: "کاربران",
icon: () => <UsersIcon className="w-6 h-6" />,
subItems: [
{
name: "users",
path: "/users",
component: () => import("../Pages/Users"),
},
],
});
}
if (checkMenuPermission("inspections", permissions)) {
items.push({
en: "inspections",
fa: "سوابق بازرسی",
icon: () => <DocumentTextIcon className="w-6 h-6" />,
subItems: [
{
name: "inspections",
path: "/inspections",
component: () => import("../Pages/UserInspections"),
},
],
});
}
if (checkMenuPermission("statics", permissions)) {
items.push({
en: "statics",
fa: "آمار",
icon: () => <ChartBarIcon className="w-6 h-6" />,
subItems: [
{
name: "statics",
path: "/statics",
component: () => import("../Pages/Statics"),
},
],
});
}
return items;
};
export const SideBar = () => { export const SideBar = () => {
const isMobile = checkIsMobile(); const isMobile = checkIsMobile();
const { profile } = useUserProfileStore(); const { profile, updateProfile } = useUserProfileStore();
const menuItems: ItemWithSubItems[] = getInspectionMenuItems( const menuItems = getInspectionMenuItems(profile?.permissions || []);
profile?.permissions || [] const hasAdmin =
); Array.isArray(profile?.permissions) &&
profile.permissions.includes("admin");
const currentProvince = profile?.province || "hamedan";
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const { isSideBarOpen, toggleSideBar } = useSideBarStore(); const { isSideBarOpen, toggleSideBar } = useSideBarStore();
@@ -161,7 +85,7 @@ export const SideBar = () => {
const getOpenedItem = () => { const getOpenedItem = () => {
if (window.location.pathname !== "/") { if (window.location.pathname !== "/") {
const matchedIndex = menuItems.findIndex((item) => const matchedIndex = menuItems.findIndex((item) =>
item.subItems.some((sub) => sub.path === window.location.pathname) item.subItems.some((sub) => sub.path === window.location.pathname),
); );
return matchedIndex; return matchedIndex;
} else { } else {
@@ -180,7 +104,7 @@ export const SideBar = () => {
getFaPermissions(subItem.name) getFaPermissions(subItem.name)
.toLowerCase() .toLowerCase()
.includes(search.toLowerCase()) || .includes(search.toLowerCase()) ||
subItem.path.toLowerCase().includes(search.toLowerCase()) subItem.path.toLowerCase().includes(search.toLowerCase()),
); );
return { return {
@@ -191,7 +115,7 @@ export const SideBar = () => {
.filter( .filter(
(item) => (item) =>
item.subItems.length > 0 || item.subItems.length > 0 ||
item.fa.toLowerCase().includes(search.toLowerCase()) item.fa.toLowerCase().includes(search.toLowerCase()),
); );
if (isMobile) return null; if (isMobile) return null;
@@ -287,6 +211,30 @@ export const SideBar = () => {
<span>مشاهده نقشه</span> <span>مشاهده نقشه</span>
</motion.button> </motion.button>
)} )}
{isSideBarOpen && hasAdmin && (
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2 text-xs font-medium text-gray-600 dark:text-gray-400">
<MapPinIcon className="w-4 h-4 shrink-0" />
<span>استان</span>
</div>
<div className="grid grid-cols-2 gap-1.5">
{ADMIN_PROVINCES.map(({ value, label }) => (
<button
key={value}
type="button"
onClick={() => updateProfile({ province: value })}
className={`px-3 py-2 rounded-lg text-sm font-medium transition-all ${
currentProvince === value
? "bg-primary-500 text-white dark:bg-primary-600 shadow-md"
: "bg-gray-100 dark:bg-dark-600 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-dark-500"
}`}
>
{label}
</button>
))}
</div>
</div>
)}
</div> </div>
</motion.div> </motion.div>
)} )}