Compare commits

...

9 Commits

Author SHA1 Message Date
aa85e05f79 Invesment
Fix: Integrasi API ke UI
- investment/[id]/(transaction-flow)/index.tsx
- investment/[id]/index.tsx

### Issue: input data untuk total lembar dan sisa lembar berisi . dianatra angkanya
2025-10-01 17:29:59 +08:00
c2acb97a37 Invesment
Fix:
- tampilan list data bada beranda dan detail data main

### No Issue
2025-10-01 16:45:23 +08:00
250b216a54 Invesment
Fix:
- tampilan dokumen dan file prospektus
- create & edit dokumen
- list rekap dokumen dan tampilan ( untuk non author )

### No Issue
2025-10-01 14:40:50 +08:00
5f05d1f7f0 Component
Fix: fix nama upload dan delete file service

Invesment:
Fix:
- Edit data dan edit prospektus
- View file dengan metode react-native-webview

### No issue
2025-09-30 17:30:07 +08:00
3d8d8568a3 Invesment
Fix: create & edit list master sudah terintegrasi ke API

### No Issue
2025-09-30 11:00:31 +08:00
ccdd7730b2 Investment
Add:
- utils/pickFile: pilih extention file sesuai kebutuhan
- utils/formatCurrencyDisplay.ts: tampillan uang 2.500
- api-client/api-investment.ts
- api-storage.ts: api strogre wibudev

Fix:
- Integrasi API pada: Create, Edit, Tampilan status & detail
- Button status dan hapus data juga sudah terintegrasi

### No Issue
2025-09-29 17:42:25 +08:00
a474aebb94 Forum:
Fix: link dari avatar ke forumku & penambahan tombol create di forumku jika id forum milik user yang sama seperti user login

### No issue
2025-09-29 10:28:53 +08:00
29b65aeebf Forum
Fix:
- Integrasi API ke semua tampilan

### No Issue
2025-09-26 17:43:50 +08:00
18beb09b42 Forum
Fix:
- Tampilan beranda forum & bisa melakukan search dan sudah terintegrasi API
- Fitur hapus edit dan ubah status sudah terintegrasi API
- List komentar sudah bisa muncul dan bisa mengahpus

### No Issue
2025-09-26 14:46:29 +08:00
60 changed files with 3067 additions and 876 deletions

View File

@@ -69,5 +69,6 @@ export default {
},
// Tambahkan environment variables ke sini
API_BASE_URL: process.env.API_BASE_URL,
BASE_URL: process.env.BASE_URL,
},
};

View File

@@ -1,9 +1,13 @@
import { BackButton, ViewWrapper } from "@/components";
import { MainColor } from "@/constants/color-palet";
import { FontAwesome } from "@expo/vector-icons";
import { Stack } from "expo-router";
import { BackButton } from "@/components";
import PdfViewer from "@/components/_ShareComponent/PdfViewer";
import API_STRORAGE from "@/constants/base-url-api-strorage";
import { Stack, useLocalSearchParams } from "expo-router";
import { SafeAreaView } from "react-native-safe-area-context";
export default function FileScreen() {
const { id } = useLocalSearchParams();
const url = API_STRORAGE.GET({ fileId: id as string });
return (
<>
<Stack.Screen
@@ -12,14 +16,9 @@ export default function FileScreen() {
headerLeft: () => <BackButton />,
}}
/>
<ViewWrapper>
<FontAwesome
name="file-pdf-o"
size={300}
style={{ alignSelf: "center" }}
color={MainColor.white}
/>
</ViewWrapper>
<SafeAreaView style={{ flex: 1 }} edges={["bottom"]}>
<PdfViewer uri={url} />
</SafeAreaView>
</>
);
}

View File

