diff --git a/app/(application)/(user)/investment/(tabs)/portofolio.tsx b/app/(application)/(user)/investment/(tabs)/portofolio.tsx index 32c2aa3..d801469 100644 --- a/app/(application)/(user)/investment/(tabs)/portofolio.tsx +++ b/app/(application)/(user)/investment/(tabs)/portofolio.tsx @@ -1,13 +1,48 @@ -import { ScrollableCustom, ViewWrapper } from "@/components"; +/* eslint-disable react-hooks/exhaustive-deps */ +import { + LoaderCustom, + ScrollableCustom, + TextCustom, + ViewWrapper, +} from "@/components"; +import { useAuth } from "@/hooks/use-auth"; import { dummyMasterStatus } from "@/lib/dummy-data/_master/status"; import Investment_StatusBox from "@/screens/Invesment/StatusBox"; -import { useState } from "react"; +import { apiInvestmentGetByStatus } from "@/service/api-client/api-investment"; +import { useFocusEffect } from "expo-router"; +import _ from "lodash"; +import { useCallback, useState } from "react"; export default function InvestmentPortofolio() { + const { user } = useAuth(); const [activeCategory, setActiveCategory] = useState( "publish" ); + const [listData, setListData] = useState([]); + const [loadingList, setLoadingList] = useState(false); + + useFocusEffect( + useCallback(() => { + onLoadData(); + }, [user?.id, activeCategory]) + ); + + const onLoadData = async () => { + try { + setLoadingList(true); + const response = await apiInvestmentGetByStatus({ + authorId: user?.id as string, + status: activeCategory as string, + }); + setListData(response.data); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoadingList(false); + } + }; + const handlePress = (item: any) => { setActiveCategory(item.value); // tambahkan logika lain seperti filter dsb. @@ -26,14 +61,20 @@ export default function InvestmentPortofolio() { ); return ( - {Array.from({ length: 10 }).map((_, index) => ( - - ))} + {loadingList ? ( + + ) : _.isEmpty(listData) ? ( + Tidak ada data {activeCategory} + ) : ( + listData.map((item: any, index: number) => ( + + )) + )} ); } diff --git a/app/(application)/(user)/investment/[id]/[status]/detail.tsx b/app/(application)/(user)/investment/[id]/[status]/detail.tsx index 032e814..fd4b115 100644 --- a/app/(application)/(user)/investment/[id]/[status]/detail.tsx +++ b/app/(application)/(user)/investment/[id]/[status]/detail.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react-hooks/exhaustive-deps */ import { BackButton, DotButton, @@ -12,16 +13,42 @@ import { ICON_SIZE_MEDIUM } from "@/constants/constans-value"; import Investment_ButtonInvestasiSection from "@/screens/Invesment/ButtonInvestasiSection"; import Invesment_ComponentBoxOnBottomDetail from "@/screens/Invesment/ComponentBoxOnBottomDetail"; import Invesment_DetailDataPublishSection from "@/screens/Invesment/DetailDataPublishSection"; +import { apiInvestmentGetById } from "@/service/api-client/api-investment"; import { AntDesign, MaterialIcons } from "@expo/vector-icons"; -import { router, Stack, useLocalSearchParams } from "expo-router"; +import { + router, + Stack, + useFocusEffect, + useLocalSearchParams, +} from "expo-router"; import _ from "lodash"; -import { useState } from "react"; +import { useCallback, useState } from "react"; export default function InvestmentDetailStatus() { const { id, status } = useLocalSearchParams(); const [openDrawerDraft, setOpenDrawerDraft] = useState(false); const [openDrawerPublish, setOpenDrawerPublish] = useState(false); + const [data, setData] = useState(null); + + useFocusEffect( + useCallback(() => { + onLoadData(); + }, [id, status]) + ); + + const onLoadData = async () => { + try { + const response = await apiInvestmentGetById({ + id: id as string, + }); + // console.log("[DATA]", JSON.stringify(response.data, null, 2)); + setData(response.data); + } catch (error) { + console.log("[ERROR]", error); + } + }; + const handlePressDraft = (item: IMenuDrawerItem) => { console.log("PATH >> ", item.path); router.navigate(item.path as any); @@ -63,6 +90,7 @@ export default function InvestmentDetailStatus() { diff --git a/app/(application)/(user)/investment/[id]/edit.tsx b/app/(application)/(user)/investment/[id]/edit.tsx index 0008f97..4986e83 100644 --- a/app/(application)/(user)/investment/[id]/edit.tsx +++ b/app/(application)/(user)/investment/[id]/edit.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react-hooks/exhaustive-deps */ import { ButtonCenteredOnly, ButtonCustom, @@ -7,34 +8,192 @@ import { Spacing, StackCustom, TextInputCustom, - ViewWrapper + ViewWrapper, } from "@/components"; +import API_STRORAGE from "@/constants/base-url-api-strorage"; +import DIRECTORY_ID from "@/constants/directory-id"; import dummyPembagianDeviden from "@/lib/dummy-data/investment/pembagian-deviden"; import dummyListPencarianInvestor from "@/lib/dummy-data/investment/pencarian-investor"; import dummyPeriodeDeviden from "@/lib/dummy-data/investment/periode-deviden"; -import { router } from "expo-router"; -import { useState } from "react"; +import { + apiInvestmentGetById, + apiInvestmentUpdateData, +} from "@/service/api-client/api-investment"; +import { + deleteImageService, + uploadImageService, +} from "@/service/upload-service"; +import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay"; +import pickFile from "@/utils/pickFile"; +import { router, useFocusEffect, useLocalSearchParams } from "expo-router"; +import { useCallback, useState } from "react"; +import Toast from "react-native-toast-message"; + +interface IInvestment { + title?: string; + targetDana?: string; + hargaLembar?: string; + totalLembar?: string; + roi?: string; + masterPencarianInvestorId?: string; + masterPeriodeDevidenId?: string; + masterPembagianDevidenId?: string; + authorId?: string; + imageId?: string; + prospektusFileId?: string; +} export default function InvestmentEdit() { - const [data, setData] = useState({ + const { id } = useLocalSearchParams(); + const [data, setData] = useState({ title: "", - targetDana: 0, - hargaPerLembar: 0, - totalLembar: 0, - rasioKeuntungan: 0, - pencarianInvestor: "", - periodeDeviden: "", - pembagianDeviden: "", + targetDana: "", + hargaLembar: "", + totalLembar: "", + roi: "", + masterPencarianInvestorId: "", + masterPeriodeDevidenId: "", + masterPembagianDevidenId: "", + authorId: "", + imageId: "", + prospektusFileId: "", }); + const [image, setImage] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + const displayTargetDana = formatCurrencyDisplay(data?.targetDana); + const displayHargaPerLembar = formatCurrencyDisplay(data?.hargaLembar); + const displayTotalLembar = formatCurrencyDisplay( + Number(data?.targetDana) / Number(data?.hargaLembar) + ); + + const handleChangeCurrency = (field: keyof typeof data) => (text: string) => { + const numeric = text.replace(/\D/g, ""); + setData((prev) => ({ ...prev, [field]: numeric })); + }; + + useFocusEffect( + useCallback(() => { + onLoadData(); + }, [id]) + ); + + const onLoadData = async () => { + try { + const response = await apiInvestmentGetById({ + id: id as string, + }); + // console.log("[DATA]", JSON.stringify(response.data, null, 2)); + setData(response.data); + } catch (error) { + console.log("[ERROR]", error); + } + }; + + const handleSubmitUpdate = async () => { + let newData = { + ...data, + }; + + if ( + newData?.title === "" || + newData?.targetDana === "" || + newData?.hargaLembar === "" || + newData?.totalLembar === "" || + newData?.roi === "" || + newData?.masterPencarianInvestorId === "" || + newData?.masterPeriodeDevidenId === "" || + newData?.masterPembagianDevidenId === "" + ) { + Toast.show({ + type: "info", + text1: "Harap isi semua data", + }); + return; + } + + try { + setIsLoading(true); + + if (image) { + const responseUploadImage = await uploadImageService({ + imageUri: image, + dirId: DIRECTORY_ID.investasi_image, + }); + + if (!responseUploadImage.success) { + Toast.show({ + type: "error", + text1: "Gagal mengunggah gambar", + }); + return; + } + + const deletePrevImage = await deleteImageService({ + id: data?.imageId as any, + }); + + if (!deletePrevImage.success) { + Toast.show({ + type: "error", + text1: "Gagal menghapus gambar", + }); + return; + } + + newData = { + ...newData, + imageId: responseUploadImage.data.id, + }; + } + + const responseUpdate = await apiInvestmentUpdateData({ + id: id as string, + data: newData, + }); + + console.log("[RESPONSE UPDATE]", JSON.parse(JSON.stringify(responseUpdate))); + + if (responseUpdate.success) { + Toast.show({ + type: "success", + text1: "Data berhasil diupdate", + }); + router.back(); + } else { + Toast.show({ + type: "error", + text1: responseUpdate.message, + }); + } + } catch (error) { + console.log("[ERROR]", error); + } finally { + setIsLoading(false); + } + }; + return ( - + router.push("/take-picture/1")} + onPress={() => { + pickFile({ + setImageUri: ({ uri }) => { + console.log("URI IMAGE", uri); + setImage(uri); + }, + allowedType: "image", + }); + }} > Upload @@ -47,7 +206,7 @@ export default function InvestmentEdit() { required placeholder="Judul" label="Judul" - value={data.title} + value={data?.title} onChangeText={(value) => setData({ ...data, title: value })} /> @@ -57,22 +216,8 @@ export default function InvestmentEdit() { placeholder="0" label="Target Dana" keyboardType="numeric" - onChangeText={(value) => - setData({ ...data, targetDana: Number(value) }) - } - value={data.targetDana === 0 ? "" : data.targetDana.toString()} - /> - - - setData({ ...data, targetDana: Number(value) }) - } - value={data.targetDana === 0 ? "" : data.targetDana.toString()} + onChangeText={handleChangeCurrency("targetDana")} + value={displayTargetDana} /> - setData({ ...data, targetDana: Number(value) }) - } - value={data.targetDana === 0 ? "" : data.targetDana.toString()} + onChangeText={handleChangeCurrency("hargaLembar")} + value={displayHargaPerLembar} /> - setData({ ...data, totalLembar: Number(value) }) - } - value={data.totalLembar === 0 ? "" : data.totalLembar.toString()} + onChangeText={(value) => setData({ ...data, totalLembar: value })} + value={displayTotalLembar} /> - setData({ ...data, rasioKeuntungan: Number(value) }) - } - value={ - data.rasioKeuntungan === 0 ? "" : data.rasioKeuntungan.toString() - } + onChangeText={(value) => setData({ ...data, roi: value })} + value={data?.roi === "" ? "" : data?.roi} /> - setData({ ...data, pencarianInvestor: value as any }) + setData({ ...data, masterPencarianInvestorId: value as any }) } - value={data.pencarianInvestor} + value={data.masterPencarianInvestorId} /> - setData({ ...data, periodeDeviden: value as any }) + setData({ ...data, masterPeriodeDevidenId: value as any }) } - value={data.periodeDeviden} + value={data.masterPeriodeDevidenId} /> - setData({ ...data, pembagianDeviden: value as any }) + setData({ ...data, masterPembagianDevidenId: value as any }) } - value={data.pembagianDeviden} + value={data.masterPembagianDevidenId} /> - router.replace("/investment/portofolio")}> + Simpan diff --git a/app/(application)/(user)/investment/create.tsx b/app/(application)/(user)/investment/create.tsx index c9450cc..17a2ec0 100644 --- a/app/(application)/(user)/investment/create.tsx +++ b/app/(application)/(user)/investment/create.tsx @@ -1,76 +1,203 @@ import { - BaseBox, - ButtonCenteredOnly, - ButtonCustom, - CenterCustom, - InformationBox, - LandscapeFrameUploaded, - SelectCustom, - Spacing, - StackCustom, - TextInputCustom, - ViewWrapper, + BaseBox, + ButtonCenteredOnly, + ButtonCustom, + CenterCustom, + InformationBox, + LandscapeFrameUploaded, + SelectCustom, + Spacing, + StackCustom, + TextCustom, + TextInputCustom, + ViewWrapper, } from "@/components"; import { MainColor } from "@/constants/color-palet"; +import DIRECTORY_ID from "@/constants/directory-id"; +import { useAuth } from "@/hooks/use-auth"; import dummyPembagianDeviden from "@/lib/dummy-data/investment/pembagian-deviden"; import dummyListPencarianInvestor from "@/lib/dummy-data/investment/pencarian-investor"; import dummyPeriodeDeviden from "@/lib/dummy-data/investment/periode-deviden"; +import { apiInvestmentCreate } from "@/service/api-client/api-investment"; +import { uploadImageService } from "@/service/upload-service"; +import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay"; +import pickFile, { IFileData } from "@/utils/pickFile"; import { FontAwesome5 } from "@expo/vector-icons"; import { router } from "expo-router"; import { useState } from "react"; +import Toast from "react-native-toast-message"; export default function InvestmentCreate() { + const { user } = useAuth(); const [data, setData] = useState({ title: "", - targetDana: 0, - hargaPerLembar: 0, - totalLembar: 0, - rasioKeuntungan: 0, + targetDana: "", + hargaPerLembar: "", + totalLembar: "", + rasioKeuntungan: "", pencarianInvestor: "", periodeDeviden: "", pembagianDeviden: "", + authorId: "", + imageId: "", + prospektusFileId: "", }); + const [image, setImage] = useState(null); + const [pdf, setPdf] = useState(null); + const [isLoading, setIsLoading] = useState(false); -// const [coba, setCoba] = useState(""); + const displayTargetDana = formatCurrencyDisplay(data.targetDana); + const displayHargaPerLembar = formatCurrencyDisplay(data.hargaPerLembar); + const displayTotalLembar = formatCurrencyDisplay( + Number(data.targetDana) / Number(data.hargaPerLembar) + ); + + const handleChangeCurrency = (field: keyof typeof data) => (text: string) => { + const numeric = text.replace(/\D/g, ""); + setData((prev) => ({ ...prev, [field]: numeric })); + }; + + const handleSubmit = async () => { + if (!image || !pdf) { + Toast.show({ + type: "error", + text1: "Harap pilih gambar dan file PDF", + }); + return; + } + + if ( + !data.title || + !data.targetDana || + !data.hargaPerLembar || + !data.rasioKeuntungan || + !data.pencarianInvestor || + !data.periodeDeviden || + !data.pembagianDeviden + ) { + Toast.show({ + type: "error", + text1: "Harap isi semua data", + }); + return; + } + + try { + setIsLoading(true); + const responseUploadImage = await uploadImageService({ + imageUri: image, + dirId: DIRECTORY_ID.investasi_image, + }); + + if (!responseUploadImage.success) { + Toast.show({ + type: "error", + text1: "Gagal mengunggah gambar", + }); + return; + } + + const imageId = responseUploadImage.data.id; + const responseUploadPdf = await uploadImageService({ + imageUri: pdf.uri, + dirId: DIRECTORY_ID.investasi_prospektus, + }); + + if (!responseUploadPdf.success) { + Toast.show({ + type: "error", + text1: "Gagal mengunggah file PDF", + }); + return; + } + + const pdfId = responseUploadPdf.data.id; + const newData = { + title: data.title, + targetDana: data.targetDana, + hargaLembar: data.hargaPerLembar, + totalLembar: displayTotalLembar, + roi: data.rasioKeuntungan, + masterPencarianInvestorId: data.pencarianInvestor, + masterPembagianDevidenId: data.pembagianDeviden, + masterPeriodeDevidenId: data.periodeDeviden, + authorId: user?.id, + imageId: imageId, + prospektusFileId: pdfId, + }; + + const response = await apiInvestmentCreate({ data: newData }); + console.log("[RESPONSE]", JSON.stringify(response, null, 2)); + if (response.success) { + Toast.show({ + type: "success", + text1: "Berhasil", + text2: response.message, + }); + router.replace("/investment/portofolio"); + } else { + Toast.show({ + type: "error", + text1: "Info", + text2: response.message, + }); + } + } catch (error) { + console.log("error", error); + } finally { + setIsLoading(false); + } + }; + + // const [coba, setCoba] = useState(""); return ( - {/* - setCoba(value)} - value={coba} - keyboardType="decimal-pad" - /> - */} - - + router.push("/take-picture/1")} + onPress={() => { + pickFile({ + setImageUri: ({ uri }) => { + console.log("URI IMAGE", uri); + setImage(uri); + }, + allowedType: "image", + }); + }} > Upload - + - + {pdf ? ( + {pdf.name} + ) : ( + + )} router.push("/take-picture/1")} + onPress={() => { + pickFile({ + setPdfUri: ({ uri, name, size }) => { + console.log("URI PDF", JSON.stringify(uri, null, 2)); + setPdf({ uri, name, size }); + }, + allowedType: "pdf", + }); + }} > Upload File @@ -90,22 +217,8 @@ export default function InvestmentCreate() { placeholder="0" label="Target Dana" keyboardType="numeric" - onChangeText={(value) => - setData({ ...data, targetDana: Number(value) }) - } - value={data.targetDana === 0 ? "" : data.targetDana.toString()} - /> - - - setData({ ...data, targetDana: Number(value) }) - } - value={data.targetDana === 0 ? "" : data.targetDana.toString()} + onChangeText={handleChangeCurrency("targetDana")} + value={displayTargetDana} /> - setData({ ...data, targetDana: Number(value) }) - } - value={data.targetDana === 0 ? "" : data.targetDana.toString()} + onChangeText={handleChangeCurrency("hargaPerLembar")} + value={displayHargaPerLembar} /> - - setData({ ...data, totalLembar: Number(value) }) - } - value={data.totalLembar === 0 ? "" : data.totalLembar.toString()} - /> + + + + *Total lembar dihitung dari, Target Dana / Harga Perlembar + + + - setData({ ...data, rasioKeuntungan: Number(value) }) - } + onChangeText={(value) => setData({ ...data, rasioKeuntungan: value })} value={ - data.rasioKeuntungan === 0 ? "" : data.rasioKeuntungan.toString() + data.rasioKeuntungan === "" ? "" : data.rasioKeuntungan.toString() } /> @@ -187,7 +300,7 @@ export default function InvestmentCreate() { value={data.pembagianDeviden} /> - router.replace("/investment/portofolio")}> + handleSubmit()}> Simpan diff --git a/constants/api-storage.ts b/constants/api-storage.ts new file mode 100644 index 0000000..52e9864 --- /dev/null +++ b/constants/api-storage.ts @@ -0,0 +1,20 @@ +const API_IMAGE = { + /** + * + * @param fileId | file id from wibu storage , atau bisa disimpan di DB + * @param size | file size 10 - 1000 , tergantung ukuran file dan kebutuhan saar di tampilkan + * @type {string} + */ + GET: ({ fileId, size }: { fileId: string; size?: string }) => + size + ? `https://wibu-storage.wibudev.com/api/files/${fileId}-size-${size}` + : `https://wibu-storage.wibudev.com/api/files/${fileId}`, + + /** + * @type {string} + * @returns alamat API dari wibu storage + */ + GET_NO_PARAMS: "https://wibu-storage.wibudev.com/api/files/", +}; + +export default API_IMAGE; diff --git a/lib/dummy-data/investment/dummy-data-not-publish.ts b/lib/dummy-data/investment/dummy-data-not-publish.ts index 77f54da..f73530e 100644 --- a/lib/dummy-data/investment/dummy-data-not-publish.ts +++ b/lib/dummy-data/investment/dummy-data-not-publish.ts @@ -1,71 +1,73 @@ -export {listDataNotPublishInvesment, listDataPublishInvesment}; +import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay"; -const listDataNotPublishInvesment = [ +export { listDataNotPublishInvesment, listDataPublishInvesment }; + +const listDataNotPublishInvesment = ({ data }: { data: any }) => [ { label: "Target Dana", - value: "Rp. 7.500.000", + value: `Rp. ${formatCurrencyDisplay(data?.targetDana) || "-"}`, }, { label: "Harga Per Lembar", - value: "Rp. 2.400", + value: `Rp. ${formatCurrencyDisplay(data?.hargaLembar) || "-"}`, }, { label: "Return Of Investment (ROI)", - value: "3 %", + value: `${data?.roi || "-"} %`, }, { label: "Total Lembar", - value: "1.200", - }, - { - label: "Jadwal Pembagian", - value: "Rp. 2.880.000", - }, - { - label: "Pembagian Deviden", - value: "Selamanya", + value: data?.totalLembar || "-", }, { label: "Pencarian Investor", - value: "30 Hari", + value: data && data?.MasterPencarianInvestor?.name + " hari" || "-", + }, + { + label: "Jadwal Pembagian", + value: data && data?.MasterPembagianDeviden?.name + " bulan" || "-", + }, + { + label: "Pembagian Deviden", + value: data?.MasterPeriodeDeviden?.name || "-", }, ]; -const listDataPublishInvesment = [ +const listDataPublishInvesment = ({ data }: { data: any }) => [ { label: "Investor", - value: "10", + value: data?.investor, }, { label: "Target Dana", - value: "Rp. 7.500.000", + value: data?.targetDana, }, { label: "Harga Per Lembar", - value: "Rp. 2.400", + value: data?.hargaPerLembar, }, { label: "Return Of Investment (ROI)", - value: "3 %", + value: data?.roi + " %", }, { label: "Total Lembar", - value: "1.200", + value: data?.totalLembar, }, { label: "Sisa Lembar", - value: "600", + value: data?.sisaLembar, }, { label: "Jadwal Pembagian", - value: "Rp. 2.880.000", + value: data?.jadwalPembagian, }, { label: "Pembagian Deviden", - value: "Selamanya", + value: data?.pembagianDeviden, }, { label: "Pencarian Investor", - value: "30 Hari", + value: data?.pencarianInvestor, }, -]; \ No newline at end of file +]; diff --git a/screens/Invesment/BoxDetailDataSection.tsx b/screens/Invesment/BoxDetailDataSection.tsx index ad07378..64e555b 100644 --- a/screens/Invesment/BoxDetailDataSection.tsx +++ b/screens/Invesment/BoxDetailDataSection.tsx @@ -10,10 +10,12 @@ import { View } from "react-native"; export default function Invesment_BoxDetailDataSection({ title, + imageId, data, bottomSection, }: { title?: string; + imageId?: string; data: any; bottomSection?: React.ReactNode; }) { @@ -21,14 +23,14 @@ export default function Invesment_BoxDetailDataSection({ <> - + {title || "Judul Investasi"} - {data.map((item: any, index: any) => ( + {data?.map((item: any, index: any) => ( {item.label} diff --git a/screens/Invesment/ButtonStatusSection.tsx b/screens/Invesment/ButtonStatusSection.tsx index f7bc1bd..4d6f1db 100644 --- a/screens/Invesment/ButtonStatusSection.tsx +++ b/screens/Invesment/ButtonStatusSection.tsx @@ -1,22 +1,54 @@ import { AlertDefaultSystem, ButtonCustom, Grid } from "@/components"; +import { + apiInvestmentDelete, + apiInvestmentUpdateStatus, +} from "@/service/api-client/api-investment"; +import { deleteImageService } from "@/service/upload-service"; import { router } from "expo-router"; +import { useState } from "react"; +import Toast from "react-native-toast-message"; export default function Investment_ButtonStatusSection({ + id, status, - buttonPublish + buttonPublish, }: { + id: string; status: string; buttonPublish?: React.ReactNode; }) { + const [isLoading, setIsLoading] = useState(false); const handleBatalkanReview = () => { AlertDefaultSystem({ title: "Batalkan Review", message: "Apakah Anda yakin ingin batalkan review ini?", textLeft: "Batal", textRight: "Ya", - onPressRight: () => { - console.log("Hapus"); - router.back(); + onPressRight: async () => { + try { + setIsLoading(true); + const response = await apiInvestmentUpdateStatus({ + id: id as string, + status: "draft", + }); + console.log("[RESPONSE]", JSON.stringify(response, null, 2)); + if (response.success) { + Toast.show({ + type: "success", + text1: "Berhasil Batalkan Review", + }); + router.back(); + } else { + Toast.show({ + type: "error", + text1: "Gagal Batalkan Review", + }); + } + } catch (error) { + console.log("[ERROR]", error); + } finally { + setIsLoading(false); + } }, }); }; @@ -27,9 +59,31 @@ export default function Investment_ButtonStatusSection({ message: "Apakah Anda yakin ingin ajukan review ini?", textLeft: "Batal", textRight: "Ya", - onPressRight: () => { - console.log("Hapus"); - router.back(); + onPressRight: async () => { + try { + setIsLoading(true); + const response = await apiInvestmentUpdateStatus({ + id: id as string, + status: "review", + }); + console.log("[RESPONSE]", JSON.stringify(response, null, 2)); + if (response.success) { + Toast.show({ + type: "success", + text1: "Berhasil Ajukan Review", + }); + router.back(); + } else { + Toast.show({ + type: "error", + text1: "Gagal Ajukan Review", + }); + } + } catch (error) { + console.log("[ERROR]", error); + } finally { + setIsLoading(false); + } }, }); }; @@ -40,9 +94,31 @@ export default function Investment_ButtonStatusSection({ message: "Apakah Anda yakin ingin edit kembali ini?", textLeft: "Batal", textRight: "Ya", - onPressRight: () => { - console.log("Hapus"); - router.back(); + onPressRight: async () => { + try { + setIsLoading(true); + const response = await apiInvestmentUpdateStatus({ + id: id as string, + status: "draft", + }); + console.log("[RESPONSE]", JSON.stringify(response, null, 2)); + if (response.success) { + Toast.show({ + type: "success", + text1: "Berhasil Update Status", + }); + router.back(); + } else { + Toast.show({ + type: "error", + text1: "Gagal Update Status", + }); + } + } catch (error) { + console.log("[ERROR]", error); + } finally { + setIsLoading(false); + } }, }); }; @@ -53,9 +129,55 @@ export default function Investment_ButtonStatusSection({ message: "Apakah Anda yakin ingin menghapus data ini?", textLeft: "Batal", textRight: "Hapus", - onPressRight: () => { - console.log("Hapus"); - router.back(); + onPressRight: async () => { + try { + setIsLoading(true); + const response = await apiInvestmentDelete({ + id: id as string, + }); + + console.log("[RESPONSE]", JSON.stringify(response, null, 2)); + if (response.success) { + const deleteImage = await deleteImageService({ + id: response?.data?.imageId as string, + }); + + if (!deleteImage.success) { + Toast.show({ + type: "error", + text1: "Gagal Hapus Data", + }); + return; + } + + const deleteFile = await deleteImageService({ + id: response?.data?.prospektusFileId as string, + }); + + if (!deleteFile.success) { + Toast.show({ + type: "error", + text1: "Gagal Hapus Data", + }); + return; + } + + Toast.show({ + type: "success", + text1: "Berhasil Hapus Data", + }); + router.back(); + } else { + Toast.show({ + type: "error", + text1: "Gagal Hapus Data", + }); + } + } catch (error) { + console.log("[ERROR]", error); + } finally { + setIsLoading(false); + } }, }); }; @@ -64,6 +186,7 @@ export default function Investment_ButtonStatusSection({ return ( <> - {buttonPublish} - ; + return <>{buttonPublish}; case "review": return ( - + Batalkan Review ); @@ -92,7 +213,7 @@ export default function Investment_ButtonStatusSection({ <> - + Ajukan Review @@ -108,7 +229,7 @@ export default function Investment_ButtonStatusSection({ <> - + Edit Kembali diff --git a/screens/Invesment/DetailDataPublishSection.tsx b/screens/Invesment/DetailDataPublishSection.tsx index 5d43ce9..c229c13 100644 --- a/screens/Invesment/DetailDataPublishSection.tsx +++ b/screens/Invesment/DetailDataPublishSection.tsx @@ -10,26 +10,32 @@ import Investment_ButtonStatusSection from "./ButtonStatusSection"; export default function Invesment_DetailDataPublishSection({ status, + data, bottomSection, buttonSection, }: { status: string; + data: any; bottomSection?: React.ReactNode; buttonSection?: React.ReactNode; }) { + // console.log("[DATA DETAIL]", JSON.stringify(data, null, 2)); return ( <> diff --git a/screens/Invesment/StatusBox.tsx b/screens/Invesment/StatusBox.tsx index 7bb81a1..5966ee7 100644 --- a/screens/Invesment/StatusBox.tsx +++ b/screens/Invesment/StatusBox.tsx @@ -1,44 +1,43 @@ import { BaseBox, Grid, Spacing, TextCustom } from "@/components"; +import API_IMAGE from "@/constants/api-storage"; import DUMMY_IMAGE from "@/constants/dummy-image-value"; +import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay"; import { Image } from "expo-image"; import { Href } from "expo-router"; import { View } from "react-native"; interface Investment_StatusBoxProps { - id: string; + data: any; status: string; - href?: Href + href?: Href; } export default function Investment_StatusBox({ - id, + data, status, - href + href, }: Investment_StatusBoxProps) { return ( - - Title here : {status} Lorem ipsum dolor sit amet consectetur - adipisicing elit. Omnis, exercitationem, sequi enim quod distinctio - maiores laudantium amet, quidem atque repellat sit vitae qui aliquam - est veritatis laborum eum voluptatum totam! - + {data?.title || ""} Target Dana: - Rp. 7.500.000 + + Rp. {formatCurrencyDisplay(data?.targetDana) || ""} + diff --git a/service/api-client/api-investment.ts b/service/api-client/api-investment.ts new file mode 100644 index 0000000..d9e9d95 --- /dev/null +++ b/service/api-client/api-investment.ts @@ -0,0 +1,80 @@ +import { apiConfig } from "../api-config"; + +export async function apiInvestmentCreate({ data }: { data: any }) { + try { + const response = await apiConfig.post(`/mobile/investment`, { + data: data, + }); + return response.data; + } catch (error) { + throw error; + } +} + +export async function apiInvestmentGetByStatus({ + authorId, + status, +}: { + authorId: string; + status: string; +}) { + try { + const response = await apiConfig.get( + `/mobile/investment/${authorId}/${status}` + ); + return response.data; + } catch (error) { + throw error; + } +} + +export async function apiInvestmentGetById({ id }: { id: string }) { + try { + const response = await apiConfig.get(`/mobile/investment/${id}`); + return response.data; + } catch (error) { + throw error; + } +} + +export async function apiInvestmentUpdateStatus({ + id, + status, +}: { + id: string; + status: "publish" | "draft" | "review" | "reject"; +}) { + console.log("[DATA FETCH]", JSON.stringify({ id, status }, null, 2)); + try { + const response = await apiConfig.put(`/mobile/investment/${id}/${status}`); + return response.data; + } catch (error) { + throw error; + } +} + +export async function apiInvestmentDelete({ id }: { id: string }) { + try { + const response = await apiConfig.delete(`/mobile/investment/${id}`); + return response.data; + } catch (error) { + throw error; + } +} + +export async function apiInvestmentUpdateData({ + id, + data, +}: { + id: string; + data: any; +}) { + try { + const response = await apiConfig.put(`/mobile/investment/${id}`, { + data: data, + }); + return response.data; + } catch (error) { + throw error; + } +} diff --git a/utils/formatCurrencyDisplay.ts b/utils/formatCurrencyDisplay.ts new file mode 100644 index 0000000..3767e18 --- /dev/null +++ b/utils/formatCurrencyDisplay.ts @@ -0,0 +1,46 @@ +/** + * Memformat angka menjadi string dengan format mata uang lokal (misal: 3500000 → "3.500.000") + * Hanya untuk keperluan tampilan. Nilai asli tetap berupa number/string mentah. + * + * @param value - Angka yang akan diformat (bisa number atau string) + * @param locale - Lokal untuk format (default: 'id-ID' untuk format Indonesia) + * @param currency - Kode mata uang (opsional, default: tidak ditampilkan) + * @returns string yang sudah diformat tanpa simbol mata uang + */ +export const formatCurrencyDisplay = ( + value: number | string | null | undefined, + locale: string = "id-ID", + currency?: string +): string => { + // Handle nilai null/undefined/empty + if (value === null || value === undefined || value === "") { + return ""; + } + + // Pastikan value adalah number + const numValue = typeof value === "string" ? parseFloat(value) : value; + + // Jika parsing gagal, kembalikan string kosong + if (isNaN(numValue)) { + return ""; + } + + // Gunakan Intl.NumberFormat untuk format lokal + const formatter = new Intl.NumberFormat(locale, { + style: currency ? "currency" : "decimal", + currency: currency, + minimumFractionDigits: 0, + maximumFractionDigits: 0, + }); + + let formatted = formatter.format(numValue); + + // Jika tidak ingin simbol mata uang, hapus simbolnya (misal: "Rp" atau "IDR") + if (!currency) { + // Hapus simbol non-digit/non-koma/non-titik (misal: "Rp", "IDR", "$", dll) + // Tapi pertahankan angka, koma, titik, dan spasi jika ada + formatted = formatted.replace(/[^\d.,\s]/g, "").trim(); + } + + return formatted; +}; diff --git a/utils/pickFile.ts b/utils/pickFile.ts new file mode 100644 index 0000000..15479a3 --- /dev/null +++ b/utils/pickFile.ts @@ -0,0 +1,117 @@ +import * as ImagePicker from "expo-image-picker"; +import * as DocumentPicker from "expo-document-picker"; +import { Alert } from "react-native"; + +const ALLOWED_IMAGE_EXTENSIONS = ["jpg", "jpeg", "png"]; +const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB + +export interface IFileData { + uri: string; + name: string; + size: number; +} + +export type AllowedFileType = "image" | "pdf" | undefined; + +export interface PickFileOptions { + setImageUri?: (file: IFileData) => void; + setPdfUri?: (file: IFileData) => void; + allowedType?: AllowedFileType; // <-- Tambahkan prop ini +} + +export default async function pickFile({ + setImageUri, + setPdfUri, + allowedType, +}: PickFileOptions): Promise { + if (allowedType === "image") { + await pickImage(setImageUri); + } else if (allowedType === "pdf") { + await pickPdf(setPdfUri); + } else { + // Mode fleksibel: tampilkan pilihan + Alert.alert( + "Pilih Jenis File", + "Pilih sumber file yang ingin diunggah:", + [ + { text: "Batal", style: "cancel" }, + { text: "Dokumen (PDF)", onPress: () => pickPdf(setPdfUri) }, + { text: "Gambar", onPress: () => pickImage(setImageUri) }, + ], + { cancelable: true } + ); + } +} + +// --- Fungsi internal: pickImage --- +async function pickImage(setImageUri?: (file: IFileData) => void) { + const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync(); + if (status !== "granted") { + Alert.alert( + "Izin Ditolak", + "Izinkan akses ke galeri untuk memilih gambar." + ); + return; + } + + const result = await ImagePicker.launchImageLibraryAsync({ + mediaTypes: ImagePicker.MediaTypeOptions.Images, + allowsEditing: true, + aspect: [4, 3], + quality: 1, + }); + + if (result.canceled || !result.assets?.[0]) return; + + const asset = result.assets[0]; + const uri = asset.uri; + const filename = uri.split("/").pop() || `image_${Date.now()}.jpg`; + const size = asset.fileSize ?? 0; + + if (size > MAX_FILE_SIZE) { + Alert.alert("File Terlalu Besar", "Ukuran maksimal adalah 5MB."); + return; + } + + const extMatch = /\.(\w+)$/.exec(filename.toLowerCase()); + const extension = extMatch ? extMatch[1] : "jpg"; + + if (!ALLOWED_IMAGE_EXTENSIONS.includes(extension)) { + Alert.alert( + "Format Tidak Didukung", + "Hanya JPG, JPEG, dan PNG yang diperbolehkan." + ); + return; + } + + setImageUri?.({ uri, name: filename, size }); +} + +// --- Fungsi internal: pickPdf --- +async function pickPdf(setPdfUri?: (file: IFileData) => void) { + const result = await DocumentPicker.getDocumentAsync({ + type: "application/pdf", // Hanya PDF + copyToCacheDirectory: true, + }); + + if (result.canceled || !result.assets?.[0]) return; + + const asset = result.assets[0]; + const { uri, name, size } = asset; + const filename = name || `document_${Date.now()}.pdf`; + const fileSize = size ?? 0; + + if (fileSize > MAX_FILE_SIZE) { + Alert.alert("File Terlalu Besar", "Ukuran maksimal adalah 5MB."); + return; + } + + // Validasi ekstensi (extra safety) + const extMatch = /\.(\w+)$/.exec(filename.toLowerCase()); + if (extMatch?.[1] !== "pdf") { + Alert.alert("File Tidak Valid", "Hanya file PDF yang diperbolehkan."); + return; + } + + setPdfUri?.({ uri, name: filename, size: fileSize }); +}