From b3209dc7ee48341ffa0e092c3794b862d1c46dfd Mon Sep 17 00:00:00 2001 From: Bagasbanuna02 Date: Wed, 29 Oct 2025 17:35:18 +0800 Subject: [PATCH] Integrasi API: Donation & Admin Donation Fix: - app/(application)/(user)/donation/[id]/fund-disbursement.tsx - app/(application)/(user)/donation/[id]/list-of-donatur.tsx - app/(application)/admin/donation/[id]/[status]/index.tsx - app/(application)/admin/donation/[id]/detail-disbursement-of-funds.tsx - app/(application)/admin/donation/[id]/disbursement-of-funds.tsx - app/(application)/admin/donation/[id]/list-disbursement-of-funds.tsx - service/api-admin/api-admin-donation.ts - service/api-client/api-donation.ts - utils/pickFile.ts: Sudah bisa memilih ukuran crop tapi hanya di android ### No issue --- .../donation/[id]/fund-disbursement.tsx | 119 +++++++++---- .../(user)/donation/[id]/list-of-donatur.tsx | 99 ++++++++--- .../admin/donation/[id]/[status]/index.tsx | 17 +- .../[id]/detail-disbursement-of-funds.tsx | 43 ++++- .../donation/[id]/disbursement-of-funds.tsx | 159 +++++++++++++++++- .../[id]/list-disbursement-of-funds.tsx | 118 +++++++++---- service/api-admin/api-admin-donation.ts | 49 ++++++ service/api-client/api-donation.ts | 13 ++ utils/pickFile.ts | 47 +++++- 9 files changed, 545 insertions(+), 119 deletions(-) diff --git a/app/(application)/(user)/donation/[id]/fund-disbursement.tsx b/app/(application)/(user)/donation/[id]/fund-disbursement.tsx index 6f28714..a61f703 100644 --- a/app/(application)/(user)/donation/[id]/fund-disbursement.tsx +++ b/app/(application)/(user)/donation/[id]/fund-disbursement.tsx @@ -1,17 +1,71 @@ +/* eslint-disable react-hooks/exhaustive-deps */ import { BaseBox, ButtonCenteredOnly, Grid, InformationBox, + LoaderCustom, StackCustom, TextCustom, ViewWrapper, } from "@/components"; +import { + apiDonationDisbursementOfFundsListById, + apiDonationGetOne, +} from "@/service/api-client/api-donation"; +import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay"; import dayjs from "dayjs"; -import { router, useLocalSearchParams } from "expo-router"; +import { router, useFocusEffect, useLocalSearchParams } from "expo-router"; +import _ from "lodash"; +import React, { useState } from "react"; export default function DonationFundDisbursement() { const { id } = useLocalSearchParams(); + + const [data, setData] = useState({ + totalPencairan: 0, + akumulasiPencairan: 0, + }); + + const [listData, setListData] = React.useState(null); + const [loadData, setLoadData] = React.useState(false); + + useFocusEffect( + React.useCallback(() => { + onLoadData(); + }, [id]) + ); + + const onLoadData = async () => { + try { + setLoadData(true); + + const responseData = await apiDonationGetOne({ + id: id as string, + category: "permanent", + }); + + if (responseData.success) { + setData({ + totalPencairan: responseData.data.totalPencairan, + akumulasiPencairan: responseData.data.akumulasiPencairan, + }); + } + + const responseList = await apiDonationDisbursementOfFundsListById({ + id: id as string, + }); + + if (responseList.success) { + setListData(responseList.data); + } + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoadData(false); + } + }; + return ( <> @@ -20,47 +74,50 @@ export default function DonationFundDisbursement() { - Rp. 0 + Rp. {formatCurrencyDisplay(data?.totalPencairan)} Total Pencairan Dana - 0 kali + {data?.akumulasiPencairan} kali Akumulasi Pencairan - {Array.from({ length: 10 }).map((_, index) => ( - - - - - Pencairan ke - {index + 1} - - - {dayjs().format("DD MMM YYYY")} - - - - Lorem ipsum dolor sit amet consectetur adipisicing elit. - Nesciunt dolor ad sit? Eaque rem nihil natus, id, esse possimus - perferendis provident velit illo consectetur distinctio ab - accusantium quis earum omnis! - - { - router.navigate(`/(application)/(file)/${id}`); - }} - icon="file-text" - > - Bukti Transaksi - - - - ))} + {loadData ? ( + + ) : _.isEmpty(listData) ? ( + + Belum ada data + + ) : ( + listData?.map((item, index) => ( + + + + + {item?.title} + + + {dayjs(item?.createdAt).format("DD MMM YYYY")} + + + {item?.deskripsi} + { + router.navigate(`/(application)/(image)/preview-image/${item?.imageId}`); + }} + icon="file-text" + > + Bukti Transaksi + + + + )) + )} ); diff --git a/app/(application)/(user)/donation/[id]/list-of-donatur.tsx b/app/(application)/(user)/donation/[id]/list-of-donatur.tsx index d9f65af..fc78cc7 100644 --- a/app/(application)/(user)/donation/[id]/list-of-donatur.tsx +++ b/app/(application)/(user)/donation/[id]/list-of-donatur.tsx @@ -1,46 +1,93 @@ +/* eslint-disable react-hooks/exhaustive-deps */ import { BaseBox, Grid, + LoaderCustom, + Spacing, StackCustom, TextCustom, ViewWrapper, } from "@/components"; import { MainColor } from "@/constants/color-palet"; +import { apiAdminDonationListOfDonaturById } from "@/service/api-admin/api-admin-donation"; +import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay"; import { FontAwesome6 } from "@expo/vector-icons"; import dayjs from "dayjs"; +import { useFocusEffect, useLocalSearchParams } from "expo-router"; +import _ from "lodash"; +import { useCallback, useState } from "react"; export default function Donation_ListOfDonatur() { + const { id } = useLocalSearchParams(); + const [listData, setListData] = useState(null); + const [loadData, setLoadData] = useState(false); + + useFocusEffect( + useCallback(() => { + onLoadData(); + }, [id]) + ); + + const onLoadData = async () => { + try { + setLoadData(true); + const response = await apiAdminDonationListOfDonaturById({ + id: id as string, + }); + + + if (response.success) { + setListData(response.data); + } + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoadData(false); + } + }; + return ( <> - {Array.from({ length: 10 }).map((_, index) => ( - - - - - - - + {loadData ? ( + + ) : _.isEmpty(listData) ? ( + + Belum ada donatur + + ) : ( + listData?.map((item: any, index: number) => ( + + + + + + - Username + {item?.Author?.username || "-"} - Berdonas sebesar - - Rp. 100.000 - - {dayjs().format("DD MMM YYYY")} - - - - - ))} + + + Berdonas sebesar + + Rp. {formatCurrencyDisplay(item?.nominal)} + + + {dayjs(item?.createdAt).format("DD MMM YYYY, HH:mm")} + + + + + + )) + )} ); diff --git a/app/(application)/admin/donation/[id]/[status]/index.tsx b/app/(application)/admin/donation/[id]/[status]/index.tsx index 87b09ea..86e4ef0 100644 --- a/app/(application)/admin/donation/[id]/[status]/index.tsx +++ b/app/(application)/admin/donation/[id]/[status]/index.tsx @@ -53,8 +53,6 @@ export default function AdminDonationDetail() { id: id as string, }); - console.log("[RES GET BY ID]", JSON.stringify(response, null, 2)); - if (response.success) { setData(response.data.donasi); setCountDonatur(response.data.donatur); @@ -79,7 +77,9 @@ export default function AdminDonationDetail() { value: data && data?.DonasiMaster_Status?.name ? ( {_.startCase(data?.DonasiMaster_Status?.name)} @@ -107,7 +107,7 @@ export default function AdminDonationDetail() { const listPencarianDana = [ { label: "Total Dana Dicairkan", - value: `Rp ${(data && data?.totalPencairan) || 0}`, + value: `Rp ${(data && formatCurrencyDisplay(data?.totalPencairan)) || 0}`, }, { label: "Sisa Dana Masuk", @@ -207,7 +207,15 @@ export default function AdminDonationDetail() { iconLeft={ } + disabled={data?.terkumpul - data?.totalPencairan <= 0} onPress={() => { + if (data?.terkumpul - data?.totalPencairan <= 0) { + Toast.show({ + type: "error", + text1: "Tidak ada dana yang tersisa", + }); + return; + } router.push(`/admin/donation/${id}/disbursement-of-funds`); }} > @@ -227,7 +235,6 @@ export default function AdminDonationDetail() { /> - Jumlah Donatur} diff --git a/app/(application)/admin/donation/[id]/detail-disbursement-of-funds.tsx b/app/(application)/admin/donation/[id]/detail-disbursement-of-funds.tsx index 2a5e4cc..4399dfd 100644 --- a/app/(application)/admin/donation/[id]/detail-disbursement-of-funds.tsx +++ b/app/(application)/admin/donation/[id]/detail-disbursement-of-funds.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react-hooks/exhaustive-deps */ import { BaseBox, ButtonCustom, @@ -7,27 +8,53 @@ import { } from "@/components"; import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle"; import { GridDetail_4_8 } from "@/components/_ShareComponent/GridDetail_4_8"; -import dayjs from "dayjs"; -import { router, useLocalSearchParams } from "expo-router"; +import { apiAdminDonationDisbursementOfFundsListById } from "@/service/api-admin/api-admin-donation"; +import { dateTimeView } from "@/utils/dateTimeView"; +import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay"; +import { router, useFocusEffect, useLocalSearchParams } from "expo-router"; +import React, { useCallback } from "react"; export default function AdminDonationDetailDisbursementOfFunds() { const { id } = useLocalSearchParams(); + const [data, setData] = React.useState(null); + + useFocusEffect( + useCallback(() => { + onLoadData(); + }, [id]) + ); + + const onLoadData = async () => { + try { + const response = await apiAdminDonationDisbursementOfFundsListById({ + id: id as string, + category: "get-one", + }); + + if (response.success) { + setData(response.data); + } + } catch (error) { + console.log("[ERROR]", error); + } + }; + const listData = [ { label: "Nominal", - value: "Rp 1.000.000", + value: `Rp ${(data && formatCurrencyDisplay(data?.nominalCair)) || 0}`, }, { label: "Tanggal", - value: dayjs().format("DD-MM-YYYY HH:mm"), + value: dateTimeView({ date: data?.createdAt }), }, { label: "Judul", - value: `Judul Pencairan Dana ${id}`, + value: (data && data?.title) || "-", }, { label: "Deskripsi", - value: `Lorem ipsum dolor sit amet consectetur adipisicing elit. Itaque velit eos facere a dicta nemo repellendus harum laboriosam quos, earum reprehenderit. Nisi sapiente, quo earum quis alias ullam temporibus quidem.`, + value: (data && data?.deskripsi) || "-", }, ]; return ( @@ -39,7 +66,7 @@ export default function AdminDonationDetailDisbursementOfFunds() { > - {listData.map((item, index) => ( + {listData?.map((item, index) => ( {item.label}} @@ -51,7 +78,7 @@ export default function AdminDonationDetailDisbursementOfFunds() { - router.push(`/(application)/(image)/preview-image/${id}`) + router.push(`/(application)/(image)/preview-image/${data?.imageId}`) } > Cek Bukti Transaksi diff --git a/app/(application)/admin/donation/[id]/disbursement-of-funds.tsx b/app/(application)/admin/donation/[id]/disbursement-of-funds.tsx index 1a9424d..cde5b11 100644 --- a/app/(application)/admin/donation/[id]/disbursement-of-funds.tsx +++ b/app/(application)/admin/donation/[id]/disbursement-of-funds.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react-hooks/exhaustive-deps */ import { BaseBox, BoxButtonOnFooter, @@ -12,15 +13,122 @@ import { ViewWrapper, } from "@/components"; import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle"; -import { router, useLocalSearchParams } from "expo-router"; +import DIRECTORY_ID from "@/constants/directory-id"; +import { apiAdminDonationDetailById, apiAdminDonationDisbursementOfFundsCreated } from "@/service/api-admin/api-admin-donation"; +import { uploadFileService } from "@/service/upload-service"; +import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay"; +import pickFile from "@/utils/pickFile"; +import { Image } from "expo-image"; +import { router, useFocusEffect, useLocalSearchParams } from "expo-router"; +import React from "react"; +import Toast from "react-native-toast-message"; export default function AdminDonationDisbursementOfFunds() { const { id } = useLocalSearchParams(); - const handleSubmit = ( + + const [data, setData] = React.useState(null); + const [isLoading, setIsLoading] = React.useState(false); + + const [value, setValue] = React.useState({ + nominalCair: "", + title: "", + deskripsi: "", + }); + + const [image, setImage] = React.useState(null); + + useFocusEffect( + React.useCallback(() => { + onLoadData(); + }, [id]) + ); + + const onLoadData = async () => { + try { + const response = await apiAdminDonationDetailById({ + id: id as string, + }); + + if (response.success) { + setData(response.data.donasi); + } + } catch (error) { + console.log("[ERROR]", error); + setData(null); + } + }; + + const handleSubmit = async () => { + if (!image) { + Toast.show({ + type: "error", + text1: "Harap upload bukti transfer", + }); + return; + } + + if (!value.nominalCair || !value.title || !value.deskripsi) { + Toast.show({ + type: "error", + text1: "Harap isi semua data", + }); + return; + } + + try { + setIsLoading(true); + const uploadImage = await uploadFileService({ + dirId: DIRECTORY_ID.donasi_bukti_trf_pencairan_dana, + imageUri: image.uri, + }); + + if (!uploadFileService) { + Toast.show({ + type: "error", + text1: "Gagal mengunggah gambar", + }); + return; + } + + const imageId = uploadImage.data.id; + + const newData = { + ...value, + imageId: imageId, + }; + + const response = await apiAdminDonationDisbursementOfFundsCreated({ + id: id as string, + data: newData, + }); + + if (!response.success) { + Toast.show({ + type: "error", + text1: response.message, + }); + return; + } + + Toast.show({ + type: "success", + text1: "Pencairan dana berhasil disimpan", + }); + + router.back(); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setIsLoading(false); + } + }; + + const buttonSubmit = ( { - router.back(); + handleSubmit(); }} > Simpan @@ -31,7 +139,7 @@ export default function AdminDonationDisbursementOfFunds() { return ( } - footerComponent={handleSubmit} + footerComponent={buttonSubmit} > @@ -39,7 +147,7 @@ export default function AdminDonationDisbursementOfFunds() { Dana Tersisa - Rp 1.000.000 + Rp {formatCurrencyDisplay(data?.terkumpul - data?.totalPencairan)} @@ -56,9 +164,27 @@ export default function AdminDonationDisbursementOfFunds() { label="Nominal" placeholder="0" iconLeft={"Rp"} + value={value.nominalCair} + onChangeText={(text) => { + setValue({ + ...value, + nominalCair: text, + }); + }} /> - + { + setValue({ + ...value, + title: text, + }); + }} + /> { + setValue({ + ...value, + deskripsi: text, + }); + }} /> - + + + { - router.push(`/(application)/(image)/take-picture/${id}`); + pickFile({ + allowedType: "image", + aspectRatio: [9, 16], + setImageUri: (file) => { + setImage(file); + }, + }); }} icon="upload" > Upload + + ); } diff --git a/app/(application)/admin/donation/[id]/list-disbursement-of-funds.tsx b/app/(application)/admin/donation/[id]/list-disbursement-of-funds.tsx index 12c2a68..173cc68 100644 --- a/app/(application)/admin/donation/[id]/list-disbursement-of-funds.tsx +++ b/app/(application)/admin/donation/[id]/list-disbursement-of-funds.tsx @@ -1,21 +1,54 @@ +/* eslint-disable react-hooks/exhaustive-deps */ import { - ActionIcon, - CenterCustom, - Divider, - StackCustom, - TextCustom, - ViewWrapper + ActionIcon, + CenterCustom, + Divider, + LoaderCustom, + StackCustom, + TextCustom, + ViewWrapper, } from "@/components"; import { IconView } from "@/components/_Icon/IconComponent"; import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle"; import { GridViewCustomSpan } from "@/components/_ShareComponent/GridViewCustomSpan"; import { ICON_SIZE_BUTTON } from "@/constants/constans-value"; +import { apiAdminDonationDisbursementOfFundsListById } from "@/service/api-admin/api-admin-donation"; +import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay"; import dayjs from "dayjs"; -import { router, useLocalSearchParams } from "expo-router"; +import { router, useFocusEffect, useLocalSearchParams } from "expo-router"; +import _ from "lodash"; +import React, { useCallback } from "react"; import { View } from "react-native"; export default function AdminDonasiListOfDisbursementOfFunds() { const { id } = useLocalSearchParams(); + const [listData, setListData] = React.useState(null); + const [loadData, setLoadData] = React.useState(false); + + useFocusEffect( + useCallback(() => { + onLoadData(); + }, [id]) + ); + + const onLoadData = async () => { + try { + setLoadData(true); + const response = await apiAdminDonationDisbursementOfFundsListById({ + id: id as string, + category: "get-all", + }); + + if (response.success) { + setListData(response.data); + } + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoadData(false); + } + }; + return ( <> - {Array.from({ length: 10 }).map((_, index) => ( - - - } - onPress={() => { - router.push( - `/admin/donation/${id}/detail-disbursement-of-funds` - ); - }} - /> - - } - component2={ - - {dayjs() - .add(index + 1, "day") - .format("DD-MM-YYYY HH:mm")} - - } - component3={Rp. 1.000.000} - /> - - - ))} + {loadData ? ( + + ) : _.isEmpty(listData) ? ( + + Belum ada data + + ) : ( + listData?.map((item, index) => ( + + + + } + onPress={() => { + router.push( + `/admin/donation/${item?.id}/detail-disbursement-of-funds` + ); + }} + /> + + } + component2={ + + {dayjs(item?.createdAt).format("DD-MM-YYYY")} + + } + component3={ + + Rp. {formatCurrencyDisplay(item?.nominalCair)} + + } + /> + + )) + )} diff --git a/service/api-admin/api-admin-donation.ts b/service/api-admin/api-admin-donation.ts index d25aa47..ebd6e9c 100644 --- a/service/api-admin/api-admin-donation.ts +++ b/service/api-admin/api-admin-donation.ts @@ -103,3 +103,52 @@ export async function apiAdminDonationInvoiceUpdateById({ throw error; } } + +export async function apiAdminDonationListOfDonaturById({ + id, +}: { + id: string; +}) { + try { + const response = await apiConfig.get(`/mobile/donation/${id}/donatur`); + return response.data; + } catch (error) { + throw error; + } +} + +export async function apiAdminDonationDisbursementOfFundsCreated({ + id, + data, +}: { + id: string; + data: any; +}) { + try { + const response = await apiConfig.post( + `/mobile/admin/donation/${id}/disbursement`, + { + data: data, + } + ); + return response.data; + } catch (error) { + throw error; + } +} + +export async function apiAdminDonationDisbursementOfFundsListById({ + id, + category, +}: { + id: string; + category: "get-all" | "get-one" +}) { + try { + const response = await apiConfig.get(`/mobile/admin/donation/${id}/disbursement?category=${category}`); + return response.data; + } catch (error) { + throw error; + } +} + diff --git a/service/api-client/api-donation.ts b/service/api-client/api-donation.ts index 675e098..f29a437 100644 --- a/service/api-client/api-donation.ts +++ b/service/api-client/api-donation.ts @@ -255,3 +255,16 @@ export async function apiDonationDeleteNews({ id }: { id: string }) { throw error; } } + +export async function apiDonationDisbursementOfFundsListById({ + id, +}: { + id: string; +}) { + try { + const response = await apiConfig.get(`/mobile/donation/${id}/disbursement`); + return response.data; + } catch (error) { + throw error; + } +} diff --git a/utils/pickFile.ts b/utils/pickFile.ts index 15479a3..613a7d7 100644 --- a/utils/pickFile.ts +++ b/utils/pickFile.ts @@ -13,19 +13,28 @@ export interface IFileData { export type AllowedFileType = "image" | "pdf" | undefined; +export type AspectRatio = [number, number]; + export interface PickFileOptions { setImageUri?: (file: IFileData) => void; setPdfUri?: (file: IFileData) => void; allowedType?: AllowedFileType; // <-- Tambahkan prop ini + aspectRatio?: AspectRatio; } export default async function pickFile({ setImageUri, setPdfUri, allowedType, + aspectRatio, }: PickFileOptions): Promise { if (allowedType === "image") { - await pickImage(setImageUri); + if (aspectRatio) { + await pickImage(setImageUri, aspectRatio); + } else { + // Jika tidak, tawarkan pilihan rasio (default [4,3]) + showAspectRatioChoice(setImageUri); + } } else if (allowedType === "pdf") { await pickPdf(setPdfUri); } else { @@ -36,15 +45,45 @@ export default async function pickFile({ [ { text: "Batal", style: "cancel" }, { text: "Dokumen (PDF)", onPress: () => pickPdf(setPdfUri) }, - { text: "Gambar", onPress: () => pickImage(setImageUri) }, + { text: "Gambar", onPress: () => pickImage(setImageUri, aspectRatio) }, ], { cancelable: true } ); } } +function showAspectRatioChoice(setImageUri?: (file: IFileData) => void) { + Alert.alert( + "Pilih Rasio Gambar", + "Pilih rasio crop yang diinginkan:", + [ + { text: "Batal", style: "cancel" }, + { + text: "1:1 (Kotak)", + onPress: () => pickImage(setImageUri, [1, 1]), + }, + // { + // text: "4:3 (Default)", + // onPress: () => pickImage(setImageUri, [4, 3]), + // }, + // { + // text: "9:16 (Landscape)", + // onPress: () => pickImage(setImageUri, [9, 16]), + // }, + { + text: "3:4 (Potret)", + onPress: () => pickImage(setImageUri, [3, 4]), + }, + ], + { cancelable: true } + ); +} + // --- Fungsi internal: pickImage --- -async function pickImage(setImageUri?: (file: IFileData) => void) { +async function pickImage( + setImageUri?: (file: IFileData) => void, + aspectRatio: AspectRatio = [4, 3] // Default [4, 3] +) { const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync(); if (status !== "granted") { Alert.alert( @@ -57,7 +96,7 @@ async function pickImage(setImageUri?: (file: IFileData) => void) { const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ImagePicker.MediaTypeOptions.Images, allowsEditing: true, - aspect: [4, 3], + aspect: aspectRatio, // 🎯 Gunakan rasio dinamis quality: 1, });