Compare commits

...

2 Commits

Author SHA1 Message Date
0e7b29bb15 Integrasi API Donation
Fix:
- (application)/(user)/donation/[id]/(news)/[news]/edit-news
- (application)/(user)/donation/[id]/(news)/[news]/index
- (application)/(user)/donation/[id]/(news)/add-news
- (application)/(user)/donation/[id]/(news)/list-of-news
- (application)/(user)/donation/[id]/(news)/recap-of-news
- (application)/(user)/donation/[id]/infromation-fundrising
- service/api-client/api-donation

### No Issue
2025-10-09 17:00:04 +08:00
b293310969 Donation:
Add:
- components/_ShareComponent/MoneyTransferAnimation.tsx

Fix:
- Invoice terintegrasi API
- Create dan list berita

### No Issue
2025-10-08 17:40:36 +08:00
15 changed files with 774 additions and 136 deletions

View File

@@ -10,7 +10,6 @@ export default function UserLayout() {
return ( return (
<> <>
<Stack screenOptions={HeaderStyles}> <Stack screenOptions={HeaderStyles}>
<Stack.Screen <Stack.Screen
name="waiting-room" name="waiting-room"
options={{ options={{
@@ -463,7 +462,7 @@ export default function UserLayout() {
}} }}
/> />
<Stack.Screen <Stack.Screen
name="donation/[id]/(transaction-flow)/[transaction]/process" name="donation/[id]/(transaction-flow)/[invoiceId]/process"
options={{ options={{
title: "Proses", title: "Proses",
headerLeft: () => ( headerLeft: () => (
@@ -477,14 +476,14 @@ export default function UserLayout() {
}} }}
/> />
<Stack.Screen <Stack.Screen
name="donation/[id]/(transaction-flow)/[transaction]/success" name="donation/[id]/(transaction-flow)/[invoiceId]/success"
options={{ options={{
title: "Donasi Berhasil", title: "Donasi Berhasil",
headerLeft: () => <BackButton />, headerLeft: () => <BackButton />,
}} }}
/> />
<Stack.Screen <Stack.Screen
name="donation/[id]/(transaction-flow)/[transaction]/failed" name="donation/[id]/(transaction-flow)/[invoiceId]/failed"
options={{ options={{
title: "Donasi Gagal", title: "Donasi Gagal",
headerLeft: () => <BackButton />, headerLeft: () => <BackButton />,

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { import {
BadgeCustom, BadgeCustom,
BaseBox, BaseBox,
@@ -9,7 +10,6 @@ import {
ViewWrapper, ViewWrapper,
} from "@/components"; } from "@/components";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import { dummyMasterStatusTransaction } from "@/lib/dummy-data/_master/status-transaction";
import { apiDonationGetAll } from "@/service/api-client/api-donation"; import { apiDonationGetAll } from "@/service/api-client/api-donation";
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay"; import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
import { Href, router, useFocusEffect } from "expo-router"; import { Href, router, useFocusEffect } from "expo-router";
@@ -25,7 +25,7 @@ export default function DonationMyDonation() {
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
onLoadData(); onLoadData();
}, []) }, [user?.id])
); );
const onLoadData = async () => { const onLoadData = async () => {
@@ -48,6 +48,18 @@ export default function DonationMyDonation() {
} }
}; };
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 = ({ const handlePress = ({
invoiceId, invoiceId,
donationId, donationId,
@@ -57,15 +69,15 @@ export default function DonationMyDonation() {
donationId: string; donationId: string;
status: string; status: string;
}) => { }) => {
const url: Href = `../${donationId}/(transaction-flow)/${invoiceId}/invoice`; const url: Href = `../${donationId}/(transaction-flow)/${invoiceId}`;
if (status === "menunggu") { if (status === "menunggu") {
router.push(url); router.push(`${url}/invoice`);
} else if (status === "proses") { } else if (status === "proses") {
router.push(url); router.push(`${url}/process`);
} else if (status === "berhasil") { } else if (status === "berhasil") {
router.push(url); router.push(`${url}/success`);
} else if (status === "gagal") { } else if (status === "gagal") {
router.push(url); router.push(`${url}/failed`);
} }
}; };
@@ -112,7 +124,11 @@ export default function DonationMyDonation() {
Rp. {formatCurrencyDisplay(item.nominal)} Rp. {formatCurrencyDisplay(item.nominal)}
</TextCustom> </TextCustom>
<BadgeCustom variant="light" color={item.color} fullWidth> <BadgeCustom
variant="light"
color={handlerColor(_.lowerCase(item.statusInvoice))}
fullWidth
>
{item.statusInvoice} {item.statusInvoice}
</BadgeCustom> </BadgeCustom>
</StackCustom> </StackCustom>

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { import {
ButtonCenteredOnly, ButtonCenteredOnly,
ButtonCustom, ButtonCustom,
@@ -9,17 +10,120 @@ import {
TextInputCustom, TextInputCustom,
ViewWrapper, ViewWrapper,
} from "@/components"; } from "@/components";
import { router } from "expo-router"; import API_STRORAGE from "@/constants/base-url-api-strorage";
import DIRECTORY_ID from "@/constants/directory-id";
import {
apiDonationGetNewsById,
apiDonationUpdateNews,
} from "@/service/api-client/api-donation";
import { uploadFileService } from "@/service/upload-service";
import pickFile, { IFileData } from "@/utils/pickFile";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, useState } from "react";
import Toast from "react-native-toast-message";
export default function DonationEditNews() { export default function DonationEditNews() {
const { news } = useLocalSearchParams();
const [data, setData] = useState<any>(null);
const [image, setImage] = useState<IFileData | null>(null);
const [isLoading, setLoading] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [news])
);
const onLoadData = async () => {
try {
const response = await apiDonationGetNewsById({
id: news as string,
category: "get-one",
});
setData(response.data);
} catch (error) {
console.log("[ERROR]", error);
}
};
const handlerSubmitUpdate = async () => {
let newData;
if (!data.title || !data.deskripsi) {
Toast.show({
type: "error",
text1: "Judul dan deskripsi harus diisi",
});
return;
}
try {
setLoading(true);
newData = {
title: data?.title,
deskripsi: data?.deskripsi,
};
if (image && image?.uri) {
const uploadNewImage = await uploadFileService({
dirId: DIRECTORY_ID.donasi_kabar,
imageUri: image?.uri,
});
newData = {
title: data?.title,
deskripsi: data?.deskripsi,
newImageId: uploadNewImage.data.id,
};
}
const response = await apiDonationUpdateNews({
id: news as string,
data: newData,
});
if (!response.success) {
Toast.show({
type: "error",
text1: "Gagal mengupdate berita",
});
return;
}
Toast.show({
type: "success",
text1: "Berita berhasil diperbarui",
});
router.back();
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoading(false);
}
};
return ( return (
<ViewWrapper> <ViewWrapper>
<StackCustom gap={"xs"}> <StackCustom gap={"xs"}>
<InformationBox text="Upload gambar bersifat opsional untuk melengkapi kabar terkait donasi Anda." /> <InformationBox text="Upload gambar bersifat opsional untuk melengkapi kabar terkait donasi Anda." />
<LandscapeFrameUploaded /> <LandscapeFrameUploaded
image={
image
? image.uri
: data && data.imageId
? API_STRORAGE.GET({ fileId: data.imageId })
: undefined
}
/>
<ButtonCenteredOnly <ButtonCenteredOnly
onPress={() => { onPress={() => {
router.push("/(application)/(image)/take-picture/123"); pickFile({
allowedType: "image",
setImageUri(file) {
setImage(file);
},
});
}} }}
icon="upload" icon="upload"
> >
@@ -30,6 +134,8 @@ export default function DonationEditNews() {
label="Judul Berita" label="Judul Berita"
placeholder="Masukan judul berita" placeholder="Masukan judul berita"
required required
value={data?.title}
onChangeText={(value) => setData({ ...data, title: value })}
/> />
<TextAreaCustom <TextAreaCustom
label="Deskripsi Berita" label="Deskripsi Berita"
@@ -37,12 +143,16 @@ export default function DonationEditNews() {
required required
showCount showCount
maxLength={1000} maxLength={1000}
value={data?.deskripsi}
onChangeText={(value) => setData({ ...data, deskripsi: value })}
/> />
<Spacing /> <Spacing />
<ButtonCustom <ButtonCustom
disabled={!data?.title || !data?.deskripsi}
isLoading={isLoading}
onPress={() => { onPress={() => {
router.back(); handlerSubmitUpdate();
}} }}
> >
Update Update

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { import {
AlertDefaultSystem, AlertDefaultSystem,
BackButton, BackButton,
@@ -12,13 +13,45 @@ import {
} from "@/components"; } from "@/components";
import { IconEdit } from "@/components/_Icon"; import { IconEdit } from "@/components/_Icon";
import { IconTrash } from "@/components/_Icon/IconTrash"; import { IconTrash } from "@/components/_Icon/IconTrash";
import dayjs from "dayjs"; import { useAuth } from "@/hooks/use-auth";
import { router, Stack, useLocalSearchParams } from "expo-router"; import {
import { useState } from "react"; apiDonationDeleteNews,
apiDonationGetNewsById,
} from "@/service/api-client/api-donation";
import { formatChatTime } from "@/utils/formatChatTime";
import {
router,
Stack,
useFocusEffect,
useLocalSearchParams,
} from "expo-router";
import { useCallback, useState } from "react";
import Toast from "react-native-toast-message";
export default function DonationNews() { export default function DonationNews() {
const { id, news } = useLocalSearchParams(); const { user } = useAuth();
const { news } = useLocalSearchParams();
const [openDrawer, setOpenDrawer] = useState(false); const [openDrawer, setOpenDrawer] = useState(false);
const [data, setData] = useState<any>(null);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [news])
);
const onLoadData = async () => {
try {
const response = await apiDonationGetNewsById({
id: news as string,
category: "get-one",
});
setData(response.data);
} catch (error) {
console.log("[ERROR]", error);
}
};
return ( return (
<> <>
@@ -26,28 +59,28 @@ export default function DonationNews() {
options={{ options={{
title: "Detail Kabar", title: "Detail Kabar",
headerLeft: () => <BackButton />, headerLeft: () => <BackButton />,
headerRight: () => <DotButton onPress={() => setOpenDrawer(true)} />, headerRight: () =>
user?.id === data?.authorId && (
<DotButton onPress={() => setOpenDrawer(true)} />
),
}} }}
/> />
<ViewWrapper> <ViewWrapper>
<BaseBox> <BaseBox>
<StackCustom> <StackCustom>
<TextCustom style={{ alignSelf: "flex-end" }}> <TextCustom style={{ alignSelf: "flex-end" }}>
{dayjs().format("DD MMM YYYY")} {formatChatTime(data?.createdAt)}
</TextCustom> </TextCustom>
<DummyLandscapeImage /> {data && data.imageId && (
<DummyLandscapeImage imageId={data.imageId} />
)}
<TextCustom bold size="large" align="center"> <TextCustom bold size="large" align="center">
Judul Berita {data?.title || "-"}
</TextCustom> </TextCustom>
<TextCustom> <TextCustom>{data?.deskripsi || "-"}</TextCustom>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Sapiente
est id temporibus perferendis eos reiciendis reprehenderit tempora
ut quibusdam dolores facilis rerum exercitationem recusandae quis
neque, adipisci dolorum, aspernatur labore?
</TextCustom>
</StackCustom> </StackCustom>
</BaseBox> </BaseBox>
</ViewWrapper> </ViewWrapper>
@@ -62,12 +95,12 @@ export default function DonationNews() {
{ {
icon: <IconEdit />, icon: <IconEdit />,
label: "Edit Berita", label: "Edit Berita",
path: `/donation/${id}/(news)/${news}/edit-news` as any, path: `/donation/[id]/(news)/${news}/edit-news` as any,
}, },
{ {
icon: <IconTrash />, icon: <IconTrash />,
label: "Hapus Berita", label: "Hapus Berita",
path: `` as any, path: "",
color: "red", color: "red",
}, },
]} ]}
@@ -79,7 +112,23 @@ export default function DonationNews() {
message: "Apakah Anda yakin ingin menghapus berita ini?", message: "Apakah Anda yakin ingin menghapus berita ini?",
textLeft: "Batal", textLeft: "Batal",
textRight: "Hapus", textRight: "Hapus",
onPressRight: () => { onPressRight: async () => {
const response = await apiDonationDeleteNews({
id: news as string,
});
if (!response.success) {
Toast.show({
type: "error",
text1: "Gagal menghapus berita",
});
return;
}
Toast.show({
type: "success",
text1: "Berita berhasil dihapus",
});
router.back(); router.back();
}, },
}); });

View File

@@ -9,17 +9,79 @@ import {
TextInputCustom, TextInputCustom,
ViewWrapper, ViewWrapper,
} from "@/components"; } from "@/components";
import { router } from "expo-router"; import DIRECTORY_ID from "@/constants/directory-id";
import { apiDonationCreateNews } from "@/service/api-client/api-donation";
import { uploadFileService } from "@/service/upload-service";
import pickFile, { IFileData } from "@/utils/pickFile";
import { router, useLocalSearchParams } from "expo-router";
import { useState } from "react";
import Toast from "react-native-toast-message";
export default function DonationAddNews() { export default function DonationAddNews() {
const { id } = useLocalSearchParams();
const [data, setData] = useState({
title: "",
deskripsi: "",
});
const [image, setImage] = useState<IFileData | null>(null);
const [isLoading, setLoading] = useState(false);
const handlerSubmit = async () => {
let newData: any = { ...data };
try {
setLoading(true);
if (image) {
const responseUploadImage = await uploadFileService({
dirId: DIRECTORY_ID.donasi_kabar,
imageUri: image?.uri,
});
newData = {
...newData,
imageId: responseUploadImage.data.id,
};
}
const response = await apiDonationCreateNews({
id: id as string,
data: newData,
});
if (!response.success) {
Toast.show({
type: "error",
text1: "Gagal menambah berita",
});
return
}
Toast.show({
type: "success",
text1: "Berita berhasil ditambahkan",
});
router.back();
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoading(false);
}
};
return ( return (
<ViewWrapper> <ViewWrapper>
<StackCustom gap={"xs"}> <StackCustom gap={"xs"}>
<InformationBox text="Upload gambar bersifat opsional untuk melengkapi kabar terkait donasi Anda." /> <InformationBox text="Upload gambar bersifat opsional untuk melengkapi kabar terkait donasi Anda." />
<LandscapeFrameUploaded /> <LandscapeFrameUploaded image={image?.uri} />
<ButtonCenteredOnly <ButtonCenteredOnly
onPress={() => { onPress={() => {
router.push("/(application)/(image)/take-picture/123"); pickFile({
allowedType: "image",
setImageUri(file) {
setImage(file);
},
});
}} }}
icon="upload" icon="upload"
> >
@@ -30,6 +92,13 @@ export default function DonationAddNews() {
label="Judul Berita" label="Judul Berita"
placeholder="Masukan judul berita" placeholder="Masukan judul berita"
required required
value={data.title}
onChangeText={(value) => {
setData({
...data,
title: value,
});
}}
/> />
<TextAreaCustom <TextAreaCustom
label="Deskripsi Berita" label="Deskripsi Berita"
@@ -37,12 +106,21 @@ export default function DonationAddNews() {
required required
showCount showCount
maxLength={1000} maxLength={1000}
value={data.deskripsi}
onChangeText={(value) => {
setData({
...data,
deskripsi: value,
});
}}
/> />
<Spacing /> <Spacing />
<ButtonCustom <ButtonCustom
disabled={!data.title || !data.deskripsi}
isLoading={isLoading}
onPress={() => { onPress={() => {
router.back(); handlerSubmit();
}} }}
> >
Simpan Simpan

View File

@@ -1,45 +1,88 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { import {
BackButton, BackButton,
BaseBox, BaseBox,
DrawerCustom, DrawerCustom,
Grid, Grid,
MenuDrawerDynamicGrid, LoaderCustom,
TextCustom, MenuDrawerDynamicGrid,
ViewWrapper TextCustom,
ViewWrapper,
} from "@/components"; } from "@/components";
import { IconPlus } from "@/components/_Icon"; import { IconPlus } from "@/components/_Icon";
import dayjs from "dayjs"; import { apiDonationGetNewsById } from "@/service/api-client/api-donation";
import { router, Stack, useLocalSearchParams } from "expo-router"; import { formatChatTime } from "@/utils/formatChatTime";
import { useState } from "react"; import {
router,
Stack,
useFocusEffect,
useLocalSearchParams,
} from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
export default function DonationRecapOfNews() { export default function DonationRecapOfNews() {
const { id } = useLocalSearchParams(); const { id } = useLocalSearchParams();
const [openDrawer, setOpenDrawer] = useState(false); const [openDrawer, setOpenDrawer] = useState(false);
const [list, setList] = useState<any[] | null>(null);
const [loadList, setLoadList] = useState<boolean>(false);
useFocusEffect(
useCallback(() => {
onLoadList();
}, [id])
);
const onLoadList = async () => {
try {
setLoadList(true);
const response = await apiDonationGetNewsById({
id: id as string,
category: "get-all",
});
setList(response.data);
} catch (error) {
console.log("[ERROR]", error);
setList([]);
} finally {
setLoadList(false);
}
};
return ( return (
<> <>
<Stack.Screen <Stack.Screen
options={{ options={{
title: "Daftar Kabar", title: "Daftar Kabar",
headerLeft: () => <BackButton />, }} headerLeft: () => <BackButton />,
}}
/> />
<ViewWrapper> <ViewWrapper>
{Array.from({ length: 15 }).map((_, index) => ( {loadList ? (
<BaseBox key={index} href={`/donation/${id}/(news)/${index}`}> <LoaderCustom />
<Grid> ) : _.isEmpty(list) ? (
<Grid.Col span={8}> <TextCustom align="center" color="gray">
<TextCustom truncate bold> Tidak ada kabar
Lorem ipsum dolor, sit amet consectetur adipisicing elit. </TextCustom>
</TextCustom> ) : (
</Grid.Col> list?.map((item: any, index: number) => (
<Grid.Col span={4} style={{ alignItems: "flex-end" }}> <BaseBox key={index} href={`/donation/[id]/(news)/${item.id}`}>
<TextCustom size="small"> <Grid>
{dayjs().format("DD MMM YYYY")} <Grid.Col span={8}>
</TextCustom> <TextCustom truncate bold>
</Grid.Col> {item?.title || "-"}
</Grid> </TextCustom>
</BaseBox> </Grid.Col>
))} <Grid.Col span={4} style={{ alignItems: "flex-end" }}>
<TextCustom size="small">
{formatChatTime(item?.createdAt)}
</TextCustom>
</Grid.Col>
</Grid>
</BaseBox>
))
)}
</ViewWrapper> </ViewWrapper>
<DrawerCustom <DrawerCustom

View File

@@ -1,21 +1,55 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { import {
BackButton, BackButton,
BaseBox, BaseBox,
DotButton, DotButton,
DrawerCustom, DrawerCustom,
Grid, Grid,
LoaderCustom,
MenuDrawerDynamicGrid, MenuDrawerDynamicGrid,
TextCustom, TextCustom,
ViewWrapper, ViewWrapper,
} from "@/components"; } from "@/components";
import { IconPlus } from "@/components/_Icon"; import { IconPlus } from "@/components/_Icon";
import dayjs from "dayjs"; import { apiDonationGetNewsById } from "@/service/api-client/api-donation";
import { router, Stack, useLocalSearchParams } from "expo-router"; import { formatChatTime } from "@/utils/formatChatTime";
import { useState } from "react"; import {
router,
Stack,
useFocusEffect,
useLocalSearchParams,
} from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
export default function DonationRecapOfNews() { export default function DonationRecapOfNews() {
const { id } = useLocalSearchParams(); const { id } = useLocalSearchParams();
const [openDrawer, setOpenDrawer] = useState(false); const [openDrawer, setOpenDrawer] = useState(false);
const [list, setList] = useState<any[] | null>(null);
const [loadList, setLoadList] = useState<boolean>(false);
useFocusEffect(
useCallback(() => {
onLoadList();
}, [id])
);
const onLoadList = async () => {
try {
setLoadList(true);
const response = await apiDonationGetNewsById({
id: id as string,
category: "get-all",
});
setList(response.data);
} catch (error) {
console.log("[ERROR]", error);
setList([]);
} finally {
setLoadList(false);
}
};
return ( return (
<> <>
@@ -27,20 +61,30 @@ export default function DonationRecapOfNews() {
}} }}
/> />
<ViewWrapper> <ViewWrapper>
{Array.from({ length: 15 }).map((_, index) => ( {loadList ? (
<BaseBox key={index} href={`/donation/${id}/(news)/${index}`}> <LoaderCustom />
<Grid> ) : _.isEmpty(list) ? (
<Grid.Col span={8}> <TextCustom align="center" color="gray">
<TextCustom truncate bold> Tidak ada kabar
Lorem ipsum dolor, sit amet consectetur adipisicing elit. </TextCustom>
</TextCustom> ) : (
</Grid.Col> list?.map((item: any, index: number) => (
<Grid.Col span={4} style={{ alignItems: "flex-end" }}> <BaseBox key={index} href={`/donation/[id]/(news)/${item.id}`}>
<TextCustom size="small">{dayjs().format("DD MMM YYYY")}</TextCustom> <Grid>
</Grid.Col> <Grid.Col span={8}>
</Grid> <TextCustom truncate bold>
</BaseBox> {item?.title || "-"}
))} </TextCustom>
</Grid.Col>
<Grid.Col span={4} style={{ alignItems: "flex-end" }}>
<TextCustom size="small">
{formatChatTime(item?.createdAt)}
</TextCustom>
</Grid.Col>
</Grid>
</BaseBox>
))
)}
</ViewWrapper> </ViewWrapper>
<DrawerCustom <DrawerCustom

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { import {
BaseBox, BaseBox,
ButtonCenteredOnly, ButtonCenteredOnly,
@@ -9,23 +10,107 @@ import {
TextCustom, TextCustom,
ViewWrapper, ViewWrapper,
} from "@/components"; } from "@/components";
import CopyButton from "@/components/Button/CoyButton";
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
import { router, useLocalSearchParams } from "expo-router"; import DIRECTORY_ID from "@/constants/directory-id";
import {
apiDonationGetInvoiceById,
apiDonationUpdateInvoice,
} from "@/service/api-client/api-donation";
import { uploadFileService } from "@/service/upload-service";
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
import pickFile 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 DonationInvoice() { export default function DonationInvoice() {
const { invoiceId } = useLocalSearchParams(); const { invoiceId } = useLocalSearchParams();
console.log("invoiceId", invoiceId); console.log("invoiceId", invoiceId);
const [data, setData] = useState<any>(null);
const [image, setImage] = useState<any>(null);
const [isLoading, setLoading] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [invoiceId])
);
const onLoadData = async () => {
try {
const response = await apiDonationGetInvoiceById({
id: invoiceId as string,
});
console.log("[RESPONSE]", JSON.stringify(response, null, 2));
setData(response.data);
} catch (error) {
console.log("[ERROR]", error);
}
};
const handlerUpdateInvoice = async () => {
try {
setLoading(true);
const responseUploadImage = await uploadFileService({
dirId: DIRECTORY_ID.donasi_bukti_transfer,
imageUri: image?.uri,
});
console.log("[RESPONSE UPLOAD IMAGE]", responseUploadImage);
if (!responseUploadImage?.data?.id) {
Toast.show({
type: "error",
text1: "Gagal mengunggah bukti transfer",
});
return;
}
const fileId = responseUploadImage?.data?.id;
const response = await apiDonationUpdateInvoice({
id: invoiceId as string,
fileId: fileId,
status: "proses",
});
console.log("[RESPONSE UPDATE]", JSON.stringify(response, null, 2));
if (!response.success) {
Toast.show({
type: "error",
text1: "Gagal mengunggah bukti transfer",
});
return;
}
Toast.show({
type: "success",
text1: "Berhasil mengunggah bukti transfer",
});
router.replace(`/donation/[id]/(transaction-flow)/${invoiceId}/process`);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoading(false);
}
};
return ( return (
<> <>
<ViewWrapper> <ViewWrapper>
<StackCustom> <StackCustom>
<InformationBox <InformationBox
text={`Mohon transfer donasi anda ke rekening dibawah dengan Id: ${invoiceId}`} text={`Mohon transfer donasi anda ke rekening dibawah`}
/> />
<BaseBox> <BaseBox>
<StackCustom gap={"xs"}> <StackCustom gap={"xs"}>
<TextCustom>Nama BANK</TextCustom> <TextCustom bold>
<TextCustom>Nama Penerima</TextCustom> BANK: {data?.DonasiMaster_Bank?.name}
</TextCustom>
{/* <TextCustom>{data?.DonasiMaster_Bank?.accountName}</TextCustom> */}
<Spacing height={10} /> <Spacing height={10} />
<BaseBox backgroundColor={MainColor.soft_darkblue}> <BaseBox backgroundColor={MainColor.soft_darkblue}>
@@ -37,7 +122,7 @@ export default function DonationInvoice() {
}} }}
> >
<TextCustom size="xlarge" bold color="yellow"> <TextCustom size="xlarge" bold color="yellow">
4567898765433567 {data?.DonasiMaster_Bank?.norek}
</TextCustom> </TextCustom>
</Grid.Col> </Grid.Col>
<Grid.Col <Grid.Col
@@ -46,7 +131,7 @@ export default function DonationInvoice() {
alignItems: "flex-end", alignItems: "flex-end",
}} }}
> >
<ButtonCustom>Salin</ButtonCustom> <CopyButton textToCopy={data?.DonasiMaster_Bank?.norek} />
</Grid.Col> </Grid.Col>
</Grid> </Grid>
</BaseBox> </BaseBox>
@@ -68,7 +153,7 @@ export default function DonationInvoice() {
}} }}
> >
<TextCustom size="xlarge" bold color="yellow"> <TextCustom size="xlarge" bold color="yellow">
Rp. 1.000.000 Rp. {formatCurrencyDisplay(data?.nominal) || "-"}
</TextCustom> </TextCustom>
</Grid.Col> </Grid.Col>
<Grid.Col <Grid.Col
@@ -77,7 +162,7 @@ export default function DonationInvoice() {
alignItems: "flex-end", alignItems: "flex-end",
}} }}
> >
<ButtonCustom>Salin</ButtonCustom> <CopyButton textToCopy={data?.nominal} />
</Grid.Col> </Grid.Col>
</Grid> </Grid>
</BaseBox> </BaseBox>
@@ -86,10 +171,32 @@ export default function DonationInvoice() {
<BaseBox> <BaseBox>
<StackCustom> <StackCustom>
<TextCustom>Upload bukti transfer anda.</TextCustom> <TextCustom bold align="center" size={"small"} color="gray">
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>
) : null}
<ButtonCenteredOnly <ButtonCenteredOnly
onPress={() => { onPress={() => {
router.push("/(application)/(image)/take-picture/123"); pickFile({
allowedType: "image",
setImageUri(file) {
setImage(file);
},
});
}} }}
icon="upload" icon="upload"
> >
@@ -99,11 +206,13 @@ export default function DonationInvoice() {
</BaseBox> </BaseBox>
<ButtonCustom <ButtonCustom
disabled={!image}
isLoading={isLoading}
onPress={() => { onPress={() => {
router.push(`/donation/${invoiceId}/(transaction-flow)/process`); handlerUpdateInvoice();
}} }}
> >
Saya Sudah Transfer Simpan
</ButtonCustom> </ButtonCustom>
</StackCustom> </StackCustom>
<Spacing /> <Spacing />

View File

@@ -1,13 +1,6 @@
import { import { BaseBox, StackCustom, TextCustom, ViewWrapper } from "@/components";
BaseBox, import MoneyTransferAnimation from "@/components/_ShareComponent/MoneyTransferAnimation";
Grid, import { View } from "react-native";
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import { MainColor } from "@/constants/color-palet";
import { Ionicons } from "@expo/vector-icons";
import { ActivityIndicator } from "react-native";
export default function DonationProcess() { export default function DonationProcess() {
return ( return (
@@ -16,13 +9,16 @@ export default function DonationProcess() {
<BaseBox> <BaseBox>
<StackCustom> <StackCustom>
<TextCustom align="center" bold> <TextCustom align="center" bold>
Admin sedang memproses transaksi donasimu Admin sedang memvalidasi data dan bukti transfer anda. Mohon
tunggu proses ini selesai.
</TextCustom> </TextCustom>
<ActivityIndicator size="large" color={MainColor.yellow} /> <View style={{ alignItems: "center", justifyContent: "center" }}>
<MoneyTransferAnimation />
</View>
</StackCustom> </StackCustom>
</BaseBox> </BaseBox>
<BaseBox> {/* <BaseBox>
<Grid> <Grid>
<Grid.Col span={10} style={{ justifyContent: "center" }}> <Grid.Col span={10} style={{ justifyContent: "center" }}>
<TextCustom size="small"> <TextCustom size="small">
@@ -38,7 +34,7 @@ export default function DonationProcess() {
/> />
</Grid.Col> </Grid.Col>
</Grid> </Grid>
</BaseBox> </BaseBox> */}
</ViewWrapper> </ViewWrapper>
</> </>
); );

View File

@@ -22,7 +22,6 @@ export default function InvestmentInputDonation() {
const handlerSubmit = async () => { const handlerSubmit = async () => {
try { try {
console.log("jumlah", nominal);
await AsyncStorage.setItem( await AsyncStorage.setItem(
LOCAL_STORAGE_KEY.transactionDonation, LOCAL_STORAGE_KEY.transactionDonation,
JSON.stringify({ nominal: nominal.toString() }) JSON.stringify({ nominal: nominal.toString() })
@@ -77,6 +76,7 @@ export default function InvestmentInputDonation() {
<BaseBox> <BaseBox>
<TextInputCustom <TextInputCustom
keyboardType="numeric"
label="Nominal lainnya" label="Nominal lainnya"
placeholder="0" placeholder="0"
iconLeft="Rp." iconLeft="Rp."

View File

@@ -7,19 +7,16 @@ import {
import { RadioCustom, RadioGroup } from "@/components/Radio/RadioCustom"; import { RadioCustom, RadioGroup } from "@/components/Radio/RadioCustom";
import { LOCAL_STORAGE_KEY } from "@/constants/local-storage-key"; import { LOCAL_STORAGE_KEY } from "@/constants/local-storage-key";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import { dummyMasterBank } from "@/lib/dummy-data/_master/bank";
import { apiDonationCreateInvoice } from "@/service/api-client/api-donation"; import { apiDonationCreateInvoice } from "@/service/api-client/api-donation";
import { apiMasterBank } from "@/service/api-client/api-master"; import { apiMasterBank } from "@/service/api-client/api-master";
import AsyncStorage from "@react-native-async-storage/async-storage"; import AsyncStorage from "@react-native-async-storage/async-storage";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router"; import { router, useLocalSearchParams } from "expo-router";
import _ from "lodash"; import _ from "lodash";
import { useCallback, useEffect, useState } from "react"; import { useEffect, useState } from "react";
export default function DonationSelectBank() { export default function DonationSelectBank() {
const { user } = useAuth(); const { user } = useAuth();
const { id } = useLocalSearchParams(); const { id } = useLocalSearchParams();
console.log("id", id);
const [select, setSelect] = useState<any | number>(""); const [select, setSelect] = useState<any | number>("");
const [listBank, setListBank] = useState<any>([]); const [listBank, setListBank] = useState<any>([]);
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
@@ -54,22 +51,16 @@ export default function DonationSelectBank() {
authorId: user?.id, authorId: user?.id,
}; };
console.log("[NEW DATA]", newData);
const response = await apiDonationCreateInvoice({ const response = await apiDonationCreateInvoice({
id: id as string, id: id as string,
data: newData, data: newData,
}); });
console.log("[RESPONSE CREATE>>]", response);
if (response.success) { if (response.success) {
const invoiceId = response.data.id; const invoiceId = response.data.id;
const delStorage = await AsyncStorage.removeItem( await AsyncStorage.removeItem(LOCAL_STORAGE_KEY.transactionDonation);
LOCAL_STORAGE_KEY.transactionDonation
);
console.log("[DEL STORAGE]", delStorage);
router.replace( router.replace(
`/(application)/(user)/donation/[id]/(transaction-flow)/${invoiceId}/invoice` `/(application)/(user)/donation/[id]/(transaction-flow)/${invoiceId}/invoice`
); );

View File

@@ -32,10 +32,6 @@ export default function DonationInformationFunrising() {
try { try {
setLoadList(true); setLoadList(true);
const response = await apiDonationFundrising({ id: id as string }); const response = await apiDonationFundrising({ id: id as string });
console.log(
"[RES GET FUNDRISING]",
JSON.stringify(response.data, null, 2)
);
setData(response?.data?.user); setData(response?.data?.user);
setList(response?.data?.donasi); setList(response?.data?.donasi);
@@ -78,7 +74,7 @@ export default function DonationInformationFunrising() {
{loadList ? ( {loadList ? (
<LoaderCustom /> <LoaderCustom />
) : _.isEmpty(list) ? ( ) : _.isEmpty(list) ? (
<TextCustom>Belum ada data</TextCustom> <TextCustom align="center" color="gray" size="small">Belum ada data</TextCustom>
) : ( ) : (
list?.map((item: any, index: number) => ( list?.map((item: any, index: number) => (
<Donation_BoxPublish key={index} id={item?.id} data={item} /> <Donation_BoxPublish key={index} id={item?.id} data={item} />

View File

@@ -1,11 +1,6 @@
import { import { BaseBox, StackCustom, TextCustom, ViewWrapper } from "@/components";
BaseBox, import MoneyTransferAnimation from "@/components/_ShareComponent/MoneyTransferAnimation";
StackCustom, import { View } from "react-native";
TextCustom,
ViewWrapper
} from "@/components";
import { MainColor } from "@/constants/color-palet";
import { ActivityIndicator } from "react-native";
export default function InvestmentProcess() { export default function InvestmentProcess() {
return ( return (
@@ -17,7 +12,9 @@ export default function InvestmentProcess() {
Admin sedang memvalidasi data dan bukti transfer anda. Mohon Admin sedang memvalidasi data dan bukti transfer anda. Mohon
tunggu proses ini selesai. tunggu proses ini selesai.
</TextCustom> </TextCustom>
<ActivityIndicator size="large" color={MainColor.yellow} /> <View style={{ alignItems: "center", justifyContent: "center" }}>
<MoneyTransferAnimation />
</View>
</StackCustom> </StackCustom>
</BaseBox> </BaseBox>

View File

@@ -0,0 +1,118 @@
/* eslint-disable react-hooks/exhaustive-deps */
// MoneyTransferAnimation.tsx
import React from "react";
import { View, StyleSheet } from "react-native";
import { FontAwesome } from "@expo/vector-icons";
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
Easing,
runOnJS,
} from "react-native-reanimated";
import { AccentColor, MainColor } from "@/constants/color-palet";
import TextCustom from "../Text/TextCustom";
const SCREEN_WIDTH = 300; // Lebar area animasi
const DURATION = 2500; // Durasi sekali jalan (kiri → kanan)
const MoneyTransferAnimation: React.FC = () => {
const progress = useSharedValue(0);
const opacity = useSharedValue(0);
// Fungsi untuk mereset dan mulai ulang animasi
const startAnimation = () => {
progress.value = 0;
opacity.value = 0;
// Fade in di awal
opacity.value = withTiming(1, { duration: 300 });
// Gerak kiri → kanan
progress.value = withTiming(
1,
{
duration: DURATION,
easing: Easing.linear,
},
(finished) => {
if (finished) {
// Fade out di akhir
opacity.value = withTiming(0, { duration: 300 }, () => {
// Setelah fade out, ulangi
runOnJS(startAnimation)();
});
}
}
);
};
React.useEffect(() => {
startAnimation();
}, []);
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [
{
translateX: progress.value * SCREEN_WIDTH,
},
],
opacity: opacity.value,
};
});
return (
<View style={styles.container}>
{/* Area animasi (track tidak wajib, tapi bisa ditambahkan) */}
<View style={styles.track} />
{/* Ikon uang animasi */}
<Animated.View style={[styles.iconContainer, animatedStyle]}>
<FontAwesome name="money" size={28} color="#2ecc71" />
</Animated.View>
{/* Teks status */}
<View style={styles.textContainer}>
<TextCustom bold>Transaksi Sedang Diproses</TextCustom>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
alignItems: "center",
justifyContent: "center",
paddingVertical: 30,
position: "relative",
width: SCREEN_WIDTH,
},
track: {
position: "absolute",
top: 20,
left: 0,
right: 0,
height: 2,
backgroundColor: AccentColor.darkblue,
borderRadius: 1,
},
iconContainer: {
position: "absolute",
top: 4,
left: -30, // Mulai di luar kiri
justifyContent: "center",
alignItems: "center",
},
textContainer: {
marginTop: 40,
},
text: {
fontSize: 16,
fontWeight: "600",
color: MainColor.white,
textAlign: "center",
},
});
export default MoneyTransferAnimation;

View File

@@ -110,7 +110,7 @@ export async function apiDonationGetAll({
category: "beranda" | "my-donation"; category: "beranda" | "my-donation";
authorId?: string; authorId?: string;
}) { }) {
const authorQuery = authorId ? `&authorId=${authorId}` : ""; const authorQuery = authorId ? `&authorId=${authorId}` : "";
try { try {
const response = await apiConfig.get( const response = await apiConfig.get(
`/mobile/donation?category=${category}${authorQuery}` `/mobile/donation?category=${category}${authorQuery}`
@@ -163,3 +163,95 @@ export async function apiDonationCreateInvoice({
throw error; throw error;
} }
} }
export async function apiDonationGetInvoiceById({ id }: { id: string }) {
try {
const response = await apiConfig.get(`/mobile/donation/${id}/invoice`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiDonationUpdateInvoice({
id,
fileId,
status,
}: {
id: string;
fileId?: string;
status: "berhasil" | "gagal" | "proses" | "menunggu";
}) {
const statusQuery = `?status=${status}`;
try {
const response = await apiConfig.put(
`/mobile/donation/${id}/invoice${statusQuery}`,
{
data: fileId,
}
);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiDonationCreateNews({
id,
data,
}: {
id: string;
data: any;
}) {
try {
const response = await apiConfig.post(`/mobile/donation/${id}/news`, {
data: data,
});
return response.data;
} catch (error) {
throw error;
}
}
export async function apiDonationGetNewsById({
id,
category,
}: {
id: string;
category: "get-all" | "get-one";
}) {
try {
const response = await apiConfig.get(
`/mobile/donation/${id}/news?category=${category}`
);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiDonationUpdateNews({
id,
data,
}: {
id: string;
data: any;
}) {
try {
const response = await apiConfig.put(`/mobile/donation/${id}/news`, {
data: data,
});
return response.data;
} catch (error) {
throw error;
}
}
export async function apiDonationDeleteNews({ id }: { id: string }) {
try {
const response = await apiConfig.delete(`/mobile/donation/${id}/news`);
return response.data;
} catch (error) {
throw error;
}
}