Compare commits

...

2 Commits

Author SHA1 Message Date
b3209dc7ee 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
2025-10-29 17:35:18 +08:00
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
17 changed files with 964 additions and 271 deletions

View File

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

View File

@@ -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<any[] | null>(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 (
<>
<ViewWrapper>
@@ -20,47 +74,50 @@ export default function DonationFundDisbursement() {
<Grid>
<Grid.Col span={6}>
<TextCustom bold color="yellow">
Rp. 0
Rp. {formatCurrencyDisplay(data?.totalPencairan)}
</TextCustom>
<TextCustom size="small">Total Pencairan Dana</TextCustom>
</Grid.Col>
<Grid.Col span={6}>
<TextCustom bold color="yellow">
0 kali
{data?.akumulasiPencairan} kali
</TextCustom>
<TextCustom size="small">Akumulasi Pencairan</TextCustom>
</Grid.Col>
</Grid>
</BaseBox>
{Array.from({ length: 10 }).map((_, index) => (
<BaseBox key={index}>
<StackCustom>
<Grid>
<Grid.Col span={8}>
<TextCustom bold>Pencairan ke - {index + 1}</TextCustom>
</Grid.Col>
<Grid.Col span={4} style={{ alignItems: "flex-end" }}>
<TextCustom>{dayjs().format("DD MMM YYYY")}</TextCustom>
</Grid.Col>
</Grid>
<TextCustom>
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!
</TextCustom>
<ButtonCenteredOnly
onPress={() => {
router.navigate(`/(application)/(file)/${id}`);
}}
icon="file-text"
>
Bukti Transaksi
</ButtonCenteredOnly>
</StackCustom>
</BaseBox>
))}
{loadData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center" color="gray">
Belum ada data
</TextCustom>
) : (
listData?.map((item, index) => (
<BaseBox key={index}>
<StackCustom>
<Grid>
<Grid.Col span={8}>
<TextCustom bold>{item?.title}</TextCustom>
</Grid.Col>
<Grid.Col span={4} style={{ alignItems: "flex-end" }}>
<TextCustom>{dayjs(item?.createdAt).format("DD MMM YYYY")}</TextCustom>
</Grid.Col>
</Grid>
<TextCustom>{item?.deskripsi}</TextCustom>
<ButtonCenteredOnly
onPress={() => {
router.navigate(`/(application)/(image)/preview-image/${item?.imageId}`);
}}
icon="file-text"
>
Bukti Transaksi
</ButtonCenteredOnly>
</StackCustom>
</BaseBox>
))
)}
</ViewWrapper>
</>
);

View File

@@ -102,7 +102,7 @@ export default function DonasiDetailBeranda() {
sisaHari={value.sisa}
reminder={value.reminder}
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_ComponentStoryFunrising

View File

@@ -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<any[] | null>(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 (
<>
<ViewWrapper>
{Array.from({ length: 10 }).map((_, index) => (
<BaseBox key={index}>
<Grid>
<Grid.Col
span={3}
style={{ alignItems: "center", justifyContent: "center" }}
>
<FontAwesome6
name="face-smile-wink"
size={50}
style={{ color: MainColor.yellow }}
/>
</Grid.Col>
<Grid.Col span={9}>
<StackCustom gap={"xs"}>
{loadData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom bold align="center">
Belum ada donatur
</TextCustom>
) : (
listData?.map((item: any, index: number) => (
<BaseBox key={index}>
<Grid>
<Grid.Col
span={3}
style={{ alignItems: "center", justifyContent: "center" }}
>
<FontAwesome6
name="face-smile-wink"
size={50}
style={{ color: MainColor.yellow }}
/>
</Grid.Col>
<Grid.Col span={9}>
<TextCustom bold size="large">
Username
{item?.Author?.username || "-"}
</TextCustom>
<TextCustom>Berdonas sebesar </TextCustom>
<TextCustom bold size="large" color="yellow">
Rp. 100.000
</TextCustom>
<TextCustom>{dayjs().format("DD MMM YYYY")}</TextCustom>
</StackCustom>
</Grid.Col>
</Grid>
</BaseBox>
))}
<Spacing/>
<StackCustom gap={"xs"}>
<TextCustom size={"small"}>Berdonas sebesar </TextCustom>
<TextCustom bold size="large" color="yellow">
Rp. {formatCurrencyDisplay(item?.nominal)}
</TextCustom>
<TextCustom>
{dayjs(item?.createdAt).format("DD MMM YYYY, HH:mm")}
</TextCustom>
</StackCustom>
</Grid.Col>
</Grid>
</BaseBox>
))
)}
</ViewWrapper>
</>
);

View File

@@ -38,6 +38,7 @@ export default function AdminDonationDetail() {
const [openDrawer, setOpenDrawer] = React.useState(false);
const [data, setData] = React.useState<any | null>(null);
const [countDonatur, setCountDonatur] = React.useState(0);
const [isLoading, setIsLoading] = React.useState(false);
useFocusEffect(
@@ -52,10 +53,9 @@ 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);
setData(response.data.donasi);
setCountDonatur(response.data.donatur);
}
} catch (error) {
console.log("[ERROR]", error);
@@ -77,7 +77,9 @@ export default function AdminDonationDetail() {
value:
data && data?.DonasiMaster_Status?.name ? (
<BadgeCustom
color={colorBadgeStatus({ status: data?.DonasiMaster_Status?.name })}
color={colorBadgeStatus({
status: data?.DonasiMaster_Status?.name,
})}
>
{_.startCase(data?.DonasiMaster_Status?.name)}
</BadgeCustom>
@@ -105,15 +107,19 @@ 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",
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,12 +202,20 @@ export default function AdminDonationDetail() {
/>
))}
</StackCustom>
<ButtonCustom
iconLeft={
<Ionicons name="cash-outline" size={ICON_SIZE_BUTTON} />
}
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`);
}}
>
@@ -211,16 +225,32 @@ export default function AdminDonationDetail() {
</BaseBox>
<BaseBox>
<ProgressCustom size="lg" />
<ProgressCustom
size="lg"
value={Number(data?.progres) || 0}
showLabel={true}
label={data?.progres + "%"}
animated
color="primary"
/>
<Spacing />
<StackCustom gap={"xs"}>
<GridDetail_4_8
label={<TextCustom bold>Jumlah Donatur</TextCustom>}
value={<TextCustom>0 orang</TextCustom>}
value={
<TextCustom>
{countDonatur ? countDonatur : 0} orang
</TextCustom>
}
/>
<GridDetail_4_8
label={<TextCustom bold>Dana Terkumpul</TextCustom>}
value={<TextCustom>Rp 0</TextCustom>}
value={
<TextCustom>
Rp {formatCurrencyDisplay(data?.terkumpul || 0)}
</TextCustom>
}
/>
</StackCustom>
</BaseBox>

View File

@@ -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 = (
<BoxButtonOnFooter>
<ButtonCustom onPress={() => router.back()}>Terima</ButtonCustom>
</BoxButtonOnFooter>
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>
<ButtonCustom
isLoading={isLoading}
onPress={() => {
handlerSubmit();
}}
>
Terima donasi
</ButtonCustom>
</BoxButtonOnFooter>
);
}
return (
<BoxButtonOnFooter>
<ButtonCustom disabled>
{data?.DonasiMaster_StatusInvoice?.name}
</ButtonCustom>
</BoxButtonOnFooter>
);
};
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: <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",
value: dayjs().format("DD-MM-YYYY HH:mm:ss"),
value: (data && dateTimeView({ date: data?.createdAt })) || "-",
},
{
label: "Bukti Transfer",
value: (
<ButtonCustom
onPress={() =>
router.push(`/(application)/(image)/preview-image/${id}`)
}
>
Cek
</ButtonCustom>
),
value:
(data && data?.imageId && (
<ButtonCustom
onPress={() =>
router.push(
`/(application)/(image)/preview-image/${data?.imageId}`
)
}
>
Cek
</ButtonCustom>
)) ||
"-",
},
];
@@ -61,7 +172,7 @@ export default function AdminDonasiTransactionDetail() {
<>
<ViewWrapper
headerComponent={<AdminBackButtonAntTitle title="Detail Transaksi" />}
footerComponent={buttonAction}
footerComponent={buttonAction()}
>
<BaseBox>
<StackCustom>

View File

@@ -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<any | null>(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() {
>
<BaseBox>
<StackCustom>
{listData.map((item, index) => (
{listData?.map((item, index) => (
<GridDetail_4_8
key={index}
label={<TextCustom bold>{item.label}</TextCustom>}
@@ -51,7 +78,7 @@ export default function AdminDonationDetailDisbursementOfFunds() {
<ButtonCustom
onPress={() =>
router.push(`/(application)/(image)/preview-image/${id}`)
router.push(`/(application)/(image)/preview-image/${data?.imageId}`)
}
>
Cek Bukti Transaksi

View File

@@ -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<any | null>(null);
const [isLoading, setIsLoading] = React.useState(false);
const [value, setValue] = React.useState({
nominalCair: "",
title: "",
deskripsi: "",
});
const [image, setImage] = React.useState<any | null>(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 = (
<BoxButtonOnFooter>
<ButtonCustom
isLoading={isLoading}
onPress={() => {
router.back();
handleSubmit();
}}
>
Simpan
@@ -31,7 +139,7 @@ export default function AdminDonationDisbursementOfFunds() {
return (
<ViewWrapper
headerComponent={<AdminBackButtonAntTitle title="Pencairan Dana" />}
footerComponent={handleSubmit}
footerComponent={buttonSubmit}
>
<BaseBox>
<StackCustom gap="md">
@@ -39,7 +147,7 @@ export default function AdminDonationDisbursementOfFunds() {
Dana Tersisa
</TextCustom>
<TextCustom align="center" bold size="large">
Rp 1.000.000
Rp {formatCurrencyDisplay(data?.terkumpul - data?.totalPencairan)}
</TextCustom>
</StackCustom>
</BaseBox>
@@ -56,9 +164,27 @@ export default function AdminDonationDisbursementOfFunds() {
label="Nominal"
placeholder="0"
iconLeft={"Rp"}
value={value.nominalCair}
onChangeText={(text) => {
setValue({
...value,
nominalCair: text,
});
}}
/>
<TextInputCustom required label="Judul" placeholder="Masukan judul" />
<TextInputCustom
required
label="Judul"
placeholder="Masukan judul"
value={value.title}
onChangeText={(text) => {
setValue({
...value,
title: text,
});
}}
/>
<TextAreaCustom
required
@@ -66,20 +192,37 @@ export default function AdminDonationDisbursementOfFunds() {
placeholder="Masukan deskripsi"
showCount
maxLength={500}
value={value.deskripsi}
onChangeText={(text) => {
setValue({
...value,
deskripsi: text,
});
}}
/>
</StackCustom>
</BaseBox>
<InformationBox text="Wajib menyertakan bukti transfer" />
<Spacing />
<InformationBox text="Wajib menyertakan bukti transfer" />
<ButtonCenteredOnly
onPress={() => {
router.push(`/(application)/(image)/take-picture/${id}`);
pickFile({
allowedType: "image",
aspectRatio: [9, 16],
setImageUri: (file) => {
setImage(file);
},
});
}}
icon="upload"
>
Upload
</ButtonCenteredOnly>
<Spacing />
<Image source={image?.uri} style={{ width: "100%", height: 300 }} />
<Spacing />
</ViewWrapper>
);
}

View File

@@ -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<any[] | null>(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 (
<>
<ViewWrapper
@@ -45,36 +78,47 @@ export default function AdminDonasiListOfDisbursementOfFunds() {
/>
<Divider />
<StackCustom>
{Array.from({ length: 10 }).map((_, index) => (
<View key={index}>
<GridViewCustomSpan
span1={3}
span2={5}
span3={4}
component1={
<CenterCustom>
<ActionIcon
icon={<IconView size={ICON_SIZE_BUTTON} color="black" />}
onPress={() => {
router.push(
`/admin/donation/${id}/detail-disbursement-of-funds`
);
}}
/>
</CenterCustom>
}
component2={
<TextCustom bold align="center" truncate>
{dayjs()
.add(index + 1, "day")
.format("DD-MM-YYYY HH:mm")}
</TextCustom>
}
component3={<TextCustom>Rp. 1.000.000</TextCustom>}
/>
<Divider />
</View>
))}
{loadData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center" color="gray">
Belum ada data
</TextCustom>
) : (
listData?.map((item, index) => (
<View key={index}>
<GridViewCustomSpan
span1={3}
span2={5}
span3={4}
component1={
<CenterCustom>
<ActionIcon
icon={
<IconView size={ICON_SIZE_BUTTON} color="black" />
}
onPress={() => {
router.push(
`/admin/donation/${item?.id}/detail-disbursement-of-funds`
);
}}
/>
</CenterCustom>
}
component2={
<TextCustom align="center" truncate>
{dayjs(item?.createdAt).format("DD-MM-YYYY")}
</TextCustom>
}
component3={
<TextCustom align="center" truncate>
Rp. {formatCurrencyDisplay(item?.nominalCair)}
</TextCustom>
}
/>
</View>
))
)}
</StackCustom>
</ViewWrapper>
</>

View File

@@ -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<any[] | null>(null);
const [loadData, setLoadData] = React.useState(false);
const [master, setMaster] = React.useState<any[]>([]);
const [selectStatus, setSelectStatus] = React.useState<
"berhasil" | "gagal" | "proses" | "menunggu" | ""
>("");
const [selectValue, setSelectValue] = React.useState<string | null>(null);
const [selectedStatus, setSelectedStatus] = React.useState<string | null>(
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
/>
</View>
);
@@ -102,63 +107,78 @@ export default function AdminDonasiListOfDonatur() {
<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>
{listData?.map((item: any, index: number) => (
<View key={index}>
<GridViewCustomSpan
span1={3}
span2={5}
span3={4}
component1={
<CenterCustom>
<ActionIcon
icon={<IconView size={ICON_SIZE_BUTTON} color="black" />}
onPress={() => {
router.push(
`/admin/donation/${id}/berhasil/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>
}
/>
<Divider />
</View>
))}
<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>
{loadData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center" color="gray">
Belum ada data
</TextCustom>
) : (
listData?.map((item: any, index: number) => (
<View key={index}>
<GridViewCustomSpan
span1={3}
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>
</ViewWrapper>
</>

View File

@@ -27,6 +27,8 @@ type SelectProps = {
onChange: (value: string | number) => void;
borderRadius?: number;
styleContainer?: StyleProp<ViewStyle>;
allowClear?: boolean; // <-- new prop
clearLabel?: string; // default: "Kosongkan"
};
const SelectCustom: React.FC<SelectProps> = ({
@@ -39,13 +41,21 @@ const SelectCustom: React.FC<SelectProps> = ({
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 (
<View style={[GStyles.inputContainerArea, styleContainer]}>
{label && (
@@ -54,29 +64,64 @@ const SelectCustom: React.FC<SelectProps> = ({
{required && <Text style={GStyles.inputRequired}> *</Text>}
</Text>
)}
<Pressable
{/* Input Container */}
<View
style={[
{ borderRadius, },
hasError ? GStyles.inputErrorBorder : null,
{
flexDirection: "row",
alignItems: "center",
// backgroundColor: "red",
// flex: 1,
borderRadius,
},
GStyles.inputContainerInput,
hasError ? GStyles.inputErrorBorder : null,
disabled && GStyles.disabledBox,
]} // <-- add error style
onPress={() => !disabled && setModalVisible(true)}
]}
>
<Text
style={
selectedItem
? disabled
? GStyles.inputTextDisabled
: GStyles.inputText
: disabled
? GStyles.inputPlaceholderDisabled
: GStyles.inputPlaceholder
}
<Pressable
style={[
{
flex: 1,
borderRadius,
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 10,
height: 50,
},
// GStyles.inputContainerInput,
// hasError ? GStyles.inputErrorBorder : null,
// disabled && GStyles.disabledBox,
]}
onPress={() => !disabled && setModalVisible(true)}
>
{selectedItem?.label || placeholder}
</Text>
</Pressable>
<Text
style={
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">
<TouchableOpacity
@@ -86,19 +131,38 @@ const SelectCustom: React.FC<SelectProps> = ({
>
<View style={GStyles.selectModalContent}>
<FlatList
data={data}
data={renderData}
keyExtractor={(item) => String(item.value)}
renderItem={({ item }) => (
<TouchableOpacity
style={GStyles.selectOption}
onPress={() => {
onChange(item.value);
setModalVisible(false);
}}
>
<Text>{item.label}</Text>
</TouchableOpacity>
)}
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
style={GStyles.selectOption}
onPress={() => {
onChange(item.value);
setModalVisible(false);
}}
>
<Text>{item.label}</Text>
</TouchableOpacity>
);
}}
/>
</View>
</TouchableOpacity>

View File

@@ -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);
}

View File

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

View File

@@ -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 (
<>
<View>
<ProgressCustom size="lg" />
<ProgressCustom
size="lg"
value={progres}
label={progres + "%"}
animated
color="primary"
/>
<Spacing />
<Grid>
<Grid.Col span={4}>

View File

@@ -53,14 +53,102 @@ 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) {
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;
}
}

View File

@@ -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;
}
}

View File

@@ -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<void> {
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,
});