Compare commits

...

4 Commits

Author SHA1 Message Date
83fa277e03 Fix Loaddata Invesment & Clearing code
UI – Investment (User)
- app/(application)/(user)/investment/(tabs)/index.tsx
- app/(application)/(user)/investment/(tabs)/portofolio.tsx
- app/(application)/(user)/investment/(tabs)/transaction.tsx
- app/(application)/(user)/investment/[id]/(document)/list-of-document.tsx
- app/(application)/(user)/investment/[id]/(document)/recap-of-document.tsx
- app/(application)/(user)/investment/[id]/(transaction-flow)/invoice.tsx
- app/(application)/(user)/investment/[id]/(transaction-flow)/select-bank.tsx

Screens – Investment
- screens/Invesment/ButtonStatusSection.tsx
- screens/Invesment/Document/RecapBoxDetail.tsx
- screens/Invesment/Document/ScreenListDocument.tsx
- screens/Invesment/Document/ScreenRecap.tsx
- screens/Invesment/ScreenBursa.tsx
- screens/Invesment/ScreenPortofolio.tsx
- screens/Invesment/ScreenTransaction.tsx

Profile
- app/(application)/(user)/profile/[id]/detail-blocked.tsx

API Client
- service/api-client/api-investment.ts

Docs
- docs/prompt-for-qwen-code.md

### No issue
2026-02-06 17:27:12 +08:00
c570a19d84 Fix Wrapper
UI – Investment (User)
- app/(application)/(user)/investment/create.tsx
- app/(application)/(user)/investment/[id]/edit.tsx

### No Issue"
2026-02-05 17:30:17 +08:00
7415c8c8ce Fix path Notification admin
UI – Notifications (User)
- app/(application)/(user)/notifications/index.tsx
- screens/Notification/ScreenNotification_V2.tsx

UI – Notifications (Admin)
- screens/Admin/Notification-Admin/ScreenNotificationAdmin2.tsx

API
- service/api-notifications.ts

Docs
- docs/prompt-for-qwen-code.md

### No Issue
2026-02-05 16:37:50 +08:00
72a3d42013 Fix Loaddata Voting
UI – Voting (User)
- app/(application)/(user)/voting/(tabs)/index.tsx
- app/(application)/(user)/voting/(tabs)/history.tsx
- app/(application)/(user)/voting/(tabs)/contribution.tsx
- app/(application)/(user)/voting/create.tsx
- app/(application)/(user)/voting/[id]/edit.tsx
- app/(application)/(user)/voting/[id]/[status]/detail.tsx
- app/(application)/(user)/voting/[id]/list-of-contributor.tsx

UI – Event
- app/(application)/(user)/event/[id]/edit.tsx
- screens/Event/ScreenStatus.tsx

UI – Voting Screens (New)
- screens/Voting/ScreenBeranda.tsx
- screens/Voting/ScreenContribution.tsx
- screens/Voting/ScreenHistory.tsx
- screens/Voting/ScreenListOfContributor.tsx
- screens/Voting/ScreenStatus.tsx
- screens/Voting/ButtonStatusSection.tsx

UI – Job
- screens/Job/ButtonStatusSection.tsx
- screens/Job/MainViewStatus2.tsx

API Client
- service/api-client/api-voting.ts

Docs
- docs/prompt-for-qwen-code.md

### No Issue
2026-02-05 15:06:14 +08:00
41 changed files with 1225 additions and 1124 deletions

View File

