From 1e1b18f860e80838140d46789925a289d749af7c Mon Sep 17 00:00:00 2001 From: Bagasbanuna02 Date: Tue, 28 Oct 2025 17:45:13 +0800 Subject: [PATCH] Integrasi API: Donation & Admin Donation Fix: - app/(application)/(user)/donation/[id]/(transaction-flow)/[invoiceId]/invoice.tsx - app/(application)/(user)/donation/[id]/index.tsx - app/(application)/admin/donation/[id]/[status]/index.tsx - app/(application)/admin/donation/[id]/[status]/transaction-detail.tsx - app/(application)/admin/donation/[id]/list-of-donatur.tsx - components/Select/SelectCustom.tsx - context/AuthContext.tsx - screens/Donation/BoxPublish.tsx - screens/Donation/ProgressSection.tsx - service/api-admin/api-admin-donation.ts ### NO Issue --- .../[invoiceId]/invoice.tsx | 4 +- .../(user)/donation/[id]/index.tsx | 2 +- .../admin/donation/[id]/[status]/index.tsx | 39 ++++- .../[id]/[status]/transaction-detail.tsx | 157 ++++++++++++++--- .../admin/donation/[id]/list-of-donatur.tsx | 162 ++++++++++-------- components/Select/SelectCustom.tsx | 126 ++++++++++---- context/AuthContext.tsx | 17 +- screens/Donation/BoxPublish.tsx | 7 +- screens/Donation/ProgressSection.tsx | 16 +- service/api-admin/api-admin-donation.ts | 43 ++++- 10 files changed, 420 insertions(+), 153 deletions(-) diff --git a/app/(application)/(user)/donation/[id]/(transaction-flow)/[invoiceId]/invoice.tsx b/app/(application)/(user)/donation/[id]/(transaction-flow)/[invoiceId]/invoice.tsx index 597d10c..4b51b55 100644 --- a/app/(application)/(user)/donation/[id]/(transaction-flow)/[invoiceId]/invoice.tsx +++ b/app/(application)/(user)/donation/[id]/(transaction-flow)/[invoiceId]/invoice.tsx @@ -122,7 +122,7 @@ export default function DonationInvoice() { }} > - {data?.DonasiMaster_Bank?.norek} + {data?.MasterBank?.norek} - + diff --git a/app/(application)/(user)/donation/[id]/index.tsx b/app/(application)/(user)/donation/[id]/index.tsx index 2d59893..3203738 100644 --- a/app/(application)/(user)/donation/[id]/index.tsx +++ b/app/(application)/(user)/donation/[id]/index.tsx @@ -102,7 +102,7 @@ export default function DonasiDetailBeranda() { sisaHari={value.sisa} reminder={value.reminder} data={data} - bottomSection={} + bottomSection={} /> (null); + const [countDonatur, setCountDonatur] = React.useState(0); const [isLoading, setIsLoading] = React.useState(false); useFocusEffect( @@ -55,7 +56,8 @@ export default function AdminDonationDetail() { console.log("[RES GET BY ID]", JSON.stringify(response, null, 2)); if (response.success) { - setData(response.data); + setData(response.data.donasi); + setCountDonatur(response.data.donatur); } } catch (error) { console.log("[ERROR]", error); @@ -108,12 +110,16 @@ export default function AdminDonationDetail() { value: `Rp ${(data && data?.totalPencairan) || 0}`, }, { - label: "Sisa Dana", - value: `Rp ${(data && data?.terkumpul - data?.totalPencairan) || 0}`, + label: "Sisa Dana Masuk", + value: `Rp ${ + (data && + formatCurrencyDisplay(data?.terkumpul - data?.totalPencairan)) || + 0 + }`, }, { label: "Akumulasi Pencairan", - value: `${(data && data?.totalPencairan) || 0} kali`, + value: `${(data && data?.akumulasiPencairan) || 0} kali`, }, { label: "Bank Tujuan", @@ -196,7 +202,7 @@ export default function AdminDonationDetail() { /> ))} - + @@ -211,16 +217,33 @@ export default function AdminDonationDetail() { - + + + Jumlah Donatur} - value={0 orang} + value={ + + {countDonatur ? countDonatur : 0} orang + + } /> Dana Terkumpul} - value={Rp 0} + value={ + + Rp {formatCurrencyDisplay(data?.terkumpul || 0)} + + } /> diff --git a/app/(application)/admin/donation/[id]/[status]/transaction-detail.tsx b/app/(application)/admin/donation/[id]/[status]/transaction-detail.tsx index 0140ad8..91e0e46 100644 --- a/app/(application)/admin/donation/[id]/[status]/transaction-detail.tsx +++ b/app/(application)/admin/donation/[id]/[status]/transaction-detail.tsx @@ -9,51 +9,162 @@ import { } from "@/components"; import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle"; import { GridDetail_4_8 } from "@/components/_ShareComponent/GridDetail_4_8"; -import { MainColor } from "@/constants/color-palet"; -import dayjs from "dayjs"; -import { router, useLocalSearchParams } from "expo-router"; +import { + apiAdminDonationInvoiceDetailById, + apiAdminDonationInvoiceUpdateById, +} from "@/service/api-admin/api-admin-donation"; +import { colorBadgeTransaction } from "@/utils/colorBadge"; +import { dateTimeView } from "@/utils/dateTimeView"; +import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay"; +import { router, useFocusEffect, useLocalSearchParams } from "expo-router"; +import _ from "lodash"; +import { useCallback, useState } from "react"; +import Toast from "react-native-toast-message"; export default function AdminDonasiTransactionDetail() { - const { id } = useLocalSearchParams(); + const { id, status } = useLocalSearchParams(); + console.log("[STATUS]", id, status); - const buttonAction = ( - - router.back()}>Terima - + const [data, setData] = useState(null); + const [isLoading, setLoading] = useState(false); + + useFocusEffect( + useCallback(() => { + onLoadData(); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [id]) ); + const onLoadData = async () => { + try { + const response = await apiAdminDonationInvoiceDetailById({ + id: id as string, + }); + + console.log("[GET INVOICE BY ID]", JSON.stringify(response, null, 2)); + if (response.success) { + setData(response.data); + } + } catch (error) { + console.log("[ERROR]", error); + } + }; + + const handlerSubmit = async () => { + try { + setLoading(true); + const newData = { + donationId: data?.donasiId, + nominal: data?.nominal, + }; + + const response = await apiAdminDonationInvoiceUpdateById({ + id: id as string, + data: newData, + status: "berhasil", + }); + + console.log("[UPDATE INVOICE]", JSON.stringify(response, null, 2)); + + if (!response.success) { + Toast.show({ + type: "error", + text1: response.message, + }); + return; + } + Toast.show({ + type: "success", + text1: response.message, + }); + router.back(); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoading(false); + } + }; + + const buttonAction = () => { + if (data && data?.DonasiMaster_StatusInvoice?.name === "Menunggu") { + return null; + } + + if (data && data?.DonasiMaster_StatusInvoice?.name === "Proses") { + return ( + + { + handlerSubmit(); + }} + > + Terima donasi + + + ); + } + + return ( + + + {data?.DonasiMaster_StatusInvoice?.name} + + + ); + }; + const listData = [ { label: "Donatur", - value: "Bagas Banuna", + value: (data && data?.Author?.username) || "-", }, { label: "Bank", - value: "BCA", + value: (data && data?.MasterBank?.namaBank) || "-", }, { label: "Jumlah Donasi", - value: "Rp. 1.000.000", + value: `Rp. ${ + (data && data?.nominal && formatCurrencyDisplay(data?.nominal)) || "-" + }`, }, { label: "Status", - value: Berhasil, + value: + (data && data?.DonasiMaster_StatusInvoice?.name && ( + + {_.startCase( + (data?.DonasiMaster_StatusInvoice?.name as any) || "-" + )} + + )) || + "-", }, { label: "Tanggal", - value: dayjs().format("DD-MM-YYYY HH:mm:ss"), + value: (data && dateTimeView({ date: data?.createdAt })) || "-", }, { label: "Bukti Transfer", - value: ( - - router.push(`/(application)/(image)/preview-image/${id}`) - } - > - Cek - - ), + value: + (data && data?.imageId && ( + + router.push( + `/(application)/(image)/preview-image/${data?.imageId}` + ) + } + > + Cek + + )) || + "-", }, ]; @@ -61,7 +172,7 @@ export default function AdminDonasiTransactionDetail() { <> } - footerComponent={buttonAction} + footerComponent={buttonAction()} > diff --git a/app/(application)/admin/donation/[id]/list-of-donatur.tsx b/app/(application)/admin/donation/[id]/list-of-donatur.tsx index 8446531..aa0a885 100644 --- a/app/(application)/admin/donation/[id]/list-of-donatur.tsx +++ b/app/(application)/admin/donation/[id]/list-of-donatur.tsx @@ -3,6 +3,7 @@ import { ActionIcon, BadgeCustom, CenterCustom, + LoaderCustom, SelectCustom, StackCustom, TextCustom, @@ -23,30 +24,29 @@ import { Divider } from "react-native-paper"; export default function AdminDonasiListOfDonatur() { const { id } = useLocalSearchParams(); - console.log("[ID >>]", id); const [listData, setListData] = React.useState(null); + const [loadData, setLoadData] = React.useState(false); const [master, setMaster] = React.useState([]); - const [selectStatus, setSelectStatus] = React.useState< - "berhasil" | "gagal" | "proses" | "menunggu" | "" - >(""); + const [selectValue, setSelectValue] = React.useState(null); + const [selectedStatus, setSelectedStatus] = React.useState( + null + ); useFocusEffect( React.useCallback(() => { onLoadData(); - }, [id, selectStatus]) + }, [id, selectValue]) ); const onLoadData = async () => { try { + setLoadData(true); const response = await apiAdminDonationListOfDonatur({ id: id as string, - status: "" as any, + status: selectedStatus as any, }); - console.log( - "[LIST OF DONATUR]", - JSON.stringify(response, null, 2) - ); + // console.log("[LIST OF DONATUR]", JSON.stringify(response, null, 2)); if (response.success) { setListData(response.data); @@ -54,6 +54,8 @@ export default function AdminDonasiListOfDonatur() { } catch (error) { console.log("[ERROR]", error); setListData([]); + } finally { + setLoadData(false); } }; @@ -64,7 +66,7 @@ export default function AdminDonasiListOfDonatur() { const onLoadMaster = async () => { try { const response = await apiMasterTransaction(); - + if (response.success) { setMaster(response.data); } @@ -83,15 +85,18 @@ export default function AdminDonasiListOfDonatur() { ? [] : master?.map((item: any) => ({ label: item.name, - value: item.name + value: item.id, })) } + value={selectValue} onChange={(value: any) => { - console.log("[SELECT STATUS]", value); - const statusChooses = _.lowerCase(value); - setSelectStatus(statusChooses as any); + setSelectValue(value); + const nameSelected = master.find((item: any) => item.id === value); + const statusChooses = _.lowerCase(nameSelected?.name); + setSelectedStatus(statusChooses); }} styleContainer={{ width: "100%", marginBottom: 0 }} + allowClear /> ); @@ -102,63 +107,78 @@ export default function AdminDonasiListOfDonatur() { } > - - Aksi - - } - component2={ - - Donatur - - } - component3={ - - Status - - } - /> - - {listData?.map((item: any, index: number) => ( - - - } - onPress={() => { - router.push( - `/admin/donation/${id}/berhasil/transaction-detail` - ); - }} - /> - - } - component2={ - - {item?.Author?.username || "-"} - - } - component3={ - - {item?.DonasiMaster_StatusInvoice?.name} - - } - /> - - - ))} + + Aksi + + } + component2={ + + Donatur + + } + component3={ + + Status + + } + /> + + + {loadData ? ( + + ) : _.isEmpty(listData) ? ( + + Belum ada data + + ) : ( + listData?.map((item: any, index: number) => ( + + + + } + onPress={() => { + router.push( + `/admin/donation/${item?.id}/${_.lowerCase( + item?.DonasiMaster_StatusInvoice?.name + )}/transaction-detail` + ); + }} + /> + + } + component2={ + + {item?.Author?.username || "-"} + + } + component3={ + + {item?.DonasiMaster_StatusInvoice?.name} + + } + /> + + )) + )} + diff --git a/components/Select/SelectCustom.tsx b/components/Select/SelectCustom.tsx index 1be160e..0de3c84 100644 --- a/components/Select/SelectCustom.tsx +++ b/components/Select/SelectCustom.tsx @@ -27,6 +27,8 @@ type SelectProps = { onChange: (value: string | number) => void; borderRadius?: number; styleContainer?: StyleProp; + allowClear?: boolean; // <-- new prop + clearLabel?: string; // default: "Kosongkan" }; const SelectCustom: React.FC = ({ @@ -39,13 +41,21 @@ const SelectCustom: React.FC = ({ onChange, borderRadius = 8, styleContainer, + allowClear = true, // bisa dimatikan jika tidak perlu + clearLabel = "Hapus Pilihan", }) => { const [modalVisible, setModalVisible] = useState(false); const selectedItem = data.find((item) => item.value === value); - const hasError = required && value === null; // <-- check if empty and required + // Gabungkan opsi clear di atas daftar + const renderData = allowClear + ? [...data, + // { label: clearLabel, value: "__clear__" } + ] + : data; + return ( {label && ( @@ -54,29 +64,64 @@ const SelectCustom: React.FC = ({ {required && *} )} - !disabled && setModalVisible(true)} + ]} > - !disabled && setModalVisible(true)} > - {selectedItem?.label || placeholder} - - + + {selectedItem?.label || placeholder} + + + + {/* Tombol Clear (Hanya muncul jika ada nilai terpilih & allowClear aktif) */} + {!disabled && allowClear && value !== null && value !== undefined && ( + onChange(null as any)} // null dikirim sebagai value + > + × + + )} + = ({ > String(item.value)} - renderItem={({ item }) => ( - { - onChange(item.value); - setModalVisible(false); - }} - > - {item.label} - - )} + renderItem={({ item }) => { + if (item.value === "__clear__") { + return ( + { + onChange(null as any); // kosongkan nilai + setModalVisible(false); + }} + > + {item.label} + + ); + } + + return ( + { + onChange(item.value); + setModalVisible(false); + }} + > + {item.label} + + ); + }} /> diff --git a/context/AuthContext.tsx b/context/AuthContext.tsx index 67d2f43..153e485 100644 --- a/context/AuthContext.tsx +++ b/context/AuthContext.tsx @@ -91,14 +91,11 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => { setToken(token); await AsyncStorage.setItem("authToken", token); - const responseUser = await apiConfig.get( - `/mobile?token=${token}`, - { - headers: { - Authorization: `Bearer ${token}`, - }, - } - ); + const responseUser = await apiConfig.get(`/mobile?token=${token}`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); const dataUser = responseUser.data.data; setUser(dataUser); @@ -147,9 +144,7 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => { await AsyncStorage.setItem("userData", JSON.stringify(dataUser)); return dataUser; } catch (error: any) { - throw new Error( - error.response?.data?.message || "Gagal mengambil data user" - ); + console.log(error.response?.data?.message + "user" || "Gagal mengambil data user"); } finally { setIsLoading(false); } diff --git a/screens/Donation/BoxPublish.tsx b/screens/Donation/BoxPublish.tsx index bdd1fba..a2f191b 100644 --- a/screens/Donation/BoxPublish.tsx +++ b/screens/Donation/BoxPublish.tsx @@ -70,9 +70,12 @@ export default function Donation_BoxPublish({ {/* Terkumpul : Rp 300.000 diff --git a/screens/Donation/ProgressSection.tsx b/screens/Donation/ProgressSection.tsx index b0fcf5b..ef87c24 100644 --- a/screens/Donation/ProgressSection.tsx +++ b/screens/Donation/ProgressSection.tsx @@ -11,11 +11,23 @@ import { Ionicons, MaterialIcons } from "@expo/vector-icons"; import { router } from "expo-router"; import { View } from "react-native"; -export default function Donation_ProgressSection({ id }: { id: string }) { +export default function Donation_ProgressSection({ + id, + progres, +}: { + id: string; + progres: number; +}) { return ( <> - + diff --git a/service/api-admin/api-admin-donation.ts b/service/api-admin/api-admin-donation.ts index a62dc22..d25aa47 100644 --- a/service/api-admin/api-admin-donation.ts +++ b/service/api-admin/api-admin-donation.ts @@ -53,11 +53,50 @@ export async function apiAdminDonationListOfDonatur({ status, }: { id: string; - status: "berhasil" | "gagal" | "proses" | "menunggu" | ""; + status: "berhasil" | "gagal" | "proses" | "menunggu" | null; }) { + const query = status && status !== null ? `?status=${status}` : ""; + try { const response = await apiConfig.get( - `/mobile/admin/donation/${id}/donatur?status=${status}` + `/mobile/admin/donation/${id}/donatur${query}` + ); + return response.data; + } catch (error) { + throw error; + } +} + +export async function apiAdminDonationInvoiceDetailById({ + id, +}: { + id: string; +}) { + try { + const response = await apiConfig.get( + `/mobile/admin/donation/${id}/invoice` + ); + return response.data; + } catch (error) { + throw error; + } +} + +export async function apiAdminDonationInvoiceUpdateById({ + id, + data, + status, +}: { + id: string; + data: any; + status: "berhasil" | "gagal" | "proses" | "menunggu"; +}) { + try { + const response = await apiConfig.put( + `/mobile/admin/donation/${id}/invoice?status=${status}`, + { + data: data, + } ); return response.data; } catch (error) {