Fixed admin user access
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
This commit is contained in:
@@ -146,14 +146,14 @@ export default function AdminLayout() {
|
||||
style={{ alignSelf: "flex-end" }}
|
||||
/>
|
||||
|
||||
{/* <NavbarMenu
|
||||
<NavbarMenu
|
||||
items={
|
||||
user?.masterUserRoleId === "2"
|
||||
? adminListMenu
|
||||
: superAdminListMenu
|
||||
}
|
||||
onClose={() => setOpenDrawerNavbar(false)}
|
||||
/> */}
|
||||
/>
|
||||
|
||||
{/* <NavbarMenu_V2
|
||||
items={
|
||||
@@ -164,14 +164,14 @@ export default function AdminLayout() {
|
||||
onClose={() => setOpenDrawerNavbar(false)}
|
||||
/> */}
|
||||
|
||||
<NavbarMenu_V3
|
||||
{/* <NavbarMenu_V3
|
||||
items={
|
||||
user?.masterUserRoleId === "2"
|
||||
? adminListMenu_V2
|
||||
: superAdminListMenu_V2
|
||||
}
|
||||
onClose={() => setOpenDrawerNavbar(false)}
|
||||
/>
|
||||
/> */}
|
||||
</StackCustom>
|
||||
</DrawerAdmin>
|
||||
|
||||
|
||||
@@ -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<any[] | null>(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 (
|
||||
<>
|
||||
<SearchInput
|
||||
containerStyle={{ width: "100%", marginBottom: 0 }}
|
||||
placeholder="Cari User"
|
||||
onChangeText={(text) => setSearch(text)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper
|
||||
headerComponent={
|
||||
<AdminComp_BoxTitle
|
||||
title="User Access"
|
||||
rightComponent={rightComponent()}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<GridViewCustomSpan
|
||||
span1={2}
|
||||
span2={5}
|
||||
span3={5}
|
||||
component1={
|
||||
<TextCustom align="center" bold>
|
||||
Aksi
|
||||
</TextCustom>
|
||||
}
|
||||
component2={<TextCustom bold>Username</TextCustom>}
|
||||
component3={
|
||||
<TextCustom align="center" bold>
|
||||
Status Akses
|
||||
</TextCustom>
|
||||
}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
<StackCustom>
|
||||
{_.isEmpty(listData) ? (
|
||||
<TextCustom align="center" color="gray" size={"small"}>
|
||||
Tidak ada data
|
||||
</TextCustom>
|
||||
) : (
|
||||
listData?.map((item: any, index: number) => (
|
||||
<GridViewCustomSpan
|
||||
key={index}
|
||||
span1={2}
|
||||
span2={5}
|
||||
span3={5}
|
||||
component1={
|
||||
<CenterCustom>
|
||||
<Ionicons
|
||||
onPress={() =>
|
||||
router.push(`/admin/user-access/${item?.id}`)
|
||||
}
|
||||
name="open"
|
||||
size={ICON_SIZE_XLARGE}
|
||||
color={MainColor.yellow}
|
||||
/>
|
||||
</CenterCustom>
|
||||
// <ButtonCustom
|
||||
// onPress={() =>
|
||||
// router.push(`/admin/user-access/${item?.id}`)
|
||||
// }
|
||||
// >
|
||||
// Detail
|
||||
// </ButtonCustom>
|
||||
}
|
||||
component2={
|
||||
<TextCustom bold truncate>
|
||||
{item?.username || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
component3={
|
||||
<CenterCustom>
|
||||
{item?.active ? (
|
||||
<BadgeCustom color="green">Aktif</BadgeCustom>
|
||||
) : (
|
||||
<BadgeCustom color="red">Tidak Aktif</BadgeCustom>
|
||||
)}
|
||||
</CenterCustom>
|
||||
}
|
||||
style3={{ alignItems: "center", justifyContent: "center" }}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</StackCustom>
|
||||
</ViewWrapper>
|
||||
</>
|
||||
);
|
||||
return <Admin_ScreenUserAccess />;
|
||||
}
|
||||
|
||||
@@ -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<string | null>(null);
|
||||
const [openKeys, setOpenKeys] = useState<string[]>([]); // 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,
|
||||
}}
|
||||
>
|
||||
<ScrollView
|
||||
@@ -165,21 +100,8 @@ export default function NavbarMenu({ items, onClose }: NavbarMenuProps) {
|
||||
onClose={onClose}
|
||||
activeLink={activeLink}
|
||||
setActiveLink={setActiveLink}
|
||||
isOpen={openKeys.includes(item.label) || shouldDropdownBeOpen(item)}
|
||||
isOpen={openKeys.includes(item.label)}
|
||||
toggleOpen={() => 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;
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
@@ -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 (
|
||||
<View>
|
||||
{/* Parent Item */}
|
||||
<TouchableOpacity style={styles.parentItem} onPress={toggleOpen}>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.parentItem,
|
||||
isMenuActive && styles.parentItemActive,
|
||||
]}
|
||||
onPress={toggleOpen}
|
||||
>
|
||||
<Ionicons
|
||||
name={item.icon}
|
||||
size={16}
|
||||
color={MainColor.white}
|
||||
color={isMenuActive ? MainColor.yellow : MainColor.white}
|
||||
style={styles.icon}
|
||||
/>
|
||||
<Text style={styles.parentText}>
|
||||
<Text
|
||||
style={[
|
||||
styles.parentText,
|
||||
isMenuActive && { color: MainColor.yellow },
|
||||
]}
|
||||
>
|
||||
{item.label}
|
||||
</Text>
|
||||
<Ionicons
|
||||
name={isOpen ? "chevron-up" : "chevron-down"}
|
||||
size={16}
|
||||
color={MainColor.white}
|
||||
color={isMenuActive ? MainColor.yellow : MainColor.white}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
@@ -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 (
|
||||
<TouchableOpacity
|
||||
key={index}
|
||||
@@ -335,6 +286,9 @@ const styles = StyleSheet.create({
|
||||
marginBottom: 5,
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
parentItemActive: {
|
||||
backgroundColor: AccentColor.blue,
|
||||
},
|
||||
parentText: {
|
||||
flex: 1,
|
||||
fontSize: 16,
|
||||
|
||||
20
components/_ShareComponent/Admin/AdminBasicBox.tsx
Normal file
20
components/_ShareComponent/Admin/AdminBasicBox.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import BaseBox from "@/components/Box/BaseBox";
|
||||
import TextCustom from "@/components/Text/TextCustom";
|
||||
import { AccentColor } from "@/constants/color-palet";
|
||||
import { StyleProp, ViewStyle } from "react-native";
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
onPress?: () => void;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
}
|
||||
|
||||
export default function AdminBasicBox({ children, onPress, style }: Props) {
|
||||
return (
|
||||
<>
|
||||
<BaseBox onPress={onPress} style={style}>
|
||||
{children}
|
||||
</BaseBox>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<>
|
||||
<BaseBox
|
||||
{/* <BaseBox
|
||||
style={{ flexDirection: "row", justifyContent: "space-between" }}
|
||||
paddingTop={5}
|
||||
paddingBottom={5}
|
||||
backgroundColor={AccentColor.blue}
|
||||
> */}
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: AccentColor.darkblue,
|
||||
borderColor: AccentColor.blue,
|
||||
|
||||
padding: 10,
|
||||
borderWidth: 1,
|
||||
borderRadius: 10,
|
||||
}}
|
||||
>
|
||||
<Grid>
|
||||
<Grid.Col span={rightComponent ? 6 : 12} style={{ justifyContent: "center" }}>
|
||||
<Grid
|
||||
// containerStyle={{
|
||||
// bottom: 0,
|
||||
// left: 0,
|
||||
// right: 0,
|
||||
// }}
|
||||
>
|
||||
<Grid.Col
|
||||
span={rightComponent ? 6 : 12}
|
||||
style={{ justifyContent: "center" }}
|
||||
>
|
||||
<TextCustom
|
||||
// style={{ alignSelf: "center" }}
|
||||
bold
|
||||
@@ -39,7 +61,8 @@ export default function AdminComp_BoxTitle({
|
||||
</Grid.Col>
|
||||
)}
|
||||
</Grid>
|
||||
</BaseBox>
|
||||
</View>
|
||||
{/* </BaseBox> */}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -45,24 +45,6 @@ Gunakan bahasa indonesia pada cli agar saya mudah membacanya.
|
||||
|
||||
|
||||
<!-- Additinal prompt -->
|
||||
Masukan kode berikut di prop ListHeaderComponent:
|
||||
<InformationBox text="Pencairan dana akan dilakukan oleh Admin HIPMI tanpa campur tangan pihak manapun, jika berita pencairan dana dibawah tidak sesuai dengan kabar yang diberikan oleh PENGGALANG DANA. Maka pegguna lain dapat melaporkannya pada Admin HIPMI !" />
|
||||
<BaseBox>
|
||||
<Grid>
|
||||
<Grid.Col span={6}>
|
||||
<TextCustom bold color="yellow">
|
||||
Rp. {formatCurrencyDisplay(data?.totalPencairan)}
|
||||
</TextCustom>
|
||||
<TextCustom size="small">Total Pencairan Dana</TextCustom>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6}>
|
||||
<TextCustom bold color="yellow">
|
||||
{data?.akumulasiPencairan} kali
|
||||
</TextCustom>
|
||||
<TextCustom size="small">Akumulasi Pencairan</TextCustom>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</BaseBox>
|
||||
|
||||
<!-- ===================== End Penerapan NewWrapper & Pagination ===================== -->
|
||||
|
||||
@@ -71,72 +53,31 @@ Terapkan NewWrapper pada file: app/(application)/(user)/donation/create.tsx
|
||||
Component yang digunakan: components/_ShareComponent/NewWrapper.tsx
|
||||
<!-- End Penerapan NewWrapper -->
|
||||
|
||||
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 ?
|
||||
|
||||
<!-- COMMIT & PUSH-->
|
||||
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 "
|
||||
<Berikan semua catatan perubahan pada branch ini, tampilan pada saya dan pastikan dalam bahasa indonesia. Saya akan cek baru saya akan berikan perintah push>
|
||||
"
|
||||
Setelah itu jalankan perintah ini: git push origin "Branch"
|
||||
|
||||
|
||||
|
||||
<!-- Start Random Prompt -->
|
||||
|
||||
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
|
||||
<!-- End Random Prompt -->
|
||||
|
||||
<!-- START Prompt Admin Refactoring -->
|
||||
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.
|
||||
<!-- END Prompt Admin Refactoring -->
|
||||
123
screens/Admin/User-Access/ScreenUserAccess.tsx
Normal file
123
screens/Admin/User-Access/ScreenUserAccess.tsx
Normal file
@@ -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 (
|
||||
<>
|
||||
<SearchInput
|
||||
containerStyle={{ width: "100%", marginBottom: 0 }}
|
||||
placeholder="Cari User"
|
||||
onChangeText={(text) => setSearch(text)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const renderItem = ({ item, index }: { item: any; index: number }) => (
|
||||
<AdminBasicBox
|
||||
key={index}
|
||||
onPress={() => router.push(`/admin/user-access/${item?.id}`)}
|
||||
style={{ marginHorizontal: 10, marginVertical: 5 }}
|
||||
>
|
||||
<Grid>
|
||||
<Grid.Col span={8}>
|
||||
<StackCustom gap={"xs"}>
|
||||
<TextCustom bold truncate>
|
||||
{item?.username || "-"}
|
||||
</TextCustom>
|
||||
<TextCustom size={"small"} bold truncate color="gray">
|
||||
{item?.nomor || "-"}
|
||||
</TextCustom>
|
||||
</StackCustom>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={4} style={{ alignItems: "flex-end" }}>
|
||||
<CenterCustom>
|
||||
{item?.active ? (
|
||||
<BadgeCustom color="green">Aktif</BadgeCustom>
|
||||
) : (
|
||||
<BadgeCustom color="red">Tidak Aktif</BadgeCustom>
|
||||
)}
|
||||
</CenterCustom>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</AdminBasicBox>
|
||||
);
|
||||
|
||||
return (
|
||||
<NewWrapper
|
||||
headerComponent={
|
||||
<AdminComp_BoxTitle
|
||||
title="User Access"
|
||||
rightComponent={rightComponent()}
|
||||
/>
|
||||
}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={pagination.refreshing}
|
||||
onRefresh={pagination.onRefresh}
|
||||
tintColor={MainColor.yellow}
|
||||
colors={[MainColor.yellow]}
|
||||
/>
|
||||
}
|
||||
renderItem={renderItem}
|
||||
listData={pagination.listData}
|
||||
onEndReached={pagination.loadMore}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
hideFooter
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user