diff --git a/app/(application)/(user)/home.tsx b/app/(application)/(user)/home.tsx
index e964490..38dc8a2 100644
--- a/app/(application)/(user)/home.tsx
+++ b/app/(application)/(user)/home.tsx
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable react-hooks/exhaustive-deps */
-import { StackCustom, ViewWrapper } from "@/components";
+import { BasicWrapper, StackCustom, ViewWrapper } from "@/components";
import { MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
import { useNotificationStore } from "@/hooks/use-notification-store";
@@ -61,12 +61,29 @@ export default function Application() {
if (data && data?.active === false) {
console.log("User is not active");
- return ;
+ return (
+
+
+
+ );
}
if (data && data?.Profile === null) {
console.log("Profile is null");
- return ;
+ return (
+
+
+
+ );
+ }
+
+ if (data && data?.masterUserRoleId !== "1") {
+ console.log("User is not admin");
+ return (
+
+
+
+ );
}
return (
diff --git a/app/(application)/admin/_layout.tsx b/app/(application)/admin/_layout.tsx
index d4ec2cb..3e4da82 100644
--- a/app/(application)/admin/_layout.tsx
+++ b/app/(application)/admin/_layout.tsx
@@ -8,6 +8,8 @@ import {
} from "@/components";
import DrawerAdmin from "@/components/Drawer/DrawerAdmin";
import NavbarMenu from "@/components/Drawer/NavbarMenu";
+import NavbarMenu_V2 from "@/components/Drawer/NavbarMenu_V2";
+import NavbarMenu_V3 from "@/components/Drawer/NavbarMenu_V3";
import { AccentColor, MainColor } from "@/constants/color-palet";
import {
ICON_SIZE_MEDIUM,
@@ -20,6 +22,10 @@ import {
adminListMenu,
superAdminListMenu,
} from "@/screens/Admin/listPageAdmin";
+import {
+ adminListMenu_V2,
+ superAdminListMenu_V2,
+} from "@/screens/Admin/listPageAdmin_V2";
import { GStyles } from "@/styles/global-styles";
import { FontAwesome6, Ionicons } from "@expo/vector-icons";
import { router, Stack } from "expo-router";
@@ -148,6 +154,24 @@ export default function AdminLayout() {
}
onClose={() => setOpenDrawerNavbar(false)}
/>
+
+ {/* setOpenDrawerNavbar(false)}
+ /> */}
+
+ {/* setOpenDrawerNavbar(false)}
+ /> */}
@@ -198,7 +222,7 @@ export default function AdminLayout() {
// size={ICON_SIZE_SMALL}
// color={MainColor.white}
// />
-
+
),
path: "/admin/notification",
},
diff --git a/app/(application)/admin/user-access/index.tsx b/app/(application)/admin/user-access/index.tsx
index cc837d9..0c1babb 100644
--- a/app/(application)/admin/user-access/index.tsx
+++ b/app/(application)/admin/user-access/index.tsx
@@ -1,139 +1,5 @@
-/* eslint-disable react-hooks/exhaustive-deps */
-import {
- BadgeCustom,
- CenterCustom,
- Divider,
- SearchInput,
- StackCustom,
- TextCustom,
- ViewWrapper,
-} from "@/components";
-import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
-import { GridViewCustomSpan } from "@/components/_ShareComponent/GridViewCustomSpan";
-import { MainColor } from "@/constants/color-palet";
-import { ICON_SIZE_XLARGE } from "@/constants/constans-value";
-import { apiAdminUserAccessGetAll } from "@/service/api-admin/api-admin-user-access";
-import { Ionicons } from "@expo/vector-icons";
-import { router, useFocusEffect } from "expo-router";
-import _ from "lodash";
-import { useCallback, useState } from "react";
+import { Admin_ScreenUserAccess } from "@/screens/Admin/User-Access/ScreenUserAccess";
export default function AdminUserAccess() {
- const [listData, setListData] = useState(null);
- const [search, setSearch] = useState("");
-
- useFocusEffect(
- useCallback(() => {
- onLoadData();
- }, [search])
- );
-
- const onLoadData = async () => {
- try {
- const response = await apiAdminUserAccessGetAll({
- search: search,
- category: "only-user",
- });
-
- if (response.success) {
- setListData(response.data);
- }
- } catch (error) {
- console.log("[ERROR LOAD DATA]", error);
- }
- };
-
- const rightComponent = () => {
- return (
- <>
- setSearch(text)}
- />
- >
- );
- };
- return (
- <>
-
- }
- >
-
- Aksi
-
- }
- component2={Username}
- component3={
-
- Status Akses
-
- }
- />
-
-
-
-
- {_.isEmpty(listData) ? (
-
- Tidak ada data
-
- ) : (
- listData?.map((item: any, index: number) => (
-
-
- router.push(`/admin/user-access/${item?.id}`)
- }
- name="open"
- size={ICON_SIZE_XLARGE}
- color={MainColor.yellow}
- />
-
- //
- // router.push(`/admin/user-access/${item?.id}`)
- // }
- // >
- // Detail
- //
- }
- component2={
-
- {item?.username || "-"}
-
- }
- component3={
-
- {item?.active ? (
- Aktif
- ) : (
- Tidak Aktif
- )}
-
- }
- style3={{ alignItems: "center", justifyContent: "center" }}
- />
- ))
- )}
-
-
- >
- );
+ return ;
}
diff --git a/components/Drawer/NavbarMenu.back.tsx b/components/Drawer/NavbarMenu.back.tsx
new file mode 100644
index 0000000..0ff8708
--- /dev/null
+++ b/components/Drawer/NavbarMenu.back.tsx
@@ -0,0 +1,276 @@
+import { AccentColor, MainColor } from "@/constants/color-palet";
+import { Ionicons } from "@expo/vector-icons";
+import { router, usePathname } from "expo-router";
+import React, { useEffect, useRef, useState } from "react";
+import {
+ Animated,
+ ScrollView,
+ StyleSheet,
+ Text,
+ TouchableOpacity,
+ View,
+} from "react-native";
+
+export interface NavbarItem {
+ label: string;
+ icon?: keyof typeof Ionicons.glyphMap;
+ color?: string;
+ link?: string;
+ links?: {
+ label: string;
+ link: string;
+ }[];
+ initiallyOpened?: boolean;
+}
+
+interface NavbarMenuProps {
+ items: NavbarItem[];
+ onClose?: () => void;
+}
+
+export default function NavbarMenuBackup({ items, onClose }: NavbarMenuProps) {
+ const pathname = usePathname();
+ const [activeLink, setActiveLink] = useState(null);
+ const [openKeys, setOpenKeys] = useState([]); // Untuk kontrol dropdown
+
+ // Normalisasi path: hapus trailing slash
+ const normalizePath = (path: string) => path.replace(/\/+$/, "");
+ const normalizedPathname = pathname ? normalizePath(pathname) : "";
+
+ // Set activeLink saat pathname berubah
+ useEffect(() => {
+ if (normalizedPathname) {
+ setActiveLink(normalizedPathname);
+ }
+ }, [normalizedPathname]);
+
+ // Toggle dropdown
+ const toggleOpen = (label: string) => {
+ setOpenKeys((prev) =>
+ prev.includes(label) ? prev.filter((key) => key !== label) : [label]
+ );
+ };
+
+ return (
+
+
+ {items.map((item) => (
+
+
+ );
+}
+
+// Komponen Item Menu
+function MenuItem({
+ item,
+ onClose,
+ activeLink,
+ setActiveLink,
+ isOpen,
+ toggleOpen,
+}: {
+ item: NavbarItem;
+ onClose?: () => void;
+ activeLink: string | null;
+ setActiveLink: (link: string | null) => void;
+ isOpen: boolean;
+ toggleOpen: () => void;
+}) {
+ const isActive = activeLink === item.link;
+ const animatedHeight = useRef(new Animated.Value(0)).current;
+
+ // Animasi saat isOpen berubah
+ React.useEffect(() => {
+ Animated.timing(animatedHeight, {
+ toValue: isOpen ? (item.links ? item.links.length * 40 : 0) : 0,
+ duration: 200,
+ useNativeDriver: false,
+ }).start();
+ }, [isOpen, item.links, animatedHeight]);
+
+ // Jika ada submenu
+ if (item.links && item.links.length > 0) {
+ return (
+
+ {/* Parent Item */}
+
+
+ {item.label}
+
+
+
+ {/* Submenu (Animated) */}
+
+ {item.links.map((subItem, index) => {
+ const isSubActive = activeLink === subItem.link;
+ return (
+ {
+ setActiveLink(subItem.link);
+ onClose?.();
+ router.push(subItem.link as any);
+ }}
+ >
+
+
+ {subItem.label}
+
+
+ );
+ })}
+
+
+ );
+ }
+
+ // Menu tanpa submenu
+ return (
+ {
+ setActiveLink(item.link || null);
+ onClose?.();
+ router.push(item.link as any);
+ }}
+ >
+
+
+ {item.label}
+
+
+ );
+}
+// Styles
+const styles = StyleSheet.create({
+ container: {
+ marginBottom: 5,
+ },
+ parentItem: {
+ flexDirection: "row",
+ alignItems: "center",
+ paddingVertical: 12,
+ paddingHorizontal: 10,
+ // backgroundColor: AccentColor.darkblue,
+ borderRadius: 8,
+ marginBottom: 5,
+ justifyContent: "space-between",
+ },
+ parentText: {
+ flex: 1,
+ fontSize: 16,
+ fontWeight: "500",
+ marginLeft: 10,
+ color: MainColor.white,
+ },
+ singleItem: {
+ flexDirection: "row",
+ alignItems: "center",
+ paddingVertical: 12,
+ paddingHorizontal: 10,
+ // backgroundColor: AccentColor.darkblue,
+ borderRadius: 8,
+ marginBottom: 5,
+ },
+ singleItemActive: {
+ backgroundColor: AccentColor.blue,
+ },
+ singleText: {
+ fontSize: 16,
+ fontWeight: "500",
+ marginLeft: 10,
+ color: MainColor.white,
+ },
+ icon: {
+ width: 24,
+ textAlign: "center",
+ paddingRight: 10,
+ },
+ submenu: {
+ overflow: "hidden",
+ marginLeft: 30,
+ marginTop: 5,
+ },
+ subItem: {
+ flexDirection: "row",
+ alignItems: "center",
+ paddingVertical: 8,
+ paddingHorizontal: 10,
+ borderRadius: 6,
+ marginBottom: 4,
+ },
+ subItemActive: {
+ backgroundColor: AccentColor.blue,
+ },
+ subText: {
+ color: MainColor.white,
+ fontSize: 16,
+ fontWeight: "500",
+ },
+});
diff --git a/components/Drawer/NavbarMenu.tsx b/components/Drawer/NavbarMenu.tsx
index 6416647..d239731 100644
--- a/components/Drawer/NavbarMenu.tsx
+++ b/components/Drawer/NavbarMenu.tsx
@@ -28,7 +28,7 @@ interface NavbarMenuProps {
onClose?: () => void;
}
-export default function NavbarMenu({ items, onClose }: NavbarMenuProps) {
+export default function NavbarMenuBackup({ items, onClose }: NavbarMenuProps) {
const pathname = usePathname();
const [activeLink, setActiveLink] = useState(null);
const [openKeys, setOpenKeys] = useState([]); // Untuk kontrol dropdown
@@ -41,13 +41,41 @@ export default function NavbarMenu({ items, onClose }: NavbarMenuProps) {
useEffect(() => {
if (normalizedPathname) {
setActiveLink(normalizedPathname);
+
+ // Temukan menu induk yang sesuai dengan path saat ini dan buka dropdown-nya
+ for (const item of items) {
+ // Cocokkan dengan link langsung
+ if (item.link && normalizedPathname.startsWith(item.link)) {
+ setOpenKeys(prev => {
+ if (!prev.includes(item.label)) {
+ return [...prev, item.label];
+ }
+ return prev;
+ });
+ break; // Hentikan loop setelah menemukan kecocokan pertama
+ }
+
+ // Cocokkan dengan submenu
+ if (item.links && item.links.length > 0) {
+ const matchingSubItem = item.links.find(link => normalizedPathname.startsWith(link.link));
+ if (matchingSubItem) {
+ setOpenKeys(prev => {
+ if (!prev.includes(item.label)) {
+ return [...prev, item.label];
+ }
+ return prev;
+ });
+ break; // Hentikan loop setelah menemukan kecocokan pertama
+ }
+ }
+ }
}
- }, [normalizedPathname]);
+ }, [normalizedPathname, items]);
// Toggle dropdown
const toggleOpen = (label: string) => {
setOpenKeys((prev) =>
- prev.includes(label) ? prev.filter((key) => key !== label) : [label]
+ prev.includes(label) ? prev.filter((key) => key !== label) : [...prev, label]
);
};
@@ -97,35 +125,71 @@ function MenuItem({
isOpen: boolean;
toggleOpen: () => void;
}) {
+ // Cek apakah menu ini atau submenu-nya yang aktif
const isActive = activeLink === item.link;
+
+ // Cek apakah path saat ini cocok dengan salah satu submenu
+ const isSubmenuActive = item.links && item.links.some(subItem => activeLink === subItem.link);
+
+ // Cek apakah path saat ini adalah detail dari submenu ini (misalnya /admin/event/123/detail)
+ const isDetailPageOfThisMenu = item.links && item.links.length > 0 && activeLink &&
+ item.links.some(link => {
+ const linkPath = link.link.replace(/\/+$/, "");
+ return activeLink.startsWith(linkPath + "/");
+ });
+
+ // Gabungkan status aktif untuk menentukan apakah menu ini harus aktif
+ const isMenuActive = isActive || isSubmenuActive || isDetailPageOfThisMenu;
+
const animatedHeight = useRef(new Animated.Value(0)).current;
// Animasi saat isOpen berubah
React.useEffect(() => {
+ // Jika ini adalah halaman detail dari menu ini, buka dropdown secara otomatis
+ const shouldAutoOpen = isDetailPageOfThisMenu && !isOpen;
+
Animated.timing(animatedHeight, {
- toValue: isOpen ? (item.links ? item.links.length * 40 : 0) : 0,
+ toValue: (isOpen || isDetailPageOfThisMenu) ? (item.links ? item.links.length * 40 : 0) : 0,
duration: 200,
useNativeDriver: false,
}).start();
- }, [isOpen, item.links, animatedHeight]);
+
+ // Jika perlu membuka dropdown otomatis, panggil toggleOpen
+ if (shouldAutoOpen) {
+ toggleOpen();
+ }
+ }, [isOpen, item.links, animatedHeight, isDetailPageOfThisMenu, toggleOpen]);
// Jika ada submenu
if (item.links && item.links.length > 0) {
return (
{/* Parent Item */}
-
+
- {item.label}
+
+ {item.label}
+
@@ -222,6 +286,9 @@ const styles = StyleSheet.create({
marginBottom: 5,
justifyContent: "space-between",
},
+ parentItemActive: {
+ backgroundColor: AccentColor.blue,
+ },
parentText: {
flex: 1,
fontSize: 16,
diff --git a/components/Drawer/NavbarMenu_V2.tsx b/components/Drawer/NavbarMenu_V2.tsx
new file mode 100644
index 0000000..4faa6fb
--- /dev/null
+++ b/components/Drawer/NavbarMenu_V2.tsx
@@ -0,0 +1,550 @@
+import { AccentColor, MainColor } from "@/constants/color-palet";
+import { Ionicons } from "@expo/vector-icons";
+import { router, usePathname } from "expo-router";
+import React, { useEffect, useRef, useState } from "react";
+import {
+ Animated,
+ ScrollView,
+ StyleSheet,
+ Text,
+ TouchableOpacity,
+ View,
+} from "react-native";
+
+export interface NavbarItem_V2 {
+ label: string;
+ icon?: keyof typeof Ionicons.glyphMap;
+ color?: string;
+ link?: string;
+ links?: {
+ label: string;
+ link: string;
+ detailPattern?: string; // NEW: Pattern untuk match detail pages
+ }[];
+ initiallyOpened?: boolean;
+}
+
+interface NavbarMenuProps {
+ items: NavbarItem_V2[];
+ onClose?: () => void;
+}
+
+export default function NavbarMenu_V2({ items, onClose }: NavbarMenuProps) {
+ const pathname = usePathname();
+ const [openKeys, setOpenKeys] = useState([]);
+
+ // Normalisasi path: hapus trailing slash
+ const normalizePath = (path: string) => path.replace(/\/+$/, "");
+ const normalizedPathname = pathname ? normalizePath(pathname) : "";
+
+ // Auto-open parent menu jika submenu aktif
+ useEffect(() => {
+ if (!normalizedPathname || !items || items.length === 0) {
+ return;
+ }
+
+ try {
+ const newOpenKeys: string[] = [];
+
+ // Helper function yang sama dengan di MenuItem
+ const checkPathMatch = (linkPath: string, detailPattern?: string) => {
+ const normalizedLink = linkPath.replace(/\/+$/, "");
+
+ // Exact match
+ if (normalizedPathname === normalizedLink) return true;
+
+ // Detail pattern match
+ if (detailPattern) {
+ const patternRegex = new RegExp(
+ "^" + detailPattern.replace(/\*/g, "[^/]+") + "(/.*)?$"
+ );
+ if (patternRegex.test(normalizedPathname)) {
+ return true;
+ }
+ }
+
+ // Detail page match (fallback)
+ if (normalizedPathname.startsWith(normalizedLink + "/")) {
+ const remainder = normalizedPathname.substring(normalizedLink.length + 1);
+ const segments = remainder.split("/").filter(s => s.length > 0);
+
+ if (segments.length === 0) return false;
+
+ const commonWords = [
+ // Actions
+ 'detail', 'edit', 'create', 'new', 'add', 'delete', 'view',
+
+ // Status types
+ 'publish', 'review', 'reject', 'status', 'active', 'inactive', 'pending',
+
+ // General pages
+ 'category', 'history', 'dashboard', 'index',
+
+ // Event specific
+ 'type-of-event', 'type-create', 'type-update',
+
+ // Forum specific
+ 'posting', 'report-posting', 'report-comment',
+
+ // Collaboration
+ 'group',
+
+ // App Information
+ 'business-field', 'information-bank', 'sticker',
+ 'bidang-update', 'sub-bidang-update',
+
+ // Transaction/Finance related
+ 'transaction-detail', 'transaction', 'payment',
+ 'disbursement-of-funds', 'detail-disbursement-of-funds',
+ 'list-disbursement-of-funds',
+
+ // List pages (CRITICAL!)
+ 'list-of-investor', 'list-of-donatur', 'list-of-participants',
+ 'list-comment', 'list-report-comment', 'list-report-posting',
+
+ // Input/Form pages
+ 'reject-input',
+
+ // Category pages
+ 'category-create', 'category-update'
+ ];
+
+ const hasIdSegment = segments.some(segment => {
+ if (commonWords.includes(segment.toLowerCase())) {
+ return false;
+ }
+
+ const isPureNumber = /^\d+$/.test(segment);
+ const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment);
+ const hasNumber = /\d/.test(segment);
+ const isAlphanumericId = /^[a-z0-9_-]+$/i.test(segment) && segment.length <= 50 && hasNumber;
+
+ return isPureNumber || isUUID || isAlphanumericId;
+ });
+
+ return hasIdSegment;
+ }
+
+ return false;
+ };
+
+ items.forEach((item) => {
+ if (item.links && item.links.length > 0) {
+ // Check jika ada submenu yang match dengan current path
+ const hasActiveSubmenu = item.links.some((subItem) => {
+ return checkPathMatch(subItem.link, subItem.detailPattern);
+ });
+
+ if (hasActiveSubmenu) {
+ newOpenKeys.push(item.label);
+ }
+ }
+ });
+
+ setOpenKeys(newOpenKeys);
+ } catch (error) {
+ console.error("Error in NavbarMenu useEffect:", error);
+ }
+ }, [normalizedPathname, items]);
+
+ // Toggle dropdown
+ const toggleOpen = (label: string) => {
+ setOpenKeys((prev) =>
+ prev.includes(label) ? prev.filter((key) => key !== label) : [...prev, label]
+ );
+ };
+
+ return (
+
+
+ {items && items.length > 0 ? (
+ items.map((item) => (
+
+
+ );
+}
+
+// Komponen Item Menu
+function MenuItem({
+ item,
+ onClose,
+ currentPath,
+ isOpen,
+ toggleOpen,
+}: {
+ item: NavbarItem_V2;
+ onClose?: () => void;
+ currentPath: string;
+ isOpen: boolean;
+ toggleOpen: () => void;
+}) {
+ const animatedHeight = useRef(new Animated.Value(0)).current;
+
+ // Helper function untuk check apakah path aktif
+ const isPathActive = (linkPath: string | undefined, detailPattern?: string) => {
+ if (!linkPath) return false;
+ const normalizedLink = linkPath.replace(/\/+$/, "");
+
+ // 1. Match exact - prioritas tertinggi
+ if (currentPath === normalizedLink) return true;
+
+ // 2. Jika ada detailPattern, cek pattern dulu
+ if (detailPattern) {
+ // detailPattern contoh: "/admin/job/*/review"
+ // akan match dengan:
+ // - /admin/job/123/review ✅
+ // - /admin/job/123/review/transaction-detail ✅
+ // - /admin/job/123/review/anything/nested ✅
+ const patternRegex = new RegExp(
+ "^" + detailPattern.replace(/\*/g, "[^/]+") + "(/.*)?$"
+ );
+ const isMatch = patternRegex.test(currentPath);
+
+ // Debug log untuk pattern matching
+ if (currentPath.includes('transaction-detail') || currentPath.includes('disbursement')) {
+ console.log('🔍 Pattern Match Check:', {
+ currentPath,
+ detailPattern,
+ regex: patternRegex.toString(),
+ isMatch
+ });
+ }
+
+ if (isMatch) {
+ return true;
+ }
+ }
+
+ // 3. Match untuk detail pages (fallback)
+ if (currentPath.startsWith(normalizedLink + "/")) {
+ const remainder = currentPath.substring(normalizedLink.length + 1);
+ const segments = remainder.split("/").filter(s => s.length > 0);
+
+ if (segments.length === 0) return false;
+
+ const commonWords = [
+ // Actions
+ 'detail', 'edit', 'create', 'new', 'add', 'delete', 'view',
+
+ // Status types
+ 'publish', 'review', 'reject', 'status', 'active', 'inactive', 'pending',
+
+ // General pages
+ 'category', 'history', 'dashboard', 'index',
+
+ // Event specific
+ 'type-of-event', 'type-create', 'type-update',
+
+ // Forum specific
+ 'posting', 'report-posting', 'report-comment',
+
+ // Collaboration
+ 'group',
+
+ // App Information
+ 'business-field', 'information-bank', 'sticker',
+ 'bidang-update', 'sub-bidang-update',
+
+ // Transaction/Finance related
+ 'transaction-detail', 'transaction', 'payment',
+ 'disbursement-of-funds', 'detail-disbursement-of-funds',
+ 'list-disbursement-of-funds',
+
+ // List pages (CRITICAL!)
+ 'list-of-investor', 'list-of-donatur', 'list-of-participants',
+ 'list-comment', 'list-report-comment', 'list-report-posting',
+
+ // Input/Form pages
+ 'reject-input',
+
+ // Category pages
+ 'category-create', 'category-update'
+ ];
+
+ const hasIdSegment = segments.some(segment => {
+ if (commonWords.includes(segment.toLowerCase())) {
+ return false;
+ }
+
+ const isPureNumber = /^\d+$/.test(segment);
+ const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment);
+ const hasNumber = /\d/.test(segment);
+ const isAlphanumericId = /^[a-z0-9_-]+$/i.test(segment) && segment.length <= 50 && hasNumber;
+
+ return isPureNumber || isUUID || isAlphanumericId;
+ });
+
+ return hasIdSegment;
+ }
+
+ return false;
+ };
+
+ // Check apakah menu item ini atau submenu-nya yang aktif
+ const isActive = isPathActive(item.link);
+ const hasActiveSubmenu =
+ item.links?.some((subItem) => isPathActive(subItem.link, subItem.detailPattern)) || false;
+
+ // Animasi saat isOpen berubah
+ useEffect(() => {
+ Animated.timing(animatedHeight, {
+ toValue: isOpen ? (item.links ? item.links.length * 44 : 0) : 0,
+ duration: 200,
+ useNativeDriver: false,
+ }).start();
+ }, [isOpen, item.links, animatedHeight]);
+
+ // Jika ada submenu
+ if (item.links && item.links.length > 0) {
+ // PRE-CALCULATE semua active states untuk submenu
+ const submenuActiveStates = item.links.map(subItem => ({
+ subItem,
+ isActive: isPathActive(subItem.link, subItem.detailPattern),
+ pathLength: subItem.link.length
+ }));
+
+ return (
+
+ {/* Parent Item */}
+
+
+
+ {item.label}
+
+
+
+
+ {/* Submenu (Animated) */}
+
+ {submenuActiveStates.map(({ subItem, isActive: isSubActive, pathLength }, index) => {
+
+ // CRITICAL FIX: Cek apakah ada submenu lain yang LEBIH PANJANG dan juga aktif
+ const hasMoreSpecificMatch = submenuActiveStates.some(other => {
+ if (other.subItem.link === subItem.link) return false; // Skip self
+
+ const isOtherLonger = other.pathLength > pathLength;
+
+ // Debug log untuk Dashboard
+ if (subItem.label === "Dashboard" && isSubActive) {
+ console.log(`🔎 Dashboard checking against ${other.subItem.label}:`, {
+ dashboardLink: subItem.link,
+ dashboardLength: pathLength,
+ otherLabel: other.subItem.label,
+ otherLink: other.subItem.link,
+ otherPattern: other.subItem.detailPattern,
+ otherLength: other.pathLength,
+ otherIsActive: other.isActive,
+ isOtherLonger,
+ willDisableDashboard: other.isActive && isOtherLonger,
+ currentURL: currentPath
+ });
+ }
+
+ // Conflict log
+ if (isSubActive && other.isActive) {
+ console.log('🔍 CONFLICT DETECTED:', {
+ current: subItem.label,
+ currentPath: subItem.link,
+ currentLength: pathLength,
+ other: other.subItem.label,
+ otherPath: other.subItem.link,
+ otherLength: other.pathLength,
+ isOtherLonger,
+ shouldDisableCurrent: isOtherLonger,
+ currentURL: currentPath
+ });
+ }
+
+ return other.isActive && isOtherLonger;
+ });
+
+ // Final decision
+ const finalIsActive = isSubActive && !hasMoreSpecificMatch;
+
+ // Debug final
+ if (isSubActive) {
+ console.log('✅ Active check:', {
+ label: subItem.label,
+ link: subItem.link,
+ isSubActive,
+ hasMoreSpecificMatch,
+ finalIsActive
+ });
+ }
+
+ return (
+ {
+ onClose?.();
+ router.push(subItem.link as any);
+ }}
+ >
+
+
+ {subItem.label}
+
+
+ );
+ })}
+
+
+ );
+ }
+
+ // Menu tanpa submenu
+ return (
+ {
+ onClose?.();
+ router.push(item.link as any);
+ }}
+ >
+
+
+ {item.label}
+
+
+ );
+}
+
+// Styles
+const styles = StyleSheet.create({
+ container: {
+ marginBottom: 5,
+ },
+ parentItem: {
+ flexDirection: "row",
+ alignItems: "center",
+ paddingVertical: 12,
+ paddingHorizontal: 10,
+ borderRadius: 8,
+ marginBottom: 5,
+ justifyContent: "space-between",
+ },
+ parentItemActive: {
+ backgroundColor: AccentColor.blue,
+ },
+ parentText: {
+ flex: 1,
+ fontSize: 16,
+ fontWeight: "500",
+ marginLeft: 10,
+ color: MainColor.white,
+ },
+ singleItem: {
+ flexDirection: "row",
+ alignItems: "center",
+ paddingVertical: 12,
+ paddingHorizontal: 10,
+ borderRadius: 8,
+ marginBottom: 5,
+ },
+ singleItemActive: {
+ backgroundColor: AccentColor.blue,
+ },
+ singleText: {
+ fontSize: 16,
+ fontWeight: "500",
+ marginLeft: 10,
+ color: MainColor.white,
+ },
+ icon: {
+ width: 24,
+ textAlign: "center",
+ paddingRight: 10,
+ },
+ submenu: {
+ overflow: "hidden",
+ marginLeft: 30,
+ marginTop: 5,
+ },
+ subItem: {
+ flexDirection: "row",
+ alignItems: "center",
+ paddingVertical: 8,
+ paddingHorizontal: 10,
+ borderRadius: 6,
+ marginBottom: 4,
+ },
+ subItemActive: {
+ backgroundColor: AccentColor.blue,
+ },
+ subText: {
+ color: MainColor.white,
+ fontSize: 16,
+ fontWeight: "500",
+ },
+});
\ No newline at end of file
diff --git a/components/Drawer/NavbarMenu_V3.tsx b/components/Drawer/NavbarMenu_V3.tsx
new file mode 100644
index 0000000..faf9b08
--- /dev/null
+++ b/components/Drawer/NavbarMenu_V3.tsx
@@ -0,0 +1,871 @@
+import { AccentColor, MainColor } from "@/constants/color-palet";
+import { Ionicons } from "@expo/vector-icons";
+import { router, usePathname } from "expo-router";
+import React, { useEffect, useRef, useState } from "react";
+import {
+ Animated,
+ ScrollView,
+ StyleSheet,
+ Text,
+ TouchableOpacity,
+ View,
+} from "react-native";
+
+export interface NavbarItem_V3 {
+ label: string;
+ icon?: keyof typeof Ionicons.glyphMap;
+ color?: string;
+ link?: string;
+ links?: {
+ label: string;
+ link: string;
+ detailPattern?: string; // NEW: Pattern untuk match detail pages
+ }[];
+ initiallyOpened?: boolean;
+}
+
+interface NavbarMenuProps {
+ items: NavbarItem_V3[];
+ onClose?: () => void;
+}
+
+export default function NavbarMenu_V3({ items, onClose }: NavbarMenuProps) {
+ const pathname = usePathname();
+ const [openKeys, setOpenKeys] = useState([]);
+
+ // Normalisasi path: hapus trailing slash
+ const normalizePath = (path: string) => path.replace(/\/+$/, "");
+ const normalizedPathname = pathname ? normalizePath(pathname) : "";
+
+ // Auto-open parent menu jika submenu aktif
+ useEffect(() => {
+ if (!normalizedPathname || !items || items.length === 0) {
+ return;
+ }
+
+ try {
+ const newOpenKeys: string[] = [];
+
+ // Helper function yang sama dengan di MenuItem
+ const checkPathMatch = (linkPath: string, detailPattern?: string) => {
+ const normalizedLink = linkPath.replace(/\/+$/, "");
+
+ // Exact match
+ if (normalizedPathname === normalizedLink) return true;
+
+ // Detail pattern match
+ if (detailPattern) {
+ const patternRegex = new RegExp(
+ "^" + detailPattern.replace(/\*/g, "[^/]+") + "(/.*)?$"
+ );
+ if (patternRegex.test(normalizedPathname)) {
+ return true;
+ }
+ }
+
+ // Detail page match (fallback)
+ if (normalizedPathname.startsWith(normalizedLink + "/")) {
+ const remainder = normalizedPathname.substring(normalizedLink.length + 1);
+ const segments = remainder.split("/").filter(s => s.length > 0);
+
+ if (segments.length === 0) return false;
+
+ const commonWords = [
+ // Actions
+ 'detail', 'edit', 'create', 'new', 'add', 'delete', 'view',
+
+ // Status types
+ 'publish', 'review', 'reject', 'status', 'active', 'inactive', 'pending',
+
+ // General pages
+ 'category', 'history', 'dashboard', 'index',
+
+ // Event specific
+ 'type-of-event', 'type-create', 'type-update',
+
+ // Forum specific
+ 'posting', 'report-posting', 'report-comment',
+
+ // Collaboration
+ 'group',
+
+ // App Information
+ 'business-field', 'information-bank', 'sticker',
+ 'bidang-update', 'sub-bidang-update',
+
+ // Transaction/Finance related
+ 'transaction-detail', 'transaction', 'payment',
+ 'disbursement-of-funds', 'detail-disbursement-of-funds',
+ 'list-disbursement-of-funds',
+
+ // List pages (CRITICAL!)
+ 'list-of-investor', 'list-of-donatur', 'list-of-participants',
+ 'list-comment', 'list-report-comment', 'list-report-posting',
+
+ // Input/Form pages
+ 'reject-input',
+
+ // Category pages
+ 'category-create', 'category-update'
+ ];
+
+ const hasIdSegment = segments.some(segment => {
+ if (commonWords.includes(segment.toLowerCase())) {
+ return false;
+ }
+
+ const isPureNumber = /^\d+$/.test(segment);
+ const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment);
+ const hasNumber = /\d/.test(segment);
+ const isAlphanumericId = /^[a-z0-9_-]+$/i.test(segment) && segment.length <= 50 && hasNumber;
+
+ return isPureNumber || isUUID || isAlphanumericId;
+ });
+
+ return hasIdSegment;
+ }
+
+ return false;
+ };
+
+ // Calculate all potential matches for conflict resolution
+ const allMatches = items.flatMap(item => {
+ if (!item.links || item.links.length === 0) return [];
+
+ return item.links
+ .filter(subItem => checkPathMatch(subItem.link, subItem.detailPattern))
+ .map(subItem => ({
+ parentLabel: item.label,
+ subItem,
+ pathLength: subItem.link.length
+ }));
+ });
+
+ // Find the most specific match for each parent
+ const uniqueParents = new Map();
+
+ allMatches.forEach(match => {
+ const existing = uniqueParents.get(match.parentLabel);
+ if (!existing || match.pathLength > existing.longestPathLength) {
+ uniqueParents.set(match.parentLabel, {
+ parentLabel: match.parentLabel,
+ longestPathLength: match.pathLength
+ });
+ }
+ });
+
+ // Add only the parents with the most specific matches
+ newOpenKeys.push(...Array.from(uniqueParents.values()).map(item => item.parentLabel));
+
+ // Additionally, if no specific submenu match was found but the current path
+ // starts with one of the parent menu links, add that parent
+ if (newOpenKeys.length === 0) {
+ // Find the parent whose link is the longest prefix of the current path
+ let longestMatchParent = null;
+ let longestMatchLength = 0;
+
+ items.forEach(item => {
+ if (item.links && item.links.length > 0) {
+ item.links.forEach(link => {
+ const linkPath = link.link.replace(/\/+$/, "");
+ if (normalizedPathname.startsWith(linkPath + "/") && linkPath.length > longestMatchLength) {
+ longestMatchLength = linkPath.length;
+ longestMatchParent = item.label;
+ }
+ });
+ }
+ });
+
+ if (longestMatchParent) {
+ newOpenKeys.push(longestMatchParent);
+ }
+ }
+
+ // NEW: Check if user is on a detail page (contains ID segments or specific keywords)
+ const isOnDetailPage = (() => {
+ // Check if current path has ID-like segments or detail keywords
+ const segments = normalizedPathname.split('/').filter(s => s.length > 0);
+
+ if (segments.length === 0) return false;
+
+ const commonWords = [
+ // Actions
+ 'detail', 'edit', 'create', 'new', 'add', 'delete', 'view',
+
+ // Status types
+ 'publish', 'review', 'reject', 'status', 'active', 'inactive', 'pending',
+
+ // General pages
+ 'category', 'history', 'dashboard', 'index',
+
+ // Event specific
+ 'type-of-event', 'type-create', 'type-update',
+
+ // Forum specific
+ 'posting', 'report-posting', 'report-comment',
+
+ // Collaboration
+ 'group',
+
+ // App Information
+ 'business-field', 'information-bank', 'sticker',
+ 'bidang-update', 'sub-bidang-update',
+
+ // Transaction/Finance related
+ 'transaction-detail', 'transaction', 'payment',
+ 'disbursement-of-funds', 'detail-disbursement-of-funds',
+ 'list-disbursement-of-funds',
+
+ // List pages (CRITICAL!)
+ 'list-of-investor', 'list-of-donatur', 'list-of-participants',
+ 'list-comment', 'list-report-comment', 'list-report-posting',
+
+ // Input/Form pages
+ 'reject-input',
+
+ // Category pages
+ 'category-create', 'category-update'
+ ];
+
+ const hasIdSegment = segments.some(segment => {
+ if (commonWords.includes(segment.toLowerCase())) {
+ return false;
+ }
+
+ const isPureNumber = /^\d+$/.test(segment);
+ const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment);
+ const hasNumber = /\d/.test(segment);
+ const isAlphanumericId = /^[a-z0-9_-]+$/i.test(segment) && segment.length <= 50 && hasNumber;
+
+ return isPureNumber || isUUID || isAlphanumericId;
+ });
+
+ return hasIdSegment;
+ })();
+
+ // NEW: Check if user is on a detail page (contains ID segments or specific keywords)
+ const isOnDetailPageGlobal = (() => {
+ // Check if current path has ID-like segments or detail keywords
+ const segments = normalizedPathname.split('/').filter(s => s.length > 0);
+
+ if (segments.length === 0) return false;
+
+ const commonWords = [
+ // Actions
+ 'detail', 'edit', 'create', 'new', 'add', 'delete', 'view',
+
+ // Status types
+ 'publish', 'review', 'reject', 'status', 'active', 'inactive', 'pending',
+
+ // General pages
+ 'category', 'history', 'dashboard', 'index',
+
+ // Event specific
+ 'type-of-event', 'type-create', 'type-update',
+
+ // Forum specific
+ 'posting', 'report-posting', 'report-comment',
+
+ // Collaboration
+ 'group',
+
+ // App Information
+ 'business-field', 'information-bank', 'sticker',
+ 'bidang-update', 'sub-bidang-update',
+
+ // Transaction/Finance related
+ 'transaction-detail', 'transaction', 'payment',
+ 'disbursement-of-funds', 'detail-disbursement-of-funds',
+ 'list-disbursement-of-funds',
+
+ // List pages (CRITICAL!)
+ 'list-of-investor', 'list-of-donatur', 'list-of-participants',
+ 'list-comment', 'list-report-comment', 'list-report-posting',
+
+ // Input/Form pages
+ 'reject-input',
+
+ // Category pages
+ 'category-create', 'category-update'
+ ];
+
+ // Check if any segment is a common word
+ const hasCommonWord = segments.some(segment => commonWords.includes(segment.toLowerCase()));
+
+ // Check if any segment looks like an ID (number, UUID, alphanumeric with numbers)
+ const hasIdSegment = segments.some(segment => {
+ const isPureNumber = /^\d+$/.test(segment);
+ const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment);
+ const hasNumber = /\d/.test(segment);
+ const isAlphanumericId = /^[a-z0-9_-]+$/i.test(segment) && segment.length <= 50 && hasNumber;
+
+ return isPureNumber || isUUID || isAlphanumericId;
+ });
+
+ // A detail page is one that has either common words or ID segments
+ return hasCommonWord || hasIdSegment;
+ })();
+
+ // NEW: Only open parent menu if the current path is a detail page of the most relevant parent
+ if (isOnDetailPageGlobal && newOpenKeys.length === 0) {
+ // Find the parent whose link is the longest prefix of the current path
+ let longestMatchParent = null;
+ let longestMatchLength = 0;
+
+ items.forEach(item => {
+ if (item.links && item.links.length > 0) {
+ item.links.forEach(link => {
+ const linkPath = link.link.replace(/\/+$/, "");
+ if (normalizedPathname.startsWith(linkPath + "/") && linkPath.length > longestMatchLength) {
+ longestMatchLength = linkPath.length;
+ longestMatchParent = item.label;
+ }
+ });
+ }
+ });
+
+ if (longestMatchParent) {
+ newOpenKeys.push(longestMatchParent);
+ }
+ }
+
+ setOpenKeys(newOpenKeys);
+ } catch (error) {
+ console.error("Error in NavbarMenu useEffect:", error);
+ }
+ }, [normalizedPathname, items]);
+
+ // Toggle dropdown
+ const toggleOpen = (label: string) => {
+ setOpenKeys((prev) =>
+ prev.includes(label) ? prev.filter((key) => key !== label) : [...prev, label]
+ );
+ };
+
+ return (
+
+
+ {items && items.length > 0 ? (
+ items.map((item) => (
+
+
+ );
+}
+
+// Komponen Item Menu
+function MenuItem({
+ item,
+ items,
+ onClose,
+ currentPath,
+ isOpen,
+ toggleOpen,
+}: {
+ item: NavbarItem_V3;
+ items: NavbarItem_V3[];
+ onClose?: () => void;
+ currentPath: string;
+ isOpen: boolean;
+ toggleOpen: () => void;
+}) {
+ const animatedHeight = useRef(new Animated.Value(0)).current;
+
+ // Helper function untuk check apakah path aktif
+ const isPathActive = (linkPath: string | undefined, detailPattern?: string) => {
+ if (!linkPath) return false;
+ const normalizedLink = linkPath.replace(/\/+$/, "");
+
+ // 1. Match exact - prioritas tertinggi
+ if (currentPath === normalizedLink) return true;
+
+ // 2. Jika ada detailPattern, cek pattern dulu
+ if (detailPattern) {
+ // detailPattern contoh: "/admin/job/*/review"
+ // akan match dengan:
+ // - /admin/job/123/review ✅
+ // - /admin/job/123/review/transaction-detail ✅
+ // - /admin/job/123/review/anything/nested ✅
+ const patternRegex = new RegExp(
+ "^" + detailPattern.replace(/\*/g, "[^/]+") + "(/.*)?$"
+ );
+ const isMatch = patternRegex.test(currentPath);
+
+ // Debug log untuk pattern matching
+ // if (currentPath.includes('transaction-detail') || currentPath.includes('disbursement')) {
+ // console.log('🔍 Pattern Match Check:', {
+ // currentPath,
+ // detailPattern,
+ // regex: patternRegex.toString(),
+ // isMatch
+ // });
+ // }
+
+ if (isMatch) {
+ return true;
+ }
+ }
+
+ // 3. Match untuk detail pages (fallback)
+ if (currentPath.startsWith(normalizedLink + "/")) {
+ const remainder = currentPath.substring(normalizedLink.length + 1);
+ const segments = remainder.split("/").filter(s => s.length > 0);
+
+ if (segments.length === 0) return false;
+
+ const commonWords = [
+ // Actions
+ 'detail', 'edit', 'create', 'new', 'add', 'delete', 'view',
+
+ // Status types
+ 'publish', 'review', 'reject', 'status', 'active', 'inactive', 'pending',
+
+ // General pages
+ 'category', 'history', 'index',
+
+ // Event specific
+ 'type-of-event', 'type-create', 'type-update',
+
+ // Forum specific
+ 'posting', 'report-posting', 'report-comment',
+
+ // Collaboration
+ 'group',
+
+ // App Information
+ 'business-field', 'information-bank', 'sticker',
+ 'bidang-update', 'sub-bidang-update',
+
+ // Transaction/Finance related
+ 'transaction-detail', 'transaction', 'payment',
+ 'disbursement-of-funds', 'detail-disbursement-of-funds',
+ 'list-disbursement-of-funds',
+
+ // List pages (CRITICAL!)
+ 'list-of-investor', 'list-of-donatur', 'list-of-participants',
+ 'list-comment', 'list-report-comment', 'list-report-posting',
+
+ // Input/Form pages
+ 'reject-input',
+
+ // Category pages
+ 'category-create', 'category-update'
+ ];
+
+ const hasCommonWord = segments.some(segment =>
+ commonWords.includes(segment.toLowerCase())
+ );
+
+ // Hanya anggap sebagai detail page jika mengandung commonWords
+ return hasCommonWord;
+ }
+
+ return false;
+ };
+
+ // Check apakah menu item ini atau submenu-nya yang aktif
+ const isActive = isPathActive(item.link);
+
+ // NEW LOGIC: Check if user is on a detail page (contains ID segments or specific keywords)
+ const isOnDetailPage = (() => {
+ // Check if current path has ID-like segments or detail keywords
+ const segments = currentPath.split('/').filter(s => s.length > 0);
+
+ if (segments.length === 0) return false;
+
+ const commonWords = [
+ // Actions
+ 'detail', 'edit', 'create', 'new', 'add', 'delete', 'view',
+
+ // Status types
+ 'publish', 'review', 'reject', 'status', 'active', 'inactive', 'pending',
+
+ // General pages
+ 'category', 'history', 'dashboard', 'index',
+
+ // Event specific
+ 'type-of-event', 'type-create', 'type-update',
+
+ // Forum specific
+ 'posting', 'report-posting', 'report-comment',
+
+ // Collaboration
+ 'group',
+
+ // App Information
+ 'business-field', 'information-bank', 'sticker',
+ 'bidang-update', 'sub-bidang-update',
+
+ // Transaction/Finance related
+ 'transaction-detail', 'transaction', 'payment',
+ 'disbursement-of-funds', 'detail-disbursement-of-funds',
+ 'list-disbursement-of-funds',
+
+ // List pages (CRITICAL!)
+ 'list-of-investor', 'list-of-donatur', 'list-of-participants',
+ 'list-comment', 'list-report-comment', 'list-report-posting',
+
+ // Input/Form pages
+ 'reject-input',
+
+ // Category pages
+ 'category-create', 'category-update'
+ ];
+
+ // Check if any segment is a common word
+ const hasCommonWord = segments.some(segment => commonWords.includes(segment.toLowerCase()));
+
+ // Check if any segment looks like an ID (number, UUID, alphanumeric with numbers)
+ const hasIdSegment = segments.some(segment => {
+ const isPureNumber = /^\d+$/.test(segment);
+ const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment);
+ const hasNumber = /\d/.test(segment);
+ const isAlphanumericId = /^[a-z0-9_-]+$/i.test(segment) && segment.length <= 50 && hasNumber;
+
+ return isPureNumber || isUUID || isAlphanumericId;
+ });
+
+ // A detail page is one that has either common words or ID segments
+ return hasCommonWord || hasIdSegment;
+ })();
+
+ // Calculate all submenu active states for conflict resolution
+ const submenuActiveStates = item.links?.map(subItem => ({
+ subItem,
+ isActive: isPathActive(subItem.link, subItem.detailPattern),
+ pathLength: subItem.link.length
+ })) || [];
+
+ // Determine if any submenu is active considering conflicts
+ const hasActiveSubmenu = submenuActiveStates.some(({ isActive: isSubActive, pathLength, subItem }) => {
+ if (!isSubActive) return false;
+
+ // Check if there's a more specific match elsewhere
+ const hasMoreSpecificMatch = submenuActiveStates.some(other => {
+ if (other.subItem.link === subItem.link) return false; // Skip self
+ return other.isActive && other.pathLength > pathLength;
+ });
+
+ return isSubActive && !hasMoreSpecificMatch;
+ }) || false;
+
+ // For parent menu detection, if current path contains common words,
+ // check if this parent menu's link is a prefix of the current path
+ const isParentOfDetailPage = !isActive && !hasActiveSubmenu && item.links && item.links.length > 0 &&
+ item.links.some(link => currentPath.startsWith(link.link.replace(/\/+$/, "") + "/"));
+
+ // Determine if this is the most relevant parent menu for the current path
+ const isMostRelevantParent = isParentOfDetailPage && (() => {
+ let longestMatchLength = 0;
+ let mostRelevantParent = null;
+
+ // Find the parent with the longest matching prefix
+ items.forEach(parentItem => {
+ if (parentItem.links && parentItem.links.length > 0) {
+ parentItem.links.forEach(link => {
+ const linkPath = link.link.replace(/\/+$/, "");
+ if (currentPath.startsWith(linkPath + "/") && linkPath.length > longestMatchLength) {
+ longestMatchLength = linkPath.length;
+ mostRelevantParent = parentItem.label;
+ }
+ });
+ }
+ });
+
+ return mostRelevantParent === item.label;
+ })();
+
+ // NEW LOGIC: If we're on a detail page, NO submenu should be active regardless of pattern matching
+ const hasActiveSubmenuOnDetailPage = isOnDetailPage ? false : hasActiveSubmenu;
+
+ // NEW LOGIC: If user is on a detail page that belongs to this parent menu,
+ // activate only the parent menu (open dropdown) without activating any submenu
+ const isDetailPageOfThisMenu = !isActive && !hasActiveSubmenuOnDetailPage &&
+ item.links && item.links.length > 0 &&
+ item.links.some(link => {
+ const linkPath = link.link.replace(/\/+$/, "");
+ return currentPath.startsWith(linkPath + "/");
+ }) &&
+ !isMostRelevantParent; // Only apply this logic if this isn't the most relevant parent
+
+ // NEW LOGIC: Check if this is a page that doesn't belong to any specific menu in the navbar
+ const isUnlistedPage = !isActive && !hasActiveSubmenu && !isMostRelevantParent && !isDetailPageOfThisMenu && isOnDetailPage;
+
+ // NEW LOGIC: If we're on a detail page and this menu is not the relevant parent or detail page owner,
+ // then it should not be highlighted even if it would normally be the most relevant
+ const isOnDetailPageAndNotRelevant = isOnDetailPage && !isMostRelevantParent && !isDetailPageOfThisMenu && !isActive;
+
+ // NEW LOGIC: If this is an unlisted page, no menu should be highlighted
+ const isUnlistedPageAndNotRelevant = isUnlistedPage;
+
+ // FINAL LOGIC: Only activate this menu if:
+ // 1. It's the exact match for current path, OR
+ // 2. It's the most relevant parent, OR
+ // 3. It's a detail page of this menu
+ // But NOT if we're on a detail page and this isn't the relevant parent
+ // And NOT if this is an unlisted page
+ const isActuallyRelevant = (isActive || isMostRelevantParent || isDetailPageOfThisMenu) && !isOnDetailPageAndNotRelevant && !isUnlistedPageAndNotRelevant;
+
+ // Animasi saat isOpen berubah
+ useEffect(() => {
+ Animated.timing(animatedHeight, {
+ toValue: (isOpen || isDetailPageOfThisMenu) ? (item.links ? item.links.length * 44 : 0) : 0,
+ duration: 200,
+ useNativeDriver: false,
+ }).start();
+ }, [isOpen, item.links, animatedHeight, isDetailPageOfThisMenu]);
+
+ // Jika ada submenu
+ if (item.links && item.links.length > 0) {
+ return (
+
+ {/* Parent Item */}
+
+
+
+ {item.label}
+
+
+
+
+ {/* Submenu (Animated) */}
+
+ {submenuActiveStates.map(({ subItem, isActive: isSubActive, pathLength }, index) => {
+
+ // CRITICAL FIX: Cek apakah ada submenu lain yang LEBIH PANJANG dan juga aktif
+ const hasMoreSpecificMatch = submenuActiveStates.some(other => {
+ if (other.subItem.link === subItem.link) return false; // Skip self
+
+ const isOtherLonger = other.pathLength > pathLength;
+
+ // Debug log untuk Dashboard
+ // if (subItem.label === "Dashboard" && isSubActive) {
+ // console.log(`🔎 Dashboard checking against ${other.subItem.label}:`, {
+ // dashboardLink: subItem.link,
+ // dashboardLength: pathLength,
+ // otherLabel: other.subItem.label,
+ // otherLink: other.subItem.link,
+ // otherPattern: other.subItem.detailPattern,
+ // otherLength: other.pathLength,
+ // otherIsActive: other.isActive,
+ // isOtherLonger,
+ // willDisableDashboard: other.isActive && isOtherLonger,
+ // currentURL: currentPath
+ // });
+ // }
+
+ // Conflict log
+ // if (isSubActive && other.isActive) {
+ // console.log('🔍 CONFLICT DETECTED:', {
+ // current: subItem.label,
+ // currentPath: subItem.link,
+ // currentLength: pathLength,
+ // other: other.subItem.label,
+ // otherPath: other.subItem.link,
+ // otherLength: other.pathLength,
+ // isOtherLonger,
+ // shouldDisableCurrent: isOtherLonger,
+ // currentURL: currentPath
+ // });
+ // }
+
+ return other.isActive && isOtherLonger;
+ });
+
+ // Final decision
+ const finalIsActive = isSubActive && !hasMoreSpecificMatch;
+
+ // NEW: If this is a detail page (regardless of which menu), don't highlight any submenu items
+ // Also don't highlight if this is an unlisted page
+ const shouldHighlight = (isOnDetailPage || isUnlistedPage) ? false : finalIsActive;
+
+ // Debug final
+ // if (isSubActive) {
+ // console.log('✅ Active check:', {
+ // label: subItem.label,
+ // link: subItem.link,
+ // isSubActive,
+ // hasMoreSpecificMatch,
+ // finalIsActive,
+ // shouldHighlight,
+ // isOnDetailPage,
+ // isUnlistedPage
+ // });
+ // }
+
+ return (
+ {
+ onClose?.();
+ router.push(subItem.link as any);
+ }}
+ >
+
+
+ {subItem.label}
+
+
+ );
+ })}
+
+
+ );
+ }
+
+ // Menu tanpa submenu
+ return (
+ {
+ onClose?.();
+ router.push(item.link as any);
+ }}
+ >
+
+
+ {item.label}
+
+
+ );
+}
+
+// Styles
+const styles = StyleSheet.create({
+ container: {
+ marginBottom: 5,
+ },
+ parentItem: {
+ flexDirection: "row",
+ alignItems: "center",
+ paddingVertical: 12,
+ paddingHorizontal: 10,
+ borderRadius: 8,
+ marginBottom: 5,
+ justifyContent: "space-between",
+ },
+ parentItemActive: {
+ backgroundColor: AccentColor.blue,
+ },
+ parentText: {
+ flex: 1,
+ fontSize: 16,
+ fontWeight: "500",
+ marginLeft: 10,
+ color: MainColor.white,
+ },
+ singleItem: {
+ flexDirection: "row",
+ alignItems: "center",
+ paddingVertical: 12,
+ paddingHorizontal: 10,
+ borderRadius: 8,
+ marginBottom: 5,
+ },
+ singleItemActive: {
+ backgroundColor: AccentColor.blue,
+ },
+ singleText: {
+ fontSize: 16,
+ fontWeight: "500",
+ marginLeft: 10,
+ color: MainColor.white,
+ },
+ icon: {
+ width: 24,
+ textAlign: "center",
+ paddingRight: 10,
+ },
+ submenu: {
+ overflow: "hidden",
+ marginLeft: 30,
+ marginTop: 5,
+ },
+ subItem: {
+ flexDirection: "row",
+ alignItems: "center",
+ paddingVertical: 8,
+ paddingHorizontal: 10,
+ borderRadius: 6,
+ marginBottom: 4,
+ },
+ subItemActive: {
+ backgroundColor: AccentColor.blue,
+ },
+ subText: {
+ color: MainColor.white,
+ fontSize: 16,
+ fontWeight: "500",
+ },
+});
\ No newline at end of file
diff --git a/components/_ShareComponent/Admin/AdminBasicBox.tsx b/components/_ShareComponent/Admin/AdminBasicBox.tsx
new file mode 100644
index 0000000..628aac9
--- /dev/null
+++ b/components/_ShareComponent/Admin/AdminBasicBox.tsx
@@ -0,0 +1,20 @@
+import BaseBox from "@/components/Box/BaseBox";
+import TextCustom from "@/components/Text/TextCustom";
+import { AccentColor } from "@/constants/color-palet";
+import { StyleProp, ViewStyle } from "react-native";
+
+interface Props {
+ children: React.ReactNode;
+ onPress?: () => void;
+ style?: StyleProp;
+}
+
+export default function AdminBasicBox({ children, onPress, style }: Props) {
+ return (
+ <>
+
+ {children}
+
+ >
+ );
+}
diff --git a/components/_ShareComponent/Admin/BoxTitlePage.tsx b/components/_ShareComponent/Admin/BoxTitlePage.tsx
index b34b230..a6ab82d 100644
--- a/components/_ShareComponent/Admin/BoxTitlePage.tsx
+++ b/components/_ShareComponent/Admin/BoxTitlePage.tsx
@@ -1,7 +1,9 @@
import BaseBox from "@/components/Box/BaseBox";
import Grid from "@/components/Grid/GridCustom";
import TextCustom from "@/components/Text/TextCustom";
+import { AccentColor } from "@/constants/color-palet";
import { TEXT_SIZE_LARGE } from "@/constants/constans-value";
+import { View } from "react-native";
export default function AdminComp_BoxTitle({
title,
@@ -12,13 +14,33 @@ export default function AdminComp_BoxTitle({
}) {
return (
<>
- */}
+
-
-
+
+
)}
-
+
+ {/* */}
>
);
}
diff --git a/components/_ShareComponent/BasicWrapper.tsx b/components/_ShareComponent/BasicWrapper.tsx
new file mode 100644
index 0000000..a26d48a
--- /dev/null
+++ b/components/_ShareComponent/BasicWrapper.tsx
@@ -0,0 +1,16 @@
+import { MainColor } from "@/constants/color-palet";
+import { View } from "react-native";
+
+export default function BasicWrapper({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+ <>
+
+ {children}
+
+ >
+ );
+}
diff --git a/components/index.ts b/components/index.ts
index f53c40f..27e7c1f 100644
--- a/components/index.ts
+++ b/components/index.ts
@@ -60,6 +60,7 @@ import SearchInput from "./_ShareComponent/SearchInput";
import DummyLandscapeImage from "./_ShareComponent/DummyLandscapeImage";
import GridComponentView from "./_ShareComponent/GridSectionView";
import NewWrapper from "./_ShareComponent/NewWrapper";
+import BasicWrapper from "./_ShareComponent/BasicWrapper";
// Progress
import ProgressCustom from "./Progress/ProgressCustom";
// Loader
@@ -121,6 +122,7 @@ export {
GridComponentView,
Spacing,
NewWrapper,
+ BasicWrapper,
// Stack
StackCustom,
TabBarBackground,
diff --git a/docs/admin-folder-structure.md b/docs/admin-folder-structure.md
new file mode 100644
index 0000000..09856ff
--- /dev/null
+++ b/docs/admin-folder-structure.md
@@ -0,0 +1,177 @@
+# Struktur Folder Admin Aplikasi HIPMI Mobile
+
+Dokumen ini menjelaskan struktur folder dan file untuk bagian admin dari aplikasi HIPMI Mobile yang terletak di `app/(application)/admin`.
+
+## File dan Folder Tingkat Atas
+
+### Folder
+- `app-information` - Manajemen informasi aplikasi
+- `collaboration` - Manajemen modul kolaborasi
+- `donation` - Manajemen modul donasi
+- `event` - Manajemen modul acara
+- `forum` - Manajemen modul forum
+- `investment` - Manajemen modul investasi
+- `job` - Manajemen modul lowongan kerja
+- `notification` - Manajemen notifikasi
+- `super-admin` - Fungsi super admin
+- `user-access` - Manajemen akses pengguna
+- `voting` - Manajemen modul voting
+
+### File
+- `_layout.tsx` - Komponen tata letak untuk bagian admin
+- `dashboard.tsx` - Tampilan dasbor admin
+- `maps.tsx` - Fungsionalitas peta untuk admin
+
+## Struktur Folder Terperinci
+
+### app-information/
+```
+app-information/
+├── business-field/
+│ ├── [id]/
+│ │ ├── bidang-update.tsx
+│ │ ├── index.tsx
+│ │ └── sub-bidang-update.tsx
+│ └── create.tsx
+├── information-bank/
+│ ├── [id]/
+│ │ └── index.tsx
+│ └── create.tsx
+├── sticker/
+│ ├── [id]/
+│ │ └── index.tsx
+│ └── create.tsx
+└── index.tsx
+```
+
+### collaboration/
+```
+collaboration/
+├── [id]/
+│ ├── [status].tsx
+│ ├── group.tsx
+│ └── reject-input.tsx
+├── group.tsx
+├── index.tsx
+├── publish.tsx
+└── reject.tsx
+```
+
+### donation/
+```
+donation/
+├── [id]/
+│ ├── [status]/
+│ │ ├── index.tsx
+│ │ └── transaction-detail.tsx
+│ ├── detail-disbursement-of-funds.tsx
+│ ├── disbursement-of-funds.tsx
+│ ├── list-disbursement-of-funds.tsx
+│ ├── list-of-donatur.tsx
+│ └── reject-input.tsx
+├── [status]/
+│ └── status.tsx
+├── category-create.tsx
+├── category-update.tsx
+├── category.tsx
+└── index.tsx
+```
+
+### event/
+```
+event/
+├── [id]/
+│ ├── [status]/
+│ │ └── index.tsx
+│ ├── list-of-participants.tsx
+│ └── reject-input.tsx
+├── [status]/
+│ └── status.tsx
+├── index.tsx
+├── type-create.tsx
+├── type-of-event.tsx
+└── type-update.tsx
+```
+
+### forum/
+```
+forum/
+├── [id]/
+│ ├── index.tsx
+│ ├── list-comment.tsx
+│ ├── list-report-comment.tsx
+│ └── list-report-posting.tsx
+├── index.tsx
+├── posting.tsx
+├── report-comment.tsx
+└── report-posting.tsx
+```
+
+### investment/
+```
+investment/
+├── [id]/
+│ ├── [status]/
+│ │ ├── index.tsx
+│ │ └── transaction-detail.tsx
+│ ├── list-of-investor.tsx
+│ └── reject-input.tsx
+├── [status]/
+│ └── status.tsx
+└── index.tsx
+```
+
+### job/
+```
+job/
+├── [id]/
+│ ├── [status]/
+│ │ ├── index.tsx
+│ │ └── reject-input.tsx
+├── [status]/
+│ └── status.tsx
+└── index.tsx
+```
+
+### notification/
+```
+notification/
+└── index.tsx
+```
+
+### super-admin/
+```
+super-admin/
+├── [id]/
+│ └── index.tsx
+└── index.tsx
+```
+
+### user-access/
+```
+user-access/
+├── [id]/
+│ └── index.tsx
+└── index.tsx
+```
+
+### voting/
+```
+voting/
+├── [id]/
+│ ├── [status]/
+│ │ ├── index.tsx
+│ │ └── reject-input.tsx
+├── [status]/
+│ └── status.tsx
+├── history.tsx
+└── index.tsx
+```
+
+## Rute Dinamis
+
+Bagian admin menggunakan rute dinamis yang ditunjukkan dengan kurung siku `[ ]`:
+- `[id]` - Rute dinamis untuk ID item tertentu
+- `[status]` - Rute dinamis untuk tampilan berdasarkan status
+
+Ini memungkinkan routing yang fleksibel berdasarkan parameter tertentu seperti ID item atau status.
\ No newline at end of file
diff --git a/docs/prompt-for-qwen-code.md b/docs/prompt-for-qwen-code.md
index 18fa314..531ee37 100644
--- a/docs/prompt-for-qwen-code.md
+++ b/docs/prompt-for-qwen-code.md
@@ -45,24 +45,6 @@ Gunakan bahasa indonesia pada cli agar saya mudah membacanya.
-Masukan kode berikut di prop ListHeaderComponent:
-
-
-
-
-
- Rp. {formatCurrencyDisplay(data?.totalPencairan)}
-
- Total Pencairan Dana
-
-
-
- {data?.akumulasiPencairan} kali
-
- Akumulasi Pencairan
-
-
-
@@ -71,19 +53,31 @@ Terapkan NewWrapper pada file: app/(application)/(user)/donation/create.tsx
Component yang digunakan: components/_ShareComponent/NewWrapper.tsx
-Bantu saya untuk memperbaiki logika path yang ada di dalam file "screens/Admin/Notification-Admin/ScreenNotificationAdmin2.tsx" , pada function fixPath
-Saya ingin jika didalam deeplink ada "/admin/..." contoh "/admin/event/review/status" maka path yang akan di redirect adalah "/admin/event/review/status"
-jika tidak maka terapkan sesuai dengan logika yang sudah ada
+
-Bagaimana menangani bug berikut pada file berikut: screens/Invesment/Document/ScreenRecap.tsx
-Ini adalah halaman yang memiliki fungsi pagination , saya membuat data dummy dimana menghasilkan data urut 1-9, saya mencoba memuat halaman setiap page nya 4 saja untuk percobaan.
-Saat awal muncul komponent box dengan data 9 - 6, kemudian saya hapus data ke 8 . lalu saya coba scroll ke bawah seharusnya angka akan tetap urut 9, 7, 6, 5, 4 ... 1. Tapi dalam case ini setelah 8 di hapus kemudian saya scroll box ke 5 tidak muncul saat di scroll. Apakah anda mengerti maksud saya ?
-
-Branch: loaddata/10-feb-26
-Jalankan perintah ini: git checkout -b "Branch"
-Setelah itu jalankan perintah ini: git add .
-Setelah itu jalankan perintah ini: git commit -m "
-
-"
-Setelah itu jalankan perintah ini: git push origin "Branch"
\ No newline at end of file
+Gunakan bahasa indonesia pada cli agar saya mudah membacanya.eclar
+
+
+
+File source: app/(application)/admin/user-access/index.tsx
+Folder tujuan: screens/Admin/User-Access
+Nama file utama: ScreenUserAccess.tsx
+Nama function utama: Admin_ScreenUserAccess
+File komponen wrapper: components/_ShareComponent/NewWrapper.tsx
+Function fecth: apiAdminUserAccessGetAll
+File function fetch: service/api-admin/api-admin-user-access.ts
+
+Buat file baru pada "Folder tujuan" dengan nama "Nama file utama" dan ubah nama function menjadi "Nama function utama" kemudian clean code, import dan panggil function tersebut pada file "File source"
+
+Analisa juga file "Nama file utama" , jika belum menggunakan NewWrapper pada file "File komponen wrapper" , maka terapkan juga dan ganti wrapper lama yaitu komponen ViewWrapper
+
+Terapkan pagination pada file "Nama file utama"
+
+Komponen pagination yang digunaka berada pada file hooks/use-pagination.tsx dan helpers/paginationHelpers.tsx
+
+Perbaiki fetch "Function fecth" , pada file "File function fetch"
+Jika tidak ada props page maka tambahkan props page dan default page: "1"
+
+Gunakan bahasa indonesia pada cli agar saya mudah membacanya.
+
\ No newline at end of file
diff --git a/screens/Admin/User-Access/ScreenUserAccess.tsx b/screens/Admin/User-Access/ScreenUserAccess.tsx
new file mode 100644
index 0000000..048eb16
--- /dev/null
+++ b/screens/Admin/User-Access/ScreenUserAccess.tsx
@@ -0,0 +1,123 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+import {
+ BadgeCustom,
+ CenterCustom,
+ Grid,
+ SearchInput,
+ StackCustom,
+ TextCustom
+} from "@/components";
+import AdminBasicBox from "@/components/_ShareComponent/Admin/AdminBasicBox";
+import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
+import NewWrapper from "@/components/_ShareComponent/NewWrapper";
+import { MainColor } from "@/constants/color-palet";
+import {
+ PAGINATION_DEFAULT_TAKE
+} from "@/constants/constans-value";
+import { createPaginationComponents } from "@/helpers/paginationHelpers";
+import { usePagination } from "@/hooks/use-pagination";
+import { apiAdminUserAccessGetAll } from "@/service/api-admin/api-admin-user-access";
+import { router } from "expo-router";
+import { useState } from "react";
+import { RefreshControl } from "react-native";
+
+export function Admin_ScreenUserAccess() {
+ const [search, setSearch] = useState("");
+
+ const pagination = usePagination({
+ fetchFunction: async (page, searchQuery) => {
+ return await apiAdminUserAccessGetAll({
+ search: searchQuery || "",
+ category: "only-user",
+ page: String(page),
+ });
+ // Pastikan mengembalikan struktur data yang sesuai dengan yang diharapkan oleh usePagination
+ },
+ pageSize: PAGINATION_DEFAULT_TAKE,
+ searchQuery: search,
+ dependencies: [],
+ onError: (error) => {
+ console.log("Error fetching data user access", error);
+ },
+ });
+
+ const { ListEmptyComponent, ListFooterComponent } =
+ createPaginationComponents({
+ loading: pagination.loading,
+ refreshing: pagination.refreshing,
+ listData: pagination.listData,
+ searchQuery: search,
+ emptyMessage: "Tidak ada data pengguna",
+ emptySearchMessage: "Tidak ada hasil pencarian",
+ skeletonCount: PAGINATION_DEFAULT_TAKE,
+ skeletonHeight: 100,
+ isInitialLoad: pagination.isInitialLoad,
+ });
+
+ const rightComponent = () => {
+ return (
+ <>
+ setSearch(text)}
+ />
+ >
+ );
+ };
+
+ const renderItem = ({ item, index }: { item: any; index: number }) => (
+ router.push(`/admin/user-access/${item?.id}`)}
+ style={{ marginHorizontal: 10, marginVertical: 5 }}
+ >
+
+
+
+
+ {item?.username || "-"}
+
+
+ {item?.nomor || "-"}
+
+
+
+
+
+ {item?.active ? (
+ Aktif
+ ) : (
+ Tidak Aktif
+ )}
+
+
+
+
+ );
+
+ return (
+
+ }
+ refreshControl={
+
+ }
+ renderItem={renderItem}
+ listData={pagination.listData}
+ onEndReached={pagination.loadMore}
+ ListEmptyComponent={ListEmptyComponent}
+ ListFooterComponent={ListFooterComponent}
+ hideFooter
+ />
+ );
+}
diff --git a/screens/Admin/listPageAdmin_V2.tsx b/screens/Admin/listPageAdmin_V2.tsx
new file mode 100644
index 0000000..dc5d759
--- /dev/null
+++ b/screens/Admin/listPageAdmin_V2.tsx
@@ -0,0 +1,461 @@
+import { NavbarItem_V2 } from "@/components/Drawer/NavbarMenu_V2";
+
+
+export { adminListMenu_V2, superAdminListMenu_V2 }
+
+const adminListMenu_V2: NavbarItem_V2[] = [
+ {
+ label: "Main Dashboard",
+ icon: "home",
+ link: "/admin/dashboard",
+ },
+ {
+ label: "Investasi",
+ icon: "wallet",
+ links: [
+ {
+ label: "Dashboard",
+ link: "/admin/investment",
+ // Dashboard tidak perlu detailPattern, akan auto-match dengan /admin/investment/123/...
+ },
+ {
+ label: "Publish",
+ link: "/admin/investment/publish/status",
+ detailPattern: "/admin/investment/*/publish", // Match: /admin/investment/123/publish
+ },
+ {
+ label: "Review",
+ link: "/admin/investment/review/status",
+ detailPattern: "/admin/investment/*/review", // Match: /admin/investment/123/review
+ },
+ {
+ label: "Reject",
+ link: "/admin/investment/reject/status",
+ detailPattern: "/admin/investment/*/reject", // Match: /admin/investment/123/reject
+ },
+ ],
+ },
+ {
+ label: "Donasi",
+ icon: "hand-right",
+ links: [
+ {
+ label: "Dashboard",
+ link: "/admin/donation",
+ },
+ {
+ label: "Publish",
+ link: "/admin/donation/publish/status",
+ detailPattern: "/admin/donation/*/publish",
+ },
+ {
+ label: "Review",
+ link: "/admin/donation/review/status",
+ detailPattern: "/admin/donation/*/review",
+ },
+ {
+ label: "Reject",
+ link: "/admin/donation/reject/status",
+ detailPattern: "/admin/donation/*/reject",
+ },
+ {
+ label: "Kategori",
+ link: "/admin/donation/category",
+ },
+ ],
+ },
+ {
+ label: "Event",
+ icon: "calendar-clear",
+ links: [
+ {
+ label: "Dashboard",
+ link: "/admin/event",
+ },
+ {
+ label: "Publish",
+ link: "/admin/event/publish/status",
+ detailPattern: "/admin/event/*/publish",
+ },
+ {
+ label: "Review",
+ link: "/admin/event/review/status",
+ detailPattern: "/admin/event/*/review",
+ },
+ {
+ label: "Reject",
+ link: "/admin/event/reject/status",
+ detailPattern: "/admin/event/*/reject",
+ },
+ {
+ label: "Tipe Acara",
+ link: "/admin/event/type-of-event",
+ },
+ {
+ label: "Riwayat",
+ link: "/admin/event/history/status",
+ detailPattern: "/admin/event/*/history",
+ },
+ ],
+ },
+ {
+ label: "Voting",
+ icon: "accessibility-outline",
+ links: [
+ {
+ label: "Dashboard",
+ link: "/admin/voting",
+ },
+ {
+ label: "Publish",
+ link: "/admin/voting/publish/status",
+ detailPattern: "/admin/voting/*/publish",
+ },
+ {
+ label: "Review",
+ link: "/admin/voting/review/status",
+ detailPattern: "/admin/voting/*/review",
+ },
+ {
+ label: "Reject",
+ link: "/admin/voting/reject/status",
+ detailPattern: "/admin/voting/*/reject",
+ },
+ {
+ label: "Riwayat",
+ link: "/admin/voting/history",
+ detailPattern: "/admin/voting/*/history",
+ },
+ ],
+ },
+ {
+ label: "Job",
+ icon: "desktop-outline",
+ links: [
+ {
+ label: "Dashboard",
+ link: "/admin/job",
+ },
+ {
+ label: "Publish",
+ link: "/admin/job/publish/status",
+ detailPattern: "/admin/job/*/publish",
+ },
+ {
+ label: "Review",
+ link: "/admin/job/review/status",
+ detailPattern: "/admin/job/*/review",
+ },
+ {
+ label: "Reject",
+ link: "/admin/job/reject/status",
+ detailPattern: "/admin/job/*/reject",
+ },
+ ],
+ },
+ {
+ label: "Forum",
+ icon: "chatbubble-ellipses-outline",
+ links: [
+ {
+ label: "Dashboard",
+ link: "/admin/forum",
+ },
+ {
+ label: "Posting",
+ link: "/admin/forum/posting",
+ },
+ {
+ label: "Report Posting",
+ link: "/admin/forum/report-posting",
+ },
+ {
+ label: "Report Komentar",
+ link: "/admin/forum/report-comment",
+ },
+ ],
+ },
+ {
+ label: "Collaboration",
+ icon: "people",
+ links: [
+ {
+ label: "Dashboard",
+ link: "/admin/collaboration",
+ },
+ {
+ label: "Publish",
+ link: "/admin/collaboration/publish",
+ },
+ {
+ label: "Group",
+ link: "/admin/collaboration/group",
+ },
+ {
+ label: "Reject",
+ link: "/admin/collaboration/reject",
+ },
+ ],
+ },
+ {
+ label: "Maps",
+ icon: "map",
+ link: "/admin/maps",
+ },
+ {
+ label: "App Information",
+ icon: "information-circle",
+ link: "/admin/app-information",
+ },
+ {
+ label: "User Access",
+ icon: "people",
+ link: "/admin/user-access",
+ },
+];
+
+const superAdminListMenu_V2: NavbarItem_V2[] = [
+ {
+ label: "Main Dashboard",
+ icon: "home",
+ link: "/admin/dashboard",
+ },
+ {
+ label: "Investasi",
+ icon: "wallet",
+ links: [
+ {
+ label: "Dashboard",
+ link: "/admin/investment",
+ },
+ {
+ label: "Publish",
+ link: "/admin/investment/publish/status",
+ detailPattern: "/admin/investment/*/publish",
+ },
+ {
+ label: "Review",
+ link: "/admin/investment/review/status",
+ detailPattern: "/admin/investment/*/review",
+ },
+ {
+ label: "Reject",
+ link: "/admin/investment/reject/status",
+ detailPattern: "/admin/investment/*/reject",
+ },
+ ],
+ },
+ {
+ label: "Donasi",
+ icon: "hand-right",
+ links: [
+ {
+ label: "Dashboard",
+ link: "/admin/donation",
+ },
+ {
+ label: "Publish",
+ link: "/admin/donation/publish/status",
+ detailPattern: "/admin/donation/*/publish",
+ },
+ {
+ label: "Review",
+ link: "/admin/donation/review/status",
+ detailPattern: "/admin/donation/*/review",
+ },
+ {
+ label: "Reject",
+ link: "/admin/donation/reject/status",
+ detailPattern: "/admin/donation/*/reject",
+ },
+ {
+ label: "Kategori",
+ link: "/admin/donation/category",
+ },
+ ],
+ },
+ {
+ label: "Event",
+ icon: "calendar-clear",
+ links: [
+ {
+ label: "Dashboard",
+ link: "/admin/event",
+ },
+ {
+ label: "Publish",
+ link: "/admin/event/publish/status",
+ detailPattern: "/admin/event/*/publish",
+ },
+ {
+ label: "Review",
+ link: "/admin/event/review/status",
+ detailPattern: "/admin/event/*/review",
+ },
+ {
+ label: "Reject",
+ link: "/admin/event/reject/status",
+ detailPattern: "/admin/event/*/reject",
+ },
+ {
+ label: "Tipe Acara",
+ link: "/admin/event/type-of-event",
+ },
+ {
+ label: "Riwayat",
+ link: "/admin/event/history/status",
+ detailPattern: "/admin/event/*/history",
+ },
+ ],
+ },
+ {
+ label: "Voting",
+ icon: "accessibility-outline",
+ links: [
+ {
+ label: "Dashboard",
+ link: "/admin/voting",
+ },
+ {
+ label: "Publish",
+ link: "/admin/voting/publish/status",
+ detailPattern: "/admin/voting/*/publish",
+ },
+ {
+ label: "Review",
+ link: "/admin/voting/review/status",
+ detailPattern: "/admin/voting/*/review",
+ },
+ {
+ label: "Reject",
+ link: "/admin/voting/reject/status",
+ detailPattern: "/admin/voting/*/reject",
+ },
+ {
+ label: "Riwayat",
+ link: "/admin/voting/history",
+ detailPattern: "/admin/voting/*/history",
+ },
+ ],
+ },
+ {
+ label: "Job",
+ icon: "desktop-outline",
+ links: [
+ {
+ label: "Dashboard",
+ link: "/admin/job",
+ },
+ {
+ label: "Publish",
+ link: "/admin/job/publish/status",
+ detailPattern: "/admin/job/*/publish",
+ },
+ {
+ label: "Review",
+ link: "/admin/job/review/status",
+ detailPattern: "/admin/job/*/review",
+ },
+ {
+ label: "Reject",
+ link: "/admin/job/reject/status",
+ detailPattern: "/admin/job/*/reject",
+ },
+ ],
+ },
+ {
+ label: "Forum",
+ icon: "chatbubble-ellipses-outline",
+ links: [
+ {
+ label: "Dashboard",
+ link: "/admin/forum",
+ },
+ {
+ label: "Posting",
+ link: "/admin/forum/posting",
+ },
+ {
+ label: "Report Posting",
+ link: "/admin/forum/report-posting",
+ },
+ {
+ label: "Report Komentar",
+ link: "/admin/forum/report-comment",
+ },
+ ],
+ },
+ {
+ label: "Collaboration",
+ icon: "people",
+ links: [
+ {
+ label: "Dashboard",
+ link: "/admin/collaboration",
+ },
+ {
+ label: "Publish",
+ link: "/admin/collaboration/publish",
+ },
+ {
+ label: "Group",
+ link: "/admin/collaboration/group",
+ },
+ {
+ label: "Reject",
+ link: "/admin/collaboration/reject",
+ },
+ ],
+ },
+ {
+ label: "Maps",
+ icon: "map",
+ link: "/admin/maps",
+ },
+ {
+ label: "App Information",
+ icon: "information-circle",
+ link: "/admin/app-information",
+ },
+ {
+ label: "User Access",
+ icon: "people",
+ link: "/admin/user-access",
+ },
+ {
+ label: "Super Admin",
+ icon: "globe",
+ link: "/admin/super-admin",
+ },
+];
+
+/*
+=================================================================================
+PENJELASAN detailPattern:
+=================================================================================
+
+detailPattern digunakan untuk match dengan URL detail page yang strukturnya:
+/admin/{module}/[id]/[status]
+
+Contoh untuk Job Review:
+- Link: /admin/job/review/status (halaman list review)
+- detailPattern: /admin/job/* /review (detail dari review)
+- Match dengan: /admin/job/123/review, /admin/job/456/review, dll
+
+Wildcard "*" akan match dengan ID apapun (angka, UUID, alphanumeric).
+
+Modul yang PERLU detailPattern:
+✅ Investasi - Publish, Review, Reject (ada [id]/[status])
+✅ Donasi - Publish, Review, Reject (ada [id]/[status])
+✅ Event - Publish, Review, Reject, Riwayat (ada [id]/[status])
+✅ Voting - Publish, Review, Reject, Riwayat (ada [id]/[status])
+✅ Job - Publish, Review, Reject (ada [id]/[status])
+
+Modul yang TIDAK PERLU detailPattern:
+❌ Forum - posting, report-posting, report-comment (struktur berbeda)
+❌ Collaboration - struktur berbeda
+❌ Maps, App Information, User Access - single page
+❌ Dashboard submenu - auto-match dengan parent path
+
+=================================================================================
+*/
\ No newline at end of file
diff --git a/service/api-admin/api-admin-user-access.ts b/service/api-admin/api-admin-user-access.ts
index c47c69b..c4cc9ef 100644
--- a/service/api-admin/api-admin-user-access.ts
+++ b/service/api-admin/api-admin-user-access.ts
@@ -3,15 +3,31 @@ import { apiConfig } from "../api-config";
export const apiAdminUserAccessGetAll = async ({
search,
category,
+ page = "1",
}: {
search?: string;
category: "only-user" | "only-admin" | "all-role";
+ page?: string;
}) => {
try {
- const response = await apiConfig.get(`/mobile/admin/user?category=${category}&search=${search}`);
- return response.data;
+ const response = await apiConfig.get(
+ `/mobile/admin/user?category=${category}&search=${search}&page=${page}`,
+ );
+ // Pastikan mengembalikan struktur data yang konsisten
+ return {
+ success: response.data.success,
+ message: response.data.message,
+ data: response.data.data || [], // Gunakan data yang sebenarnya atau array kosong
+ pagination: response.data.pagination, // Jika ada info pagination
+ };
} catch (error) {
console.log(error);
+ return {
+ success: false,
+ message: "Error fetching data",
+ data: [],
+ pagination: null,
+ };
}
};
@@ -36,12 +52,15 @@ export const apiAdminUserAccessUpdateStatus = async ({
category: "access" | "role";
}) => {
try {
- const response = await apiConfig.put(`/mobile/admin/user/${id}?category=${category}`, {
- data: {
- active,
- role,
+ const response = await apiConfig.put(
+ `/mobile/admin/user/${id}?category=${category}`,
+ {
+ data: {
+ active,
+ role,
+ },
},
- });
+ );
return response.data;
} catch (error) {
console.log(error);