@@ -58,7 +58,7 @@ export default function EventEdit() {
try { try {
setIsLoadData(true); setIsLoadData(true);
const response = await apiEventGetOne({ id: id as string }); const response = await apiEventGetOne({ id: id as string });
console.log("[DATA BY ID]", JSON.stringify(response, null, 2));
if (response.success) { if (response.success) {
setData(response.data); setData(response.data);
setSelectedDate(new Date(response.data.tanggal)); setSelectedDate(new Date(response.data.tanggal));

View File

@@ -1,56 +1,9 @@
import { import Investment_ScreenBursa from "@/screens/Invesment/ScreenBursa";
FloatingButton,
LoaderCustom,
ViewWrapper
} from "@/components";
import NoDataText from "@/components/_ShareComponent/NoDataText";
import Investment_BoxBerandaSection from "@/screens/Invesment/BoxBerandaSection";
import { apiInvestmentGetAll } from "@/service/api-client/api-investment";
import { router, useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
export default function InvestmentBursa() { export default function InvestmentBursa() {
const [list, setList] = useState<any[] | null>(null);
const [loadingList, setLoadingList] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadList();
}, [])
);
const onLoadList = async () => {
try {
setLoadingList(true);
const response = await apiInvestmentGetAll({
category: "bursa"
});
// console.log("[DATA LIST]", JSON.stringify(response.data, null, 2));
setList(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingList(false);
}
};
return ( return (
<ViewWrapper <>
hideFooter <Investment_ScreenBursa />
floatingButton={ </>
<FloatingButton onPress={() => router.push("/investment/create")} />
}
>
{loadingList ? (
<LoaderCustom />
) : _.isEmpty(list) ? (
<NoDataText />
) : (
list?.map((item: any, index: number) => (
<Investment_BoxBerandaSection id={item.id} data={item} key={index} />
))
)}
</ViewWrapper>
); );
} }

View File

@@ -1,82 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { import Investment_ScreenPortofolio from "@/screens/Invesment/ScreenPortofolio";
LoaderCustom,
ScrollableCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import { useAuth } from "@/hooks/use-auth";
import { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
import Investment_StatusBox from "@/screens/Invesment/StatusBox";
import { apiInvestmentGetByStatus } from "@/service/api-client/api-investment";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
export default function InvestmentPortofolio() { export default function InvestmentPortofolio() {
const { user } = useAuth();
const { status } = useLocalSearchParams<{ status?: string }>();
const [activeCategory, setActiveCategory] = useState<string | null>(
status || "publish"
);
const [listData, setListData] = useState<any[]>([]);
const [loadingList, setLoadingList] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [user?.id, activeCategory])
);
const onLoadData = async () => {
try {
setLoadingList(true);
const response = await apiInvestmentGetByStatus({
authorId: user?.id as string,
status: activeCategory as string,
});
setListData(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingList(false);
}
};
const handlePress = (item: any) => {
setActiveCategory(item.value);
// tambahkan logika lain seperti filter dsb.
};
const scrollComponent = (
<ScrollableCustom
data={dummyMasterStatus.map((e, i) => ({
id: i,
label: e.label,
value: e.value,
}))}
onButtonPress={handlePress}
activeId={activeCategory as any}
/>
);
return ( return (
<ViewWrapper headerComponent={scrollComponent} hideFooter> <>
{loadingList ? ( <Investment_ScreenPortofolio />
<LoaderCustom /> </>
) : _.isEmpty(listData) ? (
<TextCustom align="center">Tidak ada data {activeCategory}</TextCustom>
) : (
listData.map((item: any, index: number) => (
<Investment_StatusBox
key={index}
data={item}
status={activeCategory as string}
href={`/investment/${item.id}/${activeCategory}/detail`}
/>
))
)}
</ViewWrapper>
); );
} }

View File

@@ -1,124 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { import Investment_ScreenTransaction from "@/screens/Invesment/ScreenTransaction";
BadgeCustom,
BaseBox,
Grid,
LoaderCustom,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import NoDataText from "@/components/_ShareComponent/NoDataText";
import { useAuth } from "@/hooks/use-auth";
import { apiInvestmentGetInvoice } from "@/service/api-client/api-investment";
import { GStyles } from "@/styles/global-styles";
import { formatChatTime } from "@/utils/formatChatTime";
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
import { router, useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import { View } from "react-native";
export default function InvestmentTransaction() { export default function InvestmentTransaction() {
const { user } = useAuth();
const [list, setList] = useState<any>([]);
const [loadList, setLoadList] = useState<boolean>(false);
useFocusEffect(
useCallback(() => {
onLoadList();
}, [user?.id])
);
const onLoadList = async () => {
try {
setLoadList(true);
const response = await apiInvestmentGetInvoice({
authorId: user?.id as string,
category: "transaction",
});
console.log("[RESPONSE LIST]", JSON.stringify(response.data, null, 2));
setList(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadList(false);
}
};
const handlerColor = (status: string) => {
if (status === "menunggu") {
return "orange";
} else if (status === "proses") {
return "white";
} else if (status === "berhasil") {
return "green";
} else if (status === "gagal") {
return "red";
}
};
const handlePress = ({ id, status }: { id: string; status: string }) => {
if (status === "menunggu") {
router.push(`/investment/${id}/(transaction-flow)/invoice`);
} else if (status === "proses") {
router.push(`/investment/${id}/(transaction-flow)/process`);
} else if (status === "berhasil") {
router.push(`/investment/${id}/(transaction-flow)/success`);
} else if (status === "gagal") {
router.push(`/investment/${id}/(transaction-flow)/failed`);
}
};
return ( return (
<ViewWrapper hideFooter> <>
{loadList ? ( <Investment_ScreenTransaction />
<LoaderCustom /> </>
) : _.isEmpty(list) ? (
<NoDataText/>
) : (
list.map((item: any, i: number) => (
<BaseBox
key={i}
paddingTop={7}
paddingBottom={7}
onPress={() => {
handlePress({
id: item.id,
status: _.lowerCase(item.statusInvoice),
});
}}
>
<Grid>
<Grid.Col span={6}>
<StackCustom gap={"xs"}>
<TextCustom truncate>{item?.title || "-"}</TextCustom>
<TextCustom color="gray" size="small">
{formatChatTime(item?.createdAt)}
</TextCustom>
</StackCustom>
</Grid.Col>
<Grid.Col span={1}>
<View />
</Grid.Col>
<Grid.Col span={5} style={{ alignItems: "flex-end" }}>
<StackCustom gap={"xs"}>
<TextCustom bold truncate>
Rp. {formatCurrencyDisplay(item?.nominal) || "-"}
</TextCustom>
<BadgeCustom
variant="light"
color={handlerColor(_.lowerCase(item.statusInvoice))}
style={GStyles.alignSelfFlexEnd}
>
{item?.statusInvoice || "-"}
</BadgeCustom>
</StackCustom>
</Grid.Col>
</Grid>
</BaseBox>
))
)}
</ViewWrapper>
); );
} }

View File

@@ -1,58 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { LoaderCustom, TextCustom, ViewWrapper } from "@/components"; import Investment_ScreenListOfDocument from "@/screens/Invesment/Document/ScreenListDocument";
import Investment_BoxDetailDocument from "@/screens/Invesment/Document/RecapBoxDetail";
import { apiInvestmentGetDocument } from "@/service/api-client/api-investment";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
export default function InvestmentListOfDocument() { export default function InvestmentListOfDocument() {
const { id } = useLocalSearchParams();
console.log("ID >> ", id);
const [list, setList] = useState<any[] | null>(null);
const [loadList, setLoadList] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadListDocument();
}, [id])
);
const onLoadListDocument = async () => {
try {
setLoadList(true);
const response = await apiInvestmentGetDocument({
id: id as string,
category: "all-document",
});
setList(response.data);
} catch (error) {
console.log("[ERROR]", error);
setList([]);
} finally {
setLoadList(false);
}
};
return ( return (
<ViewWrapper> <>
{loadList ? ( <Investment_ScreenListOfDocument />
<LoaderCustom /> </>
) : _.isEmpty(list) ? (
<TextCustom align="center" color="gray">
Tidak ada data
</TextCustom>
) : (
list?.map((item: any, index: number) => (
<Investment_BoxDetailDocument
key={index}
title={item.title}
href={`/(file)/${item.fileId}`}
/>
))
)}
</ViewWrapper>
); );
} }

View File

@@ -1,213 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { import Investment_ScreenRecap from "@/screens/Invesment/Document/ScreenRecap";
AlertDefaultSystem,
BackButton,
DotButton,
DrawerCustom,
LoaderCustom,
MenuDrawerDynamicGrid,
TextCustom,
ViewWrapper,
} from "@/components";
import { IconEdit } from "@/components/_Icon";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import Investment_BoxDetailDocument from "@/screens/Invesment/Document/RecapBoxDetail";
import {
apiInvestmentDeleteDocument,
apiInvestmentGetDocument,
} from "@/service/api-client/api-investment";
import { AntDesign, Ionicons } from "@expo/vector-icons";
import {
router,
Stack,
useFocusEffect,
useLocalSearchParams,
} from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import Toast from "react-native-toast-message";
export default function InvestmentRecapOfDocument() { export default function InvestmentRecapOfDocument() {
const { id } = useLocalSearchParams();
const [openDrawer, setOpenDrawer] = useState(false);
const [openDrawerBox, setOpenDrawerBox] = useState(false);
const [list, setList] = useState<any[] | null>(null);
const [loadList, setLoadList] = useState(false);
const [selectId, setSelectId] = useState<string | null>(null);
useFocusEffect(
useCallback(() => {
onLoadListDocument();
}, [id])
);
const onLoadListDocument = async () => {
try {
setLoadList(true);
const response = await apiInvestmentGetDocument({
id: id as string,
category: "all-document",
});
setList(response.data);
} catch (error) {
console.log("[ERROR]", error);
setList([]);
} finally {
setLoadList(false);
}
};
const handlerDeleteDocument = async () => {
try {
const response = await apiInvestmentDeleteDocument({
id: selectId as string,
});
if (response.success) {
Toast.show({
type: "success",
text1: "Data berhasil dihapus",
});
setList((prev: any[] | null) => {
if (!prev) return null;
return prev.filter((item: any) => item.id !== selectId);
});
setOpenDrawerBox(false);
setSelectId(null);
}
} catch (error) {
console.log("[ERROR]", error);
Toast.show({
type: "error",
text1: "Gagal menghapus data",
});
}
};
return ( return (
<> <>
<Stack.Screen <Investment_ScreenRecap />
options={{
title: "Rekap Dokumen",
headerLeft: () => <BackButton />,
headerRight: () => (
<DotButton
onPress={() => {
setOpenDrawer(true);
setOpenDrawerBox(false);
}}
/>
),
}}
/>
<ViewWrapper>
{loadList ? (
<LoaderCustom />
) : _.isEmpty(list) ? (
<TextCustom align="center" color="gray">
Tidak ada data
</TextCustom>
) : (
list?.map((item: any, index: number) => (
<Investment_BoxDetailDocument
key={index}
title={item.title}
leftIcon={
<Ionicons
name="ellipsis-horizontal-outline"
size={ICON_SIZE_SMALL}
color={MainColor.white}
style={{
zIndex: 10,
alignSelf: "flex-end",
}}
onPress={() => {
setSelectId(item.id);
setOpenDrawerBox(true);
}}
/>
}
href={`/(file)/${item.fileId}`}
/>
))
)}
</ViewWrapper>
{/* Drawer On Header */}
<DrawerCustom
isVisible={openDrawer}
closeDrawer={() => setOpenDrawer(false)}
height={"auto"}
>
<MenuDrawerDynamicGrid
data={[
{
icon: (
<AntDesign
name="plus-circle"
size={ICON_SIZE_SMALL}
color={MainColor.white}
/>
),
label: "Tambah Dokumen",
path: `/investment/${id}/(document)/add-document`,
},
]}
onPressItem={(item) => {
router.push(item.path as any);
setOpenDrawer(false);
}}
/>
</DrawerCustom>
{/* Drawer On Box */}
<DrawerCustom
isVisible={openDrawerBox}
closeDrawer={() => setOpenDrawerBox(false)}
height={"auto"}
>
<MenuDrawerDynamicGrid
data={[
{
icon: <IconEdit />,
label: "Edit Dokumen",
path: `/investment/${selectId}/(document)/edit-document`,
},
{
icon: (
<Ionicons
name="trash-outline"
size={ICON_SIZE_SMALL}
color={MainColor.white}
/>
),
label: "Hapus Dokumen",
path: "" as any,
color: MainColor.red,
},
]}
onPressItem={(item) => {
if (item.path === ("" as any)) {
AlertDefaultSystem({
title: "Hapus Dokumen",
message: "Apakah anda yakin ingin menghapus dokumen ini?",
textLeft: "Batal",
textRight: "Hapus",
onPressRight: () => {
handlerDeleteDocument();
},
});
} else {
router.push(item.path as any);
}
setOpenDrawerBox(false);
}}
/>
</DrawerCustom>
</> </>
); );
} }

View File

@@ -1,230 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { import Investment_ScreenTransaction from "@/screens/Invesment/ScreenTransaction";
BaseBox,
ButtonCenteredOnly,
ButtonCustom,
Grid,
InformationBox,
Spacing,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import CopyButton from "@/components/Button/CoyButton";
import { MainColor } from "@/constants/color-palet";
import DIRECTORY_ID from "@/constants/directory-id";
import {
apiInvestmentGetInvoice,
apiInvestmentUpdateInvoice,
} from "@/service/api-client/api-investment";
import { uploadFileService } from "@/service/upload-service";
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
import pickFile, { IFileData } from "@/utils/pickFile";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, useState } from "react";
import { View } from "react-native";
import Toast from "react-native-toast-message";
export default function InvestmentInvoice() { export default function InvestmentInvoice() {
const { id } = useLocalSearchParams();
const [data, setData] = useState<any>({});
const [image, setImage] = useState<IFileData>({
name: "",
uri: "",
size: 0,
});
const [isLoading, setIsLoading] = useState<boolean>(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id])
);
const onLoadData = async () => {
try {
const response = await apiInvestmentGetInvoice({
id: id as string,
category: "invoice",
});
setData(response.data);
} catch (error) {
console.log("[ERROR]", error);
}
};
const handlerSubmitUpdate = async () => {
try {
setIsLoading(true);
const responseUploadImage = await uploadFileService({
dirId: DIRECTORY_ID.investasi_bukti_transfer,
imageUri: image?.uri,
});
if (!responseUploadImage?.data?.id) {
Toast.show({
type: "error",
text1: "Gagal mengunggah bukti transfer",
});
return;
}
const response = await apiInvestmentUpdateInvoice({
id: id as string,
data: {
imageId: responseUploadImage?.data?.id,
},
status: "proses",
});
if (response.success) {
Toast.show({
type: "success",
text1: "Berhasil mengunggah bukti transfer",
});
router.push(`/investment/${id}/(transaction-flow)/process`);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoading(false);
}
};
return ( return (
<> <>
<ViewWrapper> <Investment_ScreenTransaction />
<StackCustom>
<InformationBox text="Mohon transfer ke rekening dibawah" />
<BaseBox>
<StackCustom gap={"xs"}>
<Grid>
<Grid.Col span={4}>
<TextCustom>Bank</TextCustom>
</Grid.Col>
<Grid.Col span={8}>
<TextCustom>{data?.MasterBank?.namaBank}</TextCustom>
</Grid.Col>
</Grid>
<Spacing height={10} />
<Grid>
<Grid.Col span={4}>
<TextCustom>Nama Akun</TextCustom>
</Grid.Col>
<Grid.Col span={8}>
<TextCustom>{data?.MasterBank?.namaAkun}</TextCustom>
</Grid.Col>
</Grid>
<BaseBox backgroundColor={MainColor.soft_darkblue}>
<Grid containerStyle={{ justifyContent: "center" }}>
<Grid.Col
span={8}
style={{
justifyContent: "center",
}}
>
<TextCustom size="xlarge" bold color="yellow">
{data?.MasterBank?.norek}
</TextCustom>
</Grid.Col>
<Grid.Col
span={4}
style={{
alignItems: "flex-end",
}}
>
<CopyButton textToCopy={data?.MasterBank?.norek} />
</Grid.Col>
</Grid>
</BaseBox>
</StackCustom>
</BaseBox>
<BaseBox>
<StackCustom gap={"xs"}>
<TextCustom>Jumlah Transaksi</TextCustom>
<Spacing height={10} />
<BaseBox backgroundColor={MainColor.soft_darkblue}>
<Grid containerStyle={{ justifyContent: "center" }}>
<Grid.Col
span={8}
style={{
justifyContent: "center",
}}
>
<TextCustom size="xlarge" bold color="yellow">
Rp. {formatCurrencyDisplay(data?.nominal)}
</TextCustom>
</Grid.Col>
<Grid.Col
span={4}
style={{
alignItems: "flex-end",
}}
>
<CopyButton textToCopy={data?.nominal} />
</Grid.Col>
</Grid>
</BaseBox>
</StackCustom>
</BaseBox>
<BaseBox>
<StackCustom>
<TextCustom align="center">
Upload bukti transfer anda.
</TextCustom>
{image ? (
<View
style={{
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
gap: 10,
paddingInline: 20,
}}
>
<TextCustom bold align="center" truncate>
{image?.name}
</TextCustom>
</View>
) : (
<TextCustom align="center">
Tidak ada gambar yang diunggah
</TextCustom>
)}
<ButtonCenteredOnly
onPress={() => {
pickFile({
allowedType: "image",
setImageUri(file: any) {
setImage(file);
},
});
}}
icon="upload"
>
Upload
</ButtonCenteredOnly>
</StackCustom>
</BaseBox>
<ButtonCustom
isLoading={isLoading}
disabled={!image || isLoading}
onPress={() => {
handlerSubmitUpdate();
}}
>
Saya Sudah Transfer
</ButtonCustom>
</StackCustom>
<Spacing />
</ViewWrapper>
</> </>
); );
} }

View File

@@ -56,7 +56,6 @@ export default function InvestmentSelectBank() {
}); });
if (response.success) { if (response.success) {
console.log("[RESPONSE >>]", response);
const invoiceId = response.data.id; const invoiceId = response.data.id;
const delStorage = await AsyncStorage.removeItem( const delStorage = await AsyncStorage.removeItem(

View File

@@ -1,15 +1,16 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { import {
BoxButtonOnFooter,
ButtonCenteredOnly, ButtonCenteredOnly,
ButtonCustom, ButtonCustom,
InformationBox, InformationBox,
LandscapeFrameUploaded, LandscapeFrameUploaded,
LoaderCustom, LoaderCustom,
NewWrapper,
SelectCustom, SelectCustom,
Spacing, Spacing,
StackCustom, StackCustom,
TextInputCustom, TextInputCustom
ViewWrapper,
} from "@/components"; } from "@/components";
import API_STRORAGE from "@/constants/base-url-api-strorage"; import API_STRORAGE from "@/constants/base-url-api-strorage";
import DIRECTORY_ID from "@/constants/directory-id"; import DIRECTORY_ID from "@/constants/directory-id";
@@ -198,7 +199,15 @@ export default function InvestmentEdit() {
}; };
return ( return (
<ViewWrapper> <NewWrapper
footerComponent={
<BoxButtonOnFooter>
<ButtonCustom isLoading={isLoading} onPress={handleSubmitUpdate}>
Simpan
</ButtonCustom>
</BoxButtonOnFooter>
}
>
<StackCustom gap={"xs"}> <StackCustom gap={"xs"}>
<InformationBox text="Gambar investasi bisa berupa ilustrasi, poster atau foto terkait investasi." /> <InformationBox text="Gambar investasi bisa berupa ilustrasi, poster atau foto terkait investasi." />
<LandscapeFrameUploaded <LandscapeFrameUploaded
@@ -253,7 +262,8 @@ export default function InvestmentEdit() {
/> />
<TextInputCustom <TextInputCustom
disabled iconLeft="Rp."
// disabled
required required
placeholder="0" placeholder="0"
label="Total Lembar" label="Total Lembar"
@@ -339,11 +349,7 @@ export default function InvestmentEdit() {
)} )}
<Spacing /> <Spacing />
<ButtonCustom isLoading={isLoading} onPress={handleSubmitUpdate}>
Simpan
</ButtonCustom>
</StackCustom> </StackCustom>
<Spacing height={50} /> </NewWrapper>
</ViewWrapper>
); );
} }

