Integrasi API: Donation & Admin Donation #3

Merged
bagasbanuna merged 1 commits from api-admin/28-oct-25 into main 2025-10-28 17:51:23 +08:00
10 changed files with 420 additions and 153 deletions

View File

@@ -122,7 +122,7 @@ export default function DonationInvoice() {
}} }}
> >
<TextCustom size="xlarge" bold color="yellow"> <TextCustom size="xlarge" bold color="yellow">
{data?.DonasiMaster_Bank?.norek} {data?.MasterBank?.norek}
</TextCustom> </TextCustom>
</Grid.Col> </Grid.Col>
<Grid.Col <Grid.Col
@@ -131,7 +131,7 @@ export default function DonationInvoice() {
alignItems: "flex-end", alignItems: "flex-end",
}} }}
> >
<CopyButton textToCopy={data?.DonasiMaster_Bank?.norek} /> <CopyButton textToCopy={data?.MasterBank?.norek} />
</Grid.Col> </Grid.Col>
</Grid> </Grid>
</BaseBox> </BaseBox>

View File

@@ -102,7 +102,7 @@ export default function DonasiDetailBeranda() {
sisaHari={value.sisa} sisaHari={value.sisa}
reminder={value.reminder} reminder={value.reminder}
data={data} data={data}
bottomSection={<Donation_ProgressSection id={id as string} />} bottomSection={<Donation_ProgressSection id={id as string} progres={Number(data?.progres) || 0} />}
/> />
<Donation_ComponentInfoFundrising dataAuthor={data?.Author} /> <Donation_ComponentInfoFundrising dataAuthor={data?.Author} />
<Donation_ComponentStoryFunrising <Donation_ComponentStoryFunrising

View File

@@ -38,6 +38,7 @@ export default function AdminDonationDetail() {
const [openDrawer, setOpenDrawer] = React.useState(false); const [openDrawer, setOpenDrawer] = React.useState(false);
const [data, setData] = React.useState<any | null>(null); const [data, setData] = React.useState<any | null>(null);
const [countDonatur, setCountDonatur] = React.useState(0);
const [isLoading, setIsLoading] = React.useState(false); const [isLoading, setIsLoading] = React.useState(false);
useFocusEffect( useFocusEffect(
@@ -55,7 +56,8 @@ export default function AdminDonationDetail() {
console.log("[RES GET BY ID]", JSON.stringify(response, null, 2)); console.log("[RES GET BY ID]", JSON.stringify(response, null, 2));
if (response.success) { if (response.success) {
setData(response.data); setData(response.data.donasi);
setCountDonatur(response.data.donatur);
} }
} catch (error) { } catch (error) {
console.log("[ERROR]", error); console.log("[ERROR]", error);
@@ -108,12 +110,16 @@ export default function AdminDonationDetail() {
value: `Rp ${(data && data?.totalPencairan) || 0}`, value: `Rp ${(data && data?.totalPencairan) || 0}`,
}, },
{ {
label: "Sisa Dana", label: "Sisa Dana Masuk",
value: `Rp ${(data && data?.terkumpul - data?.totalPencairan) || 0}`, value: `Rp ${
(data &&
formatCurrencyDisplay(data?.terkumpul - data?.totalPencairan)) ||
0
}`,
}, },
{ {
label: "Akumulasi Pencairan", label: "Akumulasi Pencairan",
value: `${(data && data?.totalPencairan) || 0} kali`, value: `${(data && data?.akumulasiPencairan) || 0} kali`,
}, },
{ {
label: "Bank Tujuan", label: "Bank Tujuan",
@@ -196,7 +202,7 @@ export default function AdminDonationDetail() {
/> />
))} ))}
</StackCustom> </StackCustom>
<ButtonCustom <ButtonCustom
iconLeft={ iconLeft={
<Ionicons name="cash-outline" size={ICON_SIZE_BUTTON} /> <Ionicons name="cash-outline" size={ICON_SIZE_BUTTON} />
@@ -211,16 +217,33 @@ export default function AdminDonationDetail() {
</BaseBox> </BaseBox>
<BaseBox> <BaseBox>
<ProgressCustom size="lg" /> <ProgressCustom
size="lg"
value={Number(data?.progres) || 0}
showLabel={true}
label={data?.progres + "%"}
animated
color="primary"
/>
<Spacing /> <Spacing />
<StackCustom gap={"xs"}> <StackCustom gap={"xs"}>
<GridDetail_4_8 <GridDetail_4_8
label={<TextCustom bold>Jumlah Donatur</TextCustom>} label={<TextCustom bold>Jumlah Donatur</TextCustom>}
value={<TextCustom>0 orang</TextCustom>} value={
<TextCustom>
{countDonatur ? countDonatur : 0} orang
</TextCustom>
}
/> />
<GridDetail_4_8 <GridDetail_4_8
label={<TextCustom bold>Dana Terkumpul</TextCustom>} label={<TextCustom bold>Dana Terkumpul</TextCustom>}
value={<TextCustom>Rp 0</TextCustom>} value={
<TextCustom>
Rp {formatCurrencyDisplay(data?.terkumpul || 0)}
</TextCustom>
}
/> />
</StackCustom> </StackCustom>
</BaseBox> </BaseBox>

View File

@@ -9,51 +9,162 @@ import {
} from "@/components"; } from "@/components";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle"; import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import { GridDetail_4_8 } from "@/components/_ShareComponent/GridDetail_4_8"; import { GridDetail_4_8 } from "@/components/_ShareComponent/GridDetail_4_8";
import { MainColor } from "@/constants/color-palet"; import {
import dayjs from "dayjs"; apiAdminDonationInvoiceDetailById,
import { router, useLocalSearchParams } from "expo-router"; 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() { export default function AdminDonasiTransactionDetail() {
const { id } = useLocalSearchParams(); const { id, status } = useLocalSearchParams();
console.log("[STATUS]", id, status);
const buttonAction = ( const [data, setData] = useState<any | null>(null);
<BoxButtonOnFooter> const [isLoading, setLoading] = useState(false);
<ButtonCustom onPress={() => router.back()}>Terima</ButtonCustom>
</BoxButtonOnFooter> 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 (
<BoxButtonOnFooter>
<ButtonCustom
isLoading={isLoading}
onPress={() => {
handlerSubmit();
}}
>
Terima donasi
</ButtonCustom>
</BoxButtonOnFooter>
);
}
return (
<BoxButtonOnFooter>
<ButtonCustom disabled>
{data?.DonasiMaster_StatusInvoice?.name}
</ButtonCustom>
</BoxButtonOnFooter>
);
};
const listData = [ const listData = [
{ {
label: "Donatur", label: "Donatur",
value: "Bagas Banuna", value: (data && data?.Author?.username) || "-",
}, },
{ {
label: "Bank", label: "Bank",
value: "BCA", value: (data && data?.MasterBank?.namaBank) || "-",
}, },
{ {
label: "Jumlah Donasi", label: "Jumlah Donasi",
value: "Rp. 1.000.000", value: `Rp. ${
(data && data?.nominal && formatCurrencyDisplay(data?.nominal)) || "-"
}`,
}, },
{ {
label: "Status", label: "Status",
value: <BadgeCustom color={MainColor.green}>Berhasil</BadgeCustom>, value:
(data && data?.DonasiMaster_StatusInvoice?.name && (
<BadgeCustom
color={colorBadgeTransaction({
status: data?.DonasiMaster_StatusInvoice?.name as any,
})}
>
{_.startCase(
(data?.DonasiMaster_StatusInvoice?.name as any) || "-"
)}
</BadgeCustom>
)) ||
"-",
}, },
{ {
label: "Tanggal", label: "Tanggal",
value: dayjs().format("DD-MM-YYYY HH:mm:ss"), value: (data && dateTimeView({ date: data?.createdAt })) || "-",
}, },
{ {
label: "Bukti Transfer", label: "Bukti Transfer",
value: ( value:
<ButtonCustom (data && data?.imageId && (
onPress={() => <ButtonCustom
router.push(`/(application)/(image)/preview-image/${id}`) onPress={() =>
} router.push(
> `/(application)/(image)/preview-image/${data?.imageId}`
Cek )
</ButtonCustom> }
), >
Cek
</ButtonCustom>
)) ||
"-",
}, },
]; ];
@@ -61,7 +172,7 @@ export default function AdminDonasiTransactionDetail() {
<> <>
<ViewWrapper <ViewWrapper
headerComponent={<AdminBackButtonAntTitle title="Detail Transaksi" />} headerComponent={<AdminBackButtonAntTitle title="Detail Transaksi" />}
footerComponent={buttonAction} footerComponent={buttonAction()}
> >
<BaseBox> <BaseBox>
<StackCustom> <StackCustom>

View File

@@ -3,6 +3,7 @@ import {
ActionIcon, ActionIcon,
BadgeCustom, BadgeCustom,
CenterCustom, CenterCustom,
LoaderCustom,
SelectCustom, SelectCustom,
StackCustom, StackCustom,
TextCustom, TextCustom,
@@ -23,30 +24,29 @@ import { Divider } from "react-native-paper";
export default function AdminDonasiListOfDonatur() { export default function AdminDonasiListOfDonatur() {
const { id } = useLocalSearchParams(); const { id } = useLocalSearchParams();
console.log("[ID >>]", id);
const [listData, setListData] = React.useState<any[] | null>(null); const [listData, setListData] = React.useState<any[] | null>(null);
const [loadData, setLoadData] = React.useState(false);
const [master, setMaster] = React.useState<any[]>([]); const [master, setMaster] = React.useState<any[]>([]);
const [selectStatus, setSelectStatus] = React.useState< const [selectValue, setSelectValue] = React.useState<string | null>(null);
"berhasil" | "gagal" | "proses" | "menunggu" | "" const [selectedStatus, setSelectedStatus] = React.useState<string | null>(
>(""); null
);
useFocusEffect( useFocusEffect(
React.useCallback(() => { React.useCallback(() => {
onLoadData(); onLoadData();
}, [id, selectStatus]) }, [id, selectValue])
); );
const onLoadData = async () => { const onLoadData = async () => {
try { try {
setLoadData(true);
const response = await apiAdminDonationListOfDonatur({ const response = await apiAdminDonationListOfDonatur({
id: id as string, id: id as string,
status: "" as any, status: selectedStatus as any,
}); });
console.log( // console.log("[LIST OF DONATUR]", JSON.stringify(response, null, 2));
"[LIST OF DONATUR]",
JSON.stringify(response, null, 2)
);
if (response.success) { if (response.success) {
setListData(response.data); setListData(response.data);
@@ -54,6 +54,8 @@ export default function AdminDonasiListOfDonatur() {
} catch (error) { } catch (error) {
console.log("[ERROR]", error); console.log("[ERROR]", error);
setListData([]); setListData([]);
} finally {
setLoadData(false);
} }
}; };
@@ -64,7 +66,7 @@ export default function AdminDonasiListOfDonatur() {
const onLoadMaster = async () => { const onLoadMaster = async () => {
try { try {
const response = await apiMasterTransaction(); const response = await apiMasterTransaction();
if (response.success) { if (response.success) {
setMaster(response.data); setMaster(response.data);
} }
@@ -83,15 +85,18 @@ export default function AdminDonasiListOfDonatur() {
? [] ? []
: master?.map((item: any) => ({ : master?.map((item: any) => ({
label: item.name, label: item.name,
value: item.name value: item.id,
})) }))
} }
value={selectValue}
onChange={(value: any) => { onChange={(value: any) => {
console.log("[SELECT STATUS]", value); setSelectValue(value);
const statusChooses = _.lowerCase(value); const nameSelected = master.find((item: any) => item.id === value);
setSelectStatus(statusChooses as any); const statusChooses = _.lowerCase(nameSelected?.name);
setSelectedStatus(statusChooses);
}} }}
styleContainer={{ width: "100%", marginBottom: 0 }} styleContainer={{ width: "100%", marginBottom: 0 }}
allowClear
/> />
</View> </View>
); );
@@ -102,63 +107,78 @@ export default function AdminDonasiListOfDonatur() {
<AdminBackButtonAntTitle newComponent={searchComponent} /> <AdminBackButtonAntTitle newComponent={searchComponent} />
} }
> >
<GridViewCustomSpan
span1={3}
span2={5}
span3={4}
component1={
<TextCustom bold align="center">
Aksi
</TextCustom>
}
component2={
<TextCustom bold align="center">
Donatur
</TextCustom>
}
component3={
<TextCustom bold align="center">
Status
</TextCustom>
}
/>
<Divider />
<StackCustom> <StackCustom>
{listData?.map((item: any, index: number) => ( <GridViewCustomSpan
<View key={index}> span1={3}
<GridViewCustomSpan span2={5}
span1={3} span3={4}
span2={5} component1={
span3={4} <TextCustom bold align="center">
component1={ Aksi
<CenterCustom> </TextCustom>
<ActionIcon }
icon={<IconView size={ICON_SIZE_BUTTON} color="black" />} component2={
onPress={() => { <TextCustom bold align="center">
router.push( Donatur
`/admin/donation/${id}/berhasil/transaction-detail` </TextCustom>
); }
}} component3={
/> <TextCustom bold align="center">
</CenterCustom> Status
} </TextCustom>
component2={ }
<TextCustom bold align="center" truncate> />
{item?.Author?.username || "-"} <Divider />
</TextCustom> <StackCustom>
} {loadData ? (
component3={ <LoaderCustom />
<BadgeCustom ) : _.isEmpty(listData) ? (
style={{ alignSelf: "center" }} <TextCustom align="center" color="gray">
color={colorBadgeTransaction({status: item?.DonasiMaster_StatusInvoice?.name})} Belum ada data
> </TextCustom>
{item?.DonasiMaster_StatusInvoice?.name} ) : (
</BadgeCustom> listData?.map((item: any, index: number) => (
} <View key={index}>
/> <GridViewCustomSpan
<Divider /> span1={3}
</View> span2={5}
))} span3={4}
component1={
<CenterCustom>
<ActionIcon
icon={
<IconView size={ICON_SIZE_BUTTON} color="black" />
}
onPress={() => {
router.push(
`/admin/donation/${item?.id}/${_.lowerCase(
item?.DonasiMaster_StatusInvoice?.name
)}/transaction-detail`
);
}}
/>
</CenterCustom>
}
component2={
<TextCustom bold align="center" truncate>
{item?.Author?.username || "-"}
</TextCustom>
}
component3={
<BadgeCustom
style={{ alignSelf: "center" }}
color={colorBadgeTransaction({
status: item?.DonasiMaster_StatusInvoice?.name,
})}
>
{item?.DonasiMaster_StatusInvoice?.name}
</BadgeCustom>
}
/>
</View>
))
)}
</StackCustom>
</StackCustom> </StackCustom>
</ViewWrapper> </ViewWrapper>
</> </>

View File

@@ -27,6 +27,8 @@ type SelectProps = {
onChange: (value: string | number) => void; onChange: (value: string | number) => void;
borderRadius?: number; borderRadius?: number;
styleContainer?: StyleProp<ViewStyle>; styleContainer?: StyleProp<ViewStyle>;
allowClear?: boolean; // <-- new prop
clearLabel?: string; // default: "Kosongkan"
}; };
const SelectCustom: React.FC<SelectProps> = ({ const SelectCustom: React.FC<SelectProps> = ({
@@ -39,13 +41,21 @@ const SelectCustom: React.FC<SelectProps> = ({
onChange, onChange,
borderRadius = 8, borderRadius = 8,
styleContainer, styleContainer,
allowClear = true, // bisa dimatikan jika tidak perlu
clearLabel = "Hapus Pilihan",
}) => { }) => {
const [modalVisible, setModalVisible] = useState(false); const [modalVisible, setModalVisible] = useState(false);
const selectedItem = data.find((item) => item.value === value); const selectedItem = data.find((item) => item.value === value);
const hasError = required && value === null; // <-- check if empty and required 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 ( return (
<View style={[GStyles.inputContainerArea, styleContainer]}> <View style={[GStyles.inputContainerArea, styleContainer]}>
{label && ( {label && (
@@ -54,29 +64,64 @@ const SelectCustom: React.FC<SelectProps> = ({
{required && <Text style={GStyles.inputRequired}> *</Text>} {required && <Text style={GStyles.inputRequired}> *</Text>}
</Text> </Text>
)} )}
<Pressable
{/* Input Container */}
<View
style={[ style={[
{ borderRadius, }, {
hasError ? GStyles.inputErrorBorder : null, flexDirection: "row",
alignItems: "center",
// backgroundColor: "red",
// flex: 1,
borderRadius,
},
GStyles.inputContainerInput, GStyles.inputContainerInput,
hasError ? GStyles.inputErrorBorder : null,
disabled && GStyles.disabledBox, disabled && GStyles.disabledBox,
]} // <-- add error style ]}
onPress={() => !disabled && setModalVisible(true)}
> >
<Text <Pressable
style={ style={[
selectedItem {
? disabled flex: 1,
? GStyles.inputTextDisabled borderRadius,
: GStyles.inputText flexDirection: "row",
: disabled alignItems: "center",
? GStyles.inputPlaceholderDisabled paddingHorizontal: 10,
: GStyles.inputPlaceholder height: 50,
} },
// GStyles.inputContainerInput,
// hasError ? GStyles.inputErrorBorder : null,
// disabled && GStyles.disabledBox,
]}
onPress={() => !disabled && setModalVisible(true)}
> >
{selectedItem?.label || placeholder} <Text
</Text> style={
</Pressable> selectedItem
? disabled
? GStyles.inputTextDisabled
: GStyles.inputText
: disabled
? GStyles.inputPlaceholderDisabled
: GStyles.inputPlaceholder
}
>
{selectedItem?.label || placeholder}
</Text>
</Pressable>
{/* Tombol Clear (Hanya muncul jika ada nilai terpilih & allowClear aktif) */}
{!disabled && allowClear && value !== null && value !== undefined && (
<TouchableOpacity
style={{ paddingHorizontal: 10 }}
onPress={() => onChange(null as any)} // null dikirim sebagai value
>
<Text style={{ fontSize: 18, color: "#999" }}>×</Text>
</TouchableOpacity>
)}
</View>
<Modal visible={modalVisible} transparent animationType="fade"> <Modal visible={modalVisible} transparent animationType="fade">
<TouchableOpacity <TouchableOpacity
@@ -86,19 +131,38 @@ const SelectCustom: React.FC<SelectProps> = ({
> >
<View style={GStyles.selectModalContent}> <View style={GStyles.selectModalContent}>
<FlatList <FlatList
data={data} data={renderData}
keyExtractor={(item) => String(item.value)} keyExtractor={(item) => String(item.value)}
renderItem={({ item }) => ( renderItem={({ item }) => {
<TouchableOpacity if (item.value === "__clear__") {
style={GStyles.selectOption} return (
onPress={() => { <TouchableOpacity
onChange(item.value); style={[
setModalVisible(false); GStyles.selectOption,
}} { backgroundColor: "#fdd" },
> ]}
<Text>{item.label}</Text> onPress={() => {
</TouchableOpacity> onChange(null as any); // kosongkan nilai
)} setModalVisible(false);
}}
>
<Text>{item.label}</Text>
</TouchableOpacity>
);
}
return (
<TouchableOpacity
style={GStyles.selectOption}
onPress={() => {
onChange(item.value);
setModalVisible(false);
}}
>
<Text>{item.label}</Text>
</TouchableOpacity>
);
}}
/> />
</View> </View>
</TouchableOpacity> </TouchableOpacity>

View File

@@ -91,14 +91,11 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
setToken(token); setToken(token);
await AsyncStorage.setItem("authToken", token); await AsyncStorage.setItem("authToken", token);
const responseUser = await apiConfig.get( const responseUser = await apiConfig.get(`/mobile?token=${token}`, {
`/mobile?token=${token}`, headers: {
{ Authorization: `Bearer ${token}`,
headers: { },
Authorization: `Bearer ${token}`, });
},
}
);
const dataUser = responseUser.data.data; const dataUser = responseUser.data.data;
setUser(dataUser); setUser(dataUser);
@@ -147,9 +144,7 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
await AsyncStorage.setItem("userData", JSON.stringify(dataUser)); await AsyncStorage.setItem("userData", JSON.stringify(dataUser));
return dataUser; return dataUser;
} catch (error: any) { } catch (error: any) {
throw new Error( console.log(error.response?.data?.message + "user" || "Gagal mengambil data user");
error.response?.data?.message || "Gagal mengambil data user"
);
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }

View File

@@ -70,9 +70,12 @@ export default function Donation_BoxPublish({
</TextCustom> </TextCustom>
</View> </View>
<ProgressCustom <ProgressCustom
label={data?.progres + "%" || "0%"}
value={data?.progres || 0}
size="lg" size="lg"
value={Number(data?.progres) || 0}
showLabel={true}
label={data?.progres + "%"}
animated
color="primary"
/> />
{/* <TextCustom> {/* <TextCustom>
Terkumpul : Rp 300.000 Terkumpul : Rp 300.000

View File

@@ -11,11 +11,23 @@ import { Ionicons, MaterialIcons } from "@expo/vector-icons";
import { router } from "expo-router"; import { router } from "expo-router";
import { View } from "react-native"; 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 ( return (
<> <>
<View> <View>
<ProgressCustom size="lg" /> <ProgressCustom
size="lg"
value={progres}
label={progres + "%"}
animated
color="primary"
/>
<Spacing /> <Spacing />
<Grid> <Grid>
<Grid.Col span={4}> <Grid.Col span={4}>

View File

@@ -53,11 +53,50 @@ export async function apiAdminDonationListOfDonatur({
status, status,
}: { }: {
id: string; id: string;
status: "berhasil" | "gagal" | "proses" | "menunggu" | ""; status: "berhasil" | "gagal" | "proses" | "menunggu" | null;
}) { }) {
const query = status && status !== null ? `?status=${status}` : "";
try { try {
const response = await apiConfig.get( 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; return response.data;
} catch (error) { } catch (error) {