@@ -1,37 +1,98 @@
import {
BoxButtonOnFooter,
ButtonCustom,
LoaderCustom,
TextAreaCustom,
ViewWrapper,
} from "@/components";
import { router } from "expo-router";
import { useState } from "react";
import { apiForumGetOne, apiForumUpdate } from "@/service/api-client/api-forum";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, useState } from "react";
import Toast from "react-native-toast-message";
export default function ForumEdit() {
const { id } = useLocalSearchParams();
const [text, setText] = useState("");
const [loadingGetData, setLoadingGetData] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const buttonFooter = (
<BoxButtonOnFooter>
<ButtonCustom
onPress={() => {
console.log("Posting", text);
router.back();
}}
>
Update
</ButtonCustom>
</BoxButtonOnFooter>
useFocusEffect(
useCallback(() => {
onLoadData(id as string);
}, [id])
);
const onLoadData = async (id: string) => {
try {
setLoadingGetData(true);
const response = await apiForumGetOne({ id });
setText(response.data.diskusi);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingGetData(false);
}
};
const handlerUpdateData = async () => {
if (!text) {
Toast.show({
type: "error",
text1: "Harap masukkan diskusi",
});
return;
}
try {
setIsLoading(true);
const response = await apiForumUpdate({
id: id as string,
data: text,
});
if (response.success) {
Toast.show({
type: "success",
text1: "Berhasil diupdate",
});
router.back();
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoading(false);
}
};
const buttonFooter = () => {
return (
<>
{!loadingGetData && (
<BoxButtonOnFooter>
<ButtonCustom isLoading={isLoading} onPress={handlerUpdateData}>
Update
</ButtonCustom>
</BoxButtonOnFooter>
)}
</>
);
};
return (
<ViewWrapper footerComponent={buttonFooter}>
<TextAreaCustom
placeholder="Ketik diskusi anda..."
maxLength={1000}
showCount
value={text}
onChangeText={setText}
/>
<ViewWrapper footerComponent={buttonFooter()}>
{!loadingGetData ? (
<TextAreaCustom
placeholder="Ketik diskusi anda..."
maxLength={1000}
showCount
value={text}
onChangeText={(value) => {
setText(value);
}}
/>
) : (
<LoaderCustom />
)}
</ViewWrapper>
);
}

View File

@@ -1,36 +1,86 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
AlertCustom,
AvatarCustom,
AvatarComp,
ButtonCustom,
CenterCustom,
DrawerCustom,
FloatingButton,
Grid,
LoaderCustom,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import { MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
import Forum_BoxDetailSection from "@/screens/Forum/DiscussionBoxSection";
import { listDummyDiscussionForum } from "@/screens/Forum/list-data-dummy";
import Forum_MenuDrawerBerandaSection from "@/screens/Forum/MenuDrawerSection.tsx/MenuBeranda";
import { useLocalSearchParams } from "expo-router";
import { useState } from "react";
import { apiForumGetAll } from "@/service/api-client/api-forum";
import { apiUser } from "@/service/api-client/api-user";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
export default function Forumku() {
const { id } = useLocalSearchParams();
const { user } = useAuth();
const [openDrawer, setOpenDrawer] = useState(false);
const [status, setStatus] = useState("");
const [alertStatus, setAlertStatus] = useState(false);
const [deleteAlert, setDeleteAlert] = useState(false);
const [listData, setListData] = useState<any | null>(null);
const [dataUser, setDataUser] = useState<any | null>(null);
const [loadingGetList, setLoadingGetList] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
onLoadDataProfile(id as string);
}, [id])
);
const onLoadDataProfile = async (id: string) => {
try {
const response = await apiUser(id);
setDataUser(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
}
};
const onLoadData = async () => {
try {
setLoadingGetList(true);
const response = await apiForumGetAll({
search: "",
authorId: id as string,
});
setListData(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingGetList(false);
}
};
return (
<>
<ViewWrapper>
<ViewWrapper
floatingButton={
user?.id === id && (
<FloatingButton
onPress={() =>
router.navigate("/(application)/(user)/forum/create")
}
/>
)
}
>
<StackCustom>
<CenterCustom>
<AvatarCustom
href={`/(application)/(image)/preview-image/${id}`}
<AvatarComp
fileId={dataUser?.Profile?.imageId}
href={`/(application)/(image)/preview-image/${dataUser?.Profile?.imageId}`}
size="xl"
/>
</CenterCustom>
@@ -38,32 +88,43 @@ export default function Forumku() {
<Grid>
<Grid.Col span={6}>
<TextCustom bold truncate>
@bagas_banuna
@{dataUser?.username || "-"}
</TextCustom>
<TextCustom>1 postingan</TextCustom>
<TextCustom>{listData?.length || "0"} postingan</TextCustom>
</Grid.Col>
<Grid.Col span={6} style={{ alignItems: "flex-end" }}>
<ButtonCustom href={`/profile/${id}`}>
<ButtonCustom href={`/profile/${dataUser?.Profile?.id}`}>
Kunjungi Profile
</ButtonCustom>
</Grid.Col>
</Grid>
{listDummyDiscussionForum.map((e, i) => (
<Forum_BoxDetailSection
key={i}
data={e}
setOpenDrawer={setOpenDrawer}
setStatus={setStatus}
isTruncate={true}
href={`/forum/${id}`}
/>
))}
{loadingGetList ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom> Tidak ada diskusi</TextCustom>
) : (
<>
{listData?.map((item: any, index: number) => (
<Forum_BoxDetailSection
isRightComponent={false}
key={index}
data={item}
isTruncate={true}
href={`/forum/${item.id}`}
onSetData={(value) => {
setOpenDrawer(value.setOpenDrawer);
setStatus(value.setStatus);
}}
/>
))}
</>
)}
</StackCustom>
</ViewWrapper>
{/* Drawer Komponen Eksternal */}
<DrawerCustom
height={350}
height={"auto"}
isVisible={openDrawer}
closeDrawer={() => setOpenDrawer(false)}
>
@@ -73,42 +134,9 @@ export default function Forumku() {
setIsDrawerOpen={() => {
setOpenDrawer(false);
}}
setShowDeleteAlert={setDeleteAlert}
setShowAlertStatus={setAlertStatus}
authorId={id as string}
/>
</DrawerCustom>
{/* Alert Komponen Eksternal */}
<AlertCustom
isVisible={alertStatus}
onLeftPress={() => setAlertStatus(false)}
onRightPress={() => {
setOpenDrawer(false);
setAlertStatus(false);
console.log("Ubah status forum");
}}
title="Ubah Status Forum"
message="Apakah Anda yakin ingin mengubah status forum ini?"
textLeft="Batal"
textRight="Ubah"
colorRight={MainColor.green}
/>
{/* Alert Delete */}
<AlertCustom
isVisible={deleteAlert}
onLeftPress={() => setDeleteAlert(false)}
onRightPress={() => {
setOpenDrawer(false);
setDeleteAlert(false);
console.log("Hapus forum");
}}
title="Hapus Forum"
message="Apakah Anda yakin ingin menghapus forum ini?"
textLeft="Batal"
textRight="Hapus"
colorRight={MainColor.red}
/>
</>
);
}

View File

@@ -1,176 +1,263 @@
import {
AlertCustom,
ButtonCustom,
DrawerCustom,
LoaderCustom,
Spacing,
TextAreaCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import { MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
import Forum_CommentarBoxSection from "@/screens/Forum/CommentarBoxSection";
import Forum_BoxDetailSection from "@/screens/Forum/DiscussionBoxSection";
import { listDummyCommentarForum } from "@/screens/Forum/list-data-dummy";
import Forum_MenuDrawerBerandaSection from "@/screens/Forum/MenuDrawerSection.tsx/MenuBeranda";
import Forum_MenuDrawerCommentar from "@/screens/Forum/MenuDrawerSection.tsx/MenuCommentar";
import { router, useLocalSearchParams } from "expo-router";
import { useState } from "react";
import {
apiForumCreateComment,
apiForumGetComment,
apiForumGetOne,
apiForumUpdateStatus,
} from "@/service/api-client/api-forum";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useEffect, useState } from "react";
interface CommentProps {
id: string;
isActive: boolean;
komentar: string;
createdAt: Date;
authorId: string;
Author: {
id: string;
username: string;
Profile: {
id: string;
imageId: string;
};
};
}
export default function ForumDetail() {
const { id } = useLocalSearchParams();
console.log(id);
const { user } = useAuth();
const [openDrawer, setOpenDrawer] = useState(false);
const [data, setData] = useState<any | null>(null);
const [listComment, setListComment] = useState<CommentProps[] | null>(null);
const [isLoadingComment, setLoadingComment] = useState(false);
// Status
const [status, setStatus] = useState("");
const [alertStatus, setAlertStatus] = useState(false);
const [deleteAlert, setDeleteAlert] = useState(false);
const [text, setText] = useState("");
const [authorId, setAuthorId] = useState("");
const [dataId, setDataId] = useState("");
// Comentar
const [openDrawerCommentar, setOpenDrawerCommentar] = useState(false);
const [alertDeleteCommentar, setAlertDeleteCommentar] = useState(false);
const [commentId, setCommentId] = useState("");
const [commentAuthorId, setCommentAuthorId] = useState("");
const dataDummy = {
name: "Bagas",
status: "Open",
date: "14/07/2025",
deskripsi:
"Lorem ipsum dolor sit amet consectetur adipisicing elit. Vitae inventore iure pariatur, libero omnis excepturi. Ullam ad officiis deleniti quos esse odit nesciunt, ipsam adipisci cumque aliquam corporis culpa fugit?",
jumlahBalas: 2,
useFocusEffect(
useCallback(() => {
onLoadData(id as string);
}, [id])
);
const onLoadData = async (id: string) => {
try {
const response = await apiForumGetOne({ id });
setData(response.data);
} catch (error) {
console.log("[ERROR]", error);
}
};
useEffect(() => {
onLoadListComment(id as string);
}, [id]);
const onLoadListComment = async (id: string) => {
try {
const response = await apiForumGetComment({
id: id as string,
});
setListComment(response.data);
} catch (error) {
console.log("[ERROR]", error);
}
};
// Update Status
const handlerUpdateStatus = async (value: any) => {
try {
const response = await apiForumUpdateStatus({
id: id as string,
data: value,
});
if (response.success) {
setStatus(response.data);
setData({
...data,
ForumMaster_StatusPosting: {
status: response.data,
},
});
}
} catch (error) {
console.log("[ERROR]", error);
}
};
// Create Commentar
const handlerCreateCommentar = async () => {
const newData = {
comment: text,
authorId: user?.id,
};
try {
setLoadingComment(true);
const response = await apiForumCreateComment({
id: id as string,
data: newData,
});
if (response.success) {
setText("");
const newComment = {
id: response.data.id,
isActive: response.data.isActive,
komentar: response.data.komentar,
createdAt: response.data.createdAt,
authorId: response.data.authorId,
Author: response.data.Author,
};
setListComment((prev) => [newComment, ...(prev || [])]);
setData({
...data,
count: data.count + 1,
});
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingComment(false);
}
};
return (
<>
<ViewWrapper>
{/* <StackCustom>
</StackCustom> */}
<Forum_BoxDetailSection
data={dataDummy}
setOpenDrawer={setOpenDrawer}
setStatus={setStatus}
/>
{!data && !listComment ? (
<LoaderCustom />
) : (
<>
{/* Box Posting */}
<Forum_BoxDetailSection
data={data}
onSetData={() => {
setOpenDrawer(true);
setStatus(data.ForumMaster_StatusPosting?.status);
setAuthorId(data.Author?.id);
setDataId(data.id);
}}
/>
<TextAreaCustom
placeholder="Ketik diskusi anda..."
maxLength={1000}
showCount
value={text}
onChangeText={setText}
style={{
marginBottom: 0,
}}
/>
<ButtonCustom
style={{
alignSelf: "flex-end",
}}
onPress={() => {
console.log("Posting", text);
router.back();
}}
>
Balas
</ButtonCustom>
{/* Area Commentar */}
{data?.ForumMaster_StatusPosting?.status === "Open" && (
<>
<TextAreaCustom
placeholder="Ketik diskusi anda..."
maxLength={1000}
showCount
value={text}
onChangeText={setText}
style={{
marginBottom: 0,
}}
/>
<ButtonCustom
isLoading={isLoadingComment}
style={{
alignSelf: "flex-end",
}}
onPress={() => {
handlerCreateCommentar();
}}
>
Balas
</ButtonCustom>
</>
)}
<Spacing height={40} />
<Spacing height={40} />
{listDummyCommentarForum.map((e, i) => (
<Forum_CommentarBoxSection
key={i}
data={e}
setOpenDrawer={setOpenDrawerCommentar}
/>
))}
{/* List Commentar */}
{_.isEmpty(listComment) ? (
<TextCustom align="center" color="gray" size={"small"}>
Tidak ada komentar
</TextCustom>
) : (
<TextCustom color="gray">Komentar :</TextCustom>
)}
<Spacing height={5} />
{listComment?.map((item: any, index: number) => (
<Forum_CommentarBoxSection
key={index}
data={item}
onSetData={(value) => {
setCommentId(value.setCommentId);
setOpenDrawerCommentar(value.setOpenDrawer);
setCommentAuthorId(value.setCommentAuthorId);
}}
/>
))}
</>
)}
</ViewWrapper>
{/* Posting Drawer */}
<DrawerCustom
height={350}
height={"auto"}
isVisible={openDrawer}
closeDrawer={() => setOpenDrawer(false)}
>
<Forum_MenuDrawerBerandaSection
id={id as string}
id={dataId}
status={status}
setIsDrawerOpen={() => {
setOpenDrawer(false);
}}
setShowDeleteAlert={setDeleteAlert}
setShowAlertStatus={setAlertStatus}
authorId={authorId}
handlerUpdateStatus={(value: any) => {
handlerUpdateStatus(value);
}}
/>
</DrawerCustom>
{/* Alert Status */}
<AlertCustom
isVisible={alertStatus}
title="Ubah Status Forum"
message="Apakah Anda yakin ingin mengubah status forum ini?"
onLeftPress={() => {
setOpenDrawer(false);
setAlertStatus(false);
console.log("Batal");
}}
onRightPress={() => {
setOpenDrawer(false);
setAlertStatus(false);
console.log("Ubah status forum");
}}
textLeft="Batal"
textRight="Ubah"
colorRight={MainColor.green}
/>
{/* Alert Delete */}
<AlertCustom
isVisible={deleteAlert}
title="Hapus Forum"
message="Apakah Anda yakin ingin menghapus forum ini?"
onLeftPress={() => {
setOpenDrawer(false);
setDeleteAlert(false);
console.log("Batal");
}}
onRightPress={() => {
setOpenDrawer(false);
setDeleteAlert(false);
console.log("Hapus forum");
}}
textLeft="Batal"
textRight="Hapus"
colorRight={MainColor.red}
/>
{/* Commentar */}
{/* Commentar Drawer */}
<DrawerCustom
height={350}
height={"auto"}
isVisible={openDrawerCommentar}
closeDrawer={() => setOpenDrawerCommentar(false)}
>
<Forum_MenuDrawerCommentar
id={id as string}
id={commentId as string}
commentId={commentId}
commentAuthorId={commentAuthorId}
setIsDrawerOpen={() => {
setOpenDrawerCommentar(false);
}}
setShowDeleteAlert={setAlertDeleteCommentar}
listComment={listComment}
setListComment={setListComment}
countComment={data?.count}
setCountComment={(val: any) => {
setData((prev: any) => ({
...prev,
count: val,
}));
}}
/>
</DrawerCustom>
{/* Alert Delete Commentar */}
<AlertCustom
isVisible={alertDeleteCommentar}
title="Hapus Komentar"
message="Apakah Anda yakin ingin menghapus komentar ini?"
onLeftPress={() => {
setOpenDrawerCommentar(false);
setAlertDeleteCommentar(false);
console.log("Batal");
}}
onRightPress={() => {
setOpenDrawerCommentar(false);
setAlertDeleteCommentar(false);
console.log("Hapus commentar");
}}
textLeft="Batal"
textRight="Hapus"
colorRight={MainColor.red}
/>
</>
);
}

View File

@@ -5,27 +5,69 @@ import {
ViewWrapper,
} from "@/components";
import { MainColor } from "@/constants/color-palet";
import { router } from "expo-router";
import { useAuth } from "@/hooks/use-auth";
import { apiForumCreateReportCommentar } from "@/service/api-client/api-master";
import { router, useLocalSearchParams } from "expo-router";
import { useState } from "react";
import Toast from "react-native-toast-message";
export default function ForumOtherReportCommentar() {
const { id } = useLocalSearchParams();
const { user } = useAuth();
const [value, setValue] = useState<string>("");
const handlerSubmitReport = async () => {
const newData = {
authorId: user?.id,
description: value,
};
try {
const response = await apiForumCreateReportCommentar({
id: id as string,
data: newData,
});
if (response.success) {
Toast.show({
type: "success",
text1: "Laporan berhasil dikirim",
});
router.back();
}
} catch (error) {
console.log("[ERROR]", error);
Toast.show({
type: "error",
text1: "Gagal",
text2: "Laporan gagal dikirim",
});
}
};
const handleSubmit = (
<BoxButtonOnFooter>
<ButtonCustom
disabled={!value}
backgroundColor={MainColor.red}
textColor={MainColor.white}
onPress={() => {
console.log("Report lainnya");
router.back();
handlerSubmitReport();
}}
>
Report
</ButtonCustom>
</BoxButtonOnFooter>
);
return (
<>
<ViewWrapper footerComponent={handleSubmit}>
<TextAreaCustom placeholder="Laporkan Komentar" />
<TextAreaCustom
placeholder="Laporkan Komentar"
value={value}
onChangeText={setValue}
/>
</ViewWrapper>
</>
);

View File

@@ -5,17 +5,54 @@ import {
ViewWrapper,
} from "@/components";
import { MainColor } from "@/constants/color-palet";
import { router } from "expo-router";
import { useAuth } from "@/hooks/use-auth";
import { apiForumCreateReportPosting } from "@/service/api-client/api-master";
import { router, useLocalSearchParams } from "expo-router";
import { useState } from "react";
import Toast from "react-native-toast-message";
export default function ForumOtherReportPosting() {
const { id } = useLocalSearchParams();
const { user } = useAuth();
const [value, setValue] = useState<string>("");
const handlerSubmitReport = async () => {
const newData = {
authorId: user?.id,
description: value,
};
try {
const response = await apiForumCreateReportPosting({
id: id as string,
data: newData,
});
if (response.success) {
Toast.show({
type: "success",
text1: "Laporan berhasil dikirim",
});
router.back();
}
} catch (error) {
console.log("[ERROR]", error);
Toast.show({
type: "error",
text1: "Gagal",
text2: "Laporan gagal dikirim",
});
}
};
const handleSubmit = (
<BoxButtonOnFooter>
<ButtonCustom
disabled={!value}
backgroundColor={MainColor.red}
textColor={MainColor.white}
onPress={() => {
console.log("Report lainnya");
router.back();
handlerSubmitReport();
}}
>
Report
@@ -25,7 +62,11 @@ export default function ForumOtherReportPosting() {
return (
<>
<ViewWrapper footerComponent={handleSubmit}>
<TextAreaCustom placeholder="Laporkan Diskusi" />
<TextAreaCustom
placeholder="Laporkan Diskusi"
value={value}
onChangeText={setValue}
/>
</ViewWrapper>
</>
);

View File

@@ -1,41 +1,105 @@
import {
ButtonCustom,
Spacing,
StackCustom,
ViewWrapper
ButtonCustom,
LoaderCustom,
Spacing,
StackCustom,
ViewWrapper,
} from "@/components";
import { AccentColor, MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
import Forum_ReportListSection from "@/screens/Forum/ReportListSection";
import { router } from "expo-router";
import { apiForumCreateReportCommentar, apiMasterForumReportList } from "@/service/api-client/api-master";
import { router, useLocalSearchParams } from "expo-router";
import { useState, useEffect } from "react";
import Toast from "react-native-toast-message";
export default function ForumReportCommentar() {
const { id } = useLocalSearchParams();
const { user } = useAuth();
const [selectReport, setSelectReport] = useState<string>("");
const [listMaster, setListMaster] = useState<any[] | null>(null);
const [isLoadingList, setIsLoadingList] = useState(false);
useEffect(() => {
onLoadListMaster();
}, []);
const onLoadListMaster = async () => {
try {
setIsLoadingList(true);
const response = await apiMasterForumReportList();
setListMaster(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoadingList(false);
}
};
const handlerReport = async () => {
const newData = {
authorId: user?.id,
categoryId: selectReport,
};
try {
const response = await apiForumCreateReportCommentar({
id: id as string,
data: newData,
});
if (response.success) {
Toast.show({
type: "success",
text1: "Laporan berhasil dikirim",
});
router.back();
}
} catch (error) {
console.log("[ERROR]", error);
Toast.show({
type: "error",
text1: "Gagal",
text2: "Laporan gagal dikirim",
});
}
};
return (
<>
<ViewWrapper>
<StackCustom>
<Forum_ReportListSection />
<ButtonCustom
backgroundColor={MainColor.red}
textColor={MainColor.white}
onPress={() => {
console.log("Report");
router.back();
}}
>
Report
</ButtonCustom>
<ButtonCustom
backgroundColor={AccentColor.blue}
textColor={MainColor.white}
onPress={() => {
console.log("Lainnya");
router.replace("/forum/[id]/other-report-commentar");
}}
>
Lainnya
</ButtonCustom>
<Spacing/>
</StackCustom>
{isLoadingList ? (
<LoaderCustom />
) : (
<StackCustom>
<Forum_ReportListSection
listMaster={listMaster}
selectReport={selectReport}
setSelectReport={setSelectReport}
/>
<ButtonCustom
disabled={!selectReport}
backgroundColor={MainColor.red}
textColor={MainColor.white}
onPress={() => {
handlerReport();
}}
>
Report
</ButtonCustom>
<ButtonCustom
backgroundColor={AccentColor.blue}
textColor={MainColor.white}
onPress={() => {
router.replace(`/forum/${id}/other-report-commentar`);
}}
>
Lainnya
</ButtonCustom>
<Spacing />
</StackCustom>
)}
</ViewWrapper>
</>
);

View File

@@ -1,20 +1,103 @@
import { ViewWrapper, StackCustom, ButtonCustom, Spacing } from "@/components";
import { MainColor, AccentColor } from "@/constants/color-palet";
import {
AlertDefaultSystem,
ButtonCustom,
LoaderCustom,
Spacing,
StackCustom,
ViewWrapper,
} from "@/components";
import { AccentColor, MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
import Forum_ReportListSection from "@/screens/Forum/ReportListSection";
import { router } from "expo-router";
import {
apiForumCreateReportPosting,
apiMasterForumReportList,
} from "@/service/api-client/api-master";
import { router, useLocalSearchParams } from "expo-router";
import { useEffect, useState } from "react";
import Toast from "react-native-toast-message";
export default function ForumReportPosting() {
return (
<>
<ViewWrapper>
const { id } = useLocalSearchParams();
const { user } = useAuth();
const [selectReport, setSelectReport] = useState<string>("");
const [listMaster, setListMaster] = useState<any[] | null>(null);
const [isLoadingList, setIsLoadingList] = useState(false);
useEffect(() => {
onLoadListMaster();
}, []);
const onLoadListMaster = async () => {
try {
setIsLoadingList(true);
const response = await apiMasterForumReportList();
setListMaster(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoadingList(false);
}
};
const handlerReport = async () => {
const newData = {
authorId: user?.id,
categoryId: selectReport,
};
try {
const response = await apiForumCreateReportPosting({
id: id as string,
data: newData,
});
if (response.success) {
Toast.show({
type: "success",
text1: "Laporan berhasil dikirim",
});
router.back();
}
} catch (error) {
console.log("[ERROR]", error);
Toast.show({
type: "error",
text1: "Gagal",
text2: "Laporan gagal dikirim",
});
}
};
return (
<>
<ViewWrapper>
{isLoadingList ? (
<LoaderCustom />
) : (
<StackCustom>
<Forum_ReportListSection />
<Forum_ReportListSection
listMaster={listMaster}
selectReport={selectReport}
setSelectReport={setSelectReport}
/>
<ButtonCustom
disabled={!selectReport}
backgroundColor={MainColor.red}
textColor={MainColor.white}
onPress={() => {
console.log("Report");
router.back();
AlertDefaultSystem({
title: "Laporan Posting",
message: "Apakah anda yakin ingin melaporkan postingan ini?",
textLeft: "Batal",
textRight: "Laporkan",
onPressRight: () => {
handlerReport();
},
});
}}
>
Report
@@ -23,15 +106,15 @@ export default function ForumReportPosting() {
backgroundColor={AccentColor.blue}
textColor={MainColor.white}
onPress={() => {
console.log("Lainnya");
router.replace("/forum/[id]/other-report-posting");
router.replace(`/forum/${id}/other-report-posting`);
}}
>
Lainnya
</ButtonCustom>
<Spacing />
</StackCustom>
</ViewWrapper>
</>
);
}
)}
</ViewWrapper>
</>
);
}

View File

@@ -19,7 +19,6 @@ import _ from "lodash";
import { useCallback, useState } from "react";
export default function Forum() {
const id = "test-id-forum";
const [openDrawer, setOpenDrawer] = useState(false);
const [status, setStatus] = useState("");
const { user } = useAuth();
@@ -27,6 +26,8 @@ export default function Forum() {
const [listData, setListData] = useState<any[]>();
const [loadingGetList, setLoadingGetList] = useState(false);
const [search, setSearch] = useState("");
const [dataId, setDataId] = useState("");
const [authorId, setAuthorId] = useState("");
useFocusEffect(
useCallback(() => {
@@ -44,7 +45,6 @@ export default function Forum() {
try {
setLoadingGetList(true);
const response = await apiForumGetAll({ search: search });
console.log("[DATA PROFILE]", JSON.stringify(response.data, null, 2));
setListData(response.data);
} catch (error) {
@@ -96,10 +96,15 @@ export default function Forum() {
<Forum_BoxDetailSection
key={i}
data={e}
setOpenDrawer={setOpenDrawer}
setStatus={setStatus}
onSetData={() => {
setDataId(e.id);
setOpenDrawer(true);
setStatus(e.ForumMaster_StatusPosting?.status);
setAuthorId(e.Author?.id);
}}
isTruncate={true}
href={`/forum/${id}`}
href={`/forum/${e.id}`}
isRightComponent={false}
/>
))
)}
@@ -111,55 +116,14 @@ export default function Forum() {
closeDrawer={() => setOpenDrawer(false)}
>
<Forum_MenuDrawerBerandaSection
id={id}
id={dataId}
authorId={authorId}
status={status}
setIsDrawerOpen={() => {
setOpenDrawer(false);
}}
setShowDeleteAlert={() => {}}
setShowAlertStatus={() => {}}
/>
</DrawerCustom>
{/* Alert Status */}
{/* <AlertCustom
isVisible={alertStatus}
title="Ubah Status Forum"
message="Apakah Anda yakin ingin mengubah status forum ini?"
onLeftPress={() => {
setOpenDrawer(false);
setAlertStatus(false);
console.log("Batal");
}}
onRightPress={() => {
setOpenDrawer(false);
setAlertStatus(false);
console.log("Ubah status forum");
}}
textLeft="Batal"
textRight="Ubah"
colorRight={MainColor.green}
/> */}
{/* Alert Delete */}
{/* <AlertCustom
isVisible={deleteAlert}
title="Hapus Forum"
message="Apakah Anda yakin ingin menghapus forum ini?"
onLeftPress={() => {
setOpenDrawer(false);
setDeleteAlert(false);
console.log("Batal");
}}
onRightPress={() => {
setOpenDrawer(false);
setDeleteAlert(false);
console.log("Hapus forum");
}}
textLeft="Batal"
textRight="Hapus"
colorRight={MainColor.red}
/> */}
</>
);
}

View File

@@ -2,18 +2,46 @@ import {
BaseBox,
FloatingButton,
Grid,
LoaderCustom,
ProgressCustom,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import API_STRORAGE from "@/constants/base-url-api-strorage";
import DUMMY_IMAGE from "@/constants/dummy-image-value";
import { apiInvestmentGetAll } from "@/service/api-client/api-investment";
import { Ionicons } from "@expo/vector-icons";
import dayjs from "dayjs";
import { Image } from "expo-image";
import { router } from "expo-router";
import { router, useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import { View } from "react-native";
export default function InvestmentBursa() {
const [list, setList] = useState<any[] | null>(null);
const [loadingList, setLoadingList] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadList();
}, [])
);
const onLoadList = async () => {
try {
setLoadingList(true);
const response = await apiInvestmentGetAll();
console.log("[DATA LIST]", JSON.stringify(response.data, null, 2));
setList(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingList(false);
}
};
return (
<ViewWrapper
hideFooter
@@ -21,40 +49,77 @@ export default function InvestmentBursa() {
<FloatingButton onPress={() => router.push("/investment/create")} />
}
>
{Array.from({ length: 10 }).map((_, index) => (
<BaseBox key={index} paddingTop={7} paddingBottom={7} href={`/investment/${index}`}>
<Grid>
<Grid.Col span={5}>
<Image
source={DUMMY_IMAGE.background}
style={{ width: "auto", height: 100, borderRadius: 10 }}
/>
</Grid.Col>
<Grid.Col span={1}>
<View />
</Grid.Col>
<Grid.Col span={6}>
<StackCustom>
<TextCustom truncate={2}>
Title here : 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!
</TextCustom>
<ProgressCustom value={index % 5 * 20} size="lg" />
<TextCustom>
Sisa waktu: {dayjs().diff(dayjs(), "day")} hari
</TextCustom>
</StackCustom>
</Grid.Col>
</Grid>
</BaseBox>
))}
{loadingList ? (
<LoaderCustom />
) : _.isEmpty(list) ? (
<TextCustom>Belum ada data</TextCustom>
) : (
list?.map((item: any, index: number) => (
<BaseBox
key={index}
paddingTop={7}
paddingBottom={7}
href={`/investment/${item.id}`}
>
<Grid>
<Grid.Col span={5}>
<Image
source={
item && item.imageId
? API_STRORAGE.GET({ fileId: item.imageId })
: DUMMY_IMAGE.background
}
style={{ width: "auto", height: 100, borderRadius: 10 }}
/>
</Grid.Col>
<Grid.Col span={1}>
<View />
</Grid.Col>
<Grid.Col span={6}>
<StackCustom>
<TextCustom truncate={2}>{item.title}</TextCustom>
<ProgressCustom
label={`${item.progress}%`}
value={item.progress}
size="lg"
/>
{Number(item?.pencarianInvestor) -
dayjs().diff(dayjs(item.countDown), "days") <=
0 ? (
<View
style={{
flexDirection: "row",
alignItems: "center",
gap: 5,
}}
>
<Ionicons
name="alert-circle-outline"
size={16}
color="red"
/>
<TextCustom color="red" size="small">
Periode Investasi Selesai
</TextCustom>
</View>
) : (
<TextCustom>
Sisa waktu:{" "}
{Number(item?.pencarianInvestor) -
dayjs().diff(dayjs(item.countDown), "days")}{" "}
hari
</TextCustom>
)}
</StackCustom>
</Grid.Col>
</Grid>
</BaseBox>
))
)}
</ViewWrapper>
);
}
// <View style={{ padding: 20, gap: 16 }}>
// <TextCustom>Progress 70%</TextCustom>
// <ProgressCustom value={70} color="primary" size="md" />
@@ -78,4 +143,4 @@ export default function InvestmentBursa() {
// <ProgressCustom value={90} color="warning" size="lg" showLabel={false} />
// <ProgressCustom value={null} color="error" size="sm" label="Loading..." />
// </View>;
// </View>;

View File

@@ -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<string | null>(
"publish"
);
const [listData, setListData] = useState<any[]>([]);
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 (
<ViewWrapper headerComponent={scrollComponent} hideFooter>
{Array.from({ length: 10 }).map((_, index) => (
<Investment_StatusBox
key={index}
id={index.toString()}
status={activeCategory as string}
href={`/investment/${index}/${activeCategory}/detail`}
/>
))}
{loadingList ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Tidak ada data {activeCategory}</TextCustom>
) : (
listData.map((item: any, index: number) => (
<Investment_StatusBox
key={index}
data={item}
status={activeCategory as string}
href={`/investment/${item.id}/${activeCategory}/detail`}
/>
))
)}
</ViewWrapper>
);
}

View File

@@ -7,17 +7,78 @@ import {
InformationBox,
Spacing,
StackCustom,
TextCustom,
TextInputCustom,
ViewWrapper
ViewWrapper,
} from "@/components";
import { MainColor } from "@/constants/color-palet";
import DIRECTORY_ID from "@/constants/directory-id";
import { apiInvestmentUpsertDocument } from "@/service/api-client/api-investment";
import { uploadFileService } from "@/service/upload-service";
import pickFile from "@/utils/pickFile";
import { FontAwesome5 } from "@expo/vector-icons";
import { router } from "expo-router";
import { router, useLocalSearchParams } from "expo-router";
import { useState } from "react";
import Toast from "react-native-toast-message";
export default function InvestmentAddDocument() {
const { id } = useLocalSearchParams();
const [data, setData] = useState({
title: "",
});
const [pdf, setPdf] = useState<any>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
const handlerSubmit = async () => {
try {
setIsLoading(true);
const responseUploadFile = await uploadFileService({
dirId: DIRECTORY_ID.investasi_dokumen,
imageUri: pdf.uri,
});
if (!responseUploadFile.success) {
throw new Error(responseUploadFile.message);
}
const newData = {
title: data.title,
fileId: responseUploadFile.data.id,
};
const response = await apiInvestmentUpsertDocument({
id: id as string,
data: newData,
});
if (response.success) {
Toast.show({
type: "success",
text1: "Data berhasil disimpan",
});
router.back();
}
} catch (error) {
console.log("[ERROR]", error);
Toast.show({
type: "error",
text1: "Gagal menyimpan data",
});
} finally {
setIsLoading(false);
}
};
const buttonFooter = (
<BoxButtonOnFooter>
<ButtonCustom onPress={() => router.back()}>Simpan</ButtonCustom>
<ButtonCustom
isLoading={isLoading}
disabled={!pdf || data.title.length <= 0}
onPress={handlerSubmit}
>
Simpan
</ButtonCustom>
</BoxButtonOnFooter>
);
@@ -31,22 +92,33 @@ export default function InvestmentAddDocument() {
label="Judul Dokumen"
placeholder="Masukan judul dokumen"
required
value={data.title}
onChangeText={(value) => setData({ ...data, title: value })}
/>
<BaseBox>
<CenterCustom>
<FontAwesome5
name="file-pdf"
size={30}
color={MainColor.disabled}
/>
{pdf ? (
<TextCustom truncate>{pdf.name}</TextCustom>
) : (
<FontAwesome5
name="file-pdf"
size={30}
color={MainColor.disabled}
/>
)}
</CenterCustom>
</BaseBox>
<ButtonCenteredOnly
icon="upload"
onPress={() =>
router.push("/(application)/(image)/take-picture/123")
pickFile({
allowedType: "pdf",
setPdfUri(file) {
setPdf(file);
},
})
}
>
Upload

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BaseBox,
BoxButtonOnFooter,
@@ -7,17 +8,99 @@ import {
InformationBox,
Spacing,
StackCustom,
TextCustom,
TextInputCustom,
ViewWrapper,
} from "@/components";
import { MainColor } from "@/constants/color-palet";
import { FontAwesome5 } from "@expo/vector-icons";
import { router } from "expo-router";
import DIRECTORY_ID from "@/constants/directory-id";
import {
apiInvestmentGetDocument,
apiInvestmentUpsertDocument,
} from "@/service/api-client/api-investment";
import { deleteFileService, uploadFileService } from "@/service/upload-service";
import pickFile from "@/utils/pickFile";
import { router, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useEffect, useState } from "react";
import Toast from "react-native-toast-message";
export default function InvestmentEditDocument() {
const { id } = useLocalSearchParams();
const [data, setData] = useState<any>(null);
const [pdf, setPdf] = useState<any>(null);
const [titleFile, setTitleFile] = useState<string>("");
const [isLoading, setIsLoading] = useState<boolean>(false);
useEffect(() => {
onLoadData();
}, [id]);
const onLoadData = async () => {
try {
const response = await apiInvestmentGetDocument({
id: id as string,
category: "one-document",
});
setData(response.data);
setTitleFile(response.data.title);
} catch (error) {
console.log("[ERROR]", error);
}
};
const handlerUpdate = async () => {
const prevFileId = data.fileId;
try {
setIsLoading(true);
const responseUploadFile = await uploadFileService({
dirId: DIRECTORY_ID.investasi_dokumen,
imageUri: pdf.uri,
});
if (!responseUploadFile.success) {
throw new Error(responseUploadFile.message);
}
const newData = {
title: data.title,
fileId: responseUploadFile.data.id,
};
const response = await apiInvestmentUpsertDocument({
id: id as string,
data: newData,
});
if (response.success) {
const delPrevFile = await deleteFileService({
id: prevFileId,
});
console.log("[DEL PREV FILE]", delPrevFile);
Toast.show({
type: "success",
text1: "Data berhasil diupdate",
});
router.back();
}
} catch (error) {
console.log("[ERROR]", error);
Toast.show({
type: "error",
text1: "Gagal mengupdate data",
});
} finally {
setIsLoading(false);
}
};
const buttonFooter = (
<BoxButtonOnFooter>
<ButtonCustom onPress={() => router.back()}>Update</ButtonCustom>
<ButtonCustom isLoading={isLoading} disabled={!pdf} onPress={handlerUpdate}>
Update
</ButtonCustom>
</BoxButtonOnFooter>
);
@@ -31,22 +114,29 @@ export default function InvestmentEditDocument() {
label="Judul Dokumen"
placeholder="Masukan judul dokumen"
required
value={data?.title}
onChangeText={(value) => setData({ ...data, title: value })}
/>
<BaseBox>
<CenterCustom>
<FontAwesome5
name="file-pdf"
size={30}
color={MainColor.disabled}
/>
{pdf ? (
<TextCustom truncate>{pdf.name}</TextCustom>
) : (
<TextCustom truncate>{_.snakeCase(titleFile || "")}.pdf</TextCustom>
)}
</CenterCustom>
</BaseBox>
<ButtonCenteredOnly
icon="upload"
onPress={() =>
router.push("/(application)/(image)/take-picture/123")
pickFile({
allowedType: "pdf",
setPdfUri(file) {
setPdf(file);
},
})
}
>
Upload

View File

@@ -1,16 +1,58 @@
import { ViewWrapper } from "@/components";
/* eslint-disable react-hooks/exhaustive-deps */
import { LoaderCustom, TextCustom, ViewWrapper } from "@/components";
import Investment_BoxDetailDocument from "@/screens/Invesment/Document/RecapBoxDetail";
import { apiInvestmentGetDocument } from "@/service/api-client/api-investment";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
export default function InvestmentListOfDocument() {
const { id } = useLocalSearchParams();
console.log("ID >> ", id);
const [list, setList] = useState<any[] | null>(null);
const [loadList, setLoadList] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadListDocument();
}, [id])
);
const onLoadListDocument = async () => {
try {
setLoadList(true);
const response = await apiInvestmentGetDocument({
id: id as string,
category: "all-document",
});
setList(response.data);
} catch (error) {
console.log("[ERROR]", error);
setList([]);
} finally {
setLoadList(false);
}
};
return (
<ViewWrapper>
{Array.from({ length: 10 }).map((_, index) => (
<Investment_BoxDetailDocument
key={index}
title={`Judul Dokumen ${index + 1}`}
href={`/(file)/${index + 1}`}
/>
))}
{loadList ? (
<LoaderCustom />
) : _.isEmpty(list) ? (
<TextCustom align="center" color="gray">
Tidak ada data
</TextCustom>
) : (
list?.map((item: any, index: number) => (
<Investment_BoxDetailDocument
key={index}
title={item.title}
href={`/(file)/${item.fileId}`}
/>
))
)}
</ViewWrapper>
);
}

View File

@@ -1,23 +1,90 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
AlertDefaultSystem,
BackButton,
DotButton,
DrawerCustom,
LoaderCustom,
MenuDrawerDynamicGrid,
TextCustom,
ViewWrapper,
} from "@/components";
import { IconEdit } from "@/components/_Icon";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import Investment_BoxDetailDocument from "@/screens/Invesment/Document/RecapBoxDetail";
import {
apiInvestmentDeleteDocument,
apiInvestmentGetDocument,
} from "@/service/api-client/api-investment";
import { AntDesign, Ionicons } from "@expo/vector-icons";
import { router, Stack, useLocalSearchParams } from "expo-router";
import { useState } from "react";
import {
router,
Stack,
useFocusEffect,
useLocalSearchParams,
} from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import Toast from "react-native-toast-message";
export default function InvestmentRecapOfDocument() {
const { id } = useLocalSearchParams();
const [openDrawer, setOpenDrawer] = useState(false);
const [openDrawerBox, setOpenDrawerBox] = useState(false);
const [list, setList] = useState<any[] | null>(null);
const [loadList, setLoadList] = useState(false);
const [selectId, setSelectId] = useState<string | null>(null);
useFocusEffect(
useCallback(() => {
onLoadListDocument();
}, [id])
);
const onLoadListDocument = async () => {
try {
setLoadList(true);
const response = await apiInvestmentGetDocument({
id: id as string,
category: "all-document",
});
setList(response.data);
} catch (error) {
console.log("[ERROR]", error);
setList([]);
} finally {
setLoadList(false);
}
};
const handlerDeleteDocument = async () => {
try {
const response = await apiInvestmentDeleteDocument({
id: selectId as string,
});
if (response.success) {
Toast.show({
type: "success",
text1: "Data berhasil dihapus",
});
setList((prev: any[] | null) => {
if (!prev) return null;
return prev.filter((item: any) => item.id !== selectId);
});
setOpenDrawerBox(false);
setSelectId(null);
}
} catch (error) {
console.log("[ERROR]", error);
Toast.show({
type: "error",
text1: "Gagal menghapus data",
});
}
};
return (
<>
@@ -37,25 +104,36 @@ export default function InvestmentRecapOfDocument() {
/>
<ViewWrapper>
{Array.from({ length: 10 }).map((_, index) => (
<Investment_BoxDetailDocument
key={index}
title={`Judul Dokumen ${index + 1}`}
leftIcon={
<Ionicons
name="ellipsis-horizontal-outline"
size={ICON_SIZE_SMALL}
color={MainColor.white}
style={{
zIndex: 10,
alignSelf: "flex-end",
}}
onPress={() => setOpenDrawerBox(true)}
/>
}
href={`/(file)/${id}`}
/>
))}
{loadList ? (
<LoaderCustom />
) : _.isEmpty(list) ? (
<TextCustom align="center" color="gray">
Tidak ada data
</TextCustom>
) : (
list?.map((item: any, index: number) => (
<Investment_BoxDetailDocument
key={index}
title={item.title}
leftIcon={
<Ionicons
name="ellipsis-horizontal-outline"
size={ICON_SIZE_SMALL}
color={MainColor.white}
style={{
zIndex: 10,
alignSelf: "flex-end",
}}
onPress={() => {
setSelectId(item.id);
setOpenDrawerBox(true);
}}
/>
}
href={`/(file)/${item.fileId}`}
/>
))
)}
</ViewWrapper>
{/* Drawer On Header */}
@@ -69,7 +147,7 @@ export default function InvestmentRecapOfDocument() {
{
icon: (
<AntDesign
name="pluscircle"
name="plus-circle"
size={ICON_SIZE_SMALL}
color={MainColor.white}
/>
@@ -96,7 +174,7 @@ export default function InvestmentRecapOfDocument() {
{
icon: <IconEdit />,
label: "Edit Dokumen",
path: `/investment/${id}/(document)/edit-document`,
path: `/investment/${selectId}/(document)/edit-document`,
},
{
icon: (
@@ -119,12 +197,14 @@ export default function InvestmentRecapOfDocument() {
textLeft: "Batal",
textRight: "Hapus",
onPressRight: () => {
setOpenDrawerBox(false);
handlerDeleteDocument();
},
});
} else {
router.push(item.path as any);
}
router.push(item.path as any);
setOpenDrawer(false);
setOpenDrawerBox(false);
}}
/>
</DrawerCustom>

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BaseBox,
BoxButtonOnFooter,
@@ -9,16 +10,72 @@ import {
TextInputCustom,
ViewWrapper,
} from "@/components";
import { router, useLocalSearchParams } from "expo-router";
import { apiInvestmentGetOne } from "@/service/api-client/api-investment";
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, useState } from "react";
export default function InvestmentInvest() {
const { id } = useLocalSearchParams();
// console.log("[ID]", id);
const [data, setData] = useState<any>(null);
const [value, setValue] = useState<number>(0);
const [total, setTotal] = useState<number>(0);
const [sisaLembar, setSisaLembar] = useState<number>(0);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id])
);
const onLoadData = async () => {
try {
const response = await apiInvestmentGetOne({
id: id as string,
});
setData(response.data);
setSisaLembar(response.data?.sisaLembar);
} catch (error) {
console.log("[ERROR]", error);
}
};
const handleTextChange = (text: string) => {
// Izinkan input kosong → anggap sebagai 0 (atau abaikan, tergantung UX)
if (text === "") {
setValue(0);
setTotal(0);
return;
}
// Regex: hanya digit (angka bulat positif)
const integerRegex = /^\d+$/;
if (integerRegex.test(text)) {
const numValue = Number(text);
// Karena regex sudah pastikan hanya angka, isNaN biasanya false
// Tapi tetap aman untuk cek
if (!isNaN(numValue)) {
setValue(numValue);
setTotal(numValue * Number(data?.hargaLembar));
console.log("[VALUE]", numValue);
}
}
// Jika input tidak valid (misal: "12a", "12."), abaikan → state tidak berubah
};
const buttonSubmit = () => {
return (
<>
<BoxButtonOnFooter>
<ButtonCustom onPress={() => router.push(`/investment/${id}/select-bank`)}>Beli</ButtonCustom>
<ButtonCustom
disabled={value < 10 || value >= sisaLembar}
onPress={() => router.push(`/investment/${id}/select-bank`)}
>
Beli {value}, {sisaLembar}
</ButtonCustom>
</BoxButtonOnFooter>
</>
);
@@ -34,7 +91,7 @@ export default function InvestmentInvest() {
<TextCustom>Sisa Lembar Saham</TextCustom>
</Grid.Col>
<Grid.Col span={6} style={{ alignItems: "flex-end" }}>
<TextCustom>3.000</TextCustom>
<TextCustom>{data?.sisaLembar || "-"}</TextCustom>
</Grid.Col>
</Grid>
<Grid>
@@ -42,7 +99,7 @@ export default function InvestmentInvest() {
<TextCustom>Harga Per Lembar</TextCustom>
</Grid.Col>
<Grid.Col span={6} style={{ alignItems: "flex-end" }}>
<TextCustom>Rp. 1.000</TextCustom>
<TextCustom>{data?.hargaLembar || "-"}</TextCustom>
</Grid.Col>
</Grid>
<Grid>
@@ -69,6 +126,10 @@ export default function InvestmentInvest() {
}}
placeholder="0"
keyboardType="numeric"
value={value.toString()}
onChangeText={(value) => {
handleTextChange(value);
}}
/>
</Grid.Col>
</Grid>
@@ -78,7 +139,7 @@ export default function InvestmentInvest() {
<TextCustom>Total Harga</TextCustom>
</Grid.Col>
<Grid.Col span={6} style={{ alignItems: "flex-end" }}>
<TextCustom>Rp. 1.000</TextCustom>
<TextCustom> Rp. {formatCurrencyDisplay(total)}</TextCustom>
</Grid.Col>
</Grid>
</StackCustom>

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BackButton,
DotButton,
@@ -9,19 +10,47 @@ import { IconDocument, IconEdit, IconNews } from "@/components/_Icon";
import { IMenuDrawerItem } from "@/components/_Interface/types";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_MEDIUM } from "@/constants/constans-value";
import { useAuth } from "@/hooks/use-auth";
import Investment_ButtonInvestasiSection from "@/screens/Invesment/ButtonInvestasiSection";
import Invesment_ComponentBoxOnBottomDetail from "@/screens/Invesment/ComponentBoxOnBottomDetail";
import Invesment_DetailDataPublishSection from "@/screens/Invesment/DetailDataPublishSection";
import { apiInvestmentGetOne } 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 { user } = useAuth();
const { id, status } = useLocalSearchParams();
const [openDrawerDraft, setOpenDrawerDraft] = useState(false);
const [openDrawerPublish, setOpenDrawerPublish] = useState(false);
const [data, setData] = useState<any>(null);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id, status])
);
const onLoadData = async () => {
try {
const response = await apiInvestmentGetOne({
id: id as string,
});
setData(response.data);
} catch (error) {
console.log("[ERROR]", error);
}
};
const handlePressDraft = (item: IMenuDrawerItem) => {
console.log("PATH >> ", item.path);
router.navigate(item.path as any);
@@ -36,13 +65,14 @@ export default function InvestmentDetailStatus() {
const bottomSection = (
<Invesment_ComponentBoxOnBottomDetail
id={id as string}
id={data?.id}
prospectusId={data?.prospektusFileId}
status={status as string}
/>
);
const buttonSection = (
<Investment_ButtonInvestasiSection id={id as string} isMine={false} />
<Investment_ButtonInvestasiSection id={id as string} isMine={user?.id === data?.author?.id} />
);
return (
@@ -63,6 +93,7 @@ export default function InvestmentDetailStatus() {
<ViewWrapper>
<Invesment_DetailDataPublishSection
status={status as string}
data={data}
bottomSection={bottomSection}
buttonSection={buttonSection}
/>

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BaseBox,
BoxButtonOnFooter,
@@ -5,38 +6,149 @@ import {
ButtonCustom,
CenterCustom,
InformationBox,
LoaderCustom,
Spacing,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import { MainColor } from "@/constants/color-palet";
import { FontAwesome5 } from "@expo/vector-icons";
import { router } from "expo-router";
import DIRECTORY_ID from "@/constants/directory-id";
import {
apiInvestmentGetOne,
apiInvestmentUpdateData,
} from "@/service/api-client/api-investment";
import { deleteFileService, uploadFileService } from "@/service/upload-service";
import pickFile, { IFileData } from "@/utils/pickFile";
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 InvestmentEditProspectus() {
const { id } = useLocalSearchParams();
const [data, setData] = useState<any>(null);
const [loadingGet, setLoadingGet] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [pdf, setPdf] = useState<IFileData | null>(null);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id])
);
const onLoadData = async () => {
try {
setLoadingGet(true);
const response = await apiInvestmentGetOne({
id: id as string,
});
setData(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingGet(false);
}
};
const handleSubmitUpdate = async () => {
const prevProspectusFileId = data?.prospektusFileId;
try {
setIsLoading(true);
const responseUploadImage = await uploadFileService({
imageUri: pdf?.uri as any,
dirId: DIRECTORY_ID.investasi_prospektus,
});
if (!responseUploadImage.success) {
Toast.show({
type: "error",
text1: "Gagal mengunggah gambar",
});
return;
}
const prospektusFileId = responseUploadImage.data.id;
const responseUpdate = await apiInvestmentUpdateData({
id: id as string,
data: prospektusFileId,
category: "prospectus",
});
if (responseUpdate.success) {
const deletePrevImage = await deleteFileService({
id: prevProspectusFileId as any,
});
if (!deletePrevImage.success) {
console.log("[ERROR DELETE PREV IMAGE]", deletePrevImage.message);
return;
}
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);
}
};
const buttonFooter = (
<BoxButtonOnFooter>
<ButtonCustom onPress={() => router.back()}>Update</ButtonCustom>
<ButtonCustom
disabled={pdf === null}
isLoading={isLoading}
onPress={handleSubmitUpdate}
>
Update
</ButtonCustom>
</BoxButtonOnFooter>
);
return (
<ViewWrapper footerComponent={buttonFooter}>
<ViewWrapper footerComponent={!loadingGet && buttonFooter}>
<StackCustom gap={"xs"}>
<InformationBox text="File prospektus wajib untuk diupload, agar calon investor paham dengan prospek investasi yang akan anda jalankan kedepan." />
<Spacing />
<BaseBox>
<CenterCustom>
<FontAwesome5
name="file-pdf"
size={30}
color={MainColor.disabled}
/>
{loadingGet ? (
<LoaderCustom />
) : pdf ? (
<TextCustom truncate>{pdf.name}</TextCustom>
) : (
<TextCustom truncate>
{_.snakeCase(data?.title || "").replace(/_/g, "-")}.pdf
</TextCustom>
)}
</CenterCustom>
</BaseBox>
<ButtonCenteredOnly
disabled={loadingGet}
icon="upload"
onPress={() => router.push("/(application)/(image)/take-picture/123")}
onPress={() => {
pickFile({
allowedType: "pdf",
setPdfUri(file: any) {
setPdf({
uri: file.uri,
name: file.name,
size: file.size,
});
},
});
}}
>
Upload
</ButtonCenteredOnly>

View File

@@ -1,40 +1,224 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
ButtonCenteredOnly,
ButtonCustom,
InformationBox,
LandscapeFrameUploaded,
LoaderCustom,
SelectCustom,
Spacing,
StackCustom,
TextInputCustom,
ViewWrapper
ViewWrapper,
} from "@/components";
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 API_STRORAGE from "@/constants/base-url-api-strorage";
import DIRECTORY_ID from "@/constants/directory-id";
import {
apiInvestmentGetOne,
apiInvestmentUpdateData,
} from "@/service/api-client/api-investment";
import { apiMasterInvestment } from "@/service/api-client/api-master";
import {
deleteFileService,
uploadFileService,
} from "@/service/upload-service";
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
import pickFile from "@/utils/pickFile";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
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<IInvestment>({
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<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [loadingMaster, setLoadingMaster] = useState(false);
const [listPencarianInvestor, setListPencarianInvestor] = useState<any[]>([]);
const [listPeriodeDeviden, setListPeriodeDeviden] = useState<any[]>([]);
const [listPembagianDeviden, setListPembagianDeviden] = useState<any[]>([]);
useFocusEffect(
useCallback(() => {
onLoadMaster();
onLoadData();
}, [id])
);
const onLoadMaster = async () => {
try {
setLoadingMaster(true);
const response = await apiMasterInvestment({ category: "" });
setListPencarianInvestor(response.data.pencarianInvestor);
setListPeriodeDeviden(response.data.periodeDeviden);
setListPembagianDeviden(response.data.pembagianDeviden);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingMaster(false);
}
};
const onLoadData = async () => {
try {
const response = await apiInvestmentGetOne({
id: id as string,
});
setData(response.data);
} catch (error) {
console.log("[ERROR]", error);
}
};
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 }));
};
const validateData = () => {
if (
!data.title ||
!data.targetDana ||
!data.hargaLembar ||
!data.totalLembar ||
!data.roi ||
!data.masterPencarianInvestorId ||
!data.masterPeriodeDevidenId ||
!data.masterPembagianDevidenId
) {
Toast.show({
type: "info",
text1: "Harap isi semua data",
});
return false;
}
return true;
};
const handleSubmitUpdate = async () => {
let newData = {
...data,
};
if (!validateData()) {
return;
}
try {
setIsLoading(true);
if (image) {
const responseUploadImage = await uploadFileService({
imageUri: image,
dirId: DIRECTORY_ID.investasi_image,
});
if (!responseUploadImage.success) {
Toast.show({
type: "error",
text1: "Gagal mengunggah gambar",
});
return;
}
const deletePrevImage = await deleteFileService({
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,
category: "data"
});
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 (
<ViewWrapper>
<StackCustom gap={"xs"}>
<InformationBox text="Gambar investasi bisa berupa ilustrasi, poster atau foto terkait investasi." />
<LandscapeFrameUploaded />
<LandscapeFrameUploaded
image={
image ? image : API_STRORAGE.GET({ fileId: data?.imageId as any })
}
/>
<ButtonCenteredOnly
icon="upload"
onPress={() => router.push("/take-picture/1")}
onPress={() => {
pickFile({
setImageUri: ({ uri }) => {
setImage(uri);
},
allowedType: "image",
});
}}
>
Upload
</ButtonCenteredOnly>
@@ -47,7 +231,7 @@ export default function InvestmentEdit() {
required
placeholder="Judul"
label="Judul"
value={data.title}
value={data?.title}
onChangeText={(value) => setData({ ...data, title: value })}
/>
@@ -57,22 +241,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()}
/>
<TextInputCustom
required
iconLeft="Rp."
placeholder="0"
label="Target Dana"
keyboardType="numeric"
onChangeText={(value) =>
setData({ ...data, targetDana: Number(value) })
}
value={data.targetDana === 0 ? "" : data.targetDana.toString()}
onChangeText={handleChangeCurrency("targetDana")}
value={displayTargetDana}
/>
<TextInputCustom
@@ -81,10 +251,8 @@ export default function InvestmentEdit() {
placeholder="0"
label="Harga Per Lembar"
keyboardType="numeric"
onChangeText={(value) =>
setData({ ...data, targetDana: Number(value) })
}
value={data.targetDana === 0 ? "" : data.targetDana.toString()}
onChangeText={handleChangeCurrency("hargaLembar")}
value={displayHargaPerLembar}
/>
<TextInputCustom
@@ -92,10 +260,8 @@ export default function InvestmentEdit() {
placeholder="0"
label="Total Lembar"
keyboardType="numeric"
onChangeText={(value) =>
setData({ ...data, totalLembar: Number(value) })
}
value={data.totalLembar === 0 ? "" : data.totalLembar.toString()}
onChangeText={(value) => setData({ ...data, totalLembar: value })}
value={displayTotalLembar}
/>
<TextInputCustom
@@ -104,57 +270,78 @@ export default function InvestmentEdit() {
label="Rasio Keuntungan / ROI %"
placeholder="0"
keyboardType="numeric"
onChangeText={(value) =>
setData({ ...data, rasioKeuntungan: Number(value) })
}
value={
data.rasioKeuntungan === 0 ? "" : data.rasioKeuntungan.toString()
}
onChangeText={(value) => setData({ ...data, roi: value })}
value={data?.roi === "" ? "" : data?.roi}
/>
<SelectCustom
required
placeholder="Pilih batas waktu"
label="Pencarian Investor"
data={dummyListPencarianInvestor.map((item) => ({
label: item.name + `${" "}hari`,
value: item.id,
}))}
onChange={(value) =>
setData({ ...data, pencarianInvestor: value as any })
}
value={data.pencarianInvestor}
/>
{loadingMaster ? (
<LoaderCustom />
) : (
<SelectCustom
required
placeholder="Pilih batas waktu"
label="Pencarian Investor"
data={
_.isEmpty(listPencarianInvestor)
? []
: listPencarianInvestor.map((item) => ({
label: item.name + `${" "}hari`,
value: item.id,
}))
}
onChange={(value) =>
setData({ ...data, masterPencarianInvestorId: value as any })
}
value={data.masterPencarianInvestorId}
/>
)}
<SelectCustom
required
placeholder="Pilih batas waktu"
label="Pilih Periode Deviden"
data={dummyPeriodeDeviden.map((item) => ({
label: item.name,
value: item.id,
}))}
onChange={(value) =>
setData({ ...data, periodeDeviden: value as any })
}
value={data.periodeDeviden}
/>
{loadingMaster ? (
<LoaderCustom />
) : (
<SelectCustom
required
placeholder="Pilih batas waktu"
label="Pilih Periode Deviden"
data={
_.isEmpty(listPeriodeDeviden)
? []
: listPeriodeDeviden.map((item) => ({
label: item.name,
value: item.id,
}))
}
onChange={(value) =>
setData({ ...data, masterPeriodeDevidenId: value as any })
}
value={data.masterPeriodeDevidenId}
/>
)}
{loadingMaster ? (
<LoaderCustom />
) : (
<SelectCustom
required
placeholder="Pilih batas waktu"
label="Pilih Pembagian Deviden"
data={
_.isEmpty(listPembagianDeviden)
? []
: listPembagianDeviden.map((item) => ({
label: item.name + `${" "}bulan`,
value: item.id,
}))
}
onChange={(value) =>
setData({ ...data, masterPembagianDevidenId: value as any })
}
value={data.masterPembagianDevidenId}
/>
)}
<SelectCustom
required
placeholder="Pilih batas waktu"
label="Pilih Pembagian Deviden"
data={dummyPembagianDeviden.map((item) => ({
label: item.name + `${" "}bulan`,
value: item.id,
}))}
onChange={(value) =>
setData({ ...data, pembagianDeviden: value as any })
}
value={data.pembagianDeviden}
/>
<Spacing />
<ButtonCustom onPress={() => router.replace("/investment/portofolio")}>
<ButtonCustom isLoading={isLoading} onPress={handleSubmitUpdate}>
Simpan
</ButtonCustom>
</StackCustom>

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BackButton,
DotButton,
@@ -9,18 +10,45 @@ import { IconDocument, IconEdit, IconNews } from "@/components/_Icon";
import { IMenuDrawerItem } from "@/components/_Interface/types";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_MEDIUM } from "@/constants/constans-value";
import { useAuth } from "@/hooks/use-auth";
import Investment_ButtonInvestasiSection from "@/screens/Invesment/ButtonInvestasiSection";
import Invesment_ComponentBoxOnBottomDetail from "@/screens/Invesment/ComponentBoxOnBottomDetail";
import Invesment_DetailDataPublishSection from "@/screens/Invesment/DetailDataPublishSection";
import { apiInvestmentGetOne } 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 InvestmentDetail() {
const { user } = useAuth();
const { id, status } = useLocalSearchParams();
const [openDrawerDraft, setOpenDrawerDraft] = useState(false);
const [openDrawerPublish, setOpenDrawerPublish] = useState(false);
const [data, setData] = useState<any>(null);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id, status])
);
const onLoadData = async () => {
try {
const response = await apiInvestmentGetOne({
id: id as string,
});
setData(response.data);
} catch (error) {
console.log("[ERROR]", error);
}
};
const handlePressDraft = (item: IMenuDrawerItem) => {
console.log("PATH >> ", item.path);
@@ -37,11 +65,14 @@ export default function InvestmentDetail() {
const bottomSection = (
<Invesment_ComponentBoxOnBottomDetail
id={id as string}
status={'publish'}
prospectusId={data?.prospektusFileId}
status={"publish"}
/>
);
const buttonSection = <Investment_ButtonInvestasiSection id={id as string} isMine={true} />;
const buttonSection = (
<Investment_ButtonInvestasiSection id={id as string} isMine={user?.id === data?.author?.id} />
);
return (
<>
@@ -61,6 +92,7 @@ export default function InvestmentDetail() {
<ViewWrapper>
<Invesment_DetailDataPublishSection
status={"publish"}
data={data}
bottomSection={bottomSection}
buttonSection={buttonSection}
/>

View File

@@ -1,76 +1,236 @@
import {
BaseBox,
ButtonCenteredOnly,
ButtonCustom,
CenterCustom,
InformationBox,
LandscapeFrameUploaded,
SelectCustom,
Spacing,
StackCustom,
TextInputCustom,
ViewWrapper,
BaseBox,
ButtonCenteredOnly,
ButtonCustom,
CenterCustom,
InformationBox,
LandscapeFrameUploaded,
LoaderCustom,
SelectCustom,
Spacing,
StackCustom,
TextCustom,
TextInputCustom,
ViewWrapper,
} from "@/components";
import { MainColor } from "@/constants/color-palet";
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 DIRECTORY_ID from "@/constants/directory-id";
import { useAuth } from "@/hooks/use-auth";
import { apiInvestmentCreate } from "@/service/api-client/api-investment";
import { apiMasterInvestment } from "@/service/api-client/api-master";
import { uploadFileService } 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 { router, useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, 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<string | null>(null);
const [pdf, setPdf] = useState<IFileData | null>(null);
const [isLoading, setIsLoading] = useState(false);
// const [coba, setCoba] = useState("");
const [loadingMaster, setLoadingMaster] = useState(false);
const [listPencarianInvestor, setListPencarianInvestor] = useState<any[]>([]);
const [listPeriodeDeviden, setListPeriodeDeviden] = useState<any[]>([]);
const [listPembagianDeviden, setListPembagianDeviden] = useState<any[]>([]);
useFocusEffect(
useCallback(() => {
onLoadMaster();
}, [])
);
const onLoadMaster = async () => {
try {
setLoadingMaster(true);
const response = await apiMasterInvestment({ category: "" });
setListPencarianInvestor(response.data.pencarianInvestor);
setListPeriodeDeviden(response.data.periodeDeviden);
setListPembagianDeviden(response.data.pembagianDeviden);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingMaster(false);
}
};
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 validateData = () => {
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 false;
}
return true;
};
const handleSubmit = async () => {
if (!validateData()) {
return;
}
if (!image || !pdf) {
Toast.show({
type: "error",
text1: "Harap upload gambar dan file PDF",
});
return;
}
try {
setIsLoading(true);
const responseUploadImage = await uploadFileService({
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 uploadFileService({
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 });
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 (
<ViewWrapper>
<StackCustom gap={"xs"}>
{/* <View style={GStyles.inputContainerInput}>
<TextInput
style={{
...GStyles.inputText,
}}
onChangeText={(value) => setCoba(value)}
value={coba}
keyboardType="decimal-pad"
/>
</View> */}
<InformationBox text="Gambar investasi bisa berupa ilustrasi, poster atau foto terkait investasi." />
<LandscapeFrameUploaded />
<LandscapeFrameUploaded image={image as string} />
<ButtonCenteredOnly
icon="upload"
onPress={() => router.push("/take-picture/1")}
onPress={() => {
pickFile({
setImageUri: ({ uri }) => {
setImage(uri);
},
allowedType: "image",
});
}}
>
Upload
</ButtonCenteredOnly>
<Spacing />
<InformationBox text="File prospektus wajib untuk diupload, agar calon investor paham dengan prospek investasi yang akan anda jalankan kedepannya." />
<InformationBox text="File prospektus wajib untuk diupload, agar calon investor paham dengan prospek investasi yang akan anda jalankan kedepannya. Gunakan format PDF." />
<BaseBox>
<CenterCustom>
<FontAwesome5
name="file-pdf"
size={30}
color={MainColor.disabled}
/>
{pdf ? (
<TextCustom>{pdf.name}</TextCustom>
) : (
<FontAwesome5
name="file-pdf"
size={30}
color={MainColor.disabled}
/>
)}
</CenterCustom>
</BaseBox>
<ButtonCenteredOnly
icon="upload"
onPress={() => router.push("/take-picture/1")}
onPress={() => {
pickFile({
setPdfUri: ({ uri, name, size }) => {
setPdf({ uri, name, size });
},
allowedType: "pdf",
});
}}
>
Upload File
</ButtonCenteredOnly>
@@ -90,22 +250,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()}
/>
<TextInputCustom
required
iconLeft="Rp."
placeholder="0"
label="Target Dana"
keyboardType="numeric"
onChangeText={(value) =>
setData({ ...data, targetDana: Number(value) })
}
value={data.targetDana === 0 ? "" : data.targetDana.toString()}
onChangeText={handleChangeCurrency("targetDana")}
value={displayTargetDana}
/>
<TextInputCustom
@@ -114,22 +260,24 @@ export default function InvestmentCreate() {
placeholder="0"
label="Harga Per Lembar"
keyboardType="numeric"
onChangeText={(value) =>
setData({ ...data, targetDana: Number(value) })
}
value={data.targetDana === 0 ? "" : data.targetDana.toString()}
onChangeText={handleChangeCurrency("hargaPerLembar")}
value={displayHargaPerLembar}
/>
<TextInputCustom
required
placeholder="0"
label="Total Lembar"
keyboardType="numeric"
onChangeText={(value) =>
setData({ ...data, totalLembar: Number(value) })
}
value={data.totalLembar === 0 ? "" : data.totalLembar.toString()}
/>
<StackCustom gap={0}>
<TextInputCustom
required
placeholder="0"
label="Total Lembar"
keyboardType="numeric"
// onChangeText={handleChangeCurrency("totalLembar")}
value={displayTotalLembar}
/>
<TextCustom size={"small"} color="gray">
*Total lembar dihitung dari, Target Dana / Harga Perlembar
</TextCustom>
</StackCustom>
<Spacing />
<TextInputCustom
required
@@ -137,57 +285,80 @@ export default function InvestmentCreate() {
label="Rasio Keuntungan / ROI %"
placeholder="0"
keyboardType="numeric"
onChangeText={(value) =>
setData({ ...data, rasioKeuntungan: Number(value) })
}
onChangeText={(value) => setData({ ...data, rasioKeuntungan: value })}
value={
data.rasioKeuntungan === 0 ? "" : data.rasioKeuntungan.toString()
data.rasioKeuntungan === "" ? "" : data.rasioKeuntungan.toString()
}
/>
<SelectCustom
required
placeholder="Pilih batas waktu"
label="Pencarian Investor"
data={dummyListPencarianInvestor.map((item) => ({
label: item.name + `${" "}hari`,
value: item.id,
}))}
onChange={(value) =>
setData({ ...data, pencarianInvestor: value as any })
}
value={data.pencarianInvestor}
/>
{loadingMaster ? (
<LoaderCustom />
) : (
<SelectCustom
required
placeholder="Pilih batas waktu"
label="Pencarian Investor"
data={
_.isEmpty(listPencarianInvestor)
? []
: listPencarianInvestor.map((item) => ({
label: item.name + `${" "}hari`,
value: item.id,
}))
}
onChange={(value) =>
setData({ ...data, pencarianInvestor: value as any })
}
value={data.pencarianInvestor}
/>
)}
<SelectCustom
required
placeholder="Pilih batas waktu"
label="Pilih Periode Deviden"
data={dummyPeriodeDeviden.map((item) => ({
label: item.name,
value: item.id,
}))}
onChange={(value) =>
setData({ ...data, periodeDeviden: value as any })
}
value={data.periodeDeviden}
/>
{loadingMaster ? (
<LoaderCustom />
) : (
<SelectCustom
required
placeholder="Pilih batas waktu"
label="Pilih Periode Deviden"
data={
_.isEmpty(listPeriodeDeviden)
? []
: listPeriodeDeviden.map((item) => ({
label: item.name,
value: item.id,
}))
}
onChange={(value) =>
setData({ ...data, periodeDeviden: value as any })
}
value={data.periodeDeviden}
/>
)}
{loadingMaster ? (
<LoaderCustom />
) : (
<SelectCustom
required
placeholder="Pilih batas waktu"
label="Pilih Pembagian Deviden"
data={
_.isEmpty(listPembagianDeviden)
? []
: listPembagianDeviden.map((item) => ({
label: item.name + `${" "}bulan`,
value: item.id,
}))
}
onChange={(value) =>
setData({ ...data, pembagianDeviden: value as any })
}
value={data.pembagianDeviden}
/>
)}
<SelectCustom
required
placeholder="Pilih batas waktu"
label="Pilih Pembagian Deviden"
data={dummyPembagianDeviden.map((item) => ({
label: item.name + `${" "}bulan`,
value: item.id,
}))}
onChange={(value) =>
setData({ ...data, pembagianDeviden: value as any })
}
value={data.pembagianDeviden}
/>
<Spacing />
<ButtonCustom onPress={() => router.replace("/investment/portofolio")}>
<ButtonCustom isLoading={isLoading} onPress={() => handleSubmit()}>
Simpan
</ButtonCustom>
</StackCustom>

View File

@@ -1,23 +1,23 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BaseBox,
ButtonCenteredOnly,
ButtonCustom,
DummyLandscapeImage,
InformationBox,
LandscapeFrameUploaded,
LoaderCustom,
Spacing,
StackCustom,
TextAreaCustom,
TextInputCustom,
ViewWrapper,
BaseBox,
ButtonCenteredOnly,
ButtonCustom,
DummyLandscapeImage,
InformationBox,
LandscapeFrameUploaded,
LoaderCustom,
Spacing,
StackCustom,
TextAreaCustom,
TextInputCustom,
ViewWrapper,
} from "@/components";
import DIRECTORY_ID from "@/constants/directory-id";
import { apiJobGetOne, apiJobUpdateData } from "@/service/api-client/api-job";
import {
deleteImageService,
uploadImageService,
deleteFileService,
uploadFileService,
} from "@/service/upload-service";
import pickImage from "@/utils/pickImage";
import { router, useLocalSearchParams } from "expo-router";
@@ -69,7 +69,7 @@ export default function JobEdit() {
let newImageId = "";
if (imageUri) {
const responseUploadImage = await uploadImageService({
const responseUploadImage = await uploadFileService({
imageUri: imageUri,
dirId: DIRECTORY_ID.job_image,
});
@@ -80,7 +80,7 @@ export default function JobEdit() {
}
if (data?.imageId) {
const responseDeleteImage = await deleteImageService({
const responseDeleteImage = await deleteFileService({
id: data.imageId,
});

View File

@@ -4,6 +4,7 @@ import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import Job_BoxDetailSection from "@/screens/Job/BoxDetailSection";
import { apiJobGetOne } from "@/service/api-client/api-job";
import { BASE_URL } from "@/service/api-config";
import { Ionicons } from "@expo/vector-icons";
import * as Clipboard from "expo-clipboard";
import { useLocalSearchParams } from "expo-router";
@@ -32,7 +33,8 @@ export default function JobDetail() {
}
};
const linkUrl = `http://192.168.1.83:3000/job-vacancy/`;
const baseUrl = BASE_URL;
const linkUrl = `${baseUrl}/job-vacancy/`;
const OpenLinkButton = ({ id }: { id: string }) => {
const jobUrl = `${linkUrl}${id}`;

View File

@@ -12,7 +12,7 @@ import {
import DIRECTORY_ID from "@/constants/directory-id";
import { useAuth } from "@/hooks/use-auth";
import { apiJobCreate } from "@/service/api-client/api-job";
import { uploadImageService } from "@/service/upload-service";
import { uploadFileService } from "@/service/upload-service";
import pickImage from "@/utils/pickImage";
import { router } from "expo-router";
import { useState } from "react";
@@ -66,7 +66,7 @@ export default function JobCreate() {
return;
}
const responseUploadImage = await uploadImageService({
const responseUploadImage = await uploadFileService({
imageUri: image,
dirId: DIRECTORY_ID.job_image,
});

View File

@@ -1,9 +1,9 @@
import {
BaseBox,
BoxButtonOnFooter,
ButtonCenteredOnly,
ButtonCustom,
ViewWrapper
BaseBox,
BoxButtonOnFooter,
ButtonCenteredOnly,
ButtonCustom,
ViewWrapper
} from "@/components";
import API_STRORAGE from "@/constants/base-url-api-strorage";
import DIRECTORY_ID from "@/constants/directory-id";
@@ -11,10 +11,10 @@ import DUMMY_IMAGE from "@/constants/dummy-image-value";
import { useAuth } from "@/hooks/use-auth";
import { apiFileDelete } from "@/service/api-client/api-file";
import {
apiGetOnePortofolio,
apiUpdatePortofolio,
apiGetOnePortofolio,
apiUpdatePortofolio,
} from "@/service/api-client/api-portofolio";
import { uploadImageService } from "@/service/upload-service";
import { uploadFileService } from "@/service/upload-service";
import pickImage from "@/utils/pickImage";
import { Image } from "expo-image";
import { router, useLocalSearchParams } from "expo-router";
@@ -45,7 +45,7 @@ export default function PortofolioEditLogo() {
try {
setIsLoading(true);
const response = await uploadImageService({
const response = await uploadFileService({
imageUri,
dirId: DIRECTORY_ID.portofolio_logo,
});

View File

@@ -1,8 +1,8 @@
import {
BaseBox,
BoxButtonOnFooter,
ButtonCenteredOnly,
ButtonCustom,
BaseBox,
BoxButtonOnFooter,
ButtonCenteredOnly,
ButtonCustom,
} from "@/components";
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
import API_STRORAGE from "@/constants/base-url-api-strorage";
@@ -11,7 +11,7 @@ import DUMMY_IMAGE from "@/constants/dummy-image-value";
import { useAuth } from "@/hooks/use-auth";
import { apiFileDelete } from "@/service/api-client/api-file";
import { apiProfile, apiUpdateProfile } from "@/service/api-client/api-profile";
import { uploadImageService } from "@/service/upload-service";
import { uploadFileService } from "@/service/upload-service";
import { IProfile } from "@/types/Type-Profile";
import pickImage from "@/utils/pickImage";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
@@ -45,7 +45,7 @@ export default function UpdateBackgroundProfile() {
try {
setIsLoading(true);
const response = await uploadImageService({
const response = await uploadFileService({
imageUri,
dirId: DIRECTORY_ID.profile_background,
});

View File

@@ -8,16 +8,16 @@ import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
import API_STRORAGE from "@/constants/base-url-api-strorage";
import DIRECTORY_ID from "@/constants/directory-id";
import DUMMY_IMAGE from "@/constants/dummy-image-value";
import { useAuth } from "@/hooks/use-auth";
import { apiFileDelete } from "@/service/api-client/api-file";
import { apiProfile, apiUpdateProfile } from "@/service/api-client/api-profile";
import { uploadImageService } from "@/service/upload-service";
import { uploadFileService } from "@/service/upload-service";
import { IProfile } from "@/types/Type-Profile";
import pickImage from "@/utils/pickImage";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, useState } from "react";
import { Image } from "react-native";
import Toast from "react-native-toast-message";
import { useAuth } from "@/hooks/use-auth";
import { apiFileDelete } from "@/service/api-client/api-file";
export default function UpdatePhotoProfile() {
const { id } = useLocalSearchParams();
@@ -46,7 +46,7 @@ export default function UpdatePhotoProfile() {
try {
setIsLoading(true);
const response = await uploadImageService({
const response = await uploadFileService({
imageUri,
dirId: DIRECTORY_ID.profile_foto,
});

View File

@@ -1,12 +1,12 @@
import {
BaseBox,
ButtonCenteredOnly,
ButtonCustom,
SelectCustom,
Spacing,
StackCustom,
TextInputCustom,
ViewWrapper,
BaseBox,
ButtonCenteredOnly,
ButtonCustom,
SelectCustom,
Spacing,
StackCustom,
TextInputCustom,
ViewWrapper,
} from "@/components";
import BoxButtonOnFooter from "@/components/Box/BoxButtonOnFooter";
import InformationBox from "@/components/Box/InformationBox";
@@ -15,7 +15,7 @@ import DUMMY_IMAGE from "@/constants/dummy-image-value";
import { useAuth } from "@/hooks/use-auth";
import { apiCreateProfile } from "@/service/api-client/api-profile";
import { apiValidationEmail } from "@/service/api-client/api-validation";
import { uploadImageService } from "@/service/upload-service";
import { uploadFileService } from "@/service/upload-service";
import pickImage from "@/utils/pickImage";
import { router } from "expo-router";
import { useState } from "react";
@@ -69,7 +69,7 @@ export default function CreateProfile() {
if (imagePhoto) {
try {
const responseUploadPhoto = await uploadImageService({
const responseUploadPhoto = await uploadFileService({
imageUri: imagePhoto,
dirId: DIRECTORY_ID.profile_foto,
});
@@ -90,7 +90,7 @@ export default function CreateProfile() {
if (imageBackground) {
try {
const responseUploadBackground = await uploadImageService({
const responseUploadBackground = await uploadFileService({
imageUri: imageBackground,
dirId: DIRECTORY_ID.profile_background,
});

View File

@@ -1,13 +1,13 @@
import {
AvatarCustom,
BoxButtonOnFooter,
ButtonCustom,
StackCustom,
ViewWrapper,
AvatarCustom,
BoxButtonOnFooter,
ButtonCustom,
StackCustom,
ViewWrapper,
} from "@/components";
import DIRECTORY_ID from "@/constants/directory-id";
import DUMMY_IMAGE from "@/constants/dummy-image-value";
import { uploadImageService } from "@/service/upload-service";
import { uploadFileService } from "@/service/upload-service";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { router } from "expo-router";
import { useState } from "react";
@@ -21,7 +21,7 @@ export default function ScreenUpload() {
async function onUpload() {
setIsLoading(true);
try {
const response = await uploadImageService({
const response = await uploadFileService({
imageUri,
dirId: DIRECTORY_ID.profile_foto,
});

View File

@@ -23,7 +23,7 @@
"expo-constants": "~18.0.8",
"expo-dev-client": "~6.0.12",
"expo-document-picker": "~14.0.7",
"expo-file-system": "~19.0.12",
"expo-file-system": "^19.0.15",
"expo-font": "~14.0.8",
"expo-haptics": "~15.0.7",
"expo-image": "~3.0.8",
@@ -1171,7 +1171,7 @@
"expo-document-picker": ["expo-document-picker@14.0.7", "", { "peerDependencies": { "expo": "*" } }, "sha512-81Jh8RDD0GYBUoSTmIBq30hXXjmkDV1ZY2BNIp1+3HR5PDSh2WmdhD/Ezz5YFsv46hIXHsQc+Kh1q8vn6OLT9Q=="],
"expo-file-system": ["expo-file-system@19.0.12", "", { "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-gqpxpnjfhzXLcqMOi49isB5S1Af49P9410fsaFfnLZWN3X6Dwc8EplDwbaolOI/wnGwP81P+/nDn5RNmU6m7mQ=="],
"expo-file-system": ["expo-file-system@19.0.15", "", { "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-sRLW+3PVJDiuoCE2LuteHhC7OxPjh1cfqLylf1YG1TDEbbQXnzwjfsKeRm6dslEPZLkMWfSLYIrVbnuq5mF7kQ=="],
"expo-font": ["expo-font@14.0.8", "", { "dependencies": { "fontfaceobserver": "^2.1.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-bTUHaJWRZ7ywP8dg3f+wfOwv6RwMV3mWT2CDUIhsK70GjNGlCtiWOCoHsA5Od/esPaVxqc37cCBvQGQRFStRlA=="],
@@ -2567,6 +2567,8 @@
"expo/babel-preset-expo": ["babel-preset-expo@54.0.0", "", { "dependencies": { "@babel/helper-module-imports": "^7.25.9", "@babel/plugin-proposal-decorators": "^7.12.9", "@babel/plugin-proposal-export-default-from": "^7.24.7", "@babel/plugin-syntax-export-default-from": "^7.24.7", "@babel/plugin-transform-class-static-block": "^7.27.1", "@babel/plugin-transform-export-namespace-from": "^7.25.9", "@babel/plugin-transform-flow-strip-types": "^7.25.2", "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/plugin-transform-object-rest-spread": "^7.24.7", "@babel/plugin-transform-parameters": "^7.24.7", "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/plugin-transform-private-property-in-object": "^7.24.7", "@babel/plugin-transform-runtime": "^7.24.7", "@babel/preset-react": "^7.22.15", "@babel/preset-typescript": "^7.23.0", "@react-native/babel-preset": "0.81.4", "babel-plugin-react-compiler": "^19.1.0-rc.2", "babel-plugin-react-native-web": "~0.21.0", "babel-plugin-syntax-hermes-parser": "^0.25.1", "babel-plugin-transform-flow-enums": "^0.0.2", "debug": "^4.3.4", "resolve-from": "^5.0.0" }, "peerDependencies": { "@babel/runtime": "^7.20.0", "expo": "*", "react-refresh": ">=0.14.0 <1.0.0" }, "optionalPeers": ["@babel/runtime", "expo"] }, "sha512-a0Ej4ik6xzvtrA4Ivblov3XVvfntIoqnXOy2jG2k/3hzWqzrJxKyY2gUW9ZCMAicGevj2ju28q+TsK29uTe0eQ=="],
"expo/expo-file-system": ["expo-file-system@19.0.12", "", { "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-gqpxpnjfhzXLcqMOi49isB5S1Af49P9410fsaFfnLZWN3X6Dwc8EplDwbaolOI/wnGwP81P+/nDn5RNmU6m7mQ=="],
"expo-module-scripts/typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"expo-modules-autolinking/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="],

View File

@@ -10,15 +10,18 @@ interface ButtonCenteredOnlyProps {
icon?: "plus" | "upload" | string;
onPress: () => void;
isLoading?: boolean;
disabled?: boolean;
}
export default function ButtonCenteredOnly({
onPress,
children,
icon = "plus",
isLoading = false,
disabled = false,
}: ButtonCenteredOnlyProps) {
return (
<ButtonCustom
disabled={disabled}
isLoading={isLoading}
onPress={onPress}
iconLeft={

View File

@@ -0,0 +1,63 @@
// PdfViewer.tsx
import React, { useState } from "react";
import { ActivityIndicator, StyleSheet, View } from "react-native";
import WebView from "react-native-webview";
interface PdfViewerProps {
uri: string; // URL PDF dari API
}
const PdfViewer: React.FC<PdfViewerProps> = ({ uri }) => {
const [loading, setLoading] = useState(true);
return (
<>
{loading && (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#007AFF" />
</View>
)}
<WebView
source={{ uri }}
style={styles.webView}
onLoadEnd={() => setLoading(false)}
onError={(syntheticEvent) => {
const { nativeEvent } = syntheticEvent;
console.warn("WebView error:", nativeEvent);
setLoading(false);
}}
scalesPageToFit={true}
javaScriptEnabled={true}
domStorageEnabled={true}
originWhitelist={["*"]}
/>
</>
);
};
// const { width, height } = Dimensions.get("window");
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#f0f0f0",
},
loadingContainer: {
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
justifyContent: "center",
alignItems: "center",
backgroundColor: "rgba(255,255,255,0.8)",
zIndex: 1,
},
webView: {
// width: width,
// height: height,
flex: 1,
},
});
export default PdfViewer;

20
constants/api-storage.ts Normal file
View File

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

View File

@@ -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: `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",
value: data?.totalLembar || "-",
},
{
label: "Sisa Lembar",
value: "600",
},
{
label: "Jadwal Pembagian",
value: "Rp. 2.880.000",
},
{
label: "Pembagian Deviden",
value: "Selamanya",
value: data?.sisaLembar || "-",
},
{
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 || "-",
},
];

View File

@@ -30,7 +30,7 @@
"expo-constants": "~18.0.8",
"expo-dev-client": "~6.0.12",
"expo-document-picker": "~14.0.7",
"expo-file-system": "~19.0.12",
"expo-file-system": "^19.0.15",
"expo-font": "~14.0.8",
"expo-haptics": "~15.0.7",
"expo-image": "~3.0.8",

View File

@@ -1,30 +1,44 @@
import {
AvatarCustom,
BaseBox,
AvatarComp,
BoxWithHeaderSection,
ClickableCustom,
Grid,
Spacing,
TextCustom,
TextCustom
} from "@/components";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import { GStyles } from "@/styles/global-styles";
import { formatChatTime } from "@/utils/formatChatTime";
import { Entypo } from "@expo/vector-icons";
import { View } from "react-native";
export default function Forum_CommentarBoxSection({
data,
setOpenDrawer,
onSetData,
}: {
data: any;
setOpenDrawer: (value: boolean) => void;
onSetData: ({
setCommentId,
setOpenDrawer,
setCommentAuthorId,
}: {
setCommentId: string;
setOpenDrawer: boolean;
setCommentAuthorId: string;
}) => void;
}) {
return (
<>
<BaseBox>
<BoxWithHeaderSection>
<View>
<Grid>
<Grid.Col span={2}>
<AvatarCustom href={`/profile/${data.id}`} />
<AvatarComp
href={`/profile/${data?.Author?.Profile?.id}`}
fileId={data?.Author?.Profile?.imageId}
size="base"
/>
</Grid.Col>
<Grid.Col
span={8}
@@ -32,7 +46,7 @@ export default function Forum_CommentarBoxSection({
justifyContent: "center",
}}
>
<TextCustom>{data.name}</TextCustom>
<TextCustom>{data?.Author?.username}</TextCustom>
</Grid.Col>
<Grid.Col
@@ -43,7 +57,11 @@ export default function Forum_CommentarBoxSection({
>
<ClickableCustom
onPress={() => {
setOpenDrawer(true);
onSetData({
setCommentId: data?.id,
setOpenDrawer: true,
setCommentAuthorId: data?.Author?.id,
});
}}
style={{
alignItems: "flex-end",
@@ -58,14 +76,18 @@ export default function Forum_CommentarBoxSection({
</Grid.Col>
</Grid>
<TextCustom>{data.deskripsi}</TextCustom>
<View style={GStyles.forumBox}>
<TextCustom>{data.komentar}</TextCustom>
</View>
<Spacing />
<Spacing height={10} />
<View style={{ alignItems: "flex-end" }}>
<TextCustom>{data.date}</TextCustom>
<TextCustom size="small" color="gray">
{formatChatTime(data?.createdAt)}
</TextCustom>
</View>
</View>
</BaseBox>
</BoxWithHeaderSection>
</>
);
}

View File

@@ -4,10 +4,12 @@ import {
ClickableCustom,
Grid,
Spacing,
TextCustom
TextCustom,
} from "@/components";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import { GStyles } from "@/styles/global-styles";
import { formatChatTime } from "@/utils/formatChatTime";
import { Entypo, Ionicons } from "@expo/vector-icons";
import { Href, router } from "expo-router";
import { View } from "react-native";
@@ -15,25 +17,28 @@ import { View } from "react-native";
export default function Forum_BoxDetailSection({
data,
isTruncate,
setOpenDrawer,
setStatus,
href,
isRightComponent = true,
onSetData,
}: {
data: any;
isTruncate?: boolean;
setOpenDrawer: (value: boolean) => void;
setStatus: (value: string) => void;
href?: Href;
isRightComponent?: boolean;
onSetData: ({
setDataId,
setStatus,
setOpenDrawer,
setAuthorId,
}: {
setDataId: string;
setStatus: string;
setOpenDrawer: boolean;
setAuthorId: string;
}) => void;
}) {
const deskripsiView = (
<View
style={{
backgroundColor: MainColor.soft_darkblue,
padding: 8,
borderRadius: 8,
paddingBlock: 20,
}}
>
<View style={GStyles.forumBox}>
{isTruncate ? (
<TextCustom truncate={2}>{data?.diskusi}</TextCustom>
) : (
@@ -50,12 +55,12 @@ export default function Forum_BoxDetailSection({
<Grid.Col span={2}>
<AvatarComp
fileId={data?.Author?.Profile?.imageId}
href={`/profile/${data?.Author?.Profile?.id}`}
href={`/forum/${data?.Author?.id}/forumku`}
size={"base"}
/>
</Grid.Col>
<Grid.Col span={8}>
<TextCustom>{data?.Author?.username}</TextCustom>
<Grid.Col span={6}>
<TextCustom truncate>{data?.Author?.username}</TextCustom>
{data?.ForumMaster_StatusPosting?.status === "Open" ? (
<TextCustom bold size="small" color="green">
{data?.ForumMaster_StatusPosting?.status}
@@ -68,30 +73,36 @@ export default function Forum_BoxDetailSection({
</Grid.Col>
<Grid.Col
span={2}
span={4}
style={{
justifyContent: "center",
justifyContent: "flex-start",
alignItems: "flex-end",
}}
>
<ClickableCustom
onPress={() => {
setOpenDrawer(true);
setStatus(data?.ForumMaster_StatusPosting?.status);
}}
style={{
alignItems: "flex-end",
}}
>
<Entypo
name="dots-three-horizontal"
color={MainColor.white}
size={ICON_SIZE_SMALL}
/>
</ClickableCustom>
{isRightComponent && (
<ClickableCustom
onPress={() => {
onSetData({
setDataId: data?.id,
setStatus: data?.ForumMaster_StatusPosting?.status,
setAuthorId: data?.Author?.id,
setOpenDrawer: true,
});
}}
style={{
alignItems: "flex-end",
}}
>
<Entypo
name="dots-three-horizontal"
color={MainColor.white}
size={ICON_SIZE_SMALL}
/>
</ClickableCustom>
)}
</Grid.Col>
</Grid>
{href ? (
<ClickableCustom onPress={() => router.push(href as any)}>
{deskripsiView}
@@ -116,11 +127,13 @@ export default function Forum_BoxDetailSection({
size={ICON_SIZE_SMALL}
color={MainColor.white}
/>
<TextCustom>{data?.Forum_Komentar?.length}</TextCustom>
<TextCustom>{data?.count}</TextCustom>
</View>
</Grid.Col>
<Grid.Col span={6} style={{ alignItems: "flex-end" }}>
<TextCustom size="small"> {data.date}</TextCustom>
<TextCustom truncate size="small" color="gray">
{formatChatTime(data?.createdAt)}
</TextCustom>
</Grid.Col>
</Grid>
</View>

View File

@@ -2,24 +2,20 @@ import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import { Feather, Ionicons } from "@expo/vector-icons";
export { drawerItemsForumBeranda, drawerItemsForumComentar };
export {
drawerItemsForumBerandaForAuthor,
drawerItemsForumComentarForAuthor,
drawerItemsForumBerandaForNonAuthor,
drawerItemsForumComentarForNonAuthor,
};
const drawerItemsForumBeranda = ({
const drawerItemsForumBerandaForAuthor = ({
id,
status,
}: {
id: string;
status: string;
}) => [
{
icon: (
<Ionicons name="flag" size={ICON_SIZE_SMALL} color={MainColor.white} />
),
label: "Laporkan diskusi",
// color: MainColor.white,
path: `/forum/${id}/report-posting`,
},
{
icon: (
<Feather name="edit" size={ICON_SIZE_SMALL} color={MainColor.white} />
@@ -49,15 +45,18 @@ const drawerItemsForumBeranda = ({
},
];
const drawerItemsForumComentar = ({ id }: { id: string }) => [
const drawerItemsForumBerandaForNonAuthor = ({ id }: { id: string }) => [
{
icon: (
<Ionicons name="flag" size={ICON_SIZE_SMALL} color={MainColor.white} />
),
label: "Laporkan",
label: "Laporkan diskusi",
// color: MainColor.white,
path: `/forum/${id}/report-commentar`,
path: `/forum/${id}/report-posting`,
},
];
const drawerItemsForumComentarForAuthor = ({ id }: { id: string }) => [
{
icon: (
<Ionicons name="trash" size={ICON_SIZE_SMALL} color={MainColor.white} />
@@ -67,3 +66,14 @@ const drawerItemsForumComentar = ({ id }: { id: string }) => [
path: "",
},
];
const drawerItemsForumComentarForNonAuthor = ({ id }: { id: string }) => [
{
icon: (
<Ionicons name="flag" size={ICON_SIZE_SMALL} color={MainColor.white} />
),
label: "Laporkan",
// color: MainColor.white,
path: `/forum/${id}/report-commentar`,
},
];

View File

@@ -1,30 +1,56 @@
import { IMenuDrawerItem } from "@/components/_Interface/types";
import MenuDrawerDynamicGrid from "@/components/Drawer/MenuDrawerDynamicGird";
import { router } from "expo-router";
import { drawerItemsForumBeranda } from "../ListPage";
import { AlertDefaultSystem } from "@/components";
import {
drawerItemsForumBerandaForAuthor,
drawerItemsForumBerandaForNonAuthor,
} from "../ListPage";
import { useAuth } from "@/hooks/use-auth";
import { apiForumDelete } from "@/service/api-client/api-forum";
import Toast from "react-native-toast-message";
export default function Forum_MenuDrawerBerandaSection({
id,
status,
setIsDrawerOpen,
setShowDeleteAlert,
setShowAlertStatus,
authorId,
handlerUpdateStatus,
}: {
id: string;
status: string;
setIsDrawerOpen: (value: boolean) => void;
setShowDeleteAlert?: (value: boolean) => void;
setShowAlertStatus?: (value: boolean) => void;
authorId: string;
handlerUpdateStatus?: (value: string) => void;
}) {
const { user } = useAuth();
const handlePress = (item: IMenuDrawerItem) => {
if (item.label === "Hapus") {
AlertDefaultSystem({
title: "Hapus",
message: "Apakah Anda yakin ingin menghapus forum ini?",
title: "Hapus diskusi",
message: "Apakah Anda yakin ingin menghapus diskusi ini?",
textLeft: "Batal",
textRight: "Hapus",
onPressRight: () => {},
onPressRight: async () => {
try {
const response = await apiForumDelete({ id });
if (response.success) {
Toast.show({
type: "success",
text1: "Berhasil dihapus",
});
router.back();
} else {
Toast.show({
type: "error",
text1: response.message,
});
}
} catch (error) {
console.log("[ERROR]", error);
}
},
});
} else if (item.label === "Buka forum" || item.label === "Tutup forum") {
AlertDefaultSystem({
@@ -32,7 +58,9 @@ export default function Forum_MenuDrawerBerandaSection({
message: "Apakah Anda yakin ingin mengubah status forum ini?",
textLeft: "Batal",
textRight: "Ubah",
onPressRight: () => {},
onPressRight: () => {
handlerUpdateStatus?.(item.label === "Buka forum" ? "Open" : "Closed");
},
});
} else {
router.push(item.path as any);
@@ -45,7 +73,11 @@ export default function Forum_MenuDrawerBerandaSection({
<>
{/* Menu Items */}
<MenuDrawerDynamicGrid
data={drawerItemsForumBeranda({ id, status })}
data={
authorId === user?.id
? drawerItemsForumBerandaForAuthor({ id, status })
: drawerItemsForumBerandaForNonAuthor({ id })
}
columns={4} // Ubah ke 2 jika ingin 2 kolom per baris
onPressItem={handlePress as any}
/>

View File

@@ -1,19 +1,67 @@
import { MenuDrawerDynamicGrid } from "@/components";
import { drawerItemsForumComentar } from "../ListPage";
import { AlertDefaultSystem, MenuDrawerDynamicGrid } from "@/components";
import { useAuth } from "@/hooks/use-auth";
import { router } from "expo-router";
import {
drawerItemsForumComentarForAuthor,
drawerItemsForumComentarForNonAuthor,
} from "../ListPage";
import { apiForumDeleteComment } from "@/service/api-client/api-forum";
import Toast from "react-native-toast-message";
export default function Forum_MenuDrawerCommentar({
id,
setShowDeleteAlert,
setIsDrawerOpen,
commentId,
commentAuthorId,
listComment,
setListComment,
countComment,
setCountComment,
}: {
id: string;
setShowDeleteAlert: (value: boolean) => void;
setIsDrawerOpen: (value: boolean) => void;
commentId: string;
commentAuthorId: string;
listComment: any;
setListComment: (value: any) => void;
countComment: number;
setCountComment: (value: number) => void;
}) {
const { user } = useAuth();
const handlePress = (item: any) => {
if (item.label === "Hapus") {
setShowDeleteAlert(true);
AlertDefaultSystem({
title: "Hapus",
message: "Apakah Anda yakin ingin menghapus komentar ini?",
textLeft: "Batal",
textRight: "Hapus",
onPressLeft: () => {},
onPressRight: async () => {
try {
const response = await apiForumDeleteComment({ id: commentId });
if (response.success) {
setListComment(
listComment.filter((item: any) => item.id !== commentId)
);
setCountComment(countComment - 1);
Toast.show({
type: "success",
text1: "Berhasil dihapus",
});
} else {
Toast.show({
type: "error",
text1: response.message,
});
}
} catch (error) {
console.log("[ERROR]", error);
}
},
});
} else {
router.push(item.path as any);
}
@@ -24,7 +72,11 @@ export default function Forum_MenuDrawerCommentar({
return (
<>
<MenuDrawerDynamicGrid
data={drawerItemsForumComentar({ id })}
data={
commentAuthorId === user?.id
? drawerItemsForumComentarForAuthor({ id })
: drawerItemsForumComentarForNonAuthor({ id })
}
columns={4}
onPressItem={handlePress}
/>

View File

@@ -4,21 +4,30 @@ import { listDummyReportForum } from "@/lib/dummy-data/forum/report-list";
import { useState } from "react";
import { View } from "react-native";
export default function Forum_ReportListSection() {
const [value, setValue] = useState<any | number>("");
export default function Forum_ReportListSection({
listMaster,
selectReport,
setSelectReport,
}: {
listMaster: any[] | null;
selectReport: string;
setSelectReport: (value: string) => void;
}) {
return (
<>
<BaseBox>
<StackCustom>
<RadioGroup value={value} onChange={setValue}>
{listDummyReportForum.map((e, i) => (
<RadioGroup value={selectReport} onChange={(val) => {
setSelectReport(val);
}}>
{listMaster?.map((e, i) => (
<View key={i}>
<RadioCustom
label={e.title}
// value={i}
value={e.title}
value={e.id}
/>
<TextCustom>{e.desc}</TextCustom>
<TextCustom>{e.deskripsi}</TextCustom>
</View>
))}
</RadioGroup>

View File

@@ -1,34 +1,48 @@
import {
BaseBox,
AvatarUsernameAndOtherComponent,
BoxWithHeaderSection,
DummyLandscapeImage,
Grid,
Spacing,
StackCustom,
TextCustom,
TextCustom
} from "@/components";
import { View } from "react-native";
export default function Invesment_BoxDetailDataSection({
title,
author,
imageId,
data,
bottomSection,
}: {
title?: string;
author?: any;
imageId?: string;
data: any;
bottomSection?: React.ReactNode;
}) {
return (
<>
<BaseBox paddingBottom={0}>
<BoxWithHeaderSection>
<StackCustom gap={"xs"}>
<DummyLandscapeImage />
{author && (
<AvatarUsernameAndOtherComponent
avatar={author?.Profile?.imageId}
name={author?.username}
rightComponent={""}
withBottomLine={true}
/>
)}
<DummyLandscapeImage imageId={imageId} />
<Spacing />
<TextCustom align="center" size="xlarge" bold>
{title || "Judul Investasi"}
</TextCustom>
<Spacing />
{data.map((item: any, index: any) => (
{data?.map((item: any, index: any) => (
<Grid key={index}>
<Grid.Col span={4}>
<TextCustom bold>{item.label}</TextCustom>
@@ -44,7 +58,7 @@ export default function Invesment_BoxDetailDataSection({
<Spacing />
{bottomSection}
</StackCustom>
</BaseBox>
</BoxWithHeaderSection>
</>
);
}

View File

@@ -1,13 +1,13 @@
import { BaseBox, StackCustom, TextCustom, ProgressCustom } from "@/components";
export default function Invesment_BoxProgressSection({status}: {status: string}) {
export default function Invesment_BoxProgressSection({progress, status}: {progress: number, status: string}) {
return (
<>
{status === "publish" && (
<BaseBox>
<StackCustom>
<TextCustom bold>Progress Saham</TextCustom>
<ProgressCustom value={70} size="lg" />
<ProgressCustom label={progress + "%"} value={progress} size="lg" />
</StackCustom>
</BaseBox>
)}

View File

@@ -8,9 +8,12 @@ export default function Investment_ButtonInvestasiSection({
id: string;
isMine: boolean;
}) {
console.log("[IS MINE]", isMine);
return (
<>
{isMine ? (
<ButtonCustom disabled>Investasi Ini Milik Anda</ButtonCustom>
) : (
<ButtonCustom
onPress={() => {
router.navigate(`/investment/${id}/(transaction-flow)`);
@@ -18,8 +21,6 @@ export default function Investment_ButtonInvestasiSection({
>
Beli Saham
</ButtonCustom>
) : (
<ButtonCustom disabled>Investasi Ini Milik Anda</ButtonCustom>
)}
</>
);

View File

@@ -1,22 +1,53 @@
import { AlertDefaultSystem, ButtonCustom, Grid } from "@/components";
import {
apiInvestmentDelete,
apiInvestmentUpdateStatus,
} from "@/service/api-client/api-investment";
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 +58,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 +93,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 +128,33 @@ 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 DELETE]", JSON.stringify(response, null, 2));
if (response.success) {
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 +163,7 @@ export default function Investment_ButtonStatusSection({
return (
<>
<ButtonCustom
isLoading={isLoading}
backgroundColor="red"
textColor="white"
onPress={handleOpenDeleteAlert}
@@ -76,13 +176,11 @@ export default function Investment_ButtonStatusSection({
switch (status) {
case "publish":
return <>
{buttonPublish}
</>;
return <>{buttonPublish}</>;
case "review":
return (
<ButtonCustom onPress={handleBatalkanReview}>
<ButtonCustom isLoading={isLoading} onPress={handleBatalkanReview}>
Batalkan Review
</ButtonCustom>
);
@@ -92,7 +190,7 @@ export default function Investment_ButtonStatusSection({
<>
<Grid>
<Grid.Col span={6} style={{ paddingRight: 10 }}>
<ButtonCustom onPress={handleAjukanReview}>
<ButtonCustom isLoading={isLoading} onPress={handleAjukanReview}>
Ajukan Review
</ButtonCustom>
</Grid.Col>
@@ -108,7 +206,7 @@ export default function Investment_ButtonStatusSection({
<>
<Grid>
<Grid.Col span={6} style={{ paddingRight: 10 }}>
<ButtonCustom onPress={handleEditKembali}>
<ButtonCustom isLoading={isLoading} onPress={handleEditKembali}>
Edit Kembali
</ButtonCustom>
</Grid.Col>

View File

@@ -11,9 +11,11 @@ import { Ionicons } from "@expo/vector-icons";
export default function Invesment_ComponentBoxOnBottomDetail({
id,
prospectusId,
status,
}: {
id: string;
prospectusId: string;
status: string;
}) {
return (
@@ -25,7 +27,7 @@ export default function Invesment_ComponentBoxOnBottomDetail({
<BaseBox
backgroundColor={AccentColor.blue}
style={{ borderColor: AccentColor.softblue, borderWidth: 1 }}
href={`/(file)/${id}`}
href={`/(file)/${prospectusId}`}
>
<StackCustom>
<TextCustom align="center">Prospektus</TextCustom>
@@ -94,7 +96,7 @@ export default function Invesment_ComponentBoxOnBottomDetail({
<BaseBox
backgroundColor={AccentColor.blue}
style={{ borderColor: AccentColor.softblue, borderWidth: 1 }}
href={`/(file)/${id}`}
href={`/(file)/${prospectusId}`}
>
<StackCustom>
<TextCustom align="center">Prospektus</TextCustom>

View File

@@ -10,26 +10,33 @@ import Investment_ButtonStatusSection from "./ButtonStatusSection";
export default function Invesment_DetailDataPublishSection({
status,
data,
bottomSection,
buttonSection,
}: {
status: string;
data: any;
bottomSection?: React.ReactNode;
buttonSection?: React.ReactNode;
}) {
return (
<>
<StackCustom gap={"sm"}>
<Invesment_BoxProgressSection status={status as string} />
<Invesment_BoxProgressSection progress={data?.progress} status={status as string} />
<Invesment_BoxDetailDataSection
title={data?.title}
author={data?.author}
imageId={data?.imageId}
data={
status === "publish"
? listDataPublishInvesment
: listDataNotPublishInvesment
? listDataPublishInvesment({ data })
: listDataNotPublishInvesment({ data })
}
bottomSection={bottomSection}
/>
<Investment_ButtonStatusSection
id={data?.id}
status={status as string}
buttonPublish={buttonSection}
/>

View File

@@ -1,5 +1,5 @@
import { BaseBox, Grid, TextCustom } from "@/components";
import { Href } from "expo-router";
import { BaseBox, ClickableCustom, Grid, TextCustom } from "@/components";
import { Href, router } from "expo-router";
export default function Investment_BoxDetailDocument({
title,
@@ -8,17 +8,19 @@ export default function Investment_BoxDetailDocument({
}: {
title: string;
leftIcon?: React.ReactNode;
href?: Href;
href?: Href | string;
}) {
return (
<>
<BaseBox href={href}>
<BaseBox>
<Grid>
<Grid.Col span={leftIcon ? 10 : 12}>
<TextCustom truncate>
{title ||
`Judul Dokumen: Lorem, ipsum dolor sit amet consectetur adipisicing elit.`}
</TextCustom>
<ClickableCustom onPress={() => router.push(href as any)}>
<TextCustom truncate>
{title ||
`Judul Dokumen: Lorem, ipsum dolor sit amet consectetur adipisicing elit.`}
</TextCustom>
</ClickableCustom>
</Grid.Col>
{leftIcon && <Grid.Col span={2}>{leftIcon}</Grid.Col>}
</Grid>

View File

@@ -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 (
<BaseBox paddingTop={7} paddingBottom={7} href={href}>
<Grid>
<Grid.Col span={6}>
<TextCustom truncate={2}>
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!
</TextCustom>
<TextCustom truncate={2}>{data?.title || ""}</TextCustom>
<Spacing />
<TextCustom bold size="small">
Target Dana:
</TextCustom>
<TextCustom>Rp. 7.500.000</TextCustom>
<TextCustom truncate>
Rp. {formatCurrencyDisplay(data?.targetDana) || ""}
</TextCustom>
</Grid.Col>
<Grid.Col span={1}>
<View />
</Grid.Col>
<Grid.Col span={5}>
<Image
source={DUMMY_IMAGE.background}
source={API_IMAGE.GET({ fileId: data?.imageId })}
style={{ width: "auto", height: 100, borderRadius: 10 }}
/>
</Grid.Col>

View File

@@ -1,7 +1,7 @@
import { BoxButtonOnFooter, ButtonCustom } from "@/components";
import DIRECTORY_ID from "@/constants/directory-id";
import { apiPortofolioCreate } from "@/service/api-client/api-portofolio";
import { uploadImageService } from "@/service/upload-service";
import { uploadFileService } from "@/service/upload-service";
import { router } from "expo-router";
import Toast from "react-native-toast-message";
@@ -72,7 +72,7 @@ export default function Portofolio_ButtonCreate({
setIsLoadingCreate(true);
let fileId = "";
if (imageUri) {
const response = await uploadImageService({
const response = await uploadFileService({
imageUri: imageUri,
dirId: DIRECTORY_ID.portofolio_logo,
});

View File

@@ -11,9 +11,100 @@ export async function apiForumCreate({ data }: { data: any }) {
}
}
export async function apiForumGetAll({search}: {search: string}) {
export async function apiForumGetAll({
search,
authorId,
}: {
search: string;
authorId?: string;
}) {
const authorQuery = authorId ? `?authorId=${authorId}` : "";
const searchQuery = search ? `?search=${search}` : "";
const query = search ? searchQuery : authorQuery;
try {
const response = await apiConfig.get(`/mobile/forum?search=${search}`);
const response = await apiConfig.get(`/mobile/forum${query}`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiForumGetOne({ id }: { id: string }) {
try {
const response = await apiConfig.get(`/mobile/forum/${id}`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiForumUpdate({ id, data }: { id: string; data: any }) {
try {
const response = await apiConfig.put(`/mobile/forum/${id}`, {
data: data,
});
return response.data;
} catch (error) {
throw error;
}
}
export async function apiForumUpdateStatus({
id,
data,
}: {
id: string;
data: any;
}) {
try {
const response = await apiConfig.post(`/mobile/forum/${id}`, {
data: data,
});
return response.data;
} catch (error) {
throw error;
}
}
export async function apiForumDelete({ id }: { id: string }) {
try {
const response = await apiConfig.delete(`/mobile/forum/${id}`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiForumCreateComment({
id,
data,
}: {
id: string;
data: any;
}) {
try {
const response = await apiConfig.post(`/mobile/forum/${id}/comment`, {
data: data,
});
return response.data;
} catch (error) {
throw error;
}
}
export async function apiForumGetComment({ id }: { id: string }) {
try {
const response = await apiConfig.get(`/mobile/forum/${id}/comment`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiForumDeleteComment({ id }: { id: string }) {
try {
const response = await apiConfig.delete(`/mobile/forum/${id}/comment`);
return response.data;
} catch (error) {
throw error;

View File

@@ -0,0 +1,139 @@
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 apiInvestmentGetOne({ 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";
}) {
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,
category,
}: {
id: string;
data: any;
category: "data" | "prospectus";
}) {
try {
const response = await apiConfig.put(
`/mobile/investment/${id}?category=${category}`,
{
data: data,
}
);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiInvestmentUpsertDocument({
id,
data,
}: {
id: string;
data: any;
}) {
try {
const response = await apiConfig.post(`/mobile/investment/${id}/document`, {
data: data,
});
return response.data;
} catch (error) {
throw error;
}
}
export async function apiInvestmentGetDocument({
id,
category,
}: {
id: string;
category: "one-document" | "all-document";
}) {
try {
const response = await apiConfig.get(
`/mobile/investment/${id}/document?category=${category}`
);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiInvestmentDeleteDocument({ id }: { id: string }) {
try {
const response = await apiConfig.delete(
`/mobile/investment/${id}/document`
);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiInvestmentGetAll() {
try {
const response = await apiConfig.get(`/mobile/investment`);
return response.data;
} catch (error) {
throw error;
}
}

View File

@@ -1,5 +1,6 @@
import { apiConfig } from "../api-config";
// ================== START MASTER PORTFOLIO ================== //
export async function apiMasterBidangBisnis() {
try {
const response = await apiConfig.get(`/master/bidang-bisnis`);
@@ -21,6 +22,10 @@ export async function apiMasterSubBidangBisnis({ id }: { id?: string }) {
}
}
// ================== END MASTER PORTFOLIO ================== //
// ================== START MASTER EVENT ================== //
export async function apiMasterEventType() {
try {
const response = await apiConfig.get(`/mobile/master/event-type`);
@@ -30,9 +35,88 @@ export async function apiMasterEventType() {
}
}
// ================== END MASTER EVENT ================== //
// ================== START MASTER COLLABORATION ================== //
export async function apiMasterCollaborationType() {
try {
const response = await apiConfig.get(`/mobile/master/collaboration-industry`);
const response = await apiConfig.get(
`/mobile/master/collaboration-industry`
);
return response.data;
} catch (error) {
throw error;
}
}
// ================== END MASTER COLLABORATION ================== //
// ================== START MASTER FORUM ================== //
export async function apiMasterForumReportList() {
try {
const response = await apiConfig.get(`/mobile/master/forum-report`);
return response.data;
} catch (error) {
throw error;
}
}
// ================== END MASTER FORUM ================== //
export async function apiForumCreateReportPosting({
id,
data,
}: {
id: string;
data: any;
}) {
try {
const response = await apiConfig.post(
`/mobile/forum/${id}/report-posting`,
{
data: data,
}
);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiForumCreateReportCommentar({
id,
data,
}: {
id: string;
data: any;
}) {
try {
const response = await apiConfig.post(
`/mobile/forum/${id}/report-commentar`,
{
data: data,
}
);
return response.data;
} catch (error) {
throw error;
}
}
// ================== START MASTER INVESTMENT ================== //
export async function apiMasterInvestment({
category,
}: {
category?: "pencarian-investor" | "periode-deviden" | "pembagian-deviden" | string;
}) {
const selectCategory = category ? `?category=${category}` : "";
try {
const response = await apiConfig.get(
`/mobile/master/investment${selectCategory}`
);
return response.data;
} catch (error) {
throw error;

View File

@@ -1,6 +1,7 @@
import AsyncStorage from "@react-native-async-storage/async-storage";
import axios, { AxiosInstance } from "axios";
import Constants from "expo-constants";
export const BASE_URL = Constants.expoConfig?.extra?.BASE_URL;
export const API_BASE_URL = Constants.expoConfig?.extra?.API_BASE_URL;
export const apiConfig: AxiosInstance = axios.create({

View File

@@ -2,9 +2,9 @@ import { API_BASE_URL } from "@/service/api-config";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { apiFileDelete, apiFileUpload } from "./api-client/api-file";
export async function uploadImageService({
export async function uploadFileService({
dirId,
imageUri,
imageUri: fileUri,
}: {
dirId: string;
imageUri: string | null;
@@ -14,12 +14,12 @@ export async function uploadImageService({
console.log("url >>", url);
if (!imageUri) {
if (!fileUri) {
throw new Error("Harap pilih gambar terlebih dahulu");
}
try {
const uri = imageUri;
const uri = fileUri;
const filename = uri.split("/").pop();
const match = /\.(\w+)$/.exec(filename || "");
const type = match ? `image/${match[1]}` : "image";
@@ -50,7 +50,7 @@ export async function uploadImageService({
}
}
export async function deleteImageService({ id }: { id: string }) {
export async function deleteFileService({ id }: { id: string }) {
const token = await AsyncStorage.getItem("authToken");
try {

View File

@@ -323,4 +323,10 @@ export const GStyles = StyleSheet.create({
alignSelfFlexEnd: {
alignSelf: "flex-end",
},
forumBox: {
backgroundColor: MainColor.soft_darkblue,
borderRadius: 8,
paddingBlock: 20,
paddingInline: 10,
},
});

View File

@@ -22,7 +22,7 @@ export const formatChatTime = (date: string | Date): string => {
// Jika kemarin
if (messageDate.isSame(now.subtract(1, 'day'), 'day')) {
return 'Kemarin';
return messageDate.format('dddd HH:mm');
}
// Jika dalam 7 hari terakhir (tapi bukan kemarin/ hari ini)

View File

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

117
utils/pickFile.ts Normal file
View File

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