View File

@@ -1,18 +1,19 @@
import { import {
BaseBox, BaseBox,
BoxButtonOnFooter,
ButtonCenteredOnly, ButtonCenteredOnly,
ButtonCustom, ButtonCustom,
CenterCustom, CenterCustom,
InformationBox, InformationBox,
LandscapeFrameUploaded, LandscapeFrameUploaded,
LoaderCustom, NewWrapper,
SelectCustom, SelectCustom,
Spacing, Spacing,
StackCustom, StackCustom,
TextCustom, TextCustom,
TextInputCustom, TextInputCustom,
ViewWrapper,
} from "@/components"; } from "@/components";
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
import DIRECTORY_ID from "@/constants/directory-id"; import DIRECTORY_ID from "@/constants/directory-id";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
@@ -184,7 +185,19 @@ export default function InvestmentCreate() {
// const [coba, setCoba] = useState(""); // const [coba, setCoba] = useState("");
return ( return (
<ViewWrapper> <NewWrapper
footerComponent={
<BoxButtonOnFooter>
<ButtonCustom
disabled={isLoading}
isLoading={isLoading}
onPress={() => handleSubmit()}
>
Simpan
</ButtonCustom>
</BoxButtonOnFooter>
}
>
<StackCustom gap={"xs"}> <StackCustom gap={"xs"}>
<InformationBox text="Gambar investasi bisa berupa ilustrasi, poster atau foto terkait investasi." /> <InformationBox text="Gambar investasi bisa berupa ilustrasi, poster atau foto terkait investasi." />
<LandscapeFrameUploaded image={image as string} /> <LandscapeFrameUploaded image={image as string} />
@@ -264,7 +277,9 @@ export default function InvestmentCreate() {
<StackCustom gap={0}> <StackCustom gap={0}>
<TextInputCustom <TextInputCustom
disabled iconLeft="Rp."
// disabled
editable={false}
required required
placeholder="0" placeholder="0"
label="Total Lembar" label="Total Lembar"
@@ -291,7 +306,7 @@ export default function InvestmentCreate() {
/> />
{loadingMaster ? ( {loadingMaster ? (
<LoaderCustom /> <CustomSkeleton height={50} />
) : ( ) : (
<SelectCustom <SelectCustom
required required
@@ -313,7 +328,7 @@ export default function InvestmentCreate() {
)} )}
{loadingMaster ? ( {loadingMaster ? (
<LoaderCustom /> <CustomSkeleton height={50} />
) : ( ) : (
<SelectCustom <SelectCustom
required required
@@ -335,7 +350,7 @@ export default function InvestmentCreate() {
)} )}
{loadingMaster ? ( {loadingMaster ? (
<LoaderCustom /> <CustomSkeleton height={50} />
) : ( ) : (
<SelectCustom <SelectCustom
required required
@@ -357,15 +372,8 @@ export default function InvestmentCreate() {
)} )}
<Spacing /> <Spacing />
<ButtonCustom
disabled={isLoading}
isLoading={isLoading}
onPress={() => handleSubmit()}
>
Simpan
</ButtonCustom>
</StackCustom> </StackCustom>
<Spacing height={50} /> {/* <Spacing height={50} /> */}
</ViewWrapper> </NewWrapper>
); );
} }

View File

@@ -1,11 +1,11 @@
import ScreenNotification from "@/screens/Notification/ScreenNotification_V2";
import ScreenNotification_V1 from "@/screens/Notification/ScreenNotification_V1"; import ScreenNotification_V1 from "@/screens/Notification/ScreenNotification_V1";
import ScreenNotification_V2 from "@/screens/Notification/ScreenNotification_V2";
export default function Notification() { export default function Notification() {
return ( return (
<> <>
<ScreenNotification /> <ScreenNotification_V2 />
{/* <ScreenNotification_V1 /> */} {/* <ScreenNotification_V1 /> */}
</> </>
); );
} }

View File

@@ -29,7 +29,6 @@ export default function ProfileDetailBlocked() {
const fetchData = async () => { const fetchData = async () => {
const response = await apiGetBlockedById({ id: String(id) }); const response = await apiGetBlockedById({ id: String(id) });
// console.log("[RESPONSE >>]", JSON.stringify(response, null, 2));
setData(response.data); setData(response.data);
}; };

View File

@@ -1,59 +1,6 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { import Voting_ScreenContribution from "@/screens/Voting/ScreenContribution";
LoaderCustom,
TextCustom,
ViewWrapper
} from "@/components";
import { useAuth } from "@/hooks/use-auth";
import Voting_BoxPublishSection from "@/screens/Voting/BoxPublishSection";
import { apiVotingGetAll } from "@/service/api-client/api-voting";
import { useFocusEffect } from "expo-router";
import _ from "lodash";
import { useState, useCallback } from "react";
export default function VotingContribution() { export default function VotingContribution() {
const { user } = useAuth(); return <Voting_ScreenContribution />;
const [listData, setListData] = useState<any>([]);
const [loadingGetData, setLoadingGetData] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [])
);
const onLoadData = async () => {
try {
setLoadingGetData(true);
const response = await apiVotingGetAll({
category: "contribution",
authorId: user?.id as string,
});
if (response.success) {
setListData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingGetData(false);
}
};
return (
<ViewWrapper hideFooter>
{loadingGetData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Tidak ada kontribusi</TextCustom>
) : listData.map((item: any, index: number) => (
<Voting_BoxPublishSection
data={item}
key={index}
href={`/voting/${item.id}/contribution`}
/>
))}
</ViewWrapper>
);
} }

View File

@@ -1,77 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { LoaderCustom, TextCustom, ViewWrapper } from "@/components"; import Voting_ScreenHistory from "@/screens/Voting/ScreenHistory";
import TabsTwoButtonCustom from "@/components/_ShareComponent/TabsTwoHeaderCustom";
import Voting_BoxPublishSection from "@/screens/Voting/BoxPublishSection";
import { useAuth } from "@/hooks/use-auth";
import { useCallback, useState } from "react";
import { apiVotingGetAll } from "@/service/api-client/api-voting";
import { useFocusEffect } from "expo-router";
import _ from "lodash";
export default function VotingHistory() { export default function VotingHistory() {
const { user } = useAuth();
const [activeCategory, setActiveCategory] = useState<string | null>("all");
const [listData, setListData] = useState<any>([]);
const [loadingGetData, setLoadingGetData] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [activeCategory])
);
const onLoadData = async () => {
try {
setLoadingGetData(true);
const response = await apiVotingGetAll({
category: activeCategory === "all" ? "all-history" : "my-history",
authorId: user?.id as string,
});
if (response.success) {
setListData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingGetData(false);
}
};
const handlePress = (item: any) => {
setActiveCategory(item);
// tambahkan logika lain seperti filter dsb.
};
return ( return (
<ViewWrapper <>
hideFooter <Voting_ScreenHistory />
headerComponent={ </>
<TabsTwoButtonCustom
leftValue="all"
rightValue="main"
leftText="Semua Riwayat"
rightText="Riwayat Saya"
activeCategory={activeCategory}
handlePress={handlePress}
/>
}
>
{loadingGetData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Tidak ada riwayat</TextCustom>
) : (
listData.map((item: any, index: number) => (
<Voting_BoxPublishSection
key={index}
id={item.id}
data={item}
href={`/voting/${item.id}/history`}
/>
))
)}
</ViewWrapper>
); );
} }

View File

@@ -1,71 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { import Voting_ScreenBeranda from "@/screens/Voting/ScreenBeranda";
FloatingButton,
LoaderCustom,
SearchInput,
TextCustom,
ViewWrapper,
} from "@/components";
import { useAuth } from "@/hooks/use-auth";
import Voting_BoxPublishSection from "@/screens/Voting/BoxPublishSection";
import { apiVotingGetAll } from "@/service/api-client/api-voting";
import { router, useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
export default function VotingBeranda() { export default function VotingBeranda() {
const { user } = useAuth();
const [listData, setListData] = useState<any>([]);
const [loadingGetData, setLoadingGetData] = useState(false);
const [search, setSearch] = useState("");
useFocusEffect(
useCallback(() => {
onLoadData();
}, [search])
);
const onLoadData = async () => {
try {
setLoadingGetData(true);
const response = await apiVotingGetAll({
search,
category: "beranda",
userLoginId: user?.id,
});
if (response.success) {
setListData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingGetData(false);
}
};
return ( return (
<ViewWrapper <>
hideFooter <Voting_ScreenBeranda />
floatingButton={ </>
<FloatingButton onPress={() => router.push("/voting/create")} />
}
headerComponent={
<SearchInput placeholder="Cari voting" onChangeText={setSearch} />
}
>
{loadingGetData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Tidak ada data</TextCustom>
) : (
listData.map((item: any, index: number) => (
<Voting_BoxPublishSection
data={item}
key={index}
href={`/voting/${item.id}`}
/>
))
)}
</ViewWrapper>
); );
} }

View File

@@ -51,8 +51,6 @@ export default function VotingDetailStatus() {
setLoadingGetData(true); setLoadingGetData(true);
const response = await apiVotingGetOne({ id: id as string }); const response = await apiVotingGetOne({ id: id as string });
console.log("[DATA BY ID]", JSON.stringify(response, null, 2));
if (response.success) { if (response.success) {
setData(response.data); setData(response.data);
} }

View File

@@ -5,13 +5,14 @@ import {
ButtonCustom, ButtonCustom,
CenterCustom, CenterCustom,
LoaderCustom, LoaderCustom,
NewWrapper,
Spacing, Spacing,
StackCustom, StackCustom,
TextAreaCustom, TextAreaCustom,
TextCustom, TextCustom,
TextInputCustom, TextInputCustom
ViewWrapper,
} from "@/components"; } from "@/components";
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
import DateTimePickerCustom from "@/components/DateInput/DateTimePickerCustom"; import DateTimePickerCustom from "@/components/DateInput/DateTimePickerCustom";
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_XLARGE } from "@/constants/constans-value"; import { ICON_SIZE_XLARGE } from "@/constants/constans-value";
@@ -34,7 +35,7 @@ interface IEditData {
Voting_DaftarNamaVote?: [ Voting_DaftarNamaVote?: [
{ {
value?: string; value?: string;
} },
]; ];
} }
@@ -47,7 +48,7 @@ export default function VotingEdit() {
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
onLoadData(); onLoadData();
}, [id]) }, [id]),
); );
const onLoadData = async () => { const onLoadData = async () => {
@@ -188,9 +189,9 @@ export default function VotingEdit() {
}; };
return ( return (
<ViewWrapper footerComponent={buttonSubmit()}> <NewWrapper footerComponent={buttonSubmit()}>
{loadingGetData ? ( {loadingGetData ? (
<LoaderCustom /> <ListSkeletonComponent />
) : ( ) : (
<StackCustom gap={"xs"}> <StackCustom gap={"xs"}>
<TextInputCustom <TextInputCustom
@@ -210,7 +211,7 @@ export default function VotingEdit() {
onChangeText={(text) => setData({ ...data, deskripsi: text })} onChangeText={(text) => setData({ ...data, deskripsi: text })}
/> />
<Spacing />
<DateTimePickerCustom <DateTimePickerCustom
minimumDate={new Date(Date.now())} minimumDate={new Date(Date.now())}
@@ -255,7 +256,7 @@ export default function VotingEdit() {
} }
</TextCustom> </TextCustom>
)} )}
<Spacing />
</StackCustom> </StackCustom>
{data?.Voting_DaftarNamaVote?.map((item: any, index: number) => ( {data?.Voting_DaftarNamaVote?.map((item: any, index: number) => (
@@ -270,7 +271,7 @@ export default function VotingEdit() {
...(data as any), ...(data as any),
Voting_DaftarNamaVote: data?.Voting_DaftarNamaVote?.map( Voting_DaftarNamaVote: data?.Voting_DaftarNamaVote?.map(
(item: any, i: any) => (item: any, i: any) =>
i === index ? { ...item, value } : item i === index ? { ...item, value } : item,
), ),
}) })
} }
@@ -327,6 +328,6 @@ export default function VotingEdit() {
<Spacing /> <Spacing />
</StackCustom> </StackCustom>
)} )}
</ViewWrapper> </NewWrapper>
); );
} }

