From 5c931b069cc3f9775771c4573d894fe3b399b66a Mon Sep 17 00:00:00 2001 From: bagasbanuna Date: Wed, 11 Feb 2026 17:40:08 +0800 Subject: [PATCH 1/4] Fixed navbar admin User & Admin Layout - app/(application)/(user)/home.tsx - app/(application)/admin/_layout.tsx Components - components/Drawer/NavbarMenu.tsx - components/index.ts Docs - docs/prompt-for-qwen-code.md Backup Component - components/Drawer/NavbarMenu.back.tsx New Components - components/Drawer/NavbarMenu_V2.tsx - components/_ShareComponent/BasicWrapper.tsx New Admin Screen - screens/Admin/listPageAdmin_V2.tsx ### No Issue --- app/(application)/(user)/home.tsx | 23 +- app/(application)/admin/_layout.tsx | 18 +- components/Drawer/NavbarMenu.back.tsx | 276 ++++++++++ components/Drawer/NavbarMenu.tsx | 123 ++++- components/Drawer/NavbarMenu_V2.tsx | 570 ++++++++++++++++++++ components/_ShareComponent/BasicWrapper.tsx | 16 + components/index.ts | 2 + docs/prompt-for-qwen-code.md | 34 +- screens/Admin/listPageAdmin_V2.tsx | 461 ++++++++++++++++ 9 files changed, 1512 insertions(+), 11 deletions(-) create mode 100644 components/Drawer/NavbarMenu.back.tsx create mode 100644 components/Drawer/NavbarMenu_V2.tsx create mode 100644 components/_ShareComponent/BasicWrapper.tsx create mode 100644 screens/Admin/listPageAdmin_V2.tsx diff --git a/app/(application)/(user)/home.tsx b/app/(application)/(user)/home.tsx index e964490..ad768bd 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,14 +61,31 @@ 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 ( <> - setOpenDrawerNavbar(false)} + /> */} + + setOpenDrawerNavbar(false)} /> @@ -198,7 +212,7 @@ export default function AdminLayout() { // size={ICON_SIZE_SMALL} // color={MainColor.white} // /> - + ), path: "/admin/notification", }, 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) => ( + toggleOpen(item.label)} + /> + ))} + + + ); +} + +// 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..ffb379d 100644 --- a/components/Drawer/NavbarMenu.tsx +++ b/components/Drawer/NavbarMenu.tsx @@ -37,6 +37,90 @@ export default function NavbarMenu({ items, onClose }: NavbarMenuProps) { const normalizePath = (path: string) => path.replace(/\/+$/, ""); const normalizedPathname = pathname ? normalizePath(pathname) : ""; + // Fungsi untuk mengecek apakah path cocok dengan item menu + // Ini akan mengecek kecocokan eksak atau pola path + const isActivePath = (itemPath: string | undefined): boolean => { + if (!itemPath || !normalizedPathname) return false; + + // Cocokan eksak + if (normalizePath(itemPath) === normalizedPathname) return true; + + // Cocokan pola path seperti /user-access/[id]/index dengan /user-access/index + // atau /donation/[id]/detail dengan /donation/index + const normalizedItemPath = normalizePath(itemPath); + + // Jika path item adalah bagian dari path saat ini (prefix match) + if (normalizedPathname.startsWith(normalizedItemPath + '/')) return true; + + // Jika path saat ini adalah bagian dari path item (misalnya /user-access/detail cocok dengan /user-access) + if (normalizedItemPath.startsWith(normalizedPathname + '/')) return true; + + // Jika path item adalah bagian dari path saat ini tanpa id (misalnya /user-access/[id]/index cocok dengan /user-access/index) + const itemParts = normalizedItemPath.split('/'); + const currentParts = normalizedPathname.split('/'); + + // Jika panjangnya sama dan hanya berbeda di bagian dinamis [id] + if (itemParts.length === currentParts.length) { + let match = true; + for (let i = 0; i < itemParts.length; i++) { + // Jika bagian path item adalah placeholder [id], abaikan + if (itemParts[i].startsWith('[') && itemParts[i].endsWith(']')) continue; + + // Jika bagian path saat ini adalah ID (angka), abaikan + if (/^\d+$/.test(currentParts[i])) continue; + + // Jika tidak cocok dan bukan placeholder atau ID, maka tidak cocok + if (itemParts[i] !== currentParts[i]) { + match = false; + break; + } + } + if (match) return true; + } + + // Tambahkan logika khusus untuk menangani file index.tsx sebagai halaman dashboard + // Jika path saat ini adalah versi index dari path item (misalnya /admin/event/index cocok dengan /admin/event) + if (normalizedPathname === normalizedItemPath + '/index') return true; + + return false; + }; + + // Fungsi untuk menentukan item mana yang paling spesifik aktif + // Ini akan memastikan hanya satu item yang aktif pada satu waktu + const findMostSpecificActiveItem = (): { parentLabel?: string; subItemLink?: string } | null => { + // Cek setiap item menu + for (const item of items) { + // Jika item memiliki sub-menu + if (item.links && item.links.length > 0) { + // Urutkan sub-menu berdasarkan panjang path (terpanjang dulu untuk prioritas lebih spesifik) + const sortedSubItems = [...item.links].sort((a, b) => { + if (a.link && b.link) { + return b.link.length - a.link.length; // Urutan menurun (terpanjang dulu) + } + return 0; + }); + + // Cek setiap sub-menu dalam urutan yang telah diurutkan + for (const subItem of sortedSubItems) { + if (isActivePath(subItem.link)) { + return { parentLabel: item.label, subItemLink: subItem.link }; + } + } + } + + // Jika tidak ada sub-menu yang cocok, cek item utama + if (isActivePath(item.link)) { + return { parentLabel: item.label }; + } + } + + return null; + }; + + + // Hitung item aktif terlebih dahulu + const mostSpecificActive = findMostSpecificActiveItem(); + // Set activeLink saat pathname berubah useEffect(() => { if (normalizedPathname) { @@ -44,6 +128,15 @@ export default function NavbarMenu({ items, onClose }: NavbarMenuProps) { } }, [normalizedPathname]); + // Fungsi untuk menentukan apakah dropdown harus tetap terbuka + // Dropdown tetap terbuka jika salah satu dari sub-menu cocok dengan path saat ini + const shouldDropdownBeOpen = (item: NavbarItem): boolean => { + if (!normalizedPathname || !item.links || item.links.length === 0) return false; + + // Cek apakah salah satu sub-menu cocok dengan path saat ini + return item.links.some(subItem => isActivePath(subItem.link)); + }; + // Toggle dropdown const toggleOpen = (label: string) => { setOpenKeys((prev) => @@ -56,7 +149,7 @@ export default function NavbarMenu({ items, onClose }: NavbarMenuProps) { style={{ // flex: 1, // backgroundColor: MainColor.black, - marginBottom: 20, + marginBottom: 20, }} > toggleOpen(item.label)} + isActivePath={isActivePath} + isMostSpecificActive={(menuItem) => { + if (!mostSpecificActive) return false; + + // Jika item memiliki sub-menu + if (menuItem.links && menuItem.links.length > 0) { + // Jika item ini adalah parent dari sub-menu yang aktif, menu utama tidak aktif + return false; + } + + // Jika tidak ada sub-menu, hanya periksa kecocokan langsung + return mostSpecificActive.parentLabel === menuItem.label && !mostSpecificActive.subItemLink; + }} /> ))} @@ -89,6 +195,8 @@ function MenuItem({ setActiveLink, isOpen, toggleOpen, + isActivePath, + isMostSpecificActive, }: { item: NavbarItem; onClose?: () => void; @@ -96,8 +204,10 @@ function MenuItem({ setActiveLink: (link: string | null) => void; isOpen: boolean; toggleOpen: () => void; + isActivePath: (itemPath: string | undefined) => boolean; + isMostSpecificActive: (item: NavbarItem) => boolean; }) { - const isActive = activeLink === item.link; + const isActive = isMostSpecificActive(item); const animatedHeight = useRef(new Animated.Value(0)).current; // Animasi saat isOpen berubah @@ -121,7 +231,9 @@ function MenuItem({ color={MainColor.white} style={styles.icon} /> - {item.label} + + {item.label} + {item.links.map((subItem, index) => { - const isSubActive = activeLink === subItem.link; + // Untuk sub-item, kita gunakan logika aktif berdasarkan isActivePath + const isSubActive = isActivePath(subItem.link); return ( 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 = [ + // Event + "type-create", + + // Other + "detail", + "edit", + "create", + "new", + "add", + "delete", + "view", + "publish", + "review", + "reject", + "status", + "category", + "history", + "type-of-event", + "posting", + "report-posting", + "report-comment", + "group", + "dashboard", + "sticker", + "active", + "inactive", + "pending", + "transaction-detail", + "transaction", + "payment", + "disbursement", + "list-of-investor", + ]; + + 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) => ( + toggleOpen(item.label)} + /> + )) + : null} + + + ); +} + +// 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("list-of-investor") || + currentPath.includes("type-create") + ) { + console.log( + "🔍 Pattern Match Check:", + JSON.stringify( + { + currentPath, + detailPattern, + regex: patternRegex.toString(), + isMatch, + }, + null, + 2, + ), + ); + } + + 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 = [ + // Event + "type-create", + "detail", + "edit", + "create", + "new", + "add", + "delete", + "view", + "publish", + "review", + "reject", + "status", + "category", + "history", + "type-of-event", + "posting", + "report-posting", + "report-comment", + "group", + "dashboard", + "sticker", + "active", + "inactive", + "pending", + "transaction-detail", + "transaction", + "payment", + "disbursement", + ]; + + 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) { + return ( + + {/* Parent Item */} + + + + {item.label} + + + + + {/* Submenu (Animated) */} + + {item.links.map((subItem, index) => { + const isSubActive = isPathActive( + subItem.link, + subItem.detailPattern, + ); + + // CRITICAL FIX: Jika submenu ini aktif, cek apakah ada submenu lain yang LEBIH PANJANG dan juga aktif + // Jika ada yang lebih panjang dan aktif, maka yang pendek TIDAK AKTIF + const hasMoreSpecificMatch = item.links!.some((otherSubItem) => { + if (otherSubItem.link === subItem.link) return false; // Skip self + + const otherIsActive = isPathActive( + otherSubItem.link, + otherSubItem.detailPattern, + ); + const isOtherLonger = + otherSubItem.link.length > subItem.link.length; + + // Debug log + if (isSubActive && otherIsActive) { + console.log( + "🔍 CONFLICT DETECTED:", + JSON.stringify( + { + current: subItem.label, + currentPath: subItem.link, + currentLength: subItem.link.length, + other: otherSubItem.label, + otherPath: otherSubItem.link, + otherLength: otherSubItem.link.length, + isOtherLonger, + currentURL: currentPath, + }, + null, + 2, + ), + ); + } + + // Jika submenu lain JUGA aktif DAN lebih panjang (lebih spesifik), + // maka submenu yang pendek ini TIDAK boleh aktif + return otherIsActive && isOtherLonger; + }); + + // Final decision: aktif HANYA jika match DAN tidak ada yang lebih spesifik + const finalIsActive = isSubActive && !hasMoreSpecificMatch; + + // Debug final decision + if (isSubActive) { + console.log( + "✅ Active check:", + JSON.stringify( + { + label: subItem.label, + link: subItem.link, + isSubActive, + hasMoreSpecificMatch, + finalIsActive, + }, + null, + 2, + ), + ); + } + + 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", + }, +}); 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/prompt-for-qwen-code.md b/docs/prompt-for-qwen-code.md index 18fa314..d5fb8fd 100644 --- a/docs/prompt-for-qwen-code.md +++ b/docs/prompt-for-qwen-code.md @@ -86,4 +86,36 @@ 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 +Setelah itu jalankan perintah ini: git push origin "Branch" + + + + + +Saya memiliki case pada file ini: @components/Drawer/NavbarMenu.tsx +Pada file ini saya ingin jika saat pindah halaman ( ke detail contoh : /user-access/[id]/index.tsx) maka navbar tetap menandai menu yang sedang aktif, tapi yang terjadi sekarang jika masuk ke detail maka warnanya hilang karena tidak mendeteksi halaman tersebut. +Apakah anda paham maksud saya ? + + +Ya, dalam fitur yang anda perbaharui masih terjadi bug. Saya akan berikan case nya secara perlahan +Saat klik sebuah menu maka sub menu akan terbuka +Saat klik sub menu maka sub menu maka akan menuju ke halaman sesuai path +Dalam bug diawal tadi untuk menu yang aktif jika masuk ke detail memang terselesaikan. Tapi muncul bug baru jika menu tersebut memiliki sub menu dan jika sub menu tersebut di klik (kecuali dashboard) yang aktif adalah bagian sub menu dashbaord dan sub menu yang kita klik, tapi jika sub menu yang di klik adalah dashboard maka semau sub menu aktif. Apakah anda mengerti maksud dari pernyataan saya ? Jika masih kurang paham saya bisa berikan masukan yang lain + +Masih terjadi bug, mengapa saat klik menu yang memiliki dashboard maka sub menu dashboard dan sub menu yang kita klik menjadi aktif ? + + + + +export interface NavbarItem_V2 { + label: string; + icon?: keyof typeof Ionicons.glyphMap; + color?: string; + link?: string; + links?: { + label: string; + link: string; + detailPattern?: string; + }[]; + initiallyOpened?: boolean; +} \ No newline at end of file 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 -- 2.49.1 From e030b8f486f52d1850001a6a81072d2b3c56e96d Mon Sep 17 00:00:00 2001 From: bagasbanuna Date: Thu, 12 Feb 2026 11:48:01 +0800 Subject: [PATCH 2/4] Fixed navbar admin User Layout - app/(application)/(user)/home.tsx Components - components/Drawer/NavbarMenu_V2.tsx Docs - docs/admin-folder-structure.md ### Issue: saat masuk lebih dalam ke sub menu indikator aktif di navbar hilang --- app/(application)/(user)/home.tsx | 16 +- components/Drawer/NavbarMenu_V2.tsx | 402 +++++++++++++--------------- docs/admin-folder-structure.md | 177 ++++++++++++ 3 files changed, 376 insertions(+), 219 deletions(-) create mode 100644 docs/admin-folder-structure.md diff --git a/app/(application)/(user)/home.tsx b/app/(application)/(user)/home.tsx index ad768bd..38dc8a2 100644 --- a/app/(application)/(user)/home.tsx +++ b/app/(application)/(user)/home.tsx @@ -77,14 +77,14 @@ export default function Application() { ); } - // if (data && data?.masterUserRoleId !== "1") { - // console.log("User is not admin"); - // return ( - // - // - // - // ); - // } + if (data && data?.masterUserRoleId !== "1") { + console.log("User is not admin"); + return ( + + + + ); + } return ( <> diff --git a/components/Drawer/NavbarMenu_V2.tsx b/components/Drawer/NavbarMenu_V2.tsx index 95ec816..4faa6fb 100644 --- a/components/Drawer/NavbarMenu_V2.tsx +++ b/components/Drawer/NavbarMenu_V2.tsx @@ -1,7 +1,7 @@ import { AccentColor, MainColor } from "@/constants/color-palet"; import { Ionicons } from "@expo/vector-icons"; import { router, usePathname } from "expo-router"; -import { useEffect, useRef, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { Animated, ScrollView, @@ -19,7 +19,7 @@ export interface NavbarItem_V2 { links?: { label: string; link: string; - detailPattern?: string; + detailPattern?: string; // NEW: Pattern untuk match detail pages }[]; initiallyOpened?: boolean; } @@ -45,93 +45,89 @@ export default function NavbarMenu_V2({ items, onClose }: NavbarMenuProps) { 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, "[^/]+") + "(/.*)?$", + "^" + 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); - + const remainder = normalizedPathname.substring(normalizedLink.length + 1); + const segments = remainder.split("/").filter(s => s.length > 0); + if (segments.length === 0) return false; - + const commonWords = [ - // Event - "type-create", - - // Other - "detail", - "edit", - "create", - "new", - "add", - "delete", - "view", - "publish", - "review", - "reject", - "status", - "category", - "history", - "type-of-event", - "posting", - "report-posting", - "report-comment", - "group", - "dashboard", - "sticker", - "active", - "inactive", - "pending", - "transaction-detail", - "transaction", - "payment", - "disbursement", - "list-of-investor", + // 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) => { + + 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 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; - + 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 @@ -154,9 +150,7 @@ export default function NavbarMenu_V2({ items, onClose }: NavbarMenuProps) { // Toggle dropdown const toggleOpen = (label: string) => { setOpenKeys((prev) => - prev.includes(label) - ? prev.filter((key) => key !== label) - : [...prev, label], + prev.includes(label) ? prev.filter((key) => key !== label) : [...prev, label] ); }; @@ -171,18 +165,18 @@ export default function NavbarMenu_V2({ items, onClose }: NavbarMenuProps) { paddingVertical: 10, }} > - {items && items.length > 0 - ? items.map((item) => ( - toggleOpen(item.label)} - /> - )) - : null} + {items && items.length > 0 ? ( + items.map((item) => ( + toggleOpen(item.label)} + /> + )) + ) : null} ); @@ -205,121 +199,109 @@ function MenuItem({ const animatedHeight = useRef(new Animated.Value(0)).current; // Helper function untuk check apakah path aktif - const isPathActive = ( - linkPath: string | undefined, - detailPattern?: string, - ) => { + 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" + // 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, "[^/]+") + "(/.*)?$", + "^" + detailPattern.replace(/\*/g, "[^/]+") + "(/.*)?$" ); const isMatch = patternRegex.test(currentPath); - + // Debug log untuk pattern matching - if ( - currentPath.includes("list-of-investor") || - currentPath.includes("type-create") - ) { - console.log( - "🔍 Pattern Match Check:", - JSON.stringify( - { - currentPath, - detailPattern, - regex: patternRegex.toString(), - isMatch, - }, - null, - 2, - ), - ); + 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); - + const segments = remainder.split("/").filter(s => s.length > 0); + if (segments.length === 0) return false; - + const commonWords = [ - // Event - "type-create", - "detail", - "edit", - "create", - "new", - "add", - "delete", - "view", - "publish", - "review", - "reject", - "status", - "category", - "history", - "type-of-event", - "posting", - "report-posting", - "report-comment", - "group", - "dashboard", - "sticker", - "active", - "inactive", - "pending", - "transaction-detail", - "transaction", - "payment", - "disbursement", + // 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) => { + + 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 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; - + 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; + item.links?.some((subItem) => isPathActive(subItem.link, subItem.detailPattern)) || false; // Animasi saat isOpen berubah useEffect(() => { @@ -332,6 +314,13 @@ function MenuItem({ // 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 */} @@ -377,71 +366,62 @@ function MenuItem({ }, ]} > - {item.links.map((subItem, index) => { - const isSubActive = isPathActive( - subItem.link, - subItem.detailPattern, - ); - - // CRITICAL FIX: Jika submenu ini aktif, cek apakah ada submenu lain yang LEBIH PANJANG dan juga aktif - // Jika ada yang lebih panjang dan aktif, maka yang pendek TIDAK AKTIF - const hasMoreSpecificMatch = item.links!.some((otherSubItem) => { - if (otherSubItem.link === subItem.link) return false; // Skip self - - const otherIsActive = isPathActive( - otherSubItem.link, - otherSubItem.detailPattern, - ); - const isOtherLonger = - otherSubItem.link.length > subItem.link.length; - - // Debug log - if (isSubActive && otherIsActive) { - console.log( - "🔍 CONFLICT DETECTED:", - JSON.stringify( - { - current: subItem.label, - currentPath: subItem.link, - currentLength: subItem.link.length, - other: otherSubItem.label, - otherPath: otherSubItem.link, - otherLength: otherSubItem.link.length, - isOtherLonger, - currentURL: currentPath, - }, - null, - 2, - ), - ); + {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 + }); } - - // Jika submenu lain JUGA aktif DAN lebih panjang (lebih spesifik), - // maka submenu yang pendek ini TIDAK boleh aktif - return otherIsActive && isOtherLonger; + + // 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: aktif HANYA jika match DAN tidak ada yang lebih spesifik + + // Final decision const finalIsActive = isSubActive && !hasMoreSpecificMatch; - - // Debug final decision + + // Debug final if (isSubActive) { - console.log( - "✅ Active check:", - JSON.stringify( - { - label: subItem.label, - link: subItem.link, - isSubActive, - hasMoreSpecificMatch, - finalIsActive, - }, - null, - 2, - ), - ); + console.log('✅ Active check:', { + label: subItem.label, + link: subItem.link, + isSubActive, + hasMoreSpecificMatch, + finalIsActive + }); } - + return ( Date: Thu, 12 Feb 2026 14:55:05 +0800 Subject: [PATCH 3/4] Navbar menu versi 3 Admin Layout - app/(application)/admin/_layout.tsx Docs - docs/prompt-for-qwen-code.md New Component - components/Drawer/NavbarMenu_V3.tsx ### No Issue' --- app/(application)/admin/_layout.tsx | 12 +- components/Drawer/NavbarMenu_V3.tsx | 871 ++++++++++++++++++++++++++++ docs/prompt-for-qwen-code.md | 47 +- 3 files changed, 916 insertions(+), 14 deletions(-) create mode 100644 components/Drawer/NavbarMenu_V3.tsx diff --git a/app/(application)/admin/_layout.tsx b/app/(application)/admin/_layout.tsx index 94f1d99..7400232 100644 --- a/app/(application)/admin/_layout.tsx +++ b/app/(application)/admin/_layout.tsx @@ -9,6 +9,7 @@ import { 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, @@ -154,7 +155,16 @@ export default function AdminLayout() { onClose={() => setOpenDrawerNavbar(false)} /> */} - setOpenDrawerNavbar(false)} + /> */} + + 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) => ( + toggleOpen(item.label)} + /> + )) + ) : null} + + + ); +} + +// 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/docs/prompt-for-qwen-code.md b/docs/prompt-for-qwen-code.md index d5fb8fd..3870ce2 100644 --- a/docs/prompt-for-qwen-code.md +++ b/docs/prompt-for-qwen-code.md @@ -104,18 +104,39 @@ Dalam bug diawal tadi untuk menu yang aktif jika masuk ke detail memang terseles Masih terjadi bug, mengapa saat klik menu yang memiliki dashboard maka sub menu dashboard dan sub menu yang kita klik menjadi aktif ? +Nama file: NavbarMenu_V2.tsx +Source component: components/Drawer/NavbarMenu_V2.tsx +Struktur file admin: docs/admin-folder-structure.md + +Saya mengalami bug pada file "Nama file" , saya ingin jika saat pindah halaman ( ke detail contoh : app/(application)/admin/investment/[id]/list-of-investor.tsx) maka navbar tetap menandai menu yang sedang aktif, tapi yang terjadi sekarang jika masuk ke detail maka warnanya mendeteksi dashboard padahal sedang di detail investor pada source: app/(application)/admin/investment/[id]/list-of-investor.tsx +Jika anda butuh membaca struktur file admin maka anda bisa membaca file pada "Struktur file admin" + + + + +Error terjadi pada code berikut: + 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; + } + }); + } + }); + + +BUG MASIH TERJADI ! Coba perbaiki perlahan , gunakan semua data dan pengetahuan meksimaln anda agar kode ini berhasil tanpa bug ! +1: Jika user masuk lebih dalam ke detail padahal bukan menu dashboard yang di pilih, CUKUP AKTIFKAN MENU YANG DI PILIH SAJA DENGAN MEMBUKA FUNGSI DROPDOWN DAN TIDAK USAH AKTIFKAN SUB MENUNYAN , INGAT ! CUKUP MENU NYA SAJA YANG AKTIF + + + Pastikan request saya terselesaikan dan error berikut clear: +- Cannot rede block-scoped variable 'hasActiveSubmenuOnDetailPage'. +- Block-scoped variable 'isOnDetailPage' used before its declaration. +- Variable 'isOnDetailPage' is used before being assigned. +- Cannot redeclare block-scoped variable 'hasActiveSubmenuOnDetailPage'. +Gunakan bahasa indonesia pada cli agar saya mudah membacanya.eclar - -export interface NavbarItem_V2 { - label: string; - icon?: keyof typeof Ionicons.glyphMap; - color?: string; - link?: string; - links?: { - label: string; - link: string; - detailPattern?: string; - }[]; - initiallyOpened?: boolean; -} \ No newline at end of file -- 2.49.1 From fb697366fe8d439ef743270f0ee82556066fce1f Mon Sep 17 00:00:00 2001 From: bagasbanuna Date: Thu, 12 Feb 2026 17:35:28 +0800 Subject: [PATCH 4/4] Fixed admin user access MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Admin Layout & Pages - app/(application)/admin/_layout.tsx - app/(application)/admin/user-access/index.tsx Admin Components - components/Drawer/NavbarMenu.tsx - components/_ShareComponent/Admin/BoxTitlePage.tsx - components/_ShareComponent/Admin/AdminBasicBox.tsx Admin Screens - screens/Admin/User-Access/ API – Admin User Access - service/api-admin/api-admin-user-access.ts Docs - docs/prompt-for-qwen-code.md ### No issue --- app/(application)/admin/_layout.tsx | 8 +- app/(application)/admin/user-access/index.tsx | 138 +----------- components/Drawer/NavbarMenu.tsx | 202 +++++++----------- .../_ShareComponent/Admin/AdminBasicBox.tsx | 20 ++ .../_ShareComponent/Admin/BoxTitlePage.tsx | 31 ++- docs/prompt-for-qwen-code.md | 103 ++------- .../Admin/User-Access/ScreenUserAccess.tsx | 123 +++++++++++ service/api-admin/api-admin-user-access.ts | 33 ++- 8 files changed, 302 insertions(+), 356 deletions(-) create mode 100644 components/_ShareComponent/Admin/AdminBasicBox.tsx create mode 100644 screens/Admin/User-Access/ScreenUserAccess.tsx diff --git a/app/(application)/admin/_layout.tsx b/app/(application)/admin/_layout.tsx index 7400232..3e4da82 100644 --- a/app/(application)/admin/_layout.tsx +++ b/app/(application)/admin/_layout.tsx @@ -146,14 +146,14 @@ export default function AdminLayout() { style={{ alignSelf: "flex-end" }} /> - {/* setOpenDrawerNavbar(false)} - /> */} + /> {/* setOpenDrawerNavbar(false)} /> */} - setOpenDrawerNavbar(false)} - /> + /> */} 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.tsx b/components/Drawer/NavbarMenu.tsx index ffb379d..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 @@ -37,110 +37,45 @@ export default function NavbarMenu({ items, onClose }: NavbarMenuProps) { const normalizePath = (path: string) => path.replace(/\/+$/, ""); const normalizedPathname = pathname ? normalizePath(pathname) : ""; - // Fungsi untuk mengecek apakah path cocok dengan item menu - // Ini akan mengecek kecocokan eksak atau pola path - const isActivePath = (itemPath: string | undefined): boolean => { - if (!itemPath || !normalizedPathname) return false; - - // Cocokan eksak - if (normalizePath(itemPath) === normalizedPathname) return true; - - // Cocokan pola path seperti /user-access/[id]/index dengan /user-access/index - // atau /donation/[id]/detail dengan /donation/index - const normalizedItemPath = normalizePath(itemPath); - - // Jika path item adalah bagian dari path saat ini (prefix match) - if (normalizedPathname.startsWith(normalizedItemPath + '/')) return true; - - // Jika path saat ini adalah bagian dari path item (misalnya /user-access/detail cocok dengan /user-access) - if (normalizedItemPath.startsWith(normalizedPathname + '/')) return true; - - // Jika path item adalah bagian dari path saat ini tanpa id (misalnya /user-access/[id]/index cocok dengan /user-access/index) - const itemParts = normalizedItemPath.split('/'); - const currentParts = normalizedPathname.split('/'); - - // Jika panjangnya sama dan hanya berbeda di bagian dinamis [id] - if (itemParts.length === currentParts.length) { - let match = true; - for (let i = 0; i < itemParts.length; i++) { - // Jika bagian path item adalah placeholder [id], abaikan - if (itemParts[i].startsWith('[') && itemParts[i].endsWith(']')) continue; - - // Jika bagian path saat ini adalah ID (angka), abaikan - if (/^\d+$/.test(currentParts[i])) continue; - - // Jika tidak cocok dan bukan placeholder atau ID, maka tidak cocok - if (itemParts[i] !== currentParts[i]) { - match = false; - break; - } - } - if (match) return true; - } - - // Tambahkan logika khusus untuk menangani file index.tsx sebagai halaman dashboard - // Jika path saat ini adalah versi index dari path item (misalnya /admin/event/index cocok dengan /admin/event) - if (normalizedPathname === normalizedItemPath + '/index') return true; - - return false; - }; - - // Fungsi untuk menentukan item mana yang paling spesifik aktif - // Ini akan memastikan hanya satu item yang aktif pada satu waktu - const findMostSpecificActiveItem = (): { parentLabel?: string; subItemLink?: string } | null => { - // Cek setiap item menu - for (const item of items) { - // Jika item memiliki sub-menu - if (item.links && item.links.length > 0) { - // Urutkan sub-menu berdasarkan panjang path (terpanjang dulu untuk prioritas lebih spesifik) - const sortedSubItems = [...item.links].sort((a, b) => { - if (a.link && b.link) { - return b.link.length - a.link.length; // Urutan menurun (terpanjang dulu) - } - return 0; - }); - - // Cek setiap sub-menu dalam urutan yang telah diurutkan - for (const subItem of sortedSubItems) { - if (isActivePath(subItem.link)) { - return { parentLabel: item.label, subItemLink: subItem.link }; - } - } - } - - // Jika tidak ada sub-menu yang cocok, cek item utama - if (isActivePath(item.link)) { - return { parentLabel: item.label }; - } - } - - return null; - }; - - - // Hitung item aktif terlebih dahulu - const mostSpecificActive = findMostSpecificActiveItem(); - // Set activeLink saat pathname berubah 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]); - - // Fungsi untuk menentukan apakah dropdown harus tetap terbuka - // Dropdown tetap terbuka jika salah satu dari sub-menu cocok dengan path saat ini - const shouldDropdownBeOpen = (item: NavbarItem): boolean => { - if (!normalizedPathname || !item.links || item.links.length === 0) return false; - - // Cek apakah salah satu sub-menu cocok dengan path saat ini - return item.links.some(subItem => isActivePath(subItem.link)); - }; + }, [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] ); }; @@ -149,7 +84,7 @@ export default function NavbarMenu({ items, onClose }: NavbarMenuProps) { style={{ // flex: 1, // backgroundColor: MainColor.black, - marginBottom: 20, + marginBottom: 20, }} > toggleOpen(item.label)} - isActivePath={isActivePath} - isMostSpecificActive={(menuItem) => { - if (!mostSpecificActive) return false; - - // Jika item memiliki sub-menu - if (menuItem.links && menuItem.links.length > 0) { - // Jika item ini adalah parent dari sub-menu yang aktif, menu utama tidak aktif - return false; - } - - // Jika tidak ada sub-menu, hanya periksa kecocokan langsung - return mostSpecificActive.parentLabel === menuItem.label && !mostSpecificActive.subItemLink; - }} /> ))} @@ -195,8 +117,6 @@ function MenuItem({ setActiveLink, isOpen, toggleOpen, - isActivePath, - isMostSpecificActive, }: { item: NavbarItem; onClose?: () => void; @@ -204,40 +124,72 @@ function MenuItem({ setActiveLink: (link: string | null) => void; isOpen: boolean; toggleOpen: () => void; - isActivePath: (itemPath: string | undefined) => boolean; - isMostSpecificActive: (item: NavbarItem) => boolean; }) { - const isActive = isMostSpecificActive(item); + // 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} @@ -259,8 +211,7 @@ function MenuItem({ ]} > {item.links.map((subItem, index) => { - // Untuk sub-item, kita gunakan logika aktif berdasarkan isActivePath - const isSubActive = isActivePath(subItem.link); + const isSubActive = activeLink === subItem.link; return ( 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/docs/prompt-for-qwen-code.md b/docs/prompt-for-qwen-code.md index 3870ce2..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,72 +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" - - - -Saya memiliki case pada file ini: @components/Drawer/NavbarMenu.tsx -Pada file ini saya ingin jika saat pindah halaman ( ke detail contoh : /user-access/[id]/index.tsx) maka navbar tetap menandai menu yang sedang aktif, tapi yang terjadi sekarang jika masuk ke detail maka warnanya hilang karena tidak mendeteksi halaman tersebut. -Apakah anda paham maksud saya ? - -Ya, dalam fitur yang anda perbaharui masih terjadi bug. Saya akan berikan case nya secara perlahan -Saat klik sebuah menu maka sub menu akan terbuka -Saat klik sub menu maka sub menu maka akan menuju ke halaman sesuai path -Dalam bug diawal tadi untuk menu yang aktif jika masuk ke detail memang terselesaikan. Tapi muncul bug baru jika menu tersebut memiliki sub menu dan jika sub menu tersebut di klik (kecuali dashboard) yang aktif adalah bagian sub menu dashbaord dan sub menu yang kita klik, tapi jika sub menu yang di klik adalah dashboard maka semau sub menu aktif. Apakah anda mengerti maksud dari pernyataan saya ? Jika masih kurang paham saya bisa berikan masukan yang lain - -Masih terjadi bug, mengapa saat klik menu yang memiliki dashboard maka sub menu dashboard dan sub menu yang kita klik menjadi aktif ? - -Nama file: NavbarMenu_V2.tsx -Source component: components/Drawer/NavbarMenu_V2.tsx -Struktur file admin: docs/admin-folder-structure.md - -Saya mengalami bug pada file "Nama file" , saya ingin jika saat pindah halaman ( ke detail contoh : app/(application)/admin/investment/[id]/list-of-investor.tsx) maka navbar tetap menandai menu yang sedang aktif, tapi yang terjadi sekarang jika masuk ke detail maka warnanya mendeteksi dashboard padahal sedang di detail investor pada source: app/(application)/admin/investment/[id]/list-of-investor.tsx -Jika anda butuh membaca struktur file admin maka anda bisa membaca file pada "Struktur file admin" - - - - -Error terjadi pada code berikut: - 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; - } - }); - } - }); - - -BUG MASIH TERJADI ! Coba perbaiki perlahan , gunakan semua data dan pengetahuan meksimaln anda agar kode ini berhasil tanpa bug ! -1: Jika user masuk lebih dalam ke detail padahal bukan menu dashboard yang di pilih, CUKUP AKTIFKAN MENU YANG DI PILIH SAJA DENGAN MEMBUKA FUNGSI DROPDOWN DAN TIDAK USAH AKTIFKAN SUB MENUNYAN , INGAT ! CUKUP MENU NYA SAJA YANG AKTIF - - - Pastikan request saya terselesaikan dan error berikut clear: -- Cannot rede block-scoped variable 'hasActiveSubmenuOnDetailPage'. -- Block-scoped variable 'isOnDetailPage' used before its declaration. -- Variable 'isOnDetailPage' is used before being assigned. -- Cannot redeclare block-scoped variable 'hasActiveSubmenuOnDetailPage'. 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/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); -- 2.49.1