Compare commits

...

1 Commits

Author SHA1 Message Date
1e1b18f860 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
2025-10-28 17:45:13 +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",
@@ -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);
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 (
<BoxButtonOnFooter> <BoxButtonOnFooter>
<ButtonCustom onPress={() => router.back()}>Terima</ButtonCustom> <ButtonCustom
isLoading={isLoading}
onPress={() => {
handlerSubmit();
}}
>
Terima donasi
</ButtonCustom>
</BoxButtonOnFooter> </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:
(data && data?.imageId && (
<ButtonCustom <ButtonCustom
onPress={() => onPress={() =>
router.push(`/(application)/(image)/preview-image/${id}`) router.push(
`/(application)/(image)/preview-image/${data?.imageId}`
)
} }
> >
Cek Cek
</ButtonCustom> </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);
} }
}; };
@@ -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,6 +107,7 @@ export default function AdminDonasiListOfDonatur() {
<AdminBackButtonAntTitle newComponent={searchComponent} /> <AdminBackButtonAntTitle newComponent={searchComponent} />
} }
> >
<StackCustom>
<GridViewCustomSpan <GridViewCustomSpan
span1={3} span1={3}
span2={5} span2={5}
@@ -124,7 +130,14 @@ export default function AdminDonasiListOfDonatur() {
/> />
<Divider /> <Divider />
<StackCustom> <StackCustom>
{listData?.map((item: any, index: number) => ( {loadData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center" color="gray">
Belum ada data
</TextCustom>
) : (
listData?.map((item: any, index: number) => (
<View key={index}> <View key={index}>
<GridViewCustomSpan <GridViewCustomSpan
span1={3} span1={3}
@@ -133,10 +146,14 @@ export default function AdminDonasiListOfDonatur() {
component1={ component1={
<CenterCustom> <CenterCustom>
<ActionIcon <ActionIcon
icon={<IconView size={ICON_SIZE_BUTTON} color="black" />} icon={
<IconView size={ICON_SIZE_BUTTON} color="black" />
}
onPress={() => { onPress={() => {
router.push( router.push(
`/admin/donation/${id}/berhasil/transaction-detail` `/admin/donation/${item?.id}/${_.lowerCase(
item?.DonasiMaster_StatusInvoice?.name
)}/transaction-detail`
); );
}} }}
/> />
@@ -150,15 +167,18 @@ export default function AdminDonasiListOfDonatur() {
component3={ component3={
<BadgeCustom <BadgeCustom
style={{ alignSelf: "center" }} style={{ alignSelf: "center" }}
color={colorBadgeTransaction({status: item?.DonasiMaster_StatusInvoice?.name})} color={colorBadgeTransaction({
status: item?.DonasiMaster_StatusInvoice?.name,
})}
> >
{item?.DonasiMaster_StatusInvoice?.name} {item?.DonasiMaster_StatusInvoice?.name}
</BadgeCustom> </BadgeCustom>
} }
/> />
<Divider />
</View> </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,13 +64,37 @@ const SelectCustom: React.FC<SelectProps> = ({
{required && <Text style={GStyles.inputRequired}> *</Text>} {required && <Text style={GStyles.inputRequired}> *</Text>}
</Text> </Text>
)} )}
{/* Input Container */}
<View
style={[
{
flexDirection: "row",
alignItems: "center",
// backgroundColor: "red",
// flex: 1,
borderRadius,
},
GStyles.inputContainerInput,
hasError ? GStyles.inputErrorBorder : null,
disabled && GStyles.disabledBox,
]}
>
<Pressable <Pressable
style={[ style={[
{ borderRadius, }, {
hasError ? GStyles.inputErrorBorder : null, flex: 1,
GStyles.inputContainerInput, borderRadius,
disabled && GStyles.disabledBox, flexDirection: "row",
]} // <-- add error style alignItems: "center",
paddingHorizontal: 10,
height: 50,
},
// GStyles.inputContainerInput,
// hasError ? GStyles.inputErrorBorder : null,
// disabled && GStyles.disabledBox,
]}
onPress={() => !disabled && setModalVisible(true)} onPress={() => !disabled && setModalVisible(true)}
> >
<Text <Text
@@ -78,6 +112,17 @@ const SelectCustom: React.FC<SelectProps> = ({
</Text> </Text>
</Pressable> </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
style={GStyles.selectModalOverlay} style={GStyles.selectModalOverlay}
@@ -86,9 +131,27 @@ 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 }) => {
if (item.value === "__clear__") {
return (
<TouchableOpacity
style={[
GStyles.selectOption,
{ backgroundColor: "#fdd" },
]}
onPress={() => {
onChange(null as any); // kosongkan nilai
setModalVisible(false);
}}
>
<Text>{item.label}</Text>
</TouchableOpacity>
);
}
return (
<TouchableOpacity <TouchableOpacity
style={GStyles.selectOption} style={GStyles.selectOption}
onPress={() => { onPress={() => {
@@ -98,7 +161,8 @@ const SelectCustom: React.FC<SelectProps> = ({
> >
<Text>{item.label}</Text> <Text>{item.label}</Text>
</TouchableOpacity> </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: { headers: {
Authorization: `Bearer ${token}`, 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) {