View File

@@ -1,69 +1,6 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { import Voting_ScreenListOfContributor from "@/screens/Voting/ScreenListOfContributor";
AvatarUsernameAndOtherComponent,
BadgeCustom,
BaseBox,
LoaderCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import { apiVotingContribution } from "@/service/api-client/api-voting";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
export default function Voting_ListOfContributor() { export default function VotingListOfContributor() {
const { id } = useLocalSearchParams(); return <Voting_ScreenListOfContributor />;
const [listData, setListData] = useState<any>([]);
const [isLoadData, setIsLoadData] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadList();
}, [id])
);
const onLoadList = async () => {
try {
setIsLoadData(true);
const response = await apiVotingContribution({
id: id as string,
authorId: "",
category: "list",
});
if (response.success) {
setListData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoadData(false);
}
};
return (
<ViewWrapper>
{isLoadData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Tidak ada kontributor</TextCustom>
) : (
listData.map((item: any, index: number) => (
<BaseBox paddingTop={5} paddingBottom={5} key={index.toString()}>
<AvatarUsernameAndOtherComponent
avatar={item?.Author?.Profile?.imageId || ""}
name={item?.Author?.username || "Username"}
avatarHref={`/profile/${item?.Author?.Profile?.id}`}
rightComponent={
<BadgeCustom style={{ alignSelf: "flex-end" }}>
{item?.Voting_DaftarNamaVote?.value}
</BadgeCustom>
}
/>
</BaseBox>
))
)}
</ViewWrapper>
);
} }

View File

