Integrasi API: Investment & Admin Investment

Add:
- components/_ShareComponent/NoDataText.tsx
- service/api-admin/api-admin-investment.ts

Fix:
- app/(application)/(user)/investment/(tabs)/index.tsx
- app/(application)/admin/investment/[id]/[status]/index.tsx
- app/(application)/admin/investment/[id]/reject-input.tsx
- app/(application)/admin/investment/[status]/status.tsx
- app/(application)/admin/investment/index.tsx
- screens/Invesment/DetailDataPublishSection.tsx

### No Issue
This commit is contained in:
2025-10-30 15:13:33 +08:00
parent b3209dc7ee
commit f23cfe1107
8 changed files with 413 additions and 119 deletions

View File

@@ -8,6 +8,7 @@ import {
TextCustom, TextCustom,
ViewWrapper, ViewWrapper,
} from "@/components"; } from "@/components";
import NoDataText from "@/components/_ShareComponent/NoDataText";
import API_STRORAGE from "@/constants/base-url-api-strorage"; import API_STRORAGE from "@/constants/base-url-api-strorage";
import DUMMY_IMAGE from "@/constants/dummy-image-value"; import DUMMY_IMAGE from "@/constants/dummy-image-value";
import { apiInvestmentGetAll } from "@/service/api-client/api-investment"; import { apiInvestmentGetAll } from "@/service/api-client/api-investment";
@@ -52,7 +53,7 @@ export default function InvestmentBursa() {
{loadingList ? ( {loadingList ? (
<LoaderCustom /> <LoaderCustom />
) : _.isEmpty(list) ? ( ) : _.isEmpty(list) ? (
<TextCustom>Belum ada data</TextCustom> <NoDataText />
) : ( ) : (
list?.map((item: any, index: number) => ( list?.map((item: any, index: number) => (
<BaseBox <BaseBox

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { import {
ActionIcon, ActionIcon,
AlertDefaultSystem, AlertDefaultSystem,
@@ -11,7 +12,7 @@ import {
Spacing, Spacing,
StackCustom, StackCustom,
TextCustom, TextCustom,
ViewWrapper ViewWrapper,
} from "@/components"; } from "@/components";
import { IconProspectus } from "@/components/_Icon"; import { IconProspectus } from "@/components/_Icon";
import { IconDot, IconList } from "@/components/_Icon/IconComponent"; import { IconDot, IconList } from "@/components/_Icon/IconComponent";
@@ -19,75 +20,141 @@ import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButt
import AdminButtonReject from "@/components/_ShareComponent/Admin/ButtonReject"; import AdminButtonReject from "@/components/_ShareComponent/Admin/ButtonReject";
import AdminButtonReview from "@/components/_ShareComponent/Admin/ButtonReview"; import AdminButtonReview from "@/components/_ShareComponent/Admin/ButtonReview";
import { GridDetail_4_8 } from "@/components/_ShareComponent/GridDetail_4_8"; import { GridDetail_4_8 } from "@/components/_ShareComponent/GridDetail_4_8";
import ReportBox from "@/components/Box/ReportBox";
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_BUTTON } from "@/constants/constans-value"; import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
import { router, useLocalSearchParams } from "expo-router"; import {
apiAdminInvestasiUpdateByStatus,
apiAdminInvestmentDetailById,
} from "@/service/api-admin/api-admin-investment";
import { colorBadgeStatus } from "@/utils/colorBadge";
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash"; import _ from "lodash";
import React from "react"; import React from "react";
import Toast from "react-native-toast-message";
export default function AdminInvestmentDetail() { export default function AdminInvestmentDetail() {
const { id, status } = useLocalSearchParams(); const { id, status } = useLocalSearchParams();
const [openDrawer, setOpenDrawer] = React.useState(false); const [openDrawer, setOpenDrawer] = React.useState(false);
const colorBadge = () => { const [data, setData] = React.useState<any | null>(null);
if (status === "publish") { const [isLoading, setLoading] = React.useState(false);
return MainColor.green;
} else if (status === "review") { useFocusEffect(
return MainColor.orange; React.useCallback(() => {
} else if (status === "reject") { onLoadData();
return MainColor.red; }, [id])
} else { );
return MainColor.placeholder;
const onLoadData = async () => {
try {
const response = await apiAdminInvestmentDetailById({ id: id as string });
console.log("[DATA]", JSON.stringify(response, null, 2));
if (response.success) {
setData(response.data);
}
} catch (error) {
console.log(error);
} }
}; };
const listData = [ const listData = [
{ {
label: "Username", label: "Username",
value: `Bagas Banuna ${id}`, value: (data && data?.author?.username) || "-",
}, },
{ {
label: "Judul", label: "Judul",
value: `Donasi Lorem ipsum dolor sit amet, consectetur adipisicing elit.`, value: (data && data?.title) || "-",
}, },
{ {
label: "Status", label: "Status",
value: ( value:
<BadgeCustom color={colorBadge()}> data && data?.MasterStatusInvestasi?.name ? (
{_.startCase(status as string)} <BadgeCustom
</BadgeCustom> color={colorBadgeStatus({
), status: data?.MasterStatusInvestasi?.name as string,
})}
>
{_.startCase(data?.MasterStatusInvestasi?.name as string)}
</BadgeCustom>
) : (
"-"
),
}, },
{ {
label: "Dana Dibutuhkan", label: "Dana Dibutuhkan",
value: "Rp 10.000.000", value: `Rp. ${
(data && data?.targetDana && formatCurrencyDisplay(data?.targetDana)) ||
"-"
}`,
}, },
{ {
label: "Harga Perlembar", label: "Harga Perlembar",
value: "Rp 2500", value: `Rp. ${
(data &&
data?.hargaLembar &&
formatCurrencyDisplay(data?.hargaLembar)) ||
"-"
}`,
}, },
{ {
label: "Total Lembar", label: "Total Lembar",
value: "2490 lembar", value:
(data &&
data?.totalLembar &&
formatCurrencyDisplay(data?.totalLembar)) ||
"-",
}, },
{ {
label: "ROI", label: "ROI",
value: "4 %", value: `${(data && data?.roi && data?.roi) || 0} %`,
}, },
{ {
label: "Pembagian Deviden", label: "Pembagian Deviden",
value: "3 bulan", value: (data && data?.MasterPembagianDeviden?.name) + " bulan" || "-",
}, },
{ {
label: "Jadwal Pembagian", label: "Jadwal Pembagian",
value: "Selamanya", value: (data && data?.MasterPeriodeDeviden?.name) || "-",
}, },
{ {
label: "Pencarian Investor", label: "Pencarian Investor",
value: "30 Hari", value: (data && data?.MasterPencarianInvestor?.name) + " hari" || "-",
}, },
]; ];
const handlerSubmitPublish = async () => {
try {
setLoading(true);
const response = await apiAdminInvestasiUpdateByStatus({
id: id as string,
status: "publish",
data: data,
});
console.log("[RESPONSE]", JSON.stringify(response, null, 2));
if (!response.success) {
Toast.show({
type: "error",
text1: "Gagal mempublikasikan data",
});
return;
}
Toast.show({
type: "success",
text1: "Berhasil mempublikasikan data",
});
router.replace(`/admin/investment/publish/status`);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoading(false);
}
};
const rightComponent = ( const rightComponent = (
<ActionIcon <ActionIcon
icon={<IconDot size={ICON_SIZE_BUTTON} />} icon={<IconDot size={ICON_SIZE_BUTTON} />}
@@ -126,7 +193,7 @@ export default function AdminInvestmentDetail() {
<BaseBox> <BaseBox>
<StackCustom> <StackCustom>
<DummyLandscapeImage /> <DummyLandscapeImage imageId={data?.imageId} />
{listData.map((item, i) => ( {listData.map((item, i) => (
<GridDetail_4_8 <GridDetail_4_8
key={i} key={i}
@@ -150,7 +217,9 @@ export default function AdminInvestmentDetail() {
/> />
} }
onPress={() => { onPress={() => {
router.push(`/(application)/(file)/${id}`); router.push(
`/(application)/(file)/${data?.prospektusFileId}`
);
}} }}
> >
Preview Preview
@@ -161,46 +230,66 @@ export default function AdminInvestmentDetail() {
label={<TextCustom bold>File Dokumen</TextCustom>} label={<TextCustom bold>File Dokumen</TextCustom>}
value={ value={
<StackCustom> <StackCustom>
{Array.from({ length: 5 }).map((_, i) => ( {_.isEmpty(data?.DokumenInvestasi) ? (
<ButtonCustom <TextCustom align="center">-</TextCustom>
key={i} ) : (
iconLeft={ data?.DokumenInvestasi?.map((item: any, index: number) => {
<IconProspectus const titleFix = item?.title?.substring(0, 10) || "";
size={ICON_SIZE_BUTTON}
color={MainColor.darkblue} return (
/> <ButtonCustom
} key={item.id || index} // ✅ pastikan key unik
onPress={() => { iconLeft={
router.push(`/(application)/(file)/${id}`); <IconProspectus
}} size={ICON_SIZE_BUTTON}
> color={MainColor.darkblue}
Dokumen {i + 1} />
</ButtonCustom> }
))} onPress={() => {
router.push(
`/(application)/(file)/${item?.fileId}`
);
}}
>
<TextCustom color="black" truncate>
{titleFix}...
</TextCustom>
</ButtonCustom>
);
})
)}
</StackCustom> </StackCustom>
} }
/> />
</StackCustom> </StackCustom>
</BaseBox> </BaseBox>
{data &&
data?.catatan &&
(status === "review" || status === "reject") && (
<ReportBox text={data?.catatan} />
)}
{status === "review" && ( {status === "review" && (
<AdminButtonReview <AdminButtonReview
isLoading={isLoading}
onPublish={() => { onPublish={() => {
AlertDefaultSystem({ AlertDefaultSystem({
title: "Publish", title: "Publish",
message: "Apakah anda yakin ingin mempublikasikan data ini?", message: "Apakah anda yakin ingin mempublikasikan data ini?",
textLeft: "Batal", textLeft: "Batal",
textRight: "Ya", textRight: "Ya",
onPressLeft: () => {
router.back();
},
onPressRight: () => { onPressRight: () => {
router.back(); handlerSubmitPublish();
}, },
}); });
}} }}
onReject={() => { onReject={() => {
router.push(`/admin/investment/${id}/reject-input`); router.push(
`/admin/investment/${id}/reject-input?status=${_.lowerCase(
data?.MasterStatusInvestasi?.name
)}`
);
}} }}
/> />
)} )}

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { import {
AlertDefaultSystem, AlertDefaultSystem,
BoxButtonOnFooter, BoxButtonOnFooter,
@@ -6,15 +7,83 @@ import {
} from "@/components"; } from "@/components";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle"; import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import AdminButtonReject from "@/components/_ShareComponent/Admin/ButtonReject"; import AdminButtonReject from "@/components/_ShareComponent/Admin/ButtonReject";
import { router, useLocalSearchParams } from "expo-router"; import { apiAdminInvestasiUpdateByStatus, apiAdminInvestmentDetailById } from "@/service/api-admin/api-admin-investment";
import { useState } from "react"; import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, useState } from "react";
import Toast from "react-native-toast-message";
export default function AdminInvestmentRejectInput() { export default function AdminInvestmentRejectInput() {
const { id } = useLocalSearchParams(); const { id, status } = useLocalSearchParams();
const [value, setValue] = useState(id as string); console.log("[STATUS]", status);
const [value, setValue] = useState<any | null>(null);
const [isLoading , setLoading] = useState(false)
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id])
);
const onLoadData = async () => {
try {
const response = await apiAdminInvestmentDetailById({ id: id as string });
console.log("[DATA]", JSON.stringify(response, null, 2));
if (response.success) {
setValue(response.data?.catatan);
}
} catch (error) {
console.log(error);
}
};
const handlerSubmit = async () => {
if (!value) {
Toast.show({
type: "error",
text1: "Harap masukan alasan penolakan",
});
return;
}
try {
setLoading(true)
const response = await apiAdminInvestasiUpdateByStatus({
id: id as string,
status: "reject",
data: value,
});
console.log("[RESPONSE]", JSON.stringify(response, null, 2));
if (!response.success) {
Toast.show({
type: "error",
text1: "Gagal melakukan report",
});
return;
}
Toast.show({
type: "success",
text1: "Berhasil melakukan report",
});
if (status === "review") {
router.replace(`/admin/investment/reject/status`);
} else {
router.back();
}
} catch (error) {
console.error(["ERROR"], error);
} finally {
setLoading(false)
}
};
const buttonSubmit = ( const buttonSubmit = (
<BoxButtonOnFooter> <BoxButtonOnFooter>
<AdminButtonReject <AdminButtonReject
isLoading={isLoading}
title="Reject" title="Reject"
onReject={() => onReject={() =>
AlertDefaultSystem({ AlertDefaultSystem({
@@ -22,12 +91,8 @@ export default function AdminInvestmentRejectInput() {
message: "Apakah anda yakin ingin menolak data ini?", message: "Apakah anda yakin ingin menolak data ini?",
textLeft: "Batal", textLeft: "Batal",
textRight: "Ya", textRight: "Ya",
onPressLeft: () => {
router.back();
},
onPressRight: () => { onPressRight: () => {
console.log("value:", value); handlerSubmit();
router.replace(`/admin/investment/reject/status`);
}, },
}) })
} }
@@ -39,7 +104,9 @@ export default function AdminInvestmentRejectInput() {
<> <>
<ViewWrapper <ViewWrapper
footerComponent={buttonSubmit} footerComponent={buttonSubmit}
headerComponent={<AdminBackButtonAntTitle title="Penolakan Investasi" />} headerComponent={
<AdminBackButtonAntTitle title="Penolakan Investasi" />
}
> >
<TextAreaCustom <TextAreaCustom
value={value} value={value}

View File

@@ -1,69 +1,114 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { import {
ActionIcon, ActionIcon,
SearchInput, LoaderCustom,
Spacing, SearchInput,
TextCustom, StackCustom,
ViewWrapper TextCustom,
ViewWrapper
} from "@/components"; } from "@/components";
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage"; import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle"; import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle";
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue"; import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
import NoDataText from "@/components/_ShareComponent/NoDataText";
import { ICON_SIZE_BUTTON } from "@/constants/constans-value"; import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
import { apiAdminInvestment } from "@/service/api-admin/api-admin-investment";
import { Octicons } from "@expo/vector-icons"; import { Octicons } from "@expo/vector-icons";
import { router, useLocalSearchParams } from "expo-router"; import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash"; import _ from "lodash";
import React, { useCallback } from "react";
import { Divider } from "react-native-paper"; import { Divider } from "react-native-paper";
export default function AdminInvestmentStatus() { export default function AdminInvestmentStatus() {
const { status } = useLocalSearchParams(); const { status } = useLocalSearchParams();
console.log("[STATUS]", status);
const [listData, setListData] = React.useState<any[] | null>(null);
const [loadData, setLoadingData] = React.useState(false);
const [search, setSearch] = React.useState("");
useFocusEffect(
useCallback(() => {
onLoadData();
}, [status, search])
);
const onLoadData = async () => {
try {
setLoadingData(true);
const response = await apiAdminInvestment({
category: status as "publish" | "review" | "reject",
search,
});
console.log("[LIST DATA]", JSON.stringify(response, null, 2));
if (response.success) {
setListData(response.data);
}
} catch (error) {
console.log(error);
setListData([]);
} finally {
setLoadingData(false);
}
};
const rightComponent = ( const rightComponent = (
<SearchInput <SearchInput
containerStyle={{ width: "100%", marginBottom: 0 }} containerStyle={{ width: "100%", marginBottom: 0 }}
placeholder="Cari" placeholder="Cari"
value={search}
onChangeText={setSearch}
/> />
); );
return ( return (
<> <>
<ViewWrapper <ViewWrapper headerComponent={<AdminTitlePage title="Investasi" />}>
headerComponent={ <StackCustom gap={"sm"}>
<AdminComp_BoxTitle <AdminComp_BoxTitle
title={`Investasi ${_.startCase(status as string)}`} title={`${_.startCase(status as string)}`}
rightComponent={rightComponent} rightComponent={rightComponent}
/> />
} <AdminTitleTable
> title1="Aksi"
<AdminTitleTable title2="Username"
title1="Aksi" title3="Judul Investasi"
title2="Username"
title3="Judul Investasi"
/>
<Spacing />
<Divider />
{Array.from({ length: 10 }).map((_, index) => (
<AdminTableValue
key={index}
value1={
<ActionIcon
icon={
<Octicons name="eye" size={ICON_SIZE_BUTTON} color="black" />
}
onPress={() => {
router.push(`/admin/investment/${index}/${status}`);
}}
/>
}
value2={<TextCustom truncate={1}>Username username</TextCustom>}
value3={
<TextCustom truncate={2}>
Lorem ipsum dolor sit amet consectetur adipisicing elit.
Blanditiis asperiores quidem deleniti architecto eaque et
nostrum, ad consequuntur eveniet quisquam quae voluptatum
ducimus! Dolorem nobis modi officia debitis, beatae mollitia.
</TextCustom>
}
/> />
))}
<Divider />
{loadData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<NoDataText />
) : (
listData?.map((item: any, index: number) => (
<AdminTableValue
key={index}
value1={
<ActionIcon
icon={
<Octicons
name="eye"
size={ICON_SIZE_BUTTON}
color="black"
/>
}
onPress={() => {
router.push(`/admin/investment/${item.id}/${status}`);
}}
/>
}
value2={<TextCustom truncate={1}>{item?.author?.username}</TextCustom>}
value3={
<TextCustom truncate={2}>
{item?.title}
</TextCustom>
}
/>
))
)}
</StackCustom>
</ViewWrapper> </ViewWrapper>
</> </>
); );

View File

@@ -7,8 +7,51 @@ import {
import AdminComp_BoxDashboard from "@/components/_ShareComponent/Admin/BoxDashboard"; import AdminComp_BoxDashboard from "@/components/_ShareComponent/Admin/BoxDashboard";
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage"; import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
import { apiAdminInvestment } from "@/service/api-admin/api-admin-investment";
import { useFocusEffect } from "expo-router";
import React, { useCallback } from "react";
export default function AdminInvestment() { export default function AdminInvestment() {
const [data, setData] = React.useState<any | null>(null);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [])
);
const onLoadData = async () => {
try {
const response = await apiAdminInvestment({
category: "dashboard",
});
console.log(JSON.stringify(response, null, 2));
if (response.success) {
setData(response.data);
}
} catch (error) {
console.log(error);
}
};
const listData = [
{
label: "Publish",
value: (data && data.publish) || 0,
icon: <IconPublish size={25} color={MainColor.green} />,
},
{
label: "Review",
value: (data && data.review) || 0,
icon: <IconReview size={25} color={MainColor.orange} />,
},
{
label: "Reject",
value: (data && data.reject) || 0,
icon: <IconReject size={25} color={MainColor.red} />,
},
];
return ( return (
<> <>
<ViewWrapper> <ViewWrapper>
@@ -23,21 +66,3 @@ export default function AdminInvestment() {
</> </>
); );
} }
const listData = [
{
label: "Publish",
value: 3,
icon: <IconPublish size={25} color={MainColor.green} />,
},
{
label: "Review",
value: 5,
icon: <IconReview size={25} color={MainColor.orange} />,
},
{
label: "Reject",
value: 8,
icon: <IconReject size={25} color={MainColor.red} />,
},
];

View File

@@ -0,0 +1,9 @@
import TextCustom from "../Text/TextCustom";
export default function NoDataText({ text }: { text?: string }) {
return (
<TextCustom color="gray" align="center" bold size={"small"}>
{text ? text : "Belum ada data"}
</TextCustom>
);
}

View File

@@ -7,6 +7,7 @@ import React from "react";
import Invesment_BoxDetailDataSection from "./BoxDetailDataSection"; import Invesment_BoxDetailDataSection from "./BoxDetailDataSection";
import Invesment_BoxProgressSection from "./BoxProgressSection"; import Invesment_BoxProgressSection from "./BoxProgressSection";
import Investment_ButtonStatusSection from "./ButtonStatusSection"; import Investment_ButtonStatusSection from "./ButtonStatusSection";
import ReportBox from "@/components/Box/ReportBox";
export default function Invesment_DetailDataPublishSection({ export default function Invesment_DetailDataPublishSection({
status, status,
@@ -23,6 +24,9 @@ export default function Invesment_DetailDataPublishSection({
return ( return (
<> <>
<StackCustom gap={"sm"}> <StackCustom gap={"sm"}>
{data && data?.catatan && (status === "draft" || status === "reject") && (
<ReportBox text={data?.catatan} />
)}
<Invesment_BoxProgressSection progress={data?.progress} status={status as string} /> <Invesment_BoxProgressSection progress={data?.progress} status={status as string} />
<Invesment_BoxDetailDataSection <Invesment_BoxDetailDataSection
title={data?.title} title={data?.title}

View File

@@ -0,0 +1,54 @@
import { apiConfig } from "../api-config";
export async function apiAdminInvestment({
category,
search,
}: {
category: "dashboard" | "publish" | "review" | "reject";
search?: string;
}) {
const propsQuery =
category === "dashboard"
? `category=${category}`
: `category=${category}&search=${search}`;
try {
const response = await apiConfig.get(
`/mobile/admin/investment?${propsQuery}`
);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiAdminInvestmentDetailById({ id }: { id: string }) {
try {
const response = await apiConfig.get(`/mobile/admin/investment/${id}`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiAdminInvestasiUpdateByStatus({
id,
status,
data,
}: {
id: string;
status: "publish" | "review" | "reject";
data: any;
}) {
try {
const response = await apiConfig.put(
`/mobile/admin/investment/${id}?status=${status}`,
{
data: data,
}
);
return response.data;
} catch (error) {
throw error;
}
}