Compare commits
6 Commits
fixed-admi
...
fixed-admi
| Author | SHA1 | Date | |
|---|---|---|---|
| 97e1f50660 | |||
| 1cbe4ab330 | |||
| 42fa80c228 | |||
| fb697366fe | |||
| 6d71c3a86f | |||
| e030b8f486 |
@@ -77,14 +77,14 @@ export default function Application() {
|
||||
);
|
||||
}
|
||||
|
||||
// if (data && data?.masterUserRoleId !== "1") {
|
||||
// console.log("User is not admin");
|
||||
// return (
|
||||
// <BasicWrapper>
|
||||
// <Redirect href={`/admin/dashboard`} />
|
||||
// </BasicWrapper>
|
||||
// );
|
||||
// }
|
||||
if (data && data?.masterUserRoleId !== "1") {
|
||||
console.log("User is not admin");
|
||||
return (
|
||||
<BasicWrapper>
|
||||
<Redirect href={`/admin/dashboard`} />
|
||||
</BasicWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -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,
|
||||
@@ -145,23 +146,32 @@ export default function AdminLayout() {
|
||||
style={{ alignSelf: "flex-end" }}
|
||||
/>
|
||||
|
||||
{/* <NavbarMenu
|
||||
<NavbarMenu
|
||||
items={
|
||||
user?.masterUserRoleId === "2"
|
||||
? adminListMenu
|
||||
: superAdminListMenu
|
||||
}
|
||||
onClose={() => setOpenDrawerNavbar(false)}
|
||||
/> */}
|
||||
/>
|
||||
|
||||
<NavbarMenu_V2
|
||||
{/* <NavbarMenu_V2
|
||||
items={
|
||||
user?.masterUserRoleId === "2"
|
||||
? adminListMenu_V2
|
||||
: superAdminListMenu_V2
|
||||
}
|
||||
onClose={() => setOpenDrawerNavbar(false)}
|
||||
/>
|
||||
/> */}
|
||||
|
||||
{/* <NavbarMenu_V3
|
||||
items={
|
||||
user?.masterUserRoleId === "2"
|
||||
? adminListMenu_V2
|
||||
: superAdminListMenu_V2
|
||||
}
|
||||
onClose={() => setOpenDrawerNavbar(false)}
|
||||
/> */}
|
||||
</StackCustom>
|
||||
</DrawerAdmin>
|
||||
|
||||
|
||||
@@ -1,135 +1,5 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
ActionIcon,
|
||||
BaseBox,
|
||||
CenterCustom,
|
||||
LoaderCustom,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import { IconEdit } from "@/components/_Icon";
|
||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
||||
import { GridSpan_NewComponent } from "@/components/_ShareComponent/GridSpan_NewComponent";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { apiAdminMasterBusinessFieldById } from "@/service/api-admin/api-master-admin";
|
||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import { useCallback, useState } from "react";
|
||||
import { Divider } from "react-native-paper";
|
||||
import { Admin_ScreenBusinessFieldDetail } from "@/screens/Admin/App-Information/ScreenBusinessFieldDetail";
|
||||
|
||||
export default function AdminAppInformation_BusinessFieldDetail() {
|
||||
const { id } = useLocalSearchParams();
|
||||
const [data, setData] = useState<any | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadDetail();
|
||||
}, [id])
|
||||
);
|
||||
|
||||
const onLoadDetail = async () => {
|
||||
try {
|
||||
const response = await apiAdminMasterBusinessFieldById({
|
||||
id: id as string,
|
||||
category: "all",
|
||||
});
|
||||
|
||||
console.log("Response >>", JSON.stringify(response, null, 2));
|
||||
|
||||
setData(response.data);
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
setData(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper>
|
||||
<StackCustom>
|
||||
<AdminBackButtonAntTitle title="Detail Bidang & Sub Bidang" />
|
||||
|
||||
{!data ? (
|
||||
<LoaderCustom />
|
||||
) : (
|
||||
<StackCustom gap={"xs"}>
|
||||
<TextCustom bold>Nama Bidang</TextCustom>
|
||||
<Spacing height={5} />
|
||||
<BaseBox>
|
||||
<StackCustom gap={"xs"}>
|
||||
<TextCustom bold>
|
||||
Status: {data?.bidang?.active ? "Aktif" : "Tidak Aktif"}
|
||||
</TextCustom>
|
||||
<GridSpan_NewComponent
|
||||
span1={10}
|
||||
span2={2}
|
||||
text1={
|
||||
<TextCustom bold size={"large"}>
|
||||
{data?.bidang?.name}
|
||||
</TextCustom>
|
||||
}
|
||||
text2={
|
||||
<CenterCustom>
|
||||
<ActionIcon
|
||||
icon={<IconEdit size={16} color={MainColor.black} />}
|
||||
onPress={() =>
|
||||
router.push(
|
||||
`/admin/app-information/business-field/${id}/bidang-update`
|
||||
)
|
||||
}
|
||||
/>
|
||||
</CenterCustom>
|
||||
}
|
||||
/>
|
||||
</StackCustom>
|
||||
</BaseBox>
|
||||
{/* <Divider /> */}
|
||||
<Spacing height={5} />
|
||||
|
||||
<TextCustom bold>Sub Bidang Bisnis</TextCustom>
|
||||
<Spacing height={5} />
|
||||
|
||||
{data?.subBidang?.map((item: any, index: number) => (
|
||||
<BaseBox key={index}>
|
||||
<StackCustom gap={0}>
|
||||
<TextCustom bold>
|
||||
Status: {item?.isActive ? "Aktif" : "Tidak Aktif"}
|
||||
</TextCustom>
|
||||
|
||||
<GridSpan_NewComponent
|
||||
span1={10}
|
||||
span2={2}
|
||||
text1={
|
||||
<TextCustom bold size={"large"}>
|
||||
{item.name}
|
||||
</TextCustom>
|
||||
}
|
||||
text2={
|
||||
<CenterCustom>
|
||||
<ActionIcon
|
||||
icon={
|
||||
<IconEdit size={16} color={MainColor.black} />
|
||||
}
|
||||
onPress={() =>
|
||||
router.push(
|
||||
`/admin/app-information/business-field/${item?.id}/sub-bidang-update`
|
||||
)
|
||||
}
|
||||
/>
|
||||
</CenterCustom>
|
||||
}
|
||||
/>
|
||||
</StackCustom>
|
||||
</BaseBox>
|
||||
))}
|
||||
</StackCustom>
|
||||
)}
|
||||
|
||||
{/* <TextCustom>{JSON.stringify(data, null, 2)}</TextCustom> */}
|
||||
</StackCustom>
|
||||
</ViewWrapper>
|
||||
</>
|
||||
);
|
||||
return <Admin_ScreenBusinessFieldDetail />;
|
||||
}
|
||||
|
||||
@@ -1,86 +1,5 @@
|
||||
import { ScrollableCustom, StackCustom, ViewWrapper } from "@/components";
|
||||
import AdminActionIconPlus from "@/components/_ShareComponent/Admin/ActionIconPlus";
|
||||
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
||||
import AdminAppInformation_BusinessFieldSection from "@/screens/Admin/App-Information/BusinessFieldSection";
|
||||
import AdminAppInformation_Bank from "@/screens/Admin/App-Information/InformationBankSection";
|
||||
import AdminAppInformation_StickerSection from "@/screens/Admin/App-Information/StickerSection";
|
||||
import { router } from "expo-router";
|
||||
import { useState } from "react";
|
||||
import { Alert } from "react-native";
|
||||
import { Admin_ScreenAppInformation } from "@/screens/Admin/App-Information/ScreenAppInformation";
|
||||
|
||||
export default function AdminInformation() {
|
||||
const [activeCategory, setActiveCategory] = useState<string | null>("bank");
|
||||
const [activePage, setActivePage] = useState<string>("Informasi Bank");
|
||||
|
||||
const handlePress = (item: any) => {
|
||||
setActiveCategory(item.value);
|
||||
setActivePage(item.label);
|
||||
// tambahkan logika lain seperti filter dsb.
|
||||
};
|
||||
|
||||
const scrollComponent = (
|
||||
<StackCustom>
|
||||
<ScrollableCustom
|
||||
data={listPage}
|
||||
onButtonPress={handlePress}
|
||||
activeId={activeCategory as any}
|
||||
/>
|
||||
</StackCustom>
|
||||
);
|
||||
|
||||
const renderContent = () => {
|
||||
switch (activeCategory) {
|
||||
case "bank":
|
||||
return <AdminAppInformation_Bank />;
|
||||
case "business":
|
||||
return <AdminAppInformation_BusinessFieldSection />;
|
||||
case "sticker":
|
||||
return <AdminAppInformation_StickerSection />;
|
||||
default:
|
||||
return <AdminAppInformation_Bank />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper headerComponent={scrollComponent}>
|
||||
<AdminComp_BoxTitle
|
||||
title={activePage}
|
||||
rightComponent={
|
||||
<AdminActionIconPlus
|
||||
onPress={() => {
|
||||
if (activeCategory === "bank") {
|
||||
router.push("/admin/app-information/information-bank/create");
|
||||
} else if (activeCategory === "business") {
|
||||
router.push("/admin/app-information/business-field/create");
|
||||
} else if (activeCategory === "sticker") {
|
||||
Alert.alert("Coming Soon", "Next Update");
|
||||
// router.push("/admin/app-information/sticker/create");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{renderContent()}
|
||||
</ViewWrapper>
|
||||
</>
|
||||
);
|
||||
return <Admin_ScreenAppInformation />;
|
||||
}
|
||||
|
||||
const listPage = [
|
||||
{
|
||||
id: "1",
|
||||
label: "Informasi Bank",
|
||||
value: "bank",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
label: "Bidang & Sub Bidang",
|
||||
value: "business",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
label: "Stiker",
|
||||
value: "sticker",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -197,7 +197,7 @@ export default function AdminEventDetail() {
|
||||
/>
|
||||
)}
|
||||
|
||||
<TextCustom align="center">{isDevLink}</TextCustom>
|
||||
{/* <TextCustom align="center">{isDevLink}</TextCustom> */}
|
||||
</StackCustom>
|
||||
</BaseBox>
|
||||
)}
|
||||
|
||||
@@ -1,105 +1,5 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
BadgeCustom,
|
||||
BaseBox,
|
||||
Grid,
|
||||
LoaderCustom,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
||||
import { apiAdminEventListOfParticipants } from "@/service/api-admin/api-admin-event";
|
||||
import dayjs, { Dayjs } from "dayjs";
|
||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { View } from "moti";
|
||||
import { useCallback, useState } from "react";
|
||||
import { Admin_ScreenEventListOfParticipants } from "@/screens/Admin/Event/ScreenEventListOfParticipants";
|
||||
|
||||
export default function AdminEventListOfParticipants() {
|
||||
const { id } = useLocalSearchParams();
|
||||
const [listData, setListData] = useState<any[] | null>(null);
|
||||
const [loadData, setLoadData] = useState(false);
|
||||
const [startDate, setStartDate] = useState<Dayjs | undefined>();
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [id])
|
||||
);
|
||||
|
||||
const onLoadData = async () => {
|
||||
try {
|
||||
setLoadData(true);
|
||||
const response = await apiAdminEventListOfParticipants({
|
||||
id: id as string,
|
||||
});
|
||||
|
||||
console.log("[DATA]", JSON.stringify(response, null, 2));
|
||||
|
||||
if (response.success) {
|
||||
setListData(response.data);
|
||||
setStartDate(dayjs(response.data.Event.tanggal));
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
} finally {
|
||||
setLoadData(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper
|
||||
headerComponent={<AdminBackButtonAntTitle title="Daftar Peserta" />}
|
||||
>
|
||||
{loadData ? (
|
||||
<LoaderCustom />
|
||||
) : _.isEmpty(listData) ? (
|
||||
<TextCustom align="center" color="gray">
|
||||
Belum ada peserta
|
||||
</TextCustom>
|
||||
) : (
|
||||
listData?.map((item: any, index: number) => (
|
||||
<BaseBox key={index}>
|
||||
<Grid>
|
||||
<Grid.Col span={6}>
|
||||
<StackCustom gap={"sm"}>
|
||||
<TextCustom bold truncate>
|
||||
{item?.User?.username}
|
||||
</TextCustom>
|
||||
<TextCustom>+{item?.User?.nomor}</TextCustom>
|
||||
</StackCustom>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6} style={{ justifyContent: "center" }}>
|
||||
{startDate &&
|
||||
startDate.subtract(1, "hour").diff(dayjs()) < 0 ? (
|
||||
<BadgeCustom
|
||||
style={{ alignSelf: "flex-end" }}
|
||||
color={item?.isPresent ? "green" : "red"}
|
||||
>
|
||||
{item?.isPresent ? "Hadir" : "Tidak Hadir"}
|
||||
</BadgeCustom>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "flex-end",
|
||||
}}
|
||||
>
|
||||
<BadgeCustom
|
||||
style={{ alignSelf: "flex-end" }}
|
||||
color="gray"
|
||||
>
|
||||
-
|
||||
</BadgeCustom>
|
||||
</View>
|
||||
)}
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</BaseBox>
|
||||
))
|
||||
)}
|
||||
</ViewWrapper>
|
||||
</>
|
||||
);
|
||||
return <Admin_ScreenEventListOfParticipants />;
|
||||
}
|
||||
|
||||
@@ -1,135 +1,5 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
ActionIcon,
|
||||
ClickableCustom,
|
||||
LoaderCustom,
|
||||
SearchInput,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
||||
import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle";
|
||||
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
|
||||
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
|
||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
||||
import { apiAdminEvent } from "@/service/api-admin/api-admin-event";
|
||||
import { dateTimeView } from "@/utils/dateTimeView";
|
||||
import { Octicons } from "@expo/vector-icons";
|
||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
import { Divider } from "react-native-paper";
|
||||
import { Admin_ScreenEventStatus } from "@/screens/Admin/Event/ScreenEventStatus";
|
||||
|
||||
export default function AdminEventStatus() {
|
||||
const { status } = useLocalSearchParams();
|
||||
console.log("[STATUS EVENT]", status);
|
||||
|
||||
const [listData, setListData] = useState<any[] | null>(null);
|
||||
const [loadData, setLoadData] = useState(false);
|
||||
const [search, setSearch] = useState<string>("");
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [status, search])
|
||||
);
|
||||
|
||||
const onLoadData = async () => {
|
||||
try {
|
||||
setLoadData(true);
|
||||
const response = await apiAdminEvent({
|
||||
category: status as "publish" | "review" | "reject" | "history" as any,
|
||||
search,
|
||||
});
|
||||
|
||||
console.log(
|
||||
`[RES LIST BY STATUS: ${status}]`,
|
||||
JSON.stringify(response, null, 2)
|
||||
);
|
||||
|
||||
if (response.success) {
|
||||
setListData(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
} finally {
|
||||
setLoadData(false);
|
||||
}
|
||||
};
|
||||
|
||||
const rightComponent = (
|
||||
<SearchInput
|
||||
containerStyle={{ width: "100%", marginBottom: 0 }}
|
||||
placeholder="Cari"
|
||||
value={search}
|
||||
onChangeText={(value) => setSearch(value)}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper headerComponent={<AdminTitlePage title="Event" />}>
|
||||
<AdminComp_BoxTitle
|
||||
title={`${_.startCase(status as string)}`}
|
||||
rightComponent={rightComponent}
|
||||
/>
|
||||
|
||||
<StackCustom gap={"sm"}>
|
||||
<AdminTitleTable
|
||||
title1="Username"
|
||||
title2="Tanggal"
|
||||
title3="Judul Event"
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
{loadData ? (
|
||||
<LoaderCustom />
|
||||
) : _.isEmpty(listData) ? (
|
||||
<TextCustom align="center" size="small" color="gray">
|
||||
Belum ada data
|
||||
</TextCustom>
|
||||
) : (
|
||||
listData?.map((item, index) => (
|
||||
<ClickableCustom
|
||||
key={index}
|
||||
onPress={() => {
|
||||
router.push(`/admin/event/${item.id}/${status}`);
|
||||
}}
|
||||
>
|
||||
<AdminTableValue
|
||||
key={index}
|
||||
value1={
|
||||
<TextCustom truncate={1}>
|
||||
{item?.Author?.username || "-"}
|
||||
</TextCustom>
|
||||
// <ActionIcon
|
||||
// icon={
|
||||
// <Octicons
|
||||
// name="eye"
|
||||
// size={ICON_SIZE_BUTTON}
|
||||
// color="black"
|
||||
// />
|
||||
// }
|
||||
// onPress={() => {
|
||||
// router.push(`/admin/event/${item.id}/${status}`);
|
||||
// }}
|
||||
// />
|
||||
}
|
||||
value2={
|
||||
<TextCustom truncate={1}>
|
||||
{dateTimeView({ date: item?.tanggal })}
|
||||
</TextCustom>
|
||||
}
|
||||
value3={
|
||||
<TextCustom truncate={2}>{item?.title || "-"}</TextCustom>
|
||||
}
|
||||
/>
|
||||
<Divider/>
|
||||
</ClickableCustom>
|
||||
))
|
||||
)}
|
||||
</StackCustom>
|
||||
</ViewWrapper>
|
||||
</>
|
||||
);
|
||||
return <Admin_ScreenEventStatus />;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
BaseBox,
|
||||
DummyLandscapeImage,
|
||||
Grid,
|
||||
NewWrapper,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
@@ -120,7 +121,7 @@ export default function AdminJobDetailStatus() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper
|
||||
<NewWrapper
|
||||
headerComponent={<AdminBackButtonAntTitle title={`Detail Data`} />}
|
||||
>
|
||||
<BaseBox>
|
||||
@@ -184,7 +185,7 @@ export default function AdminJobDetailStatus() {
|
||||
/>
|
||||
)}
|
||||
<Spacing />
|
||||
</ViewWrapper>
|
||||
</NewWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import {
|
||||
AlertDefaultSystem,
|
||||
BoxButtonOnFooter,
|
||||
NewWrapper,
|
||||
TextAreaCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
@@ -100,7 +101,7 @@ export default function AdminJobRejectInput() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper
|
||||
<NewWrapper
|
||||
footerComponent={buttonSubmit}
|
||||
headerComponent={<AdminBackButtonAntTitle title="Penolakan Job" />}
|
||||
>
|
||||
@@ -112,7 +113,7 @@ export default function AdminJobRejectInput() {
|
||||
showCount
|
||||
maxLength={1000}
|
||||
/>
|
||||
</ViewWrapper>
|
||||
</NewWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,117 +1,5 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
ActionIcon,
|
||||
LoaderCustom,
|
||||
SearchInput,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
ViewWrapper
|
||||
} from "@/components";
|
||||
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
||||
import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle";
|
||||
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
|
||||
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
|
||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
||||
import { apiAdminJob } from "@/service/api-admin/api-admin-job";
|
||||
import { Octicons } from "@expo/vector-icons";
|
||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
import { Divider } from "react-native-paper";
|
||||
import { Admin_ScreenJobStatus } from "@/screens/Admin/Job/ScreenJobStatus";
|
||||
|
||||
export default function AdminJobStatus() {
|
||||
const { status } = useLocalSearchParams();
|
||||
const [list, setList] = useState<any | null>(null);
|
||||
const [loadList, setLoadList] = useState(false);
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
handlerLoadList();
|
||||
}, [status, search])
|
||||
);
|
||||
|
||||
const handlerLoadList = async () => {
|
||||
try {
|
||||
setLoadList(true);
|
||||
const response = await apiAdminJob({
|
||||
category: status as "publish" | "review" | "reject",
|
||||
search,
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
setList(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
} finally {
|
||||
setLoadList(false);
|
||||
}
|
||||
};
|
||||
|
||||
const rightComponent = (
|
||||
<SearchInput
|
||||
placeholder="Cari"
|
||||
onChangeText={setSearch}
|
||||
value={search}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper headerComponent={<AdminTitlePage title="Job Vacancy" />}>
|
||||
<AdminComp_BoxTitle
|
||||
title={`${_.startCase(status as string)}`}
|
||||
rightComponent={rightComponent}
|
||||
/>
|
||||
|
||||
<StackCustom>
|
||||
<AdminTitleTable
|
||||
title1="Aksi"
|
||||
title2="Username"
|
||||
title3="Judul Pekerjaan"
|
||||
/>
|
||||
{/* <Spacing /> */}
|
||||
<Divider />
|
||||
|
||||
{loadList ? (
|
||||
<LoaderCustom />
|
||||
) : _.isEmpty(list) ? (
|
||||
<TextCustom align="center" color="gray">
|
||||
Tidak ada data
|
||||
</TextCustom>
|
||||
) : (
|
||||
list?.map((item: any, index: number) => (
|
||||
<AdminTableValue
|
||||
key={index}
|
||||
value1={
|
||||
<ActionIcon
|
||||
icon={
|
||||
<Octicons
|
||||
name="eye"
|
||||
size={ICON_SIZE_BUTTON}
|
||||
color="black"
|
||||
/>
|
||||
}
|
||||
onPress={() => {
|
||||
router.push(`/admin/job/${item.id}/${status}`);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
value2={
|
||||
<TextCustom align="center" truncate={1}>
|
||||
{item?.Author?.username || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
value3={
|
||||
<TextCustom truncate={2} align="center">
|
||||
{item?.title || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</StackCustom>
|
||||
</ViewWrapper>
|
||||
</>
|
||||
);
|
||||
return <Admin_ScreenJobStatus />;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) => (
|
||||
<MenuItem
|
||||
key={item.label}
|
||||
item={item}
|
||||
onClose={onClose}
|
||||
currentPath={normalizedPathname}
|
||||
isOpen={openKeys.includes(item.label)}
|
||||
toggleOpen={() => toggleOpen(item.label)}
|
||||
/>
|
||||
))
|
||||
: null}
|
||||
{items && items.length > 0 ? (
|
||||
items.map((item) => (
|
||||
<MenuItem
|
||||
key={item.label}
|
||||
item={item}
|
||||
onClose={onClose}
|
||||
currentPath={normalizedPathname}
|
||||
isOpen={openKeys.includes(item.label)}
|
||||
toggleOpen={() => toggleOpen(item.label)}
|
||||
/>
|
||||
))
|
||||
) : null}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
@@ -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 (
|
||||
<View>
|
||||
{/* 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 (
|
||||
<TouchableOpacity
|
||||
key={index}
|
||||
@@ -567,4 +547,4 @@ const styles = StyleSheet.create({
|
||||
fontSize: 16,
|
||||
fontWeight: "500",
|
||||
},
|
||||
});
|
||||
});
|
||||
871
components/Drawer/NavbarMenu_V3.tsx
Normal file
871
components/Drawer/NavbarMenu_V3.tsx
Normal file
@@ -0,0 +1,871 @@
|
||||
import { AccentColor, MainColor } from "@/constants/color-palet";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { router, usePathname } from "expo-router";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
Animated,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from "react-native";
|
||||
|
||||
export interface NavbarItem_V3 {
|
||||
label: string;
|
||||
icon?: keyof typeof Ionicons.glyphMap;
|
||||
color?: string;
|
||||
link?: string;
|
||||
links?: {
|
||||
label: string;
|
||||
link: string;
|
||||
detailPattern?: string; // NEW: Pattern untuk match detail pages
|
||||
}[];
|
||||
initiallyOpened?: boolean;
|
||||
}
|
||||
|
||||
interface NavbarMenuProps {
|
||||
items: NavbarItem_V3[];
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export default function NavbarMenu_V3({ items, onClose }: NavbarMenuProps) {
|
||||
const pathname = usePathname();
|
||||
const [openKeys, setOpenKeys] = useState<string[]>([]);
|
||||
|
||||
// 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<string, { parentLabel: string, longestPathLength: number }>();
|
||||
|
||||
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 (
|
||||
<View
|
||||
style={{
|
||||
marginBottom: 20,
|
||||
}}
|
||||
>
|
||||
<ScrollView
|
||||
contentContainerStyle={{
|
||||
paddingVertical: 10,
|
||||
}}
|
||||
>
|
||||
{items && items.length > 0 ? (
|
||||
items.map((item) => (
|
||||
<MenuItem
|
||||
key={item.label}
|
||||
item={item}
|
||||
items={items}
|
||||
onClose={onClose}
|
||||
currentPath={normalizedPathname}
|
||||
isOpen={openKeys.includes(item.label)}
|
||||
toggleOpen={() => toggleOpen(item.label)}
|
||||
/>
|
||||
))
|
||||
) : null}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<View>
|
||||
{/* Parent Item */}
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.parentItem,
|
||||
isActuallyRelevant && styles.parentItemActive,
|
||||
]}
|
||||
onPress={toggleOpen}
|
||||
>
|
||||
<Ionicons
|
||||
name={item.icon}
|
||||
size={16}
|
||||
color={isActuallyRelevant ? MainColor.yellow : MainColor.white}
|
||||
style={styles.icon}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
styles.parentText,
|
||||
isActuallyRelevant && { color: MainColor.yellow },
|
||||
]}
|
||||
>
|
||||
{item.label}
|
||||
</Text>
|
||||
<Ionicons
|
||||
name={isOpen ? "chevron-up" : "chevron-down"}
|
||||
size={16}
|
||||
color={isActuallyRelevant ? MainColor.yellow : MainColor.white}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Submenu (Animated) */}
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.submenu,
|
||||
{
|
||||
height: animatedHeight,
|
||||
opacity: animatedHeight.interpolate({
|
||||
inputRange: [0, item.links.length * 44],
|
||||
outputRange: [0, 1],
|
||||
extrapolate: "clamp",
|
||||
}),
|
||||
},
|
||||
]}
|
||||
>
|
||||
{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 (
|
||||
<TouchableOpacity
|
||||
key={index}
|
||||
style={[styles.subItem, shouldHighlight && styles.subItemActive]}
|
||||
onPress={() => {
|
||||
onClose?.();
|
||||
router.push(subItem.link as any);
|
||||
}}
|
||||
>
|
||||
<Ionicons
|
||||
name="radio-button-on-outline"
|
||||
size={16}
|
||||
color={shouldHighlight ? MainColor.yellow : MainColor.white}
|
||||
style={styles.icon}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
styles.subText,
|
||||
shouldHighlight && { color: MainColor.yellow },
|
||||
]}
|
||||
>
|
||||
{subItem.label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</Animated.View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// Menu tanpa submenu
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[styles.singleItem, isActive && styles.singleItemActive]}
|
||||
onPress={() => {
|
||||
onClose?.();
|
||||
router.push(item.link as any);
|
||||
}}
|
||||
>
|
||||
<Ionicons
|
||||
name={item.icon}
|
||||
size={16}
|
||||
color={isActive ? MainColor.yellow : MainColor.white}
|
||||
style={styles.icon}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
styles.singleText,
|
||||
{ color: isActive ? MainColor.yellow : MainColor.white },
|
||||
]}
|
||||
>
|
||||
{item.label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
// 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",
|
||||
},
|
||||
});
|
||||
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,
|
||||
paddingBlock: 5,
|
||||
paddingInline: 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> */}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
177
docs/admin-folder-structure.md
Normal file
177
docs/admin-folder-structure.md
Normal file
@@ -0,0 +1,177 @@
|
||||
# Struktur Folder Admin Aplikasi HIPMI Mobile
|
||||
|
||||
Dokumen ini menjelaskan struktur folder dan file untuk bagian admin dari aplikasi HIPMI Mobile yang terletak di `app/(application)/admin`.
|
||||
|
||||
## File dan Folder Tingkat Atas
|
||||
|
||||
### Folder
|
||||
- `app-information` - Manajemen informasi aplikasi
|
||||
- `collaboration` - Manajemen modul kolaborasi
|
||||
- `donation` - Manajemen modul donasi
|
||||
- `event` - Manajemen modul acara
|
||||
- `forum` - Manajemen modul forum
|
||||
- `investment` - Manajemen modul investasi
|
||||
- `job` - Manajemen modul lowongan kerja
|
||||
- `notification` - Manajemen notifikasi
|
||||
- `super-admin` - Fungsi super admin
|
||||
- `user-access` - Manajemen akses pengguna
|
||||
- `voting` - Manajemen modul voting
|
||||
|
||||
### File
|
||||
- `_layout.tsx` - Komponen tata letak untuk bagian admin
|
||||
- `dashboard.tsx` - Tampilan dasbor admin
|
||||
- `maps.tsx` - Fungsionalitas peta untuk admin
|
||||
|
||||
## Struktur Folder Terperinci
|
||||
|
||||
### app-information/
|
||||
```
|
||||
app-information/
|
||||
├── business-field/
|
||||
│ ├── [id]/
|
||||
│ │ ├── bidang-update.tsx
|
||||
│ │ ├── index.tsx
|
||||
│ │ └── sub-bidang-update.tsx
|
||||
│ └── create.tsx
|
||||
├── information-bank/
|
||||
│ ├── [id]/
|
||||
│ │ └── index.tsx
|
||||
│ └── create.tsx
|
||||
├── sticker/
|
||||
│ ├── [id]/
|
||||
│ │ └── index.tsx
|
||||
│ └── create.tsx
|
||||
└── index.tsx
|
||||
```
|
||||
|
||||
### collaboration/
|
||||
```
|
||||
collaboration/
|
||||
├── [id]/
|
||||
│ ├── [status].tsx
|
||||
│ ├── group.tsx
|
||||
│ └── reject-input.tsx
|
||||
├── group.tsx
|
||||
├── index.tsx
|
||||
├── publish.tsx
|
||||
└── reject.tsx
|
||||
```
|
||||
|
||||
### donation/
|
||||
```
|
||||
donation/
|
||||
├── [id]/
|
||||
│ ├── [status]/
|
||||
│ │ ├── index.tsx
|
||||
│ │ └── transaction-detail.tsx
|
||||
│ ├── detail-disbursement-of-funds.tsx
|
||||
│ ├── disbursement-of-funds.tsx
|
||||
│ ├── list-disbursement-of-funds.tsx
|
||||
│ ├── list-of-donatur.tsx
|
||||
│ └── reject-input.tsx
|
||||
├── [status]/
|
||||
│ └── status.tsx
|
||||
├── category-create.tsx
|
||||
├── category-update.tsx
|
||||
├── category.tsx
|
||||
└── index.tsx
|
||||
```
|
||||
|
||||
### event/
|
||||
```
|
||||
event/
|
||||
├── [id]/
|
||||
│ ├── [status]/
|
||||
│ │ └── index.tsx
|
||||
│ ├── list-of-participants.tsx
|
||||
│ └── reject-input.tsx
|
||||
├── [status]/
|
||||
│ └── status.tsx
|
||||
├── index.tsx
|
||||
├── type-create.tsx
|
||||
├── type-of-event.tsx
|
||||
└── type-update.tsx
|
||||
```
|
||||
|
||||
### forum/
|
||||
```
|
||||
forum/
|
||||
├── [id]/
|
||||
│ ├── index.tsx
|
||||
│ ├── list-comment.tsx
|
||||
│ ├── list-report-comment.tsx
|
||||
│ └── list-report-posting.tsx
|
||||
├── index.tsx
|
||||
├── posting.tsx
|
||||
├── report-comment.tsx
|
||||
└── report-posting.tsx
|
||||
```
|
||||
|
||||
### investment/
|
||||
```
|
||||
investment/
|
||||
├── [id]/
|
||||
│ ├── [status]/
|
||||
│ │ ├── index.tsx
|
||||
│ │ └── transaction-detail.tsx
|
||||
│ ├── list-of-investor.tsx
|
||||
│ └── reject-input.tsx
|
||||
├── [status]/
|
||||
│ └── status.tsx
|
||||
└── index.tsx
|
||||
```
|
||||
|
||||
### job/
|
||||
```
|
||||
job/
|
||||
├── [id]/
|
||||
│ ├── [status]/
|
||||
│ │ ├── index.tsx
|
||||
│ │ └── reject-input.tsx
|
||||
├── [status]/
|
||||
│ └── status.tsx
|
||||
└── index.tsx
|
||||
```
|
||||
|
||||
### notification/
|
||||
```
|
||||
notification/
|
||||
└── index.tsx
|
||||
```
|
||||
|
||||
### super-admin/
|
||||
```
|
||||
super-admin/
|
||||
├── [id]/
|
||||
│ └── index.tsx
|
||||
└── index.tsx
|
||||
```
|
||||
|
||||
### user-access/
|
||||
```
|
||||
user-access/
|
||||
├── [id]/
|
||||
│ └── index.tsx
|
||||
└── index.tsx
|
||||
```
|
||||
|
||||
### voting/
|
||||
```
|
||||
voting/
|
||||
├── [id]/
|
||||
│ ├── [status]/
|
||||
│ │ ├── index.tsx
|
||||
│ │ └── reject-input.tsx
|
||||
├── [status]/
|
||||
│ └── status.tsx
|
||||
├── history.tsx
|
||||
└── index.tsx
|
||||
```
|
||||
|
||||
## Rute Dinamis
|
||||
|
||||
Bagian admin menggunakan rute dinamis yang ditunjukkan dengan kurung siku `[ ]`:
|
||||
- `[id]` - Rute dinamis untuk ID item tertentu
|
||||
- `[status]` - Rute dinamis untuk tampilan berdasarkan status
|
||||
|
||||
Ini memungkinkan routing yang fleksibel berdasarkan parameter tertentu seperti ID item atau status.
|
||||
@@ -22,8 +22,8 @@ Jika tidak ada props page maka tambahkan props page dan default page: "1"
|
||||
Gunakan bahasa indonesia pada cli agar saya mudah membacanya.
|
||||
|
||||
<!-- Additional Prompt -->
|
||||
File refrensi: screens/Donation/ScreenListOfNews.tsx
|
||||
Anda bisa menggunakan refrensi dari "File refrensi" jika butuh pemahaman dengan tipe fitur yang sama
|
||||
File refrensi: screens/Admin/Event/ScreenEventStatus.tsx
|
||||
Anda bisa menggunakan refrensi dari "File refrensi" jika butuh pemahaman dengan tipe fitur yang hampir sama
|
||||
|
||||
<!-- ===================== End Penerapan Pagination ` ===================== -->
|
||||
|
||||
@@ -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,51 +53,54 @@ 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 ?
|
||||
|
||||
Gunakan bahasa indonesia pada cli agar saya mudah membacanya.eclar
|
||||
<!-- End Random Prompt -->
|
||||
|
||||
<!-- START Prompt Admin Refactoring -->
|
||||
<!-- Pindah kode ke Screen Component -->
|
||||
File source: app/(application)/admin/event/[id]/list-of-participants.tsx
|
||||
Folder tujuan: screens/Admin/Event
|
||||
Nama file utama: ScreenEventListOfParticipants.tsx
|
||||
Nama function utama: Admin_ScreenEventListOfParticipants
|
||||
File komponen wrapper: components/_ShareComponent/NewWrapper.tsx
|
||||
|
||||
export interface NavbarItem_V2 {
|
||||
label: string;
|
||||
icon?: keyof typeof Ionicons.glyphMap;
|
||||
color?: string;
|
||||
link?: string;
|
||||
links?: {
|
||||
label: string;
|
||||
link: string;
|
||||
detailPattern?: string;
|
||||
}[];
|
||||
initiallyOpened?: boolean;
|
||||
}
|
||||
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
|
||||
|
||||
|
||||
<!-- Penerapan Pagination -->
|
||||
Function fecth: apiAdminEventListOfParticipants
|
||||
File function fetch: service/api-admin/api-admin-event.ts
|
||||
|
||||
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" ( string )
|
||||
Kemudian rapikan code nya pisah komponen seperti render item dan lainnya agar lebih rapi dan di dalam return panggil komponen tersebut
|
||||
|
||||
Gunakan bahasa indonesia pada cli agar saya mudah membacanya.
|
||||
<!-- END Prompt Admin Refactoring -->
|
||||
|
||||
<!-- Use Prompt Now -->
|
||||
Terapkan NewWrapper pada file: screens/Admin/App-Information/InformationBankSection.tsx
|
||||
Component yang digunakan: components/_ShareComponent/NewWrapper.tsx
|
||||
|
||||
Function fecth: apiAdminMasterBank
|
||||
File function fetch: service/api-admin/api-master-admin.ts
|
||||
|
||||
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" ( string )
|
||||
<!-- Baru -->
|
||||
File Utama: screens/Admin/App-Information/InformationBankSection.tsx
|
||||
Terapkan FlatList dan pagination pada file "File Utama"
|
||||
Komponen pagination yang digunaka berada pada file hooks/use-pagination.tsx dan helpers/paginationHelpers.tsx
|
||||
Function fecth: apiAdminMasterBank
|
||||
File function fetch: service/api-admin/api-master-admin.ts
|
||||
Jika tidak ada props page maka tambahkan props page dan default page: "1" ( string )
|
||||
Jika butuh refrensi FlatList bisa lihat pada file components/_ShareComponent/NewWrapper.tsx
|
||||
<!-- END Use Prompt Now -->
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"expo-dev-client": "~6.0.12",
|
||||
"expo-device": "^8.0.9",
|
||||
"expo-document-picker": "~14.0.7",
|
||||
"expo-file-system": "^19.0.15",
|
||||
"expo-file-system": "^19.0.21",
|
||||
"expo-font": "~14.0.8",
|
||||
"expo-haptics": "~15.0.7",
|
||||
"expo-image": "~3.0.8",
|
||||
|
||||
@@ -1,114 +1,56 @@
|
||||
import {
|
||||
ActionIcon,
|
||||
BadgeCustom,
|
||||
CenterCustom,
|
||||
Grid,
|
||||
LoaderCustom,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
TextCustom
|
||||
} from "@/components";
|
||||
import { AccentColor } from "@/constants/color-palet";
|
||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
||||
import { apiAdminMasterBusinessField } from "@/service/api-admin/api-master-admin";
|
||||
import { FontAwesome5 } from "@expo/vector-icons";
|
||||
import { router, useFocusEffect } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { Divider } from "react-native-paper";
|
||||
import AdminBasicBox from "@/components/_ShareComponent/Admin/AdminBasicBox";
|
||||
import { router } from "expo-router";
|
||||
|
||||
export default function AdminAppInformation_BusinessFieldSection() {
|
||||
const [listData, setListData] = useState<any[] | null>(null);
|
||||
const [loadData, setLoadData] = useState(false);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadList();
|
||||
}, [])
|
||||
);
|
||||
|
||||
const onLoadList = async () => {
|
||||
try {
|
||||
setLoadData(true);
|
||||
const response = await apiAdminMasterBusinessField();
|
||||
|
||||
|
||||
if (response.success) {
|
||||
setListData(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[ERROR LIST BUSINESS FIELD]", error);
|
||||
setListData([]);
|
||||
} finally {
|
||||
setLoadData(false);
|
||||
}
|
||||
interface Bidang {
|
||||
item: {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
active: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default function AdminAppInformation_BusinessFieldSection({
|
||||
item,
|
||||
}: {
|
||||
item: any;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<StackCustom>
|
||||
<AdminBasicBox
|
||||
onPress={() =>
|
||||
router.push(`/admin/app-information/business-field/${item.item.id}`)
|
||||
}
|
||||
style={{ marginHorizontal: 10, marginVertical: 5 }}
|
||||
>
|
||||
<Grid>
|
||||
<Grid.Col span={2} style={{ alignItems: "center" }}>
|
||||
<TextCustom bold>Aksi</TextCustom>
|
||||
<Grid.Col span={8} style={{ alignSelf: "center" }}>
|
||||
<StackCustom gap={"xs"}>
|
||||
<TextCustom bold truncate>
|
||||
{item?.item?.name || "-"}
|
||||
</TextCustom>
|
||||
</StackCustom>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={4} style={{ alignItems: "center" }}>
|
||||
<TextCustom bold>Status</TextCustom>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6}>
|
||||
<TextCustom bold>Nama Bidang Bisnis</TextCustom>
|
||||
<Grid.Col span={4} style={{ alignItems: "flex-end" }}>
|
||||
<CenterCustom>
|
||||
{item?.item?.active ? (
|
||||
<BadgeCustom color="green">Aktif</BadgeCustom>
|
||||
) : (
|
||||
<BadgeCustom color="red">Tidak Aktif</BadgeCustom>
|
||||
)}
|
||||
</CenterCustom>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
|
||||
<Divider />
|
||||
|
||||
{loadData ? (
|
||||
<LoaderCustom />
|
||||
) : _.isEmpty(listData) ? (
|
||||
<TextCustom align="center">Tidak ada data</TextCustom>
|
||||
) : (
|
||||
<StackCustom>
|
||||
{listData?.map((item: any, index: number) => (
|
||||
<View key={index}>
|
||||
<Grid>
|
||||
<Grid.Col span={2} style={{ alignItems: "center" }}>
|
||||
<ActionIcon
|
||||
icon={
|
||||
<FontAwesome5
|
||||
name="edit"
|
||||
size={ICON_SIZE_BUTTON}
|
||||
color="black"
|
||||
/>
|
||||
}
|
||||
onPress={() => {
|
||||
router.push(
|
||||
`/admin/app-information/business-field/${item.id}`
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col
|
||||
span={4}
|
||||
style={{ alignItems: "center", justifyContent: "center" }}
|
||||
>
|
||||
<CenterCustom>
|
||||
<BadgeCustom
|
||||
color={
|
||||
item.active ? AccentColor.blue : AccentColor.blackgray
|
||||
}
|
||||
>
|
||||
{item.active ? "Aktif" : "Tidak Aktif"}
|
||||
</BadgeCustom>
|
||||
</CenterCustom>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6} style={{ justifyContent: "center" }}>
|
||||
<TextCustom>{item.name}</TextCustom>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</View>
|
||||
))}
|
||||
</StackCustom>
|
||||
)}
|
||||
</StackCustom>
|
||||
</AdminBasicBox>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,119 +1,59 @@
|
||||
import {
|
||||
ActionIcon,
|
||||
BadgeCustom,
|
||||
CenterCustom,
|
||||
Grid,
|
||||
LoaderCustom,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
TextCustom
|
||||
} from "@/components";
|
||||
import { AccentColor } from "@/constants/color-palet";
|
||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
||||
import { apiAdminMasterBank } from "@/service/api-admin/api-master-admin";
|
||||
import { FontAwesome5 } from "@expo/vector-icons";
|
||||
import { router, useFocusEffect } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { Divider } from "react-native-paper";
|
||||
import AdminBasicBox from "@/components/_ShareComponent/Admin/AdminBasicBox";
|
||||
import { router } from "expo-router";
|
||||
|
||||
export default function AdminAppInformation_Bank() {
|
||||
const [listData, setListData] = useState<any | null>(null);
|
||||
const [loadData, setLoadData] = useState(false);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
loadMasterBank();
|
||||
}, [])
|
||||
);
|
||||
|
||||
const loadMasterBank = async () => {
|
||||
try {
|
||||
setLoadData(true);
|
||||
const response = await apiAdminMasterBank();
|
||||
|
||||
setListData(response.data);
|
||||
} catch (error) {
|
||||
console.log("[ERROR LIST BANK]", error);
|
||||
setListData([]);
|
||||
} finally {
|
||||
setLoadData(false);
|
||||
}
|
||||
interface BankProps {
|
||||
item: {
|
||||
id: string;
|
||||
namaBank: string;
|
||||
namaAkun: string;
|
||||
norek: string;
|
||||
isActive: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
}
|
||||
export default function AdminAppInformation_Bank({
|
||||
item,
|
||||
}: {
|
||||
item: BankProps;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<StackCustom>
|
||||
<AdminBasicBox
|
||||
onPress={() =>
|
||||
router.push(`/admin/app-information/information-bank/${item.item.id}`)
|
||||
}
|
||||
style={{ marginHorizontal: 10, marginVertical: 5 }}
|
||||
>
|
||||
<Grid>
|
||||
<Grid.Col span={3}>
|
||||
<TextCustom bold align="center">
|
||||
Aksi
|
||||
</TextCustom>
|
||||
<Grid.Col span={8}>
|
||||
<StackCustom gap={"xs"}>
|
||||
<TextCustom bold truncate>
|
||||
{item?.item?.namaBank || "-"}
|
||||
</TextCustom>
|
||||
<TextCustom size={"small"} bold truncate color="gray">
|
||||
{item?.item?.norek || "-"}
|
||||
</TextCustom>
|
||||
</StackCustom>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={3}>
|
||||
<TextCustom bold align="center">
|
||||
Status
|
||||
</TextCustom>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6}>
|
||||
<TextCustom bold align="center">
|
||||
Nama Bank
|
||||
</TextCustom>
|
||||
<Grid.Col span={4} style={{ alignItems: "flex-end" }}>
|
||||
<CenterCustom>
|
||||
{item?.item?.isActive ? (
|
||||
<BadgeCustom color="green">Aktif</BadgeCustom>
|
||||
) : (
|
||||
<BadgeCustom color="red">Tidak Aktif</BadgeCustom>
|
||||
)}
|
||||
</CenterCustom>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
|
||||
<Divider />
|
||||
|
||||
{loadData ? (
|
||||
<LoaderCustom />
|
||||
) : _.isEmpty(listData) ? (
|
||||
<TextCustom align="center">Tidak ada data</TextCustom>
|
||||
) : (
|
||||
<StackCustom>
|
||||
{listData?.map((item: any, index: number) => (
|
||||
<View key={index}>
|
||||
<Grid>
|
||||
<Grid.Col span={3} style={{ alignItems: "center" }}>
|
||||
<ActionIcon
|
||||
icon={
|
||||
<FontAwesome5
|
||||
name="edit"
|
||||
size={ICON_SIZE_BUTTON}
|
||||
color="black"
|
||||
/>
|
||||
}
|
||||
onPress={() => {
|
||||
router.push(
|
||||
`/admin/app-information/information-bank/${item.id}`
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col
|
||||
span={3}
|
||||
style={{ alignItems: "center", justifyContent: "center" }}
|
||||
>
|
||||
<CenterCustom>
|
||||
<BadgeCustom
|
||||
color={
|
||||
item.isActive
|
||||
? AccentColor.blue
|
||||
: AccentColor.blackgray
|
||||
}
|
||||
>
|
||||
{item.isActive ? "Aktif" : "Tidak Aktif"}
|
||||
</BadgeCustom>
|
||||
</CenterCustom>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6} style={{ justifyContent: "center" }}>
|
||||
<TextCustom align="center">{item.namaBank}</TextCustom>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</View>
|
||||
))}
|
||||
</StackCustom>
|
||||
)}
|
||||
</StackCustom>
|
||||
</AdminBasicBox>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
153
screens/Admin/App-Information/ScreenAppInformation.tsx
Normal file
153
screens/Admin/App-Information/ScreenAppInformation.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
import { ScrollableCustom, StackCustom } from "@/components";
|
||||
import AdminActionIconPlus from "@/components/_ShareComponent/Admin/ActionIconPlus";
|
||||
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 AdminAppInformation_BusinessFieldSection from "@/screens/Admin/App-Information/BusinessFieldSection";
|
||||
import AdminAppInformation_Bank_Component from "@/screens/Admin/App-Information/InformationBankSection";
|
||||
import { apiFetchAdminMasterAppInformation } from "@/service/api-admin/api-master-admin";
|
||||
import { router, useFocusEffect } from "expo-router";
|
||||
import { useCallback, useState } from "react";
|
||||
import { Alert, RefreshControl } from "react-native";
|
||||
|
||||
export function Admin_ScreenAppInformation() {
|
||||
const [activeCategory, setActiveCategory] = useState<string | null>("bank");
|
||||
const [activePage, setActivePage] = useState<string>("Informasi Bank");
|
||||
|
||||
const pagination = usePagination({
|
||||
fetchFunction: async (page) => {
|
||||
return await apiFetchAdminMasterAppInformation({
|
||||
category: activeCategory as string,
|
||||
page: String(page),
|
||||
});
|
||||
},
|
||||
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||
dependencies: [activeCategory],
|
||||
onError: (error) => console.error("[ERROR] Fetch job by status:", error),
|
||||
});
|
||||
|
||||
const { ListEmptyComponent, ListFooterComponent } =
|
||||
createPaginationComponents({
|
||||
loading: pagination.loading,
|
||||
refreshing: pagination.refreshing,
|
||||
listData: pagination.listData,
|
||||
emptyMessage: `Tidak ada data ${activeCategory}`,
|
||||
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||
skeletonHeight: 100,
|
||||
});
|
||||
|
||||
const handlePress = (item: any) => {
|
||||
setActiveCategory(item.value);
|
||||
setActivePage(item.label);
|
||||
// tambahkan logika lain seperti filter dsb.
|
||||
};
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
pagination.onRefresh();
|
||||
}, [activeCategory]),
|
||||
);
|
||||
|
||||
const scrollComponent = (
|
||||
<StackCustom>
|
||||
<ScrollableCustom
|
||||
data={listPage.map((e, i) => ({
|
||||
id: i,
|
||||
label: e.label,
|
||||
value: e.value,
|
||||
}))}
|
||||
onButtonPress={handlePress}
|
||||
activeId={activeCategory as any}
|
||||
/>
|
||||
<AdminComp_BoxTitle
|
||||
title={activePage}
|
||||
rightComponent={
|
||||
<AdminActionIconPlus
|
||||
onPress={() => {
|
||||
if (activeCategory === "bank") {
|
||||
router.push("/admin/app-information/information-bank/create");
|
||||
} else if (activeCategory === "business") {
|
||||
router.push("/admin/app-information/business-field/create");
|
||||
} else if (activeCategory === "sticker") {
|
||||
Alert.alert("Coming Soon", "Next Update");
|
||||
// router.push("/admin/app-information/sticker/create");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</StackCustom>
|
||||
);
|
||||
|
||||
// const renderContent = () => {
|
||||
// switch (activeCategory) {
|
||||
// case "bank":
|
||||
// return <AdminAppInformation_Bank_Component />;
|
||||
// case "business":
|
||||
// return <AdminAppInformation_BusinessFieldSection />;
|
||||
// // case "sticker":
|
||||
// // return <AdminAppInformation_StickerSection />;
|
||||
// default:
|
||||
// return <AdminAppInformation_Bank_Component />;
|
||||
// }
|
||||
// };
|
||||
|
||||
const renderItem = (item: any) => {
|
||||
if (activeCategory === "bank") {
|
||||
return <AdminAppInformation_Bank_Component key={item.id} item={item} />;
|
||||
} else if (activeCategory === "business") {
|
||||
return (
|
||||
<AdminAppInformation_BusinessFieldSection key={item.id} item={item} />
|
||||
);
|
||||
} else {
|
||||
return <AdminAppInformation_Bank_Component key={item.id} item={item} />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<NewWrapper
|
||||
headerComponent={scrollComponent}
|
||||
// ListHeaderComponent={
|
||||
|
||||
// }
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
tintColor={MainColor.yellow}
|
||||
colors={[MainColor.yellow]}
|
||||
refreshing={pagination.refreshing}
|
||||
onRefresh={pagination.onRefresh}
|
||||
/>
|
||||
}
|
||||
onEndReached={pagination.loadMore}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
hideFooter
|
||||
// Data dan render
|
||||
listData={pagination.listData}
|
||||
renderItem={(item: any) => renderItem(item)}
|
||||
/>
|
||||
// {renderContent()}
|
||||
// </NewWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
const listPage = [
|
||||
{
|
||||
id: "1",
|
||||
label: "Informasi Bank",
|
||||
value: "bank",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
label: "Bidang & Sub Bidang",
|
||||
value: "business",
|
||||
},
|
||||
// {
|
||||
// id: "3",
|
||||
// label: "Stiker",
|
||||
// value: "sticker",
|
||||
// },
|
||||
];
|
||||
185
screens/Admin/App-Information/ScreenBusinessFieldDetail.tsx
Normal file
185
screens/Admin/App-Information/ScreenBusinessFieldDetail.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
BadgeCustom,
|
||||
BaseBox,
|
||||
CenterCustom,
|
||||
NewWrapper,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
} from "@/components";
|
||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
||||
import { GridSpan_NewComponent } from "@/components/_ShareComponent/GridSpan_NewComponent";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import {
|
||||
ICON_SIZE_SMALL,
|
||||
PAGINATION_DEFAULT_TAKE,
|
||||
} from "@/constants/constans-value";
|
||||
import { createPaginationComponents } from "@/helpers/paginationHelpers";
|
||||
import { usePagination } from "@/hooks/use-pagination";
|
||||
import { apiAdminMasterBusinessFieldById } from "@/service/api-admin/api-master-admin";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import { useCallback, useState } from "react";
|
||||
import { RefreshControl, View } from "react-native";
|
||||
|
||||
export function Admin_ScreenBusinessFieldDetail() {
|
||||
const { id } = useLocalSearchParams();
|
||||
const [bidang, setBidang] = useState<any | null>(null);
|
||||
|
||||
const pagination = usePagination({
|
||||
fetchFunction: async (page) => {
|
||||
return await apiAdminMasterBusinessFieldById({
|
||||
category: "only-sub-bidang",
|
||||
id: id as any,
|
||||
page: String(page),
|
||||
});
|
||||
// Pastikan mengembalikan struktur data yang sesuai dengan yang diharapkan oleh usePagination
|
||||
},
|
||||
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||
dependencies: [id],
|
||||
onError: (error) => {
|
||||
console.log("Error fetching data sub bidang", error);
|
||||
},
|
||||
});
|
||||
|
||||
const { ListEmptyComponent, ListFooterComponent } =
|
||||
createPaginationComponents({
|
||||
loading: pagination.loading,
|
||||
refreshing: pagination.refreshing,
|
||||
listData: pagination.listData,
|
||||
searchQuery: "",
|
||||
emptyMessage: "Tidak ada data pengguna",
|
||||
emptySearchMessage: "Tidak ada hasil pencarian",
|
||||
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||
skeletonHeight: 100,
|
||||
isInitialLoad: pagination.isInitialLoad,
|
||||
});
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadBidang();
|
||||
pagination.onRefresh();
|
||||
}, [id]),
|
||||
);
|
||||
|
||||
const onLoadBidang = async () => {
|
||||
try {
|
||||
const response = await apiAdminMasterBusinessFieldById({
|
||||
id: id as string,
|
||||
category: "all",
|
||||
});
|
||||
setBidang(response.data);
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
setBidang(null);
|
||||
}
|
||||
};
|
||||
const renderHeader = () => (
|
||||
<View>
|
||||
<BaseBox
|
||||
onPress={() =>
|
||||
router.push(
|
||||
`/admin/app-information/business-field/${id}/bidang-update`,
|
||||
)
|
||||
}
|
||||
>
|
||||
<StackCustom gap={"xs"}>
|
||||
<GridSpan_NewComponent
|
||||
span1={10}
|
||||
span2={2}
|
||||
text1={
|
||||
<StackCustom>
|
||||
<TextCustom bold size={"large"}>
|
||||
{bidang?.bidang?.name}
|
||||
</TextCustom>
|
||||
{bidang?.bidang.active ? (
|
||||
<BadgeCustom color="green">Aktif</BadgeCustom>
|
||||
) : (
|
||||
<BadgeCustom color="red">Tidak Aktif</BadgeCustom>
|
||||
)}
|
||||
</StackCustom>
|
||||
}
|
||||
text2={
|
||||
<CenterCustom>
|
||||
<Ionicons
|
||||
name="caret-forward"
|
||||
size={ICON_SIZE_SMALL}
|
||||
color="white"
|
||||
/>
|
||||
</CenterCustom>
|
||||
}
|
||||
/>
|
||||
</StackCustom>
|
||||
</BaseBox>
|
||||
|
||||
<CenterCustom>
|
||||
<TextCustom bold>Sub Bidang</TextCustom>
|
||||
</CenterCustom>
|
||||
<Spacing height={5} />
|
||||
</View>
|
||||
);
|
||||
|
||||
const renderItem = ({ item }: { item: any }) => (
|
||||
<BaseBox
|
||||
onPress={() =>
|
||||
router.push(
|
||||
`/admin/app-information/business-field/${item?.id}/sub-bidang-update`,
|
||||
)
|
||||
}
|
||||
>
|
||||
<StackCustom gap={"xs"}>
|
||||
<GridSpan_NewComponent
|
||||
span1={10}
|
||||
span2={2}
|
||||
text1={
|
||||
<StackCustom>
|
||||
<TextCustom bold size={"large"}>
|
||||
{item.name}
|
||||
</TextCustom>
|
||||
{item?.isActive ? (
|
||||
<BadgeCustom color="green">Aktif</BadgeCustom>
|
||||
) : (
|
||||
<BadgeCustom color="red">Tidak Aktif</BadgeCustom>
|
||||
)}
|
||||
</StackCustom>
|
||||
}
|
||||
text2={
|
||||
<CenterCustom>
|
||||
<Ionicons
|
||||
name="caret-forward"
|
||||
size={ICON_SIZE_SMALL}
|
||||
color="white"
|
||||
/>
|
||||
</CenterCustom>
|
||||
}
|
||||
/>
|
||||
</StackCustom>
|
||||
</BaseBox>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<NewWrapper
|
||||
listData={pagination.listData}
|
||||
onEndReached={pagination.loadMore}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
hideFooter
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={pagination.refreshing}
|
||||
onRefresh={pagination.onRefresh}
|
||||
tintColor={MainColor.yellow}
|
||||
colors={[MainColor.yellow]}
|
||||
/>
|
||||
}
|
||||
headerComponent={
|
||||
<AdminBackButtonAntTitle title="Detail Bidang & Sub Bidang" />
|
||||
}
|
||||
ListHeaderComponent={renderHeader()}
|
||||
renderItem={renderItem}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
55
screens/Admin/Event/BoxEventParticipant.tsx
Normal file
55
screens/Admin/Event/BoxEventParticipant.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import {
|
||||
BadgeCustom,
|
||||
BaseBox,
|
||||
Grid,
|
||||
StackCustom,
|
||||
TextCustom
|
||||
} from "@/components";
|
||||
import dayjs from "dayjs";
|
||||
import { View } from "moti";
|
||||
|
||||
interface Admin_BoxEventParticipantProps {
|
||||
item: any;
|
||||
startDate?: dayjs.Dayjs;
|
||||
}
|
||||
|
||||
export function Admin_BoxEventParticipant({
|
||||
item,
|
||||
startDate,
|
||||
}: Admin_BoxEventParticipantProps) {
|
||||
|
||||
return (
|
||||
<BaseBox>
|
||||
<Grid>
|
||||
<Grid.Col span={6}>
|
||||
<StackCustom gap={"sm"}>
|
||||
<TextCustom bold truncate>
|
||||
{item?.User?.username}
|
||||
</TextCustom>
|
||||
<TextCustom>+{item?.User?.nomor}</TextCustom>
|
||||
</StackCustom>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6} style={{ justifyContent: "center" }}>
|
||||
{startDate && startDate.subtract(1, "hour").diff(dayjs()) < 0 ? (
|
||||
<BadgeCustom
|
||||
style={{ alignSelf: "flex-end" }}
|
||||
color={item?.isPresent ? "green" : "red"}
|
||||
>
|
||||
{item?.isPresent ? "Hadir" : "Tidak Hadir"}
|
||||
</BadgeCustom>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "flex-end",
|
||||
}}
|
||||
>
|
||||
<BadgeCustom style={{ alignSelf: "flex-end" }} color="gray">
|
||||
-
|
||||
</BadgeCustom>
|
||||
</View>
|
||||
)}
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</BaseBox>
|
||||
);
|
||||
}
|
||||
48
screens/Admin/Event/BoxEventStatus.tsx
Normal file
48
screens/Admin/Event/BoxEventStatus.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { StackCustom, TextCustom } from "@/components";
|
||||
import AdminBasicBox from "@/components/_ShareComponent/Admin/AdminBasicBox";
|
||||
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
|
||||
import { dateTimeView } from "@/utils/dateTimeView";
|
||||
import { router } from "expo-router";
|
||||
import { View } from "react-native";
|
||||
import { Divider } from "react-native-paper";
|
||||
|
||||
interface Admin_BoxEventStatusProps {
|
||||
item: any;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export function Admin_BoxEventStatus({ item, status }: Admin_BoxEventStatusProps) {
|
||||
return (
|
||||
<AdminBasicBox
|
||||
style={{ marginHorizontal: 10, marginVertical: 5 }}
|
||||
onPress={() => {
|
||||
router.push(`/admin/event/${item.id}/${status}`);
|
||||
}}
|
||||
>
|
||||
<StackCustom gap={0}>
|
||||
<View style={{ paddingBlock: 8 }}>
|
||||
<TextCustom size={"large"} bold truncate={2}>
|
||||
{item?.title || "-"}
|
||||
</TextCustom>
|
||||
</View>
|
||||
<Divider />
|
||||
<GridSpan_4_8
|
||||
label={<TextCustom>Mulai</TextCustom>}
|
||||
value={
|
||||
<TextCustom>
|
||||
{dateTimeView({ date: item?.tanggal }) || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
/>
|
||||
<GridSpan_4_8
|
||||
label={<TextCustom>Berakhir</TextCustom>}
|
||||
value={
|
||||
<TextCustom>
|
||||
{dateTimeView({ date: item?.tanggalSelesai }) || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
/>
|
||||
</StackCustom>
|
||||
</AdminBasicBox>
|
||||
);
|
||||
}
|
||||
83
screens/Admin/Event/ScreenEventListOfParticipants.tsx
Normal file
83
screens/Admin/Event/ScreenEventListOfParticipants.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
||||
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 { apiAdminEventListOfParticipants } from "@/service/api-admin/api-admin-event";
|
||||
import dayjs from "dayjs";
|
||||
import { useLocalSearchParams } from "expo-router";
|
||||
import { useCallback } from "react";
|
||||
import { RefreshControl } from "react-native";
|
||||
import { Admin_BoxEventParticipant } from "./BoxEventParticipant";
|
||||
|
||||
export function Admin_ScreenEventListOfParticipants() {
|
||||
const { id } = useLocalSearchParams();
|
||||
|
||||
// Gunakan hook pagination
|
||||
const pagination = usePagination({
|
||||
fetchFunction: async (page, searchQuery) => {
|
||||
const response = await apiAdminEventListOfParticipants({
|
||||
id: id as string,
|
||||
page: String(page),
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
return { data: response.data };
|
||||
} else {
|
||||
return { data: [] };
|
||||
}
|
||||
},
|
||||
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||
dependencies: [id],
|
||||
onError: (error) => {
|
||||
console.error("Error loading participants:", error);
|
||||
},
|
||||
});
|
||||
|
||||
// Render item untuk daftar peserta
|
||||
const renderItem = useCallback(
|
||||
({ item, index }: { item: any; index: number }) => (
|
||||
<Admin_BoxEventParticipant
|
||||
key={index}
|
||||
item={item}
|
||||
startDate={dayjs(item?.Event?.tanggal)}
|
||||
/>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
// Buat komponen-komponen pagination
|
||||
const { ListEmptyComponent, ListFooterComponent } =
|
||||
createPaginationComponents({
|
||||
loading: pagination.loading,
|
||||
refreshing: pagination.refreshing,
|
||||
listData: pagination.listData,
|
||||
searchQuery: "",
|
||||
emptyMessage: "Belum ada peserta",
|
||||
emptySearchMessage: "Tidak ada hasil pencarian",
|
||||
isInitialLoad: pagination.isInitialLoad,
|
||||
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||
skeletonHeight: 60,
|
||||
});
|
||||
|
||||
return (
|
||||
<NewWrapper
|
||||
listData={pagination.listData}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={(item: any) => item.id.toString()}
|
||||
headerComponent={<AdminBackButtonAntTitle title="Daftar Peserta" />}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
onEndReached={pagination.loadMore}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={pagination.refreshing}
|
||||
onRefresh={pagination.onRefresh}
|
||||
tintColor={MainColor.yellow}
|
||||
colors={[MainColor.yellow]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
108
screens/Admin/Event/ScreenEventStatus.tsx
Normal file
108
screens/Admin/Event/ScreenEventStatus.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import { SearchInput } from "@/components";
|
||||
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 { apiAdminEvent } from "@/service/api-admin/api-admin-event";
|
||||
import { useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { RefreshControl } from "react-native";
|
||||
import { Admin_BoxEventStatus } from "./BoxEventStatus";
|
||||
|
||||
export function Admin_ScreenEventStatus() {
|
||||
const { status } = useLocalSearchParams();
|
||||
const [search, setSearch] = useState<string>("");
|
||||
|
||||
// Gunakan hook pagination
|
||||
const pagination = usePagination({
|
||||
fetchFunction: async (page, searchQuery) => {
|
||||
const response = await apiAdminEvent({
|
||||
category: status as
|
||||
| "publish"
|
||||
| "review"
|
||||
| "history"
|
||||
| "dashboard"
|
||||
| "type-of-event",
|
||||
search: searchQuery,
|
||||
page: String(page),
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
return { data: response.data };
|
||||
} else {
|
||||
return { data: [] };
|
||||
}
|
||||
},
|
||||
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||
searchQuery: search,
|
||||
dependencies: [status],
|
||||
});
|
||||
|
||||
// Komponen kanan untuk header
|
||||
const rightComponent = useMemo(
|
||||
() => (
|
||||
<SearchInput
|
||||
containerStyle={{ width: "100%", marginBottom: 0 }}
|
||||
placeholder="Cari"
|
||||
value={search}
|
||||
onChangeText={(value) => setSearch(value)}
|
||||
/>
|
||||
),
|
||||
[search],
|
||||
);
|
||||
|
||||
// Render item untuk daftar event
|
||||
const renderItem = useCallback(
|
||||
({ item, index }: { item: any; index: number }) => (
|
||||
<Admin_BoxEventStatus key={index} item={item} status={status as string} />
|
||||
),
|
||||
[status],
|
||||
);
|
||||
|
||||
const headerComponent = useMemo(
|
||||
() => (
|
||||
<AdminComp_BoxTitle
|
||||
title={`Event ${_.startCase(status as string)}`}
|
||||
rightComponent={rightComponent}
|
||||
/>
|
||||
),
|
||||
[status, rightComponent],
|
||||
);
|
||||
|
||||
// Buat komponen-komponen pagination
|
||||
const { ListEmptyComponent, ListFooterComponent } =
|
||||
createPaginationComponents({
|
||||
loading: pagination.loading,
|
||||
refreshing: pagination.refreshing,
|
||||
listData: pagination.listData,
|
||||
searchQuery: search,
|
||||
emptyMessage: "Belum ada data",
|
||||
emptySearchMessage: "Tidak ada hasil pencarian",
|
||||
isInitialLoad: pagination.isInitialLoad,
|
||||
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||
skeletonHeight: 100,
|
||||
});
|
||||
|
||||
return (
|
||||
<NewWrapper
|
||||
listData={pagination.listData}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={(item: any) => item.id.toString()}
|
||||
headerComponent={headerComponent}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
onEndReached={pagination.loadMore}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={pagination.refreshing}
|
||||
onRefresh={pagination.onRefresh}
|
||||
tintColor={MainColor.yellow}
|
||||
colors={[MainColor.yellow]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
29
screens/Admin/Job/BoxStatusJob.tsx
Normal file
29
screens/Admin/Job/BoxStatusJob.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Spacing, StackCustom, TextCustom } from "@/components";
|
||||
import AdminBasicBox from "@/components/_ShareComponent/Admin/AdminBasicBox";
|
||||
import { router } from "expo-router";
|
||||
import { View } from "react-native";
|
||||
import { Divider } from "react-native-paper";
|
||||
|
||||
interface BoxStatusJobProps {
|
||||
item: any;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export function BoxStatusJob({ item, status }: BoxStatusJobProps) {
|
||||
return (
|
||||
<AdminBasicBox
|
||||
style={{ marginHorizontal: 10, marginVertical: 5 }}
|
||||
onPress={() => {
|
||||
router.push(`/admin/job/${item.id}/${status}`);
|
||||
}}
|
||||
>
|
||||
<StackCustom>
|
||||
<View style={{paddingBlock: 8}}>
|
||||
<TextCustom size={"large"} align="center" bold truncate={2}>
|
||||
{item?.title || "-"}
|
||||
</TextCustom>
|
||||
</View>
|
||||
</StackCustom>
|
||||
</AdminBasicBox>
|
||||
);
|
||||
}
|
||||
103
screens/Admin/Job/ScreenJobStatus.tsx
Normal file
103
screens/Admin/Job/ScreenJobStatus.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import { SearchInput } from "@/components";
|
||||
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 { apiAdminJob } from "@/service/api-admin/api-admin-job";
|
||||
import { router, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { RefreshControl } from "react-native";
|
||||
import { Divider } from "react-native-paper";
|
||||
import { BoxStatusJob } from "./BoxStatusJob";
|
||||
|
||||
export function Admin_ScreenJobStatus() {
|
||||
const { status } = useLocalSearchParams();
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
// Gunakan hook pagination
|
||||
const pagination = usePagination({
|
||||
fetchFunction: async (page, searchQuery) => {
|
||||
const response = await apiAdminJob({
|
||||
category: status as "publish" | "review" | "reject",
|
||||
search: searchQuery,
|
||||
page: String(page),
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
return { data: response.data };
|
||||
} else {
|
||||
return { data: [] };
|
||||
}
|
||||
},
|
||||
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||
searchQuery: search,
|
||||
dependencies: [status],
|
||||
});
|
||||
|
||||
// Komponen kanan untuk header
|
||||
const rightComponent = useMemo(
|
||||
() => (
|
||||
<SearchInput
|
||||
placeholder="Cari perkerjaan"
|
||||
onChangeText={setSearch}
|
||||
value={search}
|
||||
/>
|
||||
),
|
||||
[search],
|
||||
);
|
||||
|
||||
// Render item untuk daftar pekerjaan
|
||||
const renderItem = useCallback(
|
||||
({ item, index }: { item: any; index: number }) => (
|
||||
<BoxStatusJob key={index} item={item} status={status as string} />
|
||||
),
|
||||
[status],
|
||||
);
|
||||
|
||||
const headerComponent = useMemo(
|
||||
() => (
|
||||
<AdminComp_BoxTitle
|
||||
title={`Job ${_.startCase(status as string)}`}
|
||||
rightComponent={rightComponent}
|
||||
/>
|
||||
),
|
||||
[status, rightComponent],
|
||||
);
|
||||
|
||||
// Buat komponen-komponen pagination
|
||||
const { ListEmptyComponent, ListFooterComponent } =
|
||||
createPaginationComponents({
|
||||
loading: pagination.loading,
|
||||
refreshing: pagination.refreshing,
|
||||
listData: pagination.listData,
|
||||
searchQuery: search,
|
||||
emptyMessage: "Tidak ada data",
|
||||
emptySearchMessage: "Tidak ada hasil pencarian",
|
||||
isInitialLoad: pagination.isInitialLoad,
|
||||
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||
skeletonHeight: 100,
|
||||
});
|
||||
|
||||
return (
|
||||
<NewWrapper
|
||||
listData={pagination.listData}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={(item: any) => item.id.toString()}
|
||||
headerComponent={headerComponent}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
onEndReached={pagination.loadMore}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={pagination.refreshing}
|
||||
onRefresh={pagination.onRefresh}
|
||||
tintColor={MainColor.yellow}
|
||||
colors={[MainColor.yellow]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
127
screens/Admin/User-Access/ScreenUserAccess.tsx
Normal file
127
screens/Admin/User-Access/ScreenUserAccess.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
/* 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, useFocusEffect } from "expo-router";
|
||||
import { useCallback, 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,
|
||||
});
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
pagination.onRefresh();
|
||||
}, []),
|
||||
);
|
||||
|
||||
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,13 +3,15 @@ import { apiConfig } from "../api-config";
|
||||
export async function apiAdminEvent({
|
||||
category,
|
||||
search,
|
||||
page = "1",
|
||||
}: {
|
||||
category: "dashboard" | "history" | "publish" | "review" | "type-of-event";
|
||||
search?: string;
|
||||
page?: string;
|
||||
}) {
|
||||
try {
|
||||
const response = await apiConfig.get(
|
||||
`/mobile/admin/event?category=${category}&search=${search}`
|
||||
`/mobile/admin/event?category=${category}&search=${search}&page=${page}`
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
@@ -48,10 +50,18 @@ export async function apiAdminEventUpdateStatus({
|
||||
}
|
||||
}
|
||||
|
||||
export async function apiAdminEventListOfParticipants({ id }: { id: string }) {
|
||||
export async function apiAdminEventListOfParticipants({
|
||||
id,
|
||||
page = "1",
|
||||
search = ""
|
||||
}: {
|
||||
id: string;
|
||||
page?: string;
|
||||
search?: string;
|
||||
}) {
|
||||
try {
|
||||
const response = await apiConfig.get(
|
||||
`/mobile/admin/event/${id}/participants`
|
||||
`/mobile/admin/event/${id}/participants?page=${page}&search=${search}`
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
|
||||
@@ -3,13 +3,15 @@ import { apiConfig } from "../api-config";
|
||||
export async function apiAdminJob({
|
||||
category,
|
||||
search,
|
||||
page = "1",
|
||||
}: {
|
||||
category: "dashboard" | "publish" | "review" | "reject";
|
||||
search?: string;
|
||||
page?: string;
|
||||
}) {
|
||||
try {
|
||||
const response = await apiConfig.get(
|
||||
`/mobile/admin/job?category=${category}&search=${search}`
|
||||
`/mobile/admin/job?category=${category}&search=${search}&page=${page}`
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { apiConfig } from "../api-config";
|
||||
|
||||
// ================== START MASTER BANK ================== //
|
||||
export async function apiAdminMasterBank() {
|
||||
export async function apiAdminMasterBank({ page = "1" }: { page?: string }) {
|
||||
try {
|
||||
const response = await apiConfig.get(`/mobile/admin/master/bank`);
|
||||
const response = await apiConfig.get(
|
||||
`/mobile/admin/master/bank?page=${page}`,
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
@@ -51,9 +53,15 @@ export async function apiAdminMasterBankCreate({ data }: { data: any }) {
|
||||
|
||||
// ================== START BUSINNES FIELD ================== //
|
||||
|
||||
export async function apiAdminMasterBusinessField() {
|
||||
export async function apiAdminMasterBusinessField({
|
||||
page = "1",
|
||||
}: {
|
||||
page: string;
|
||||
}) {
|
||||
try {
|
||||
const response = await apiConfig.get(`/mobile/admin/master/business-field`);
|
||||
const response = await apiConfig.get(
|
||||
`/mobile/admin/master/business-field?page=${page}`,
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
@@ -64,16 +72,19 @@ export async function apiAdminMasterBusinessFieldById({
|
||||
id,
|
||||
subBidangId,
|
||||
category,
|
||||
page = "1"
|
||||
}: {
|
||||
id: string;
|
||||
subBidangId?: string | null;
|
||||
category: "bidang" | "sub-bidang" | "all";
|
||||
category: "bidang" | "sub-bidang" | "all" | "only-sub-bidang"
|
||||
page?: string
|
||||
}) {
|
||||
const queryCategory = category ? `?category=${category}` : "";
|
||||
const querySubBidang = subBidangId ? `&subBidangId=${subBidangId}` : "";
|
||||
const queryPage = page ? `&page=${page}` : "";
|
||||
try {
|
||||
const response = await apiConfig.get(
|
||||
`/mobile/admin/master/business-field/${id}${queryCategory}${querySubBidang}`
|
||||
`/mobile/admin/master/business-field/${id}${queryCategory}${querySubBidang}${queryPage}`,
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
@@ -95,7 +106,7 @@ export async function apiAdminMasterBusinessFieldUpdate({
|
||||
`/mobile/admin/master/business-field/${id}?category=${category}`,
|
||||
{
|
||||
data: data,
|
||||
}
|
||||
},
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
@@ -113,7 +124,7 @@ export async function apiAdminMasterBusinessFieldCreate({
|
||||
`/mobile/admin/master/business-field`,
|
||||
{
|
||||
data: data,
|
||||
}
|
||||
},
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
@@ -139,7 +150,7 @@ export async function apiEventCreateTypeOfEvent({ data }: { data: string }) {
|
||||
`/mobile/admin/master/type-of-event`,
|
||||
{
|
||||
data: data,
|
||||
}
|
||||
},
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
@@ -150,7 +161,7 @@ export async function apiEventCreateTypeOfEvent({ data }: { data: string }) {
|
||||
export async function apiAdminMasterTypeOfEventGetOne({ id }: { id: string }) {
|
||||
try {
|
||||
const response = await apiConfig.get(
|
||||
`/mobile/admin/master/type-of-event/${id}`
|
||||
`/mobile/admin/master/type-of-event/${id}`,
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
@@ -170,7 +181,7 @@ export async function apiAdminMasterTypeOfEventUpdate({
|
||||
`/mobile/admin/master/type-of-event/${id}`,
|
||||
{
|
||||
data: data,
|
||||
}
|
||||
},
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
@@ -216,7 +227,7 @@ export async function apiAdminMasterDonationCategoryUpdate({
|
||||
`/mobile/admin/master/donation/${id}`,
|
||||
{
|
||||
data: data,
|
||||
}
|
||||
},
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
@@ -240,3 +251,27 @@ export async function apiAdminMasterDonationCategoryCreate({
|
||||
}
|
||||
|
||||
// ================== END DONATION ================== //
|
||||
|
||||
// ================== START FECTH APP INFORMATION ================== //
|
||||
|
||||
export async function apiFetchAdminMasterAppInformation({
|
||||
page = "1",
|
||||
category,
|
||||
}: {
|
||||
page: string;
|
||||
category?: "bank" | "business" | string
|
||||
}) {
|
||||
if (category === "bank") {
|
||||
const response = await apiAdminMasterBank({ page });
|
||||
// TODO: implement bank logic
|
||||
return response;
|
||||
} else if (category === "business") {
|
||||
const response = await apiAdminMasterBusinessField({ page });
|
||||
// TODO: implement business logic
|
||||
return response
|
||||
} else {
|
||||
throw new Error("Category is required");
|
||||
}
|
||||
}
|
||||
|
||||
// ================== END FECTH APP INFORMATION ================== //
|
||||
|
||||
@@ -16,7 +16,7 @@ export const GStyles = StyleSheet.create({
|
||||
// =============== Main Styles =============== //
|
||||
container: {
|
||||
flex: 1,
|
||||
paddingInline: PADDING_MEDIUM,
|
||||
paddingInline: PADDING_SMALL,
|
||||
paddingTop: PADDING_EXTRA_SMALL,
|
||||
paddingBottom: 5,
|
||||
// paddingBlock: PADDING_EXTRA_SMALL,
|
||||
|
||||
Reference in New Issue
Block a user