@@ -80,9 +80,7 @@ export default function VotingCreate() {
type: "success", type: "success",
text1: "Data berhasil disimpan", text1: "Data berhasil disimpan",
}); });
router.replace( router.replace("/voting/(tabs)/status?status=review");
"/(application)/(user)/voting/(tabs)/status?status=review",
);
} else { } else {
Toast.show({ Toast.show({
type: "error", type: "error",

View File

@@ -1,8 +1,8 @@
<!-- ===================== Start Penerapan Pagination ===================== --> <!-- ===================== Start Penerapan Pagination ===================== -->
File utama: screens/Voting/ScreenStatus.tsx File utama: screens/Invesment/ScreenTransaction.tsx
Function fecth: apiVotingGetByStatus Function fecth: apiInvestmentGetInvoice
File function fetch: service/api-client/api-voting.ts File function fetch: service/api-client/api-investment.ts
File komponen wrapper: components/_ShareComponent/NewWrapper.tsx File komponen wrapper: components/_ShareComponent/NewWrapper.tsx
Terapkan pagination pada file "File utama" Terapkan pagination pada file "File utama"
@@ -15,6 +15,7 @@ Jika tidak ada props page maka tambahkan props page dan default page: "1"
Gunakan bahasa indonesia pada cli agar saya mudah membacanya. Gunakan bahasa indonesia pada cli agar saya mudah membacanya.
<!-- Additional Prompt -->
File refrensi: screens/Event/ScreenStatus.tsx File refrensi: screens/Event/ScreenStatus.tsx
Anda bisa menggunakan refrensi dari "File refrensi" jika butuh pemahaman dengan tipe fitur yang sama Anda bisa menggunakan refrensi dari "File refrensi" jika butuh pemahaman dengan tipe fitur yang sama
@@ -23,4 +24,12 @@ Anda bisa menggunakan refrensi dari "File refrensi" jika butuh pemahaman dengan
<!-- Start Penerapan NewWrapper --> <!-- Start Penerapan NewWrapper -->
Terapkan NewWrapper pada file: screens/Forum/DetailForum.tsx Terapkan NewWrapper pada file: screens/Forum/DetailForum.tsx
Component yang digunakan: components/_ShareComponent/NewWrapper.tsx , karena ini adalah halaman detail saya ingin anda fokus pada props pada NewWrapper. Seperti Component yang digunakan: components/_ShareComponent/NewWrapper.tsx , karena ini adalah halaman detail saya ingin anda fokus pada props pada NewWrapper. Seperti
<!-- --> <!-- -->
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 ?

View File

@@ -12,7 +12,7 @@ import {
import { IconPlus } from "@/components/_Icon"; import { IconPlus } from "@/components/_Icon";
import { IconDot } from "@/components/_Icon/IconComponent"; import { IconDot } from "@/components/_Icon/IconComponent";
import { AccentColor, MainColor } from "@/constants/color-palet"; import { AccentColor, MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value"; import { ICON_SIZE_SMALL, PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers"; import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import { useNotificationStore } from "@/hooks/use-notification-store"; import { useNotificationStore } from "@/hooks/use-notification-store";
@@ -26,8 +26,6 @@ import _ from "lodash";
import { useState } from "react"; import { useState } from "react";
import { RefreshControl, View } from "react-native"; import { RefreshControl, View } from "react-native";
const PAGE_SIZE = 10;
const selectedCategory = (value: string) => { const selectedCategory = (value: string) => {
const category = listOfcategoriesAppNotification.find( const category = listOfcategoriesAppNotification.find(
(c) => c.value === value, (c) => c.value === value,
@@ -35,6 +33,37 @@ const selectedCategory = (value: string) => {
return category?.label; return category?.label;
}; };
const fixPath = ({
deepLink,
categoryApp,
}: {
deepLink: string;
categoryApp: string;
}) => {
// Jika categoryApp adalah "OTHER", kembalikan deepLink tanpa perubahan
if (categoryApp === "OTHER") {
return deepLink;
}
// Jika dalam deepLink terdapat "/admin/", kembalikan path tersebut tanpa modifikasi tambahan
if (deepLink.includes("/admin/")) {
return deepLink;
}
console.log("Category App", categoryApp);
console.log("Deep Link", deepLink);
const separator = deepLink.includes("?") ? "&" : "?";
const fixedPath = `${deepLink}${separator}from=notifications&category=${_.lowerCase(
categoryApp,
)}`;
console.log("Fix Path", fixedPath);
return fixedPath;
};
const BoxNotification = ({ const BoxNotification = ({
data, data,
activeCategory, activeCategory,
@@ -50,17 +79,22 @@ const BoxNotification = ({
<BaseBox <BaseBox
backgroundColor={data.isRead ? AccentColor.darkblue : AccentColor.blue} backgroundColor={data.isRead ? AccentColor.darkblue : AccentColor.blue}
onPress={() => { onPress={() => {
console.log( const newPath = fixPath({
"Notification >", deepLink: data.deepLink,
selectedCategory(activeCategory as string), categoryApp: data.kategoriApp,
); });
router.push(data.deepLink);
markAsRead(data.id); selectedCategory(activeCategory as string);
setListData((prev: any) => router.navigate(newPath as any);
prev.map((item: any) =>
item.id === data.id ? { ...item, isRead: true } : item, if (!data.isRead) {
), markAsRead(data.id);
); setListData((prev: any) =>
prev.map((item: any) =>
item.id === data.id ? { ...item, isRead: true } : item,
),
);
}
}} }}
> >
<StackCustom> <StackCustom>
@@ -97,7 +131,7 @@ export default function Admin_ScreenNotification2() {
page: String(page), page: String(page),
}); });
}, },
pageSize: PAGE_SIZE, pageSize: PAGINATION_DEFAULT_TAKE,
dependencies: [user?.id, activeCategory], dependencies: [user?.id, activeCategory],
onError: (error) => onError: (error) =>
console.error("[ERROR] Fetch admin notifications:", error), console.error("[ERROR] Fetch admin notifications:", error),
@@ -110,7 +144,7 @@ export default function Admin_ScreenNotification2() {
refreshing: pagination.refreshing, refreshing: pagination.refreshing,
listData: pagination.listData, listData: pagination.listData,
emptyMessage: "Belum ada notifikasi", emptyMessage: "Belum ada notifikasi",
skeletonCount: 5, skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 100, skeletonHeight: 100,
}); });

View File

@@ -13,8 +13,8 @@ import { useAuth } from "@/hooks/use-auth";
import { usePagination } from "@/hooks/use-pagination"; import { usePagination } from "@/hooks/use-pagination";
import { dummyMasterStatus } from "@/lib/dummy-data/_master/status"; import { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
import { apiEventGetByStatus } from "@/service/api-client/api-event"; import { apiEventGetByStatus } from "@/service/api-client/api-event";
import { useLocalSearchParams } from "expo-router"; import { useFocusEffect, useLocalSearchParams } from "expo-router";
import { useState } from "react"; import { useCallback, useState } from "react";
import { RefreshControl, View } from "react-native"; import { RefreshControl, View } from "react-native";
export default function Event_ScreenStatus() { export default function Event_ScreenStatus() {
@@ -84,6 +84,12 @@ export default function Event_ScreenStatus() {
pagination.reset(); pagination.reset();
}; };
useFocusEffect(
useCallback(() => {
pagination.onRefresh();
}, [activeCategory])
);
const tabsComponent = ( const tabsComponent = (
<ScrollableCustom <ScrollableCustom
data={dummyMasterStatus.map((e, i) => ({ data={dummyMasterStatus.map((e, i) => ({

View File

@@ -16,6 +16,9 @@ export default function Investment_ButtonStatusSection({
status: string; status: string;
buttonPublish?: React.ReactNode; buttonPublish?: React.ReactNode;
}) { }) {
const path : any= (status: string) => {
return `/investment/(tabs)/portofolio?status=${status}`;
};
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const handleBatalkanReview = () => { const handleBatalkanReview = () => {
AlertDefaultSystem({ AlertDefaultSystem({
@@ -30,13 +33,13 @@ export default function Investment_ButtonStatusSection({
id: id as string, id: id as string,
status: "draft", status: "draft",
}); });
console.log("[RESPONSE]", JSON.stringify(response, null, 2));
if (response.success) { if (response.success) {
Toast.show({ Toast.show({
type: "success", type: "success",
text1: "Berhasil Batalkan Review", text1: "Berhasil Batalkan Review",
}); });
router.back(); router.replace(path("draft"));
} else { } else {
Toast.show({ Toast.show({
type: "error", type: "error",
@@ -65,13 +68,13 @@ export default function Investment_ButtonStatusSection({
id: id as string, id: id as string,
status: "review", status: "review",
}); });
console.log("[RESPONSE]", JSON.stringify(response, null, 2));
if (response.success) { if (response.success) {
Toast.show({ Toast.show({
type: "success", type: "success",
text1: "Berhasil Ajukan Review", text1: "Berhasil Ajukan Review",
}); });
router.back(); router.replace(path("review"));
} else { } else {
Toast.show({ Toast.show({
type: "error", type: "error",
@@ -100,13 +103,13 @@ export default function Investment_ButtonStatusSection({
id: id as string, id: id as string,
status: "draft", status: "draft",
}); });
console.log("[RESPONSE]", JSON.stringify(response, null, 2));
if (response.success) { if (response.success) {
Toast.show({ Toast.show({
type: "success", type: "success",
text1: "Berhasil Update Status", text1: "Berhasil Update Status",
}); });
router.back(); router.replace(path("draft"));
} else { } else {
Toast.show({ Toast.show({
type: "error", type: "error",
@@ -135,8 +138,6 @@ export default function Investment_ButtonStatusSection({
id: id as string, id: id as string,
}); });
console.log("[RESPONSE DELETE]", JSON.stringify(response, null, 2));
if (response.success) { if (response.success) {
Toast.show({ Toast.show({
type: "success", type: "success",

View File

@@ -16,10 +16,7 @@ export default function Investment_BoxDetailDocument({
<Grid> <Grid>
<Grid.Col span={leftIcon ? 10 : 12}> <Grid.Col span={leftIcon ? 10 : 12}>
<ClickableCustom onPress={() => router.push(href as any)}> <ClickableCustom onPress={() => router.push(href as any)}>
<TextCustom truncate> <TextCustom truncate>{title || "-"}</TextCustom>
{title ||
`Judul Dokumen: Lorem, ipsum dolor sit amet consectetur adipisicing elit.`}
</TextCustom>
</ClickableCustom> </ClickableCustom>
</Grid.Col> </Grid.Col>
{leftIcon && <Grid.Col span={2}>{leftIcon}</Grid.Col>} {leftIcon && <Grid.Col span={2}>{leftIcon}</Grid.Col>}

View File

@@ -0,0 +1,76 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { TextCustom } from "@/components";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { usePagination } from "@/hooks/use-pagination";
import Investment_BoxDetailDocument from "@/screens/Invesment/Document/RecapBoxDetail";
import { apiInvestmentGetDocument } from "@/service/api-client/api-investment";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback } from "react";
import { RefreshControl } from "react-native";
export default function Investment_ScreenListOfDocument() {
const { id } = useLocalSearchParams();
console.log("ID >> ", id);
// Setup pagination
const pagination = usePagination({
fetchFunction: async (page) => {
if (!id) return { data: [] };
return await apiInvestmentGetDocument({
id: id as string,
category: "all-document",
page: String(page),
});
},
pageSize: PAGINATION_DEFAULT_TAKE,
dependencies: [id],
onError: (error) => console.error("[ERROR] Fetch document:", error),
});
// Generate komponen
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
emptyMessage: "Tidak ada dokumen",
skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 100,
});
// Render item dokumen
const renderDocumentItem = ({ item }: { item: any }) => (
<Investment_BoxDetailDocument
key={item.id}
title={item.title}
href={`/(file)/${item.fileId}`}
/>
);
useFocusEffect(
useCallback(() => {
pagination.onRefresh();
}, [id]),
);
return (
<NewWrapper
hideFooter
listData={pagination.listData}
renderItem={renderDocumentItem}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
/>
}
onEndReached={pagination.loadMore}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
/>
);
}

View File

@@ -0,0 +1,233 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
AlertDefaultSystem,
BackButton,
DotButton,
DrawerCustom,
MenuDrawerDynamicGrid,
TextCustom,
} from "@/components";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { IconEdit } from "@/components/_Icon";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { usePagination } from "@/hooks/use-pagination";
import Investment_BoxDetailDocument from "@/screens/Invesment/Document/RecapBoxDetail";
import {
apiInvestmentDeleteDocument,
apiInvestmentGetDocument,
} from "@/service/api-client/api-investment";
import { AntDesign, Ionicons } from "@expo/vector-icons";
import {
router,
Stack,
useFocusEffect,
useLocalSearchParams,
} from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import { RefreshControl } from "react-native";
import Toast from "react-native-toast-message";
export default function Investment_ScreenRecap() {
const { id } = useLocalSearchParams();
const [openDrawer, setOpenDrawer] = useState(false);
const [openDrawerBox, setOpenDrawerBox] = useState(false);
const [selectId, setSelectId] = useState<string | null>(null);
// Setup pagination
const pagination = usePagination({
fetchFunction: async (page) => {
if (!id) return { data: [] };
return await apiInvestmentGetDocument({
id: id as string,
category: "all-document",
page: String(page),
});
},
pageSize: PAGINATION_DEFAULT_TAKE,
dependencies: [id],
onError: (error) => console.error("[ERROR] Fetch document:", error),
});
// Generate komponen
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
emptyMessage: "Tidak ada dokumen",
skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 100,
});
const handlerDeleteDocument = async () => {
try {
const response = await apiInvestmentDeleteDocument({
id: selectId as string,
});
if (response.success) {
Toast.show({
type: "success",
text1: "Data berhasil dihapus",
});
// Hapus item dari list
pagination.setListData((prev: any) => {
if (!prev) return null;
return prev.filter((item: any) => item.id !== selectId);
});
pagination.onRefresh();
setOpenDrawerBox(false);
setSelectId(null);
}
} catch (error) {
console.log("[ERROR]", error);
Toast.show({
type: "error",
text1: "Gagal menghapus data",
});
}
};
useFocusEffect(
useCallback(() => {
pagination.onRefresh();
}, []),
);
// Render item dokumen
const renderDocumentItem = ({ item }: { item: any }) => (
<Investment_BoxDetailDocument
key={item.id}
title={item.title}
leftIcon={
<Ionicons
name="ellipsis-horizontal-outline"
size={ICON_SIZE_SMALL}
color={MainColor.white}
style={{
zIndex: 10,
alignSelf: "flex-end",
}}
onPress={() => {
setSelectId(item.id);
setOpenDrawerBox(true);
}}
/>
}
href={`/(file)/${item.fileId}`}
/>
);
return (
<>
<Stack.Screen
options={{
title: "Rekap Dokumen",
headerLeft: () => <BackButton />,
headerRight: () => (
<DotButton
onPress={() => {
setOpenDrawer(true);
setOpenDrawerBox(false);
}}
/>
),
}}
/>
<NewWrapper
hideFooter
listData={pagination.listData}
renderItem={renderDocumentItem}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
/>
}
onEndReached={pagination.loadMore}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
/>
{/* Drawer On Header */}
<DrawerCustom
isVisible={openDrawer}
closeDrawer={() => setOpenDrawer(false)}
height={"auto"}
>
<MenuDrawerDynamicGrid
data={[
{
icon: (
<AntDesign
name="plus-circle"
size={ICON_SIZE_SMALL}
color={MainColor.white}
/>
),
label: "Tambah Dokumen",
path: `/investment/${id}/(document)/add-document`,
},
]}
onPressItem={(item) => {
router.push(item.path as any);
setOpenDrawer(false);
}}
/>
</DrawerCustom>
{/* Drawer On Box */}
<DrawerCustom
isVisible={openDrawerBox}
closeDrawer={() => setOpenDrawerBox(false)}
height={"auto"}
>
<MenuDrawerDynamicGrid
data={[
{
icon: <IconEdit />,
label: "Edit Dokumen",
path: `/investment/${selectId}/(document)/edit-document`,
},
{
icon: (
<Ionicons
name="trash-outline"
size={ICON_SIZE_SMALL}
color={MainColor.white}
/>
),
label: "Hapus Dokumen",
path: "" as any,
color: MainColor.red,
},
]}
onPressItem={(item) => {
if (item.path === ("" as any)) {
AlertDefaultSystem({
title: "Hapus Dokumen",
message: "Apakah anda yakin ingin menghapus dokumen ini?",
textLeft: "Batal",
textRight: "Hapus",
onPressRight: () => {
handlerDeleteDocument();
},
});
} else {
router.push(item.path as any);
}
setOpenDrawerBox(false);
}}
/>
</DrawerCustom>
</>
);
}

View File

@@ -0,0 +1,67 @@
import {
FloatingButton,
TextCustom,
} from "@/components";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { usePagination } from "@/hooks/use-pagination";
import Investment_BoxBerandaSection from "@/screens/Invesment/BoxBerandaSection";
import { apiInvestmentGetAll } from "@/service/api-client/api-investment";
import { router } from "expo-router";
import _ from "lodash";
import { RefreshControl } from "react-native";
export default function Investment_ScreenBursa() {
// Setup pagination
const pagination = usePagination({
fetchFunction: async (page) => {
return await apiInvestmentGetAll({
category: "bursa",
page: String(page),
});
},
pageSize: PAGINATION_DEFAULT_TAKE,
dependencies: [],
onError: (error) => console.error("[ERROR] Fetch investment bursa:", error),
});
// Generate komponen
const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
emptyMessage: "Belum ada investasi",
skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 100,
});
// Render item investment
const renderInvestmentItem = ({ item }: { item: any }) => (
<Investment_BoxBerandaSection
key={item.id}
id={item.id}
data={item}
/>
);
return (
<NewWrapper
listData={pagination.listData}
renderItem={renderInvestmentItem}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
/>
}
onEndReached={pagination.loadMore}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
hideFooter
floatingButton={
<FloatingButton onPress={() => router.push("/investment/create")} />
}
/>
);
}

View File

@@ -0,0 +1,102 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { ScrollableCustom, TextCustom } from "@/components";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { useAuth } from "@/hooks/use-auth";
import { usePagination } from "@/hooks/use-pagination";
import { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
import Investment_StatusBox from "@/screens/Invesment/StatusBox";
import { apiInvestmentGetByStatus } from "@/service/api-client/api-investment";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, useState } from "react";
import { RefreshControl } from "react-native";
export default function Investment_ScreenPortofolio() {
const { user } = useAuth();
const { status } = useLocalSearchParams<{ status?: string }>();
const [activeCategory, setActiveCategory] = useState<string | null>(
status || "publish",
);
// Setup pagination
const pagination = usePagination({
fetchFunction: async (page) => {
if (!user?.id) return { data: [] };
return await apiInvestmentGetByStatus({
authorId: user.id,
status: activeCategory!,
page: String(page),
});
},
pageSize: PAGINATION_DEFAULT_TAKE,
dependencies: [user?.id, activeCategory],
onError: (error) =>
console.error("[ERROR] Fetch investment by status:", error),
});
// Generate komponen
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
emptyMessage: `Tidak ada data ${activeCategory}`,
skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 150,
});
// Render item investment
const renderInvestmentItem = ({ item }: { item: any }) => (
<Investment_StatusBox
key={item.id}
data={item}
status={activeCategory as string}
href={`/investment/${item.id}/${activeCategory}/detail`}
/>
);
const handlePress = (item: any) => {
setActiveCategory(item.value);
// Reset pagination saat kategori berubah
pagination.reset();
};
useFocusEffect(
useCallback(() => {
pagination.onRefresh();
}, [activeCategory]),
);
const tabsComponent = (
<ScrollableCustom
data={dummyMasterStatus.map((e, i) => ({
id: i,
label: e.label,
value: e.value,
}))}
onButtonPress={handlePress}
activeId={activeCategory as any}
/>
);
return (
<NewWrapper
hideFooter
headerComponent={tabsComponent}
listData={pagination.listData}
renderItem={renderInvestmentItem}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
/>
}
onEndReached={pagination.loadMore}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
/>
);
}

View File

@@ -0,0 +1,144 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BadgeCustom,
BaseBox,
Grid,
Spacing,
StackCustom,
TextCustom,
} from "@/components";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import NoDataText from "@/components/_ShareComponent/NoDataText";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { useAuth } from "@/hooks/use-auth";
import { usePagination } from "@/hooks/use-pagination";
import { apiInvestmentGetInvoice } from "@/service/api-client/api-investment";
import { GStyles } from "@/styles/global-styles";
import { formatChatTime } from "@/utils/formatChatTime";
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
import { router, useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback } from "react";
import { RefreshControl, View } from "react-native";
export default function Investment_ScreenTransaction() {
const { user } = useAuth();
// Setup pagination
const pagination = usePagination({
fetchFunction: async (page) => {
if (!user?.id) return { data: [] };
return await apiInvestmentGetInvoice({
authorId: user.id,
category: "transaction",
page: String(page),
});
},
pageSize: PAGINATION_DEFAULT_TAKE,
dependencies: [user?.id],
onError: (error) => console.error("[ERROR] Fetch transaction:", error),
});
// Generate komponen
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
emptyMessage: "Tidak ada transaksi",
skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 100,
});
const handlerColor = (status: string) => {
if (status === "menunggu") {
return "orange";
} else if (status === "proses") {
return "white";
} else if (status === "berhasil") {
return "green";
} else if (status === "gagal") {
return "red";
}
};
const handlePress = ({ id, status }: { id: string; status: string }) => {
if (status === "menunggu") {
router.push(`/investment/${id}/(transaction-flow)/invoice`);
} else if (status === "proses") {
router.push(`/investment/${id}/(transaction-flow)/process`);
} else if (status === "berhasil") {
router.push(`/investment/${id}/(transaction-flow)/success`);
} else if (status === "gagal") {
router.push(`/investment/${id}/(transaction-flow)/failed`);
}
};
// Render item transaksi
const renderTransactionItem = ({ item }: { item: any }) => (
<BaseBox
key={item.id}
paddingTop={7}
paddingBottom={7}
onPress={() => {
handlePress({
id: item.id,
status: _.lowerCase(item.statusInvoice),
});
}}
>
<Grid>
<Grid.Col span={6}>
<StackCustom gap={"xs"}>
<TextCustom truncate>{item?.title || "-"}</TextCustom>
<TextCustom color="gray" size="small">
{formatChatTime(item?.createdAt)}
</TextCustom>
</StackCustom>
</Grid.Col>
<Grid.Col span={1}>
<View />
</Grid.Col>
<Grid.Col span={5} style={{ alignItems: "flex-end" }}>
<StackCustom gap={"xs"}>
<TextCustom bold truncate>
Rp. {formatCurrencyDisplay(item?.nominal) || "-"}
</TextCustom>
<BadgeCustom
variant="light"
color={handlerColor(_.lowerCase(item.statusInvoice))}
style={GStyles.alignSelfFlexEnd}
>
{item?.statusInvoice || "-"}
</BadgeCustom>
</StackCustom>
</Grid.Col>
</Grid>
</BaseBox>
);
useFocusEffect(
useCallback(() => {
pagination.onRefresh();
}, [user?.id])
);
return (
<NewWrapper
hideFooter
listData={pagination.listData}
renderItem={renderTransactionItem}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
/>
}
onEndReached={pagination.loadMore}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
/>
);
}

View File

@@ -20,6 +20,9 @@ export default function Job_ButtonStatusSection({
onSetLoading: (value: boolean) => void; onSetLoading: (value: boolean) => void;
isArchive?: boolean; isArchive?: boolean;
}) { }) {
const path : any =(status: string) => {
return `/job/(tabs)/status?status=${status}`
}
const handleBatalkanReview = () => { const handleBatalkanReview = () => {
AlertDefaultSystem({ AlertDefaultSystem({
title: "Batalkan Review", title: "Batalkan Review",
@@ -39,7 +42,7 @@ export default function Job_ButtonStatusSection({
type: "success", type: "success",
text1: response.message, text1: response.message,
}); });
router.back(); router.replace(path("draft"));
} else { } else {
Toast.show({ Toast.show({
type: "info", type: "info",
@@ -76,14 +79,14 @@ export default function Job_ButtonStatusSection({
type: "success", type: "success",
text1: response.message, text1: response.message,
}); });
router.back(); router.replace(path("review"));
} else { } else {
Toast.show({ Toast.show({
type: "info", type: "info",
text1: "Info", text1: "Info",
text2: response.message, text2: response.message,
}); });
router.back(); router.back()
} }
} catch (error) { } catch (error) {
console.log("[ERROR]", error); console.log("[ERROR]", error);
@@ -113,14 +116,14 @@ export default function Job_ButtonStatusSection({
type: "success", type: "success",
text1: response.message, text1: response.message,
}); });
router.back(); router.replace(path("draft"));
} else { } else {
Toast.show({ Toast.show({
type: "info", type: "info",
text1: "Info", text1: "Info",
text2: response.message, text2: response.message,
}); });
router.back(); router. back();
} }
} catch (error) { } catch (error) {
console.log("[ERROR]", error); console.log("[ERROR]", error);

View File

@@ -62,19 +62,17 @@ export default function Job_MainViewStatus2() {
</BaseBox> </BaseBox>
); );
// useFocusEffect(
// useCallback(() => {
// // Reset and load first page when category changes
// pagination.reset();
// // pagination.onRefresh();
// }, [activeCategory]),
// );
const handlePress = (item: any) => { const handlePress = (item: any) => {
setActiveCategory(item.value); setActiveCategory(item.value);
// Reset pagination saat kategori berubah // Reset pagination saat kategori berubah
pagination.reset(); pagination.reset();
}; };
useFocusEffect(
useCallback(() => {
pagination.onRefresh();
}, [activeCategory])
);
const scrollComponent = ( const scrollComponent = (
<ScrollableCustom <ScrollableCustom

View File

@@ -52,6 +52,9 @@ const fixPath = ({
return deepLink; return deepLink;
} }
console.log("Category App", categoryApp);
console.log("Deep Link", deepLink);
const separator = deepLink.includes("?") ? "&" : "?"; const separator = deepLink.includes("?") ? "&" : "?";
const fixedPath = `${deepLink}${separator}from=notifications&category=${_.lowerCase( const fixedPath = `${deepLink}${separator}from=notifications&category=${_.lowerCase(
@@ -83,8 +86,8 @@ const BoxNotification = ({
categoryApp: data.kategoriApp, categoryApp: data.kategoriApp,
}); });
router.navigate(newPath as any);
selectedCategory(activeCategory as string); selectedCategory(activeCategory as string);
router.navigate(newPath as any);
if (!data.isRead) { if (!data.isRead) {
markAsRead(data.id); markAsRead(data.id);
@@ -112,7 +115,7 @@ const BoxNotification = ({
); );
}; };
export default function ScreenNotification() { export default function ScreenNotification_V2() {
const { user } = useAuth(); const { user } = useAuth();
const { category } = useLocalSearchParams<{ category?: string }>(); const { category } = useLocalSearchParams<{ category?: string }>();
const [activeCategory, setActiveCategory] = useState<string | null>( const [activeCategory, setActiveCategory] = useState<string | null>(
@@ -140,7 +143,7 @@ export default function ScreenNotification() {
// useFocusEffect( // useFocusEffect(
// useCallback(() => { // useCallback(() => {
// // Reset and load first page when category changes // // Reset and load first page when category changes
// pagination.reset();
// pagination.onRefresh(); // pagination.onRefresh();
// }, [activeCategory]), // }, [activeCategory]),
// ); // );
@@ -171,7 +174,7 @@ export default function ScreenNotification() {
listData: pagination.listData, listData: pagination.listData,
isInitialLoad: pagination.isInitialLoad, isInitialLoad: pagination.isInitialLoad,
emptyMessage: "Belum ada notifikasi", emptyMessage: "Belum ada notifikasi",
skeletonCount: 5, skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 100, skeletonHeight: 100,
}); });
@@ -237,7 +240,6 @@ export default function ScreenNotification() {
}, },
]} ]}
onPressItem={(item: any) => { onPressItem={(item: any) => {
// console.log("Item", item.value);
if (item.value === "read-all") { if (item.value === "read-all") {
AlertDefaultSystem({ AlertDefaultSystem({
title: "Tandai Semua Dibaca", title: "Tandai Semua Dibaca",

View File

@@ -3,7 +3,7 @@ import {
apiVotingDelete, apiVotingDelete,
apiVotingUpdateStatus, apiVotingUpdateStatus,
} from "@/service/api-client/api-voting"; } from "@/service/api-client/api-voting";
import { router } from "expo-router"; import { RelativePathString, router } from "expo-router";
import Toast from "react-native-toast-message"; import Toast from "react-native-toast-message";
export default function Voting_ButtonStatusSection({ export default function Voting_ButtonStatusSection({
@@ -17,6 +17,10 @@ export default function Voting_ButtonStatusSection({
isLoading: boolean; isLoading: boolean;
onSetLoading: (value: boolean) => void; onSetLoading: (value: boolean) => void;
}) { }) {
const path: any = (status: string) => {
return `/voting/(tabs)/status?status=${status}`;
};
const handleBatalkanReview = () => { const handleBatalkanReview = () => {
AlertDefaultSystem({ AlertDefaultSystem({
title: "Batalkan Review", title: "Batalkan Review",
@@ -34,9 +38,9 @@ export default function Voting_ButtonStatusSection({
if (response?.success) { if (response?.success) {
Toast.show({ Toast.show({
type: "success", type: "success",
text1: response.message, text1: "Berhasil batalkan review",
}); });
router.replace(`/voting/${id}/draft/detail`); router.replace(path("draft"));
} else { } else {
Toast.show({ Toast.show({
type: "info", type: "info",
@@ -71,9 +75,9 @@ export default function Voting_ButtonStatusSection({
if (response?.success) { if (response?.success) {
Toast.show({ Toast.show({
type: "success", type: "success",
text1: response.message, text1: "Berhasil ajukan review",
}); });
router.replace(`/voting/${id}/review/detail`); router.replace(path("review"));
} else { } else {
Toast.show({ Toast.show({
type: "info", type: "info",
@@ -108,9 +112,9 @@ export default function Voting_ButtonStatusSection({
if (response?.success) { if (response?.success) {
Toast.show({ Toast.show({
type: "success", type: "success",
text1: response.message, text1: "Berhasil edit kembali",
}); });
router.replace(`/voting/${id}/draft/detail`); router.replace(path("draft"));
} else { } else {
Toast.show({ Toast.show({
type: "info", type: "info",
@@ -135,31 +139,31 @@ export default function Voting_ButtonStatusSection({
textLeft: "Batal", textLeft: "Batal",
textRight: "Hapus", textRight: "Hapus",
onPressRight: async () => { onPressRight: async () => {
try { try {
onSetLoading(true); onSetLoading(true);
const response = await apiVotingDelete({ const response = await apiVotingDelete({
id: id as string, id: id as string,
}); });
if (response?.success) { if (response?.success) {
Toast.show({ Toast.show({
type: "success", type: "success",
text1: response.message, text1: "Berhasil hapus data",
}); });
router.back(); router.back();
} else { } else {
Toast.show({ Toast.show({
type: "info", type: "info",
text1: "Info", text1: "Info",
text2: response.message, text2: response.message,
}); });
router.back(); router.back();
} }
} catch (error) { } catch (error) {
console.log("[ERROR]", error); console.log("[ERROR]", error);
} finally { } finally {
onSetLoading(false); onSetLoading(false);
} }
}, },
}); });
}; };

View File

@@ -0,0 +1,82 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { FloatingButton, SearchInput } from "@/components";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { useAuth } from "@/hooks/use-auth";
import { usePagination } from "@/hooks/use-pagination";
import Voting_BoxPublishSection from "@/screens/Voting/BoxPublishSection";
import { apiVotingGetAll } from "@/service/api-client/api-voting";
import { router } from "expo-router";
import { useMemo, useState } from "react";
import { RefreshControl } from "react-native";
export default function Voting_ScreenBeranda() {
const { user } = useAuth();
const [search, setSearch] = useState("");
const pagination = usePagination({
fetchFunction: async (page, searchQuery) => {
return await apiVotingGetAll({
search: searchQuery || "",
category: "beranda",
userLoginId: user?.id,
page: String(page),
});
},
pageSize: 5,
searchQuery: search,
dependencies: [user?.id],
onError: (error) => console.error("[ERROR] Fetch voting:", error),
});
// Gunakan helper untuk membuat 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",
skeletonCount: 5,
skeletonHeight: 200,
isInitialLoad: pagination.isInitialLoad,
});
// Render item untuk FlatList
const renderItem = useMemo(
() =>
({ item }: { item: any }) =>
(
<Voting_BoxPublishSection
data={item}
key={item.id}
href={`/voting/${item.id}`}
/>
),
[],
);
return (
<NewWrapper
listData={pagination.listData}
renderItem={renderItem}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
onEndReached={pagination.loadMore}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
/>
}
hideFooter
floatingButton={
<FloatingButton onPress={() => router.push("/voting/create")} />
}
headerComponent={
<SearchInput placeholder="Cari voting" onChangeText={setSearch} />
}
/>
);
}

View File

@@ -0,0 +1,70 @@
/* eslint-disable react-hooks/exhaustive-deps */
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { useAuth } from "@/hooks/use-auth";
import { usePagination } from "@/hooks/use-pagination";
import Voting_BoxPublishSection from "@/screens/Voting/BoxPublishSection";
import { apiVotingGetAll } from "@/service/api-client/api-voting";
import { useMemo } from "react";
import { RefreshControl } from "react-native";
export default function Voting_ScreenContribution() {
const { user } = useAuth();
const pagination = usePagination({
fetchFunction: async (page) => {
return await apiVotingGetAll({
category: "contribution",
authorId: user?.id as string,
page: String(page),
});
},
pageSize: 4,
dependencies: [user?.id],
onError: (error) => console.error("[ERROR] Fetch contribution:", error),
});
// Gunakan helper untuk membuat komponen-komponen pagination
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
emptyMessage: "Tidak ada kontribusi",
emptySearchMessage: "Tidak ada hasil pencarian",
skeletonCount: 5,
skeletonHeight: 200,
isInitialLoad: pagination.isInitialLoad,
});
// Render item untuk FlatList
const renderItem = useMemo(
() =>
({ item }: { item: any }) =>
(
<Voting_BoxPublishSection
data={item}
key={item.id}
href={`/voting/${item.id}/contribution`}
/>
),
[],
);
return (
<NewWrapper
listData={pagination.listData}
renderItem={renderItem}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
onEndReached={pagination.loadMore}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
/>
}
hideFooter
/>
);
}

View File

@@ -0,0 +1,114 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { ButtonCustom, Spacing, TextCustom } from "@/components";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { AccentColor, MainColor } from "@/constants/color-palet";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { useAuth } from "@/hooks/use-auth";
import { usePagination } from "@/hooks/use-pagination";
import Voting_BoxPublishSection from "@/screens/Voting/BoxPublishSection";
import { apiVotingGetAll } from "@/service/api-client/api-voting";
import { useState } from "react";
import { RefreshControl, View } from "react-native";
export default function Voting_ScreenHistory() {
const [activeCategory, setActiveCategory] = useState<string | null>("all");
const { user } = useAuth();
// Setup pagination
const pagination = usePagination({
fetchFunction: async (page) => {
return await apiVotingGetAll({
category: activeCategory === "all" ? "all-history" : "my-history",
authorId: user?.id as string,
page: String(page),
});
},
pageSize: PAGINATION_DEFAULT_TAKE,
dependencies: [user?.id, activeCategory],
onError: (error) => console.error("[ERROR] Fetch voting history:", error),
});
// Generate komponen
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
emptyMessage: "Belum ada riwayat",
skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 100,
});
// Render item voting
const renderVotingItem = ({ item }: { item: any }) => (
<Voting_BoxPublishSection
key={item && item?.id}
data={item}
href={`/voting/${item.id}/history`}
/>
);
const handlePress = (item: any) => {
setActiveCategory(item);
// Reset pagination saat kategori berubah
pagination.reset();
};
const headerComponent = (
<View
style={{
flexDirection: "row",
alignItems: "center",
padding: 5,
backgroundColor: MainColor.soft_darkblue,
borderRadius: 50,
width: "100%",
}}
>
<ButtonCustom
backgroundColor={
activeCategory === "all" ? MainColor.yellow : AccentColor.blue
}
textColor={activeCategory === "all" ? MainColor.black : MainColor.white}
style={{ width: "49%" }}
onPress={() => handlePress("all")}
>
Semua Riwayat
</ButtonCustom>
<Spacing width={"2%"} />
<ButtonCustom
backgroundColor={
activeCategory === "main" ? MainColor.yellow : AccentColor.blue
}
textColor={
activeCategory === "main" ? MainColor.black : MainColor.white
}
style={{ width: "49%" }}
onPress={() => handlePress("main")}
>
Riwayat Saya
</ButtonCustom>
</View>
);
return (
<NewWrapper
headerComponent={headerComponent}
listData={pagination.listData}
renderItem={renderVotingItem}
refreshControl={
<RefreshControl
tintColor={MainColor.yellow}
colors={[MainColor.yellow]}
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
/>
}
onEndReached={pagination.loadMore}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
hideFooter
/>
);
}

View File

@@ -0,0 +1,80 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
AvatarUsernameAndOtherComponent,
BadgeCustom,
BaseBox,
Spacing,
TextCustom,
} from "@/components";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { usePagination } from "@/hooks/use-pagination";
import { apiVotingContribution } from "@/service/api-client/api-voting";
import { useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { RefreshControl, View } from "react-native";
export default function Voting_ScreenListOfContributor() {
const { id } = useLocalSearchParams();
// Setup pagination
const pagination = usePagination({
fetchFunction: async (page) => {
return await apiVotingContribution({
id: id as string,
authorId: "",
category: "list",
page: String(page),
});
},
pageSize: PAGINATION_DEFAULT_TAKE,
dependencies: [id],
onError: (error) =>
console.error("[ERROR] Fetch voting contributors:", error),
});
// Generate komponen
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
emptyMessage: "Tidak ada kontributor",
skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 100,
});
// Render item contributor
const renderContributorItem = ({ item }: { item: any }) => (
<BaseBox paddingTop={5} paddingBottom={5} key={item?.id}>
<AvatarUsernameAndOtherComponent
avatar={item?.Author?.Profile?.imageId || ""}
name={item?.Author?.username || "Username"}
avatarHref={`/profile/${item?.Author?.Profile?.id}`}
rightComponent={
<BadgeCustom style={{ alignSelf: "flex-end" }}>
{item?.Voting_DaftarNamaVote?.value}
</BadgeCustom>
}
/>
</BaseBox>
);
return (
<NewWrapper
hideFooter
listData={pagination.listData}
renderItem={renderContributorItem}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
/>
}
onEndReached={pagination.loadMore}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
/>
);
}

View File

@@ -8,17 +8,18 @@ import {
} from "@/components"; } from "@/components";
import NewWrapper from "@/components/_ShareComponent/NewWrapper"; import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers"; import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import { usePagination } from "@/hooks/use-pagination"; import { usePagination } from "@/hooks/use-pagination";
import { dummyMasterStatus } from "@/lib/dummy-data/_master/status"; import { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
import { apiVotingGetByStatus } from "@/service/api-client/api-voting"; import { apiVotingGetByStatus } from "@/service/api-client/api-voting";
import { dateTimeView } from "@/utils/dateTimeView"; import { dateTimeView } from "@/utils/dateTimeView";
import { useLocalSearchParams } from "expo-router"; import { useFocusEffect, useLocalSearchParams } from "expo-router";
import { useState } from "react"; import { useCallback, useState } from "react";
import { RefreshControl, View } from "react-native"; import { RefreshControl, View } from "react-native";
const PAGE_SIZE = 10; const PAGE_SIZE = 6
export default function Voting_ScreenStatus() { export default function Voting_ScreenStatus() {
const { user } = useAuth(); const { user } = useAuth();
@@ -40,7 +41,7 @@ export default function Voting_ScreenStatus() {
page: String(page), page: String(page),
}); });
}, },
pageSize: PAGE_SIZE, pageSize: PAGINATION_DEFAULT_TAKE,
dependencies: [id, activeCategory], dependencies: [id, activeCategory],
onError: (error) => console.error("[ERROR] Fetch voting by status:", error), onError: (error) => console.error("[ERROR] Fetch voting by status:", error),
}); });
@@ -91,6 +92,12 @@ export default function Voting_ScreenStatus() {
pagination.reset(); pagination.reset();
}; };
useFocusEffect(
useCallback(() => {
pagination.onRefresh();
}, [activeCategory])
);
const scrollComponent = ( const scrollComponent = (
<ScrollableCustom <ScrollableCustom
data={dummyMasterStatus.map((e, i) => ({ data={dummyMasterStatus.map((e, i) => ({

View File

@@ -14,13 +14,15 @@ export async function apiInvestmentCreate({ data }: { data: any }) {
export async function apiInvestmentGetByStatus({ export async function apiInvestmentGetByStatus({
authorId, authorId,
status, status,
page = "1",
}: { }: {
authorId: string; authorId: string;
status: string; status: string;
page?: string;
}) { }) {
try { try {
const response = await apiConfig.get( const response = await apiConfig.get(
`/mobile/investment/${authorId}/${status}` `/mobile/investment/${authorId}/${status}?page=${page}`
); );
return response.data; return response.data;
} catch (error) { } catch (error) {
@@ -103,13 +105,15 @@ export async function apiInvestmentUpsertDocument({
export async function apiInvestmentGetDocument({ export async function apiInvestmentGetDocument({
id, id,
category, category,
page = "1",
}: { }: {
id: string; id: string;
category: "one-document" | "all-document"; category: "one-document" | "all-document";
page?: string;
}) { }) {
try { try {
const response = await apiConfig.get( const response = await apiConfig.get(
`/mobile/investment/${id}/document?category=${category}` `/mobile/investment/${id}/document?category=${category}&page=${page}`
); );
return response.data; return response.data;
} catch (error) { } catch (error) {
@@ -131,15 +135,17 @@ export async function apiInvestmentDeleteDocument({ id }: { id: string }) {
export async function apiInvestmentGetAll({ export async function apiInvestmentGetAll({
category, category,
authorId, authorId,
page = "1",
}: { }: {
category: "my-holding" | "bursa"; category: "my-holding" | "bursa";
authorId?: string; authorId?: string;
page?: string;
}) { }) {
try { try {
const response = await apiConfig.get( const response = await apiConfig.get(
`/mobile/investment?category=${category}${ `/mobile/investment?category=${category}${
authorId ? `&authorId=${authorId}` : "" authorId ? `&authorId=${authorId}` : ""
}` }&page=${page}`
); );
return response.data; return response.data;
} catch (error) { } catch (error) {
@@ -168,16 +174,19 @@ export async function apiInvestmentGetInvoice({
id, id,
authorId, authorId,
category, category,
page = "1",
}: { }: {
id?: string; id?: string;
authorId?: string; authorId?: string;
category: "my-invest" | "transaction" | "invoice"; category: "my-invest" | "transaction" | "invoice";
page?: string;
}) { }) {
const categoryQuery = `?category=${category}`; const categoryQuery = `?category=${category}`;
const authorIdQuery = authorId ? `&authorId=${authorId}` : ""; const authorIdQuery = authorId ? `&authorId=${authorId}` : "";
const pageQuery = `&page=${page}`;
try { try {
const response = await apiConfig.get( const response = await apiConfig.get(
`/mobile/investment/${id}/invoice${categoryQuery}${authorIdQuery}` `/mobile/investment/${id}/invoice${categoryQuery}${authorIdQuery}${pageQuery}`
); );
return response.data; return response.data;
} catch (error) { } catch (error) {

View File

@@ -81,14 +81,15 @@ export async function apiVotingUpdateData({
} }
} }
export async function apiVotingGetAll({ search, category, authorId, userLoginId }: { search?: string, category: "beranda" | "contribution" | "all-history" | "my-history", authorId?: string, userLoginId?: string }) { export async function apiVotingGetAll({ search, category, authorId, userLoginId, page = "1" }: { search?: string, category: "beranda" | "contribution" | "all-history" | "my-history", authorId?: string, userLoginId?: string, page?: string }) {
try { try {
console.log("userLoginId", userLoginId); console.log("userLoginId", userLoginId);
const categoryQuery = category ? `?category=${category}` : ""; const categoryQuery = category ? `?category=${category}` : "";
const searchQuery = search ? `&search=${search}` : ""; const searchQuery = search ? `&search=${search}` : "";
const authorIdQuery = authorId ? `&authorId=${authorId}` : ""; const authorIdQuery = authorId ? `&authorId=${authorId}` : "";
const userLoginIdQuery = userLoginId ? `&userLoginId=${userLoginId}` : ""; const userLoginIdQuery = userLoginId ? `&userLoginId=${userLoginId}` : "";
const response = await apiConfig.get(`/mobile/voting${categoryQuery}${searchQuery}${authorIdQuery}${userLoginIdQuery}`); const pageQuery = `&page=${page}`;
const response = await apiConfig.get(`/mobile/voting${categoryQuery}${searchQuery}${authorIdQuery}${userLoginIdQuery}${pageQuery}`);
return response.data; return response.data;
} catch (error) { } catch (error) {
throw error; throw error;
@@ -110,15 +111,17 @@ export async function apiVotingContribution({
id, id,
authorId, authorId,
category, category,
page = "1",
}: { }: {
id: string; id: string;
authorId: string; authorId: string;
category: "list" | "checked"; category: "list" | "checked";
page?: string;
}) { }) {
const query = const query =
category === "list" category === "list"
? "?category=list" ? `?category=list&page=${page}`
: `?category=checked&authorId=${authorId}`; : `?category=checked&authorId=${authorId}&page=${page}`;
try { try {
const response = await apiConfig.get( const response = await apiConfig.get(
`/mobile/voting/${id}/contribution${query}` `/mobile/voting/${id}/contribution${query}`

View File

@@ -47,9 +47,8 @@ export async function apiGetNotificationsById({
category: TypeNotificationCategoryApp; category: TypeNotificationCategoryApp;
page?: string; page?: string;
}) { }) {
console.log("ID", id);
console.log("Category", category); console.log("Category", category);
console.log("Page", page);
try { try {
const response = await apiConfig.get( const response = await apiConfig.get(