Compare commits

...

12 Commits

Author SHA1 Message Date
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
54af104f8a Forum
Add:
- api-client/api-forum

Fix:
- Integrasi API: create dan beranda file

### No Issue
2025-09-24 17:30:06 +08:00
8c5602b809 Collaboration
Add:
- Collaboration/GroupChatSection.tsx : fitur room chat

Fix:
- Clear code: Hapus console pada beberapa file

### No Issue
2025-09-24 15:28:16 +08:00
99f058a92f Collaboration
Add:
- (user)/collaboration/[id]/select-of-participants

Fix:
- Integrasi ke api di bagian beranda , partisipan dan group

### No Issue
2025-09-23 17:41:03 +08:00
821a211f58 Collaboration
Fix:
- Integrasi API: Beranda, create, list partisipan, check sudah berpartisipasi ?

### No Issue
2025-09-22 17:31:40 +08:00
333b1d2512 Voting
Fix: Semua tampilan sudah terintegrasi API

### No Issue
2025-09-19 17:51:08 +08:00
391430de46 Voting
Fix:
- Integrasi API pada (tabs) status & detail
- Integrasi API beranda & detail
- Integrasi API pada voting

### No Issue
2025-09-18 17:35:18 +08:00
ce79d7c240 Voting
Add:
- api-client/api-voting: kumpulan fetching api voting

Fix:
- UI create dan (tabs) status udah terintegrasi ke API

### No Isuue
2025-09-17 17:31:44 +08:00
d09a566903 Job
Add:
- add file: (user)/job/[id]/archive

Fix:
- Semua tampilan telah terintergrasi ke API Job

### No Issue
2025-09-17 14:26:10 +08:00
87 changed files with 5894 additions and 1396 deletions

View File

@@ -26,6 +26,7 @@ export default {
},
edgeToEdgeEnabled: true,
package: 'com.bip.hipmimobileapp',
// softwareKeyboardLayoutMode: 'resize', // option: untuk mengatur keyboard pada room chst collaboration
},
web: {
@@ -68,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

@@ -118,14 +118,13 @@ export default function UserLayout() {
headerLeft: () => <BackButton />,
}}
/>
<Stack.Screen
{/* <Stack.Screen
name="collaboration/[id]/detail-participant"
options={{
title: "Partisipasi Proyek",
headerLeft: () => <BackButton />,
}}
/>
/> */}
<Stack.Screen
name="collaboration/[id]/edit"
options={{
@@ -133,6 +132,20 @@ export default function UserLayout() {
headerLeft: () => <BackButton />,
}}
/>
<Stack.Screen
name="collaboration/[id]/create-pacticipants"
options={{
title: "Ajukan Partisipasi",
headerLeft: () => <BackButton />,
}}
/>
<Stack.Screen
name="collaboration/[id]/select-of-participants"
options={{
title: "Pilih Partisipan",
headerLeft: () => <BackButton />,
}}
/>
{/* ========== End Collaboration Section ========= */}
@@ -509,6 +522,13 @@ export default function UserLayout() {
headerLeft: () => <BackButton />,
}}
/>
<Stack.Screen
name="job/[id]/archive"
options={{
title: "Arsip Job",
headerLeft: () => <BackButton />,
}}
/>
{/* ========== End Job Section ========= */}

View File

@@ -1,38 +1,96 @@
import { BaseBox, Grid, TextCustom } from "@/components";
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable react-hooks/exhaustive-deps */
import {
BaseBox,
Grid,
LoaderCustom,
StackCustom,
TextCustom,
} from "@/components";
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
import { MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
import { apiCollaborationGetAll } from "@/service/api-client/api-collaboration";
import { Feather } from "@expo/vector-icons";
import { useFocusEffect } from "expo-router";
import _ from "lodash";
import { useState, useCallback } from "react";
export default function CollaborationGroup() {
const { user } = useAuth();
const [listData, setListData] = useState<any[]>();
const [loadingGetData, setLoadingGetData] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [user?.id])
);
const onLoadData = async () => {
try {
setLoadingGetData(true);
const response = await apiCollaborationGetAll({
category: "group",
authorId: user?.id,
});
if (response.success) {
setListData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingGetData(false);
}
};
return (
<ViewWrapper hideFooter>
{Array.from({ length: 10 }).map((_, index) => (
<BaseBox
key={index}
paddingBlock={5}
href={`/collaboration/${index}/${generateProjectName()}/room-chat`}
>
<Grid>
<Grid.Col span={10}>
<TextCustom bold>{generateProjectName()}</TextCustom>
<TextCustom size="small">2 Anggota</TextCustom>
</Grid.Col>
<Grid.Col
span={2}
style={{ alignItems: "flex-end", justifyContent: "center" }}
{loadingGetData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Tidak ada data</TextCustom>
) : (
<StackCustom>
{listData?.map((item: any, index: any) => (
<BaseBox
key={index}
paddingBlock={5}
href={`/collaboration/${item?.ProjectCollaboration_RoomChat?.id}/${item?.ProjectCollaboration_RoomChat?.name}/room-chat`}
>
<Feather name="chevron-right" size={20} color={MainColor.white} />
</Grid.Col>
</Grid>
</BaseBox>
))}
<Grid>
<Grid.Col span={10}>
<TextCustom bold>
{item?.ProjectCollaboration_RoomChat?.name}
</TextCustom>
<TextCustom size="small">
{
item?.ProjectCollaboration_RoomChat
?.ProjectCollaboration_AnggotaRoomChat?.length
}{" "}
Anggota
</TextCustom>
</Grid.Col>
<Grid.Col
span={2}
style={{ alignItems: "flex-end", justifyContent: "center" }}
>
<Feather
name="chevron-right"
size={20}
color={MainColor.white}
/>
</Grid.Col>
</Grid>
</BaseBox>
))}
</StackCustom>
)}
</ViewWrapper>
);
}
function generateProjectName() {
const adjectives = [
"Blue",
@@ -65,4 +123,4 @@ function generateProjectName() {
const randomNoun = nouns[Math.floor(Math.random() * nouns.length)];
return randomAdjective + randomNoun;
}
}

View File

@@ -1,8 +1,40 @@
import { FloatingButton, ViewWrapper } from "@/components";
import {
FloatingButton,
LoaderCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import Collaboration_BoxPublishSection from "@/screens/Collaboration/BoxPublishSection";
import { router } from "expo-router";
import { apiCollaborationGetAll } from "@/service/api-client/api-collaboration";
import { router, useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
export default function CollaborationBeranda() {
const [listData, setListData] = useState<any[]>();
const [loadingGetData, setLoadingGetData] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [])
);
const onLoadData = async () => {
try {
setLoadingGetData(true);
const response = await apiCollaborationGetAll({
category: "beranda",
});
setListData(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingGetData(false);
}
};
return (
<>
<ViewWrapper
@@ -15,13 +47,19 @@ export default function CollaborationBeranda() {
/>
}
>
{Array.from({ length: 10 }).map((_, index) => (
<Collaboration_BoxPublishSection
key={index}
id={index.toString()}
href={`/collaboration/${index}`}
/>
))}
{loadingGetData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Tidak ada data</TextCustom>
) : (
listData?.map((item: any, index: number) => (
<Collaboration_BoxPublishSection
key={index}
href={`/collaboration/${item.id}`}
data={item}
/>
))
)}
</ViewWrapper>
</>
);

View File

@@ -1,15 +1,46 @@
import { ButtonCustom, Spacing } from "@/components";
/* eslint-disable react-hooks/exhaustive-deps */
import { ButtonCustom, LoaderCustom, Spacing, TextCustom } from "@/components";
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
import { AccentColor, MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
import Collaboration_BoxPublishSection from "@/screens/Collaboration/BoxPublishSection";
import { useState } from "react";
import { apiCollaborationGetAll } from "@/service/api-client/api-collaboration";
import { useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import { View } from "react-native";
export default function CollaborationParticipans() {
const [activeCategory, setActiveCategory] = useState<string | null>(
"participant"
const [activeCategory, setActiveCategory] = useState<
"participant" | "my-project"
>("participant");
const { user } = useAuth();
const [listData, setListData] = useState<any[]>();
const [loadingGetData, setLoadingGetData] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [activeCategory])
);
const onLoadData = async () => {
try {
setLoadingGetData(true);
const response = await apiCollaborationGetAll({
category:
activeCategory === "participant" ? "participant" : "my-project",
authorId: user?.id,
});
setListData(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingGetData(false);
}
};
const handlePress = (item: any) => {
setActiveCategory(item);
// tambahkan logika lain seperti filter dsb.
@@ -41,13 +72,13 @@ export default function CollaborationParticipans() {
<Spacing width={"2%"} />
<ButtonCustom
backgroundColor={
activeCategory === "main" ? MainColor.yellow : AccentColor.blue
activeCategory === "my-project" ? MainColor.yellow : AccentColor.blue
}
textColor={
activeCategory === "main" ? MainColor.black : MainColor.white
activeCategory === "my-project" ? MainColor.black : MainColor.white
}
style={{ width: "49%" }}
onPress={() => handlePress("main")}
onPress={() => handlePress("my-project")}
>
Proyek Saya
</ButtonCustom>
@@ -56,22 +87,27 @@ export default function CollaborationParticipans() {
return (
<ViewWrapper hideFooter headerComponent={headerComponent}>
{Array.from({ length: 10 }).map((_, index) => (
<Collaboration_BoxPublishSection
key={index.toString()}
id={index.toString()}
username={` ${
activeCategory === "participant"
? "Partisipasi Proyek"
: "Proyek Saya"
}`}
href={
activeCategory === "participant"
? `/collaboration/${index}/detail-participant`
: `/collaboration/${index}/detail-project-main`
}
/>
))}
{loadingGetData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Tidak ada data</TextCustom>
) : activeCategory === "participant" ? (
listData?.map((item: any, index: number) => (
<Collaboration_BoxPublishSection
key={index.toString()}
data={item?.ProjectCollaboration}
href={`/collaboration/${item?.ProjectCollaboration?.id}/detail-participant`}
/>
))
) : (
listData?.map((item: any, index: number) => (
<Collaboration_BoxPublishSection
key={index.toString()}
data={item}
href={`/collaboration/${item?.id}/detail-project-main`}
/>
))
)}
</ViewWrapper>
);
}

View File

@@ -1,17 +1,41 @@
/* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable @typescript-eslint/no-unused-vars */
import {
AvatarUsernameAndOtherComponent,
BackButton,
BaseBox,
BoxWithHeaderSection,
Grid,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import { Stack, useLocalSearchParams } from "expo-router";
import { apiCollaborationGroup } from "@/service/api-client/api-collaboration";
import { Stack, useFocusEffect, useLocalSearchParams } from "expo-router";
import { useState, useCallback } from "react";
export default function CollaborationRoomInfo() {
const { id, detail } = useLocalSearchParams();
const [data, setData] = useState<any>();
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id])
);
const onLoadData = async () => {
try {
const response = await apiCollaborationGroup({ id: id as string });
if (response.success) {
setData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
}
};
return (
<>
<Stack.Screen
@@ -24,7 +48,7 @@ export default function CollaborationRoomInfo() {
<ViewWrapper>
<BoxWithHeaderSection>
<StackCustom>
{listData.map((item, index) => (
{listData({ data }).map((item, index) => (
<Grid key={index}>
<Grid.Col span={4}>
<TextCustom bold>{item.title}</TextCustom>
@@ -37,37 +61,42 @@ export default function CollaborationRoomInfo() {
</StackCustom>
</BoxWithHeaderSection>
<BoxWithHeaderSection>
{Array.from({ length: 10 }).map((_, index) => (
<AvatarUsernameAndOtherComponent key={index} avatarHref={`/profile/${index}`} />
))}
</BoxWithHeaderSection>
<BaseBox>
<StackCustom gap={10}>
{data?.ProjectCollaboration_AnggotaRoomChat?.map(
(item: any, index: number) => (
<AvatarUsernameAndOtherComponent
key={index}
avatarHref={`/profile/${item?.User?.Profile?.id}`}
name={item?.User?.username}
avatar={item?.User?.Profile?.imageId}
/>
)
)}
</StackCustom>
</BaseBox>
</ViewWrapper>
</>
);
}
const listData = [
const listData = ({ data }: { data: any }) => [
{
title: "Judul Proyek",
value: "Judul Proyek",
value: data?.ProjectCollaboration?.title || "-",
},
{
title: "Industri",
value: "Pilihan Industri",
},
{
title: "Deskripsi",
value: "Deskripsi Proyek",
value:
data?.ProjectCollaboration?.ProjectCollaborationMaster_Industri?.name ||
"-",
},
{
title: "Tujuan Proyek",
value:
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
value: data?.ProjectCollaboration?.purpose || "-",
},
{
title: "Keuntungan Proyek",
value:
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
value: data?.ProjectCollaboration?.benefit || "-",
},
];

View File

@@ -1,68 +1,13 @@
import {
BackButton,
BoxButtonOnFooter,
Grid,
TextCustom,
TextInputCustom,
ViewWrapper,
} from "@/components";
import { AccentColor, MainColor } from "@/constants/color-palet";
import { BackButton } from "@/components";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import ChatScreen from "@/screens/Collaboration/GroupChatSection";
import { Feather } from "@expo/vector-icons";
import { router, Stack, useLocalSearchParams } from "expo-router";
import { StyleSheet, TouchableOpacity, View } from "react-native";
export default function CollaborationRoomChat() {
const { id, detail } = useLocalSearchParams();
const inputChat = () => {
return (
<>
<BoxButtonOnFooter>
{/* <View style={{flexDirection: 'row', alignItems: 'center'}}>
<TextInputCustom placeholder="Ketik pesan..." />
<TouchableOpacity
activeOpacity={0.5}
onPress={() => console.log("Send")}
style={{
backgroundColor: AccentColor.blue,
padding: 10,
borderRadius: 50,
}}
>
<Feather name="send" size={30} color={MainColor.white} />
</TouchableOpacity>
</View> */}
<Grid>
<Grid.Col span={9}>
<TextInputCustom placeholder="Ketik pesan..." />
</Grid.Col>
<Grid.Col span={1}>
<View />
</Grid.Col>
<Grid.Col span={2} style={{ alignItems: "center" }}>
<TouchableOpacity
activeOpacity={0.5}
onPress={() => console.log("Send")}
style={{
backgroundColor: AccentColor.blue,
padding: 10,
borderRadius: 50,
}}
>
<Feather
name="send"
size={30}
color={MainColor.white}
/>
</TouchableOpacity>
</Grid.Col>
</Grid>
</BoxButtonOnFooter>
</>
);
};
return (
<>
<Stack.Screen
@@ -79,114 +24,8 @@ export default function CollaborationRoomChat() {
),
}}
/>
<ViewWrapper footerComponent={inputChat()}>
{dummyData.map((item, index) => (
<View
key={index}
style={[
styles.messageRow,
item.role === 1 ? styles.rightAlign : styles.leftAlign,
]}
>
<View
style={[
styles.bubble,
item.role === 1 ? styles.bubbleRight : styles.bubbleLeft,
]}
>
<TextCustom style={styles.sender}>{item.nama}</TextCustom>
<TextCustom style={styles.message}>{item.chat}</TextCustom>
<TextCustom style={styles.time}>
{new Date(item.time).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}
</TextCustom>
</View>
</View>
))}
{/* <TextInputCustom placeholder="Ketik pesan..." />
<Spacing/> */}
</ViewWrapper>
<ChatScreen id={id as string} />
</>
);
}
const dummyData = [
{
nama: "Dina",
role: 1,
chat: "Hai! Kamu udah lihat dokumen proyek yang baru?",
time: "2025-07-24T09:01:15Z",
},
{
nama: "Rafi",
role: 2,
chat: "Halo! Iya, aku baru aja baca. Kayaknya kita harus revisi bagian akhir deh.",
time: "2025-07-24T09:02:03Z",
},
{
nama: "Dina",
role: 1,
chat: "Setuju. Aku juga kurang sreg sama penutupnya.",
time: "2025-07-24T09:02:45Z",
},
{
nama: "Rafi",
role: 2,
chat: "Oke, aku coba edit malam ini ya. Nanti aku share ulang versinya.",
time: "2025-07-24T09:03:10Z",
},
{
nama: "Dina",
role: 1,
chat: "Siap, makasih ya. Jangan begadang!",
time: "2025-07-24T09:03:30Z",
},
];
const styles = StyleSheet.create({
container: {
paddingVertical: 10,
paddingHorizontal: 12,
},
messageRow: {
flexDirection: "row",
marginBottom: 12,
},
rightAlign: {
justifyContent: "flex-end",
},
leftAlign: {
justifyContent: "flex-start",
},
bubble: {
maxWidth: "75%",
padding: 10,
borderRadius: 12,
},
bubbleRight: {
backgroundColor: "#DCF8C6", // hijau muda
borderTopRightRadius: 0,
},
bubbleLeft: {
backgroundColor: "#F0F0F0", // abu-abu terang
borderTopLeftRadius: 0,
},
sender: {
fontSize: 12,
fontWeight: "bold",
marginBottom: 2,
color: "#555",
},
message: {
fontSize: 15,
color: "#000",
},
time: {
fontSize: 10,
color: "#888",
textAlign: "right",
marginTop: 4,
},
});

View File

@@ -0,0 +1,80 @@
import {
AlertDefaultSystem,
ButtonCustom,
TextAreaCustom,
ViewWrapper,
} from "@/components";
import { useAuth } from "@/hooks/use-auth";
import { apiCollaborationCreatePartisipasi } from "@/service/api-client/api-collaboration";
import { router, useLocalSearchParams } from "expo-router";
import { useState } from "react";
import Toast from "react-native-toast-message";
export default function CollaborationCreatePartisipans() {
const { user } = useAuth();
const { id } = useLocalSearchParams();
const [description, setDescription] = useState("");
const [isLoading, setLoading] = useState(false);
const handlerSubmitParticipans = async () => {
try {
setLoading(true);
const response = await apiCollaborationCreatePartisipasi({
id: id as string,
data: {
authorId: user?.id,
description,
},
});
if (response.success) {
Toast.show({
type: "success",
text1: "Data berhasil disimpan",
});
router.replace(`/collaboration/${id}/list-of-participants`);
} else {
Toast.show({
type: "error",
text1: "Gagal menyimpan data",
});
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoading(false);
}
};
return (
<ViewWrapper>
<TextAreaCustom
// label="Deskripsi"
placeholder="Masukan deskripsi diri anda .."
value={description}
onChangeText={setDescription}
required
showCount
maxLength={1000}
/>
<ButtonCustom
disabled={description.length === 0}
isLoading={isLoading}
onPress={() => {
AlertDefaultSystem({
title: "Simpan data deskripsi",
message: "Apakah anda sudah yakin ingin menyimpan data ini ?",
textLeft: "Batal",
textRight: "Simpan",
onPressRight: () => {
handlerSubmitParticipans();
},
});
}}
>
Simpan
</ButtonCustom>
</ViewWrapper>
);
}

View File

@@ -1,26 +1,54 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
AvatarUsernameAndOtherComponent,
BaseBox,
BackButton,
DotButton,
DrawerCustom,
StackCustom,
TextCustom,
ViewWrapper,
MenuDrawerDynamicGrid,
ViewWrapper
} from "@/components";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import Collaboration_BoxDetailSection from "@/screens/Collaboration/BoxDetailSection";
import { MaterialIcons } from "@expo/vector-icons";
import { useLocalSearchParams } from "expo-router";
import { useState } from "react";
import { apiCollaborationGetOne } from "@/service/api-client/api-collaboration";
import { Ionicons } from "@expo/vector-icons";
import { router, Stack, useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, useState } from "react";
export default function CollaborationDetailParticipant() {
const { id } = useLocalSearchParams();
const [openDrawerParticipant, setOpenDrawerParticipant] = useState(false);
const [data, setData] = useState<any>();
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id])
);
const onLoadData = async () => {
try {
const response = await apiCollaborationGetOne({ id: id as string });
if (response.success) {
setData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
}
};
return (
<>
<Stack.Screen
options={{
title: "Detail Proyek",
headerLeft: () => <BackButton />,
headerRight: () => (
<DotButton onPress={() => setOpenDrawerParticipant(true)} />
),
}}
/>
<ViewWrapper>
<Collaboration_BoxDetailSection id={id as string} />
<BaseBox style={{ height: 500 }}>
<Collaboration_BoxDetailSection data={data} />
{/* <BaseBox style={{ height: 500 }}>
<TextCustom align="center" bold size="large">
Partisipan
</TextCustom>
@@ -39,13 +67,33 @@ export default function CollaborationDetailParticipant() {
}
/>
))}
</BaseBox>
</BaseBox> */}
</ViewWrapper>
<DrawerCustom
isVisible={openDrawerParticipant}
closeDrawer={() => setOpenDrawerParticipant(false)}
height={"auto"}
>
<MenuDrawerDynamicGrid
data={[
{
icon: <Ionicons name="people" size={24} color="white" />,
label: "Daftar Partisipan",
path: `/collaboration/${id}/list-of-participants`,
},
]}
onPressItem={(item) => {
router.push(item.path as any);
setOpenDrawerParticipant(false);
}}
/>
</DrawerCustom>
{/* <DrawerCustom
isVisible={openDrawerParticipant}
closeDrawer={() => setOpenDrawerParticipant(false)}
height={"auto"}
>
<StackCustom>
<TextCustom bold>Deskripsi Diri</TextCustom>
@@ -56,7 +104,7 @@ export default function CollaborationDetailParticipant() {
Temporibus iusto soluta necessitatibus.
</TextCustom>
</StackCustom>
</DrawerCustom>
</DrawerCustom> */}
</>
);
}

View File

@@ -1,30 +1,65 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
AlertDefaultSystem,
BackButton,
ButtonCustom,
DotButton,
DrawerCustom,
LoaderCustom,
MenuDrawerDynamicGrid,
Spacing,
StackCustom,
TextCustom,
ViewWrapper,
ViewWrapper
} from "@/components";
import { IconEdit } from "@/components/_Icon";
import Collaboration_BoxDetailSection from "@/screens/Collaboration/BoxDetailSection";
import Collaboration_MainParticipanSelectedSection from "@/screens/Collaboration/ProjectMainSelectedSection";
import { router, Stack, useLocalSearchParams } from "expo-router";
import { useState } from "react";
import {
apiCollaborationGetOne
} from "@/service/api-client/api-collaboration";
import { MaterialIcons } from "@expo/vector-icons";
import {
router,
Stack,
useFocusEffect,
useLocalSearchParams,
} from "expo-router";
import { useCallback, useState } from "react";
export default function CollaborationDetailProjectMain() {
const { id } = useLocalSearchParams();
const [openDrawer, setOpenDrawer] = useState(false);
const [openDrawerParticipant, setOpenDrawerParticipant] = useState(false);
const [selected, setSelected] = useState<(string | number)[]>([]);
const [data, setData] = useState<any>();
const [loadingGetData, setLoadingGetData] = useState(false);
const handleEdit = () => {
console.log("Edit collaboration");
router.push("/(application)/(user)/collaboration/(id)/edit");
useFocusEffect(
useCallback(() => {
handlerLoadData();
}, [id])
);
const handlerLoadData = async () => {
try {
setLoadingGetData(true);
await onLoadData();
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingGetData(false);
}
};
const onLoadData = async () => {
try {
const response = await apiCollaborationGetOne({ id: id as string });
if (response.success) {
setData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
}
};
const handleSubmit = (item: any) => {
console.log("item :", item);
router.push(item.path);
setOpenDrawer(false);
};
return (
@@ -37,34 +72,21 @@ export default function CollaborationDetailProjectMain() {
}}
/>
<ViewWrapper>
<Collaboration_BoxDetailSection id={id as string} />
<Collaboration_MainParticipanSelectedSection
selected={selected}
setSelected={setSelected}
setOpenDrawerParticipant={setOpenDrawerParticipant}
/>
{loadingGetData ? (
<LoaderCustom />
) : (
<>
<Collaboration_BoxDetailSection data={data} />
{/* <Collaboration_MainParticipanSelectedSection
selected={selected}
setSelected={setSelected}
setOpenDrawerParticipant={setOpenDrawerParticipant}
listData={listData as any}
/> */}
<ButtonCustom
onPress={() => {
AlertDefaultSystem({
title: "Buat Grup",
message:
"Apakah anda yakin ingin membuat grup untuk proyek ini ?",
textLeft: "Tidak",
textRight: "Ya",
onPressLeft: () => {},
onPressRight: () => {
router.navigate(
"/(application)/(user)/collaboration/(tabs)/group"
);
console.log("selected :", selected);
},
});
}}
>
Buat Grup
</ButtonCustom>
<Spacing />
<Spacing />
</>
)}
</ViewWrapper>
<DrawerCustom
@@ -76,31 +98,20 @@ export default function CollaborationDetailProjectMain() {
data={[
{
label: "Edit",
path: "/(application)/(user)/collaboration/(tabs)/group",
path: `/(application)/(user)/collaboration/${id}/edit`,
icon: <IconEdit />,
},
{
label: "Pilih Partisipan",
path: `/(application)/(user)/collaboration/${id}/select-of-participants`,
icon: <MaterialIcons name="checklist" size={24} color="white" />,
},
]}
onPressItem={(item) => {
handleEdit();
onPressItem={(item: any) => {
handleSubmit(item);
}}
/>
</DrawerCustom>
<DrawerCustom
isVisible={openDrawerParticipant}
closeDrawer={() => setOpenDrawerParticipant(false)}
height={"auto"}
>
<StackCustom>
<TextCustom bold>Deskripsi Diri</TextCustom>
<TextCustom>
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Commodi,
itaque adipisci. Voluptas, sed quod! Ad facere labore voluptates,
neque quidem aut reprehenderit ducimus mollitia quisquam temporibus!
Temporibus iusto soluta necessitatibus.
</TextCustom>
</StackCustom>
</DrawerCustom>
</>
);
}

View File

@@ -1,53 +1,171 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
ButtonCustom,
LoaderCustom,
SelectCustom,
StackCustom,
TextAreaCustom,
TextInputCustom,
ViewWrapper,
} from "@/components";
import { router } from "expo-router";
import {
apiCollaborationEditData,
apiCollaborationGetOne,
} from "@/service/api-client/api-collaboration";
import { apiMasterCollaborationType } from "@/service/api-client/api-master";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, useState } from "react";
import Toast from "react-native-toast-message";
export default function CollaborationEdit() {
const { id } = useLocalSearchParams();
console.log("id :", id);
const [data, setData] = useState<any>();
const [listMaster, setListMaster] = useState<any[]>([]);
const [loadingData, setLoadingData] = useState(false);
const [isLoading, setIsLoading] = useState(false);
useFocusEffect(
useCallback(() => {
const fetchData = async () => {
try {
setLoadingData(true);
await onLoadData();
await onLoadMaster();
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingData(false);
}
};
fetchData();
}, [id])
);
const onLoadData = async () => {
try {
const response = await apiCollaborationGetOne({ id: id as string });
if (response.success) {
setData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
}
};
async function onLoadMaster() {
try {
const response = await apiMasterCollaborationType();
setListMaster(response.data);
} catch (error) {
console.log("[ERROR]", error);
}
}
const handlerSubmitUpdate = async () => {
if (
!data?.title ||
!data?.lokasi ||
!data?.projectCollaborationMaster_IndustriId ||
!data?.purpose ||
!data?.benefit
) {
Toast.show({
type: "error",
text1: "Gagal",
text2: "Harap isi semua data",
});
return;
}
try {
setIsLoading(true);
const response = await apiCollaborationEditData({
id: id as string,
data: data,
});
if (response.success) {
Toast.show({
type: "success",
text1: response.message,
});
router.back();
} else {
Toast.show({
type: "error",
text1: response.message,
});
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoading(false);
}
};
return (
<ViewWrapper>
<StackCustom gap={"xs"}>
<TextInputCustom label="Judul" placeholder="Masukan judul" required />
<TextInputCustom label="Lokasi" placeholder="Masukan lokasi" required />
<SelectCustom
label="Pilih Industri"
data={[
{ label: "Industri 1", value: "industri-1" },
{ label: "Industri 2", value: "industri-2" },
{ label: "Industri 3", value: "industri-3" },
]}
onChange={(value) => console.log(value)}
/>
{loadingData ? (
<LoaderCustom />
) : (
<StackCustom gap={"xs"}>
<TextInputCustom
label="Judul"
placeholder="Masukan judul"
required
value={data?.title}
onChangeText={(value) => setData({ ...data, title: value })}
/>
<TextInputCustom
label="Lokasi"
placeholder="Masukan lokasi"
required
value={data?.lokasi}
onChangeText={(value) => setData({ ...data, lokasi: value })}
/>
<SelectCustom
label="Pilih Industri"
data={listMaster?.map((item: any) => ({
label: item.name,
value: item.id,
}))}
value={data?.projectCollaborationMaster_IndustriId}
onChange={(value) =>
setData({ ...data, projectCollaborationMaster_IndustriId: value })
}
/>
<TextAreaCustom
required
label="Tujuan Proyek"
placeholder="Masukan tujuan proyek"
showCount
maxLength={1000}
/>
<TextAreaCustom
required
label="Tujuan Proyek"
placeholder="Masukan tujuan proyek"
showCount
maxLength={1000}
value={data?.purpose}
onChangeText={(value) => setData({ ...data, purpose: value })}
/>
<TextAreaCustom
required
label="Keuntungan Proyek"
placeholder="Masukan keuntungan proyek"
showCount
maxLength={1000}
/>
<TextAreaCustom
required
label="Keuntungan Proyek"
placeholder="Masukan keuntungan proyek"
showCount
maxLength={1000}
value={data?.benefit}
onChangeText={(value) => setData({ ...data, benefit: value })}
/>
<ButtonCustom
title="Update"
onPress={() => {
console.log("Update proyek");
router.back();
}}
/>
</StackCustom>
<ButtonCustom
isLoading={isLoading}
title="Update"
onPress={() => {
handlerSubmitUpdate();
}}
/>
</StackCustom>
)}
</ViewWrapper>
);
}

View File

@@ -1,22 +1,75 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
AlertDefaultSystem,
BackButton,
ButtonCustom,
DotButton,
DrawerCustom,
InformationBox,
LoaderCustom,
MenuDrawerDynamicGrid,
TextAreaCustom,
ViewWrapper,
} from "@/components";
import { useAuth } from "@/hooks/use-auth";
import Collaboration_BoxDetailSection from "@/screens/Collaboration/BoxDetailSection";
import {
apiCollaborationGetOne,
apiCollaborationGetParticipants,
} from "@/service/api-client/api-collaboration";
import { 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 { useCallback, useState } from "react";
export default function CollaborationDetail() {
const { user } = useAuth();
const { id } = useLocalSearchParams();
const [openDrawerPartisipasi, setOpenDrawerPartisipasi] = useState(false);
const [data, setData] = useState<any>();
const [openDrawerMenu, setOpenDrawerMenu] = useState(false);
const [isParticipant, setIsParticipant] = useState(false);
const [loadingIsParticipant, setLoadingIsParticipant] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
onLoadParticipants();
}, [id])
);
const onLoadData = async () => {
try {
const response = await apiCollaborationGetOne({ id: id as string });
if (response.success) {
setData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
}
};
const onLoadParticipants = async () => {
try {
setLoadingIsParticipant(true);
const response = await apiCollaborationGetParticipants({
category: "check-participant",
id: id as string,
authorId: user?.id,
});
if (response.success) {
setIsParticipant(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingIsParticipant(false);
}
};
return (
<>
<Stack.Screen
@@ -29,15 +82,35 @@ export default function CollaborationDetail() {
}}
/>
<ViewWrapper>
<Collaboration_BoxDetailSection id={id as string} />
<ButtonCustom onPress={() => setOpenDrawerPartisipasi(true)}>
Partisipasi
</ButtonCustom>
{!data && !isParticipant ? (
<LoaderCustom />
) : (
<>
{user?.id === data?.Author?.id && (
<InformationBox
text={
"Tombol partisipasi hanya muncul pada proyek milik orang lain"
}
/>
)}
<Collaboration_BoxDetailSection data={data} />
{user?.id !== data?.Author?.id && (
<ButtonCustom
disabled={isParticipant || loadingIsParticipant}
onPress={() => {
router.push(`/collaboration/${id}/create-pacticipants`);
// setOpenDrawerPartisipasi(true);
}}
>
{isParticipant ? "Anda telah berpartisipasi" : "Partisipasi"}
</ButtonCustom>
)}
</>
)}
</ViewWrapper>
{/* Drawer Partisipasi */}
<DrawerCustom
{/* <DrawerCustom
isVisible={openDrawerPartisipasi}
closeDrawer={() => setOpenDrawerPartisipasi(false)}
height={300}
@@ -48,6 +121,8 @@ export default function CollaborationDetail() {
required
showCount
maxLength={500}
value={description}
onChangeText={setDescription}
/>
<ButtonCustom
@@ -58,19 +133,21 @@ export default function CollaborationDetail() {
message: "Apakah anda sudah yakin ingin menyimpan data ini ?",
textLeft: "Batal",
textRight: "Simpan",
onPressRight: () => router.replace(`/collaboration/(tabs)/group`),
onPressRight: () => {
handlerSubmitParticipans();
},
});
}}
>
Simpan
</ButtonCustom>
</DrawerCustom>
</DrawerCustom> */}
{/* Drawer Menu */}
<DrawerCustom
isVisible={openDrawerMenu}
closeDrawer={() => setOpenDrawerMenu(false)}
height={250}
height={"auto"}
>
<MenuDrawerDynamicGrid
data={[

View File

@@ -1,38 +1,79 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
AvatarUsernameAndOtherComponent,
BaseBox,
DrawerCustom,
Spacing,
StackCustom,
TextCustom,
ViewWrapper
AvatarUsernameAndOtherComponent,
BaseBox,
DrawerCustom,
LoaderCustom,
Spacing,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import { apiCollaborationGetParticipants } from "@/service/api-client/api-collaboration";
import { Feather } from "@expo/vector-icons";
import { useLocalSearchParams } from "expo-router";
import { useState } from "react";
import _ from "lodash";
import { useEffect, useState } from "react";
import { ScrollView } from "react-native";
export default function CollaborationListOfParticipants() {
const { id } = useLocalSearchParams();
const [listData, setListData] = useState<any[]>();
const [loadingGetData, setLoadingGetData] = useState(false);
const [description, setDescription] = useState("");
useEffect(() => {
onLoadData();
}, [id]);
const onLoadData = async () => {
try {
setLoadingGetData(true);
const response = await apiCollaborationGetParticipants({
category: "list",
id: id as string,
});
if (response.success) {
setListData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingGetData(false);
}
};
const [openDrawer, setOpenDrawer] = useState(false);
return (
<>
<ViewWrapper>
{Array.from({ length: 10 }).map((_, index) => (
<BaseBox key={index} paddingBlock={5}>
<AvatarUsernameAndOtherComponent
avatarHref={`/profile/${id}`}
rightComponent={
<Feather
name="chevron-right"
size={24}
color="white"
onPress={() => setOpenDrawer(true)}
/>
}
/>
</BaseBox>
))}
{loadingGetData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Tidak ada partisipan</TextCustom>
) : (
listData?.map((item: any, index: number) => (
<BaseBox key={index} paddingBlock={5}>
<AvatarUsernameAndOtherComponent
avatar={item?.User?.Profile?.imageId}
avatarHref={`/profile/${item?.User?.Profile?.id}`}
name={item?.User?.username}
rightComponent={
<Feather
name="chevron-right"
size={24}
color="white"
onPress={() => {
setDescription(item?.deskripsi_diri);
setOpenDrawer(true);
}}
/>
}
/>
</BaseBox>
))
)}
</ViewWrapper>
{/* Drawer */}
@@ -44,34 +85,7 @@ export default function CollaborationListOfParticipants() {
<TextCustom bold>Deskripsi diri</TextCustom>
<BaseBox>
<ScrollView style={{ height: "80%" }}>
<TextCustom>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua.
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua.Lorem
ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua.Lorem
ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua.Lorem
ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut iqua.Lorem ipsum dolor sit amet,
consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
labore et dolore magna aliqua.Lorem ipsum dolor sit amet,
consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
labore et dolore magna aliqua.Lorem ipsum dolor sit amet,
consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
labore et dolore magna aliqua.Lorem ipsum dolor sit amet,
consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
labore et dolore magna aliqua.Lorem ipsum dolor sit amet,
consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
labore et dolore magna aliqua.Lorem ipsum dolor sit amet,
consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
labore et dolore magna aliqua.Lorem ipsum dolor sit amet,
consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
labore et dolore magna aliqua.Lorem ipsum dolor sit amet,
consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
labore et dolore magna aliqua.
</TextCustom>
<TextCustom>{description}</TextCustom>
</ScrollView>
</BaseBox>
<Spacing />

View File

@@ -0,0 +1,251 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
AlertDefaultSystem,
AvatarComp,
BaseBox,
BoxButtonOnFooter,
ButtonCustom,
CheckboxCustom,
CheckboxGroup,
DrawerCustom,
Grid,
LoaderCustom,
Spacing,
StackCustom,
TextAreaCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import ModalCustom from "@/components/Modal/ModalCustom";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import { useAuth } from "@/hooks/use-auth";
import {
apiCollaborationCreateGroup,
apiCollaborationGetParticipants,
} from "@/service/api-client/api-collaboration";
import { MaterialIcons } from "@expo/vector-icons";
import { router, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useEffect, useState } from "react";
import { ScrollView, View } from "react-native";
import Toast from "react-native-toast-message";
export default function CollaborationSelectOfParticipants() {
const { user } = useAuth();
const { id } = useLocalSearchParams();
const [listData, setListData] = useState<any[]>();
const [loadingGetData, setLoadingGetData] = useState(false);
const [description, setDescription] = useState("");
const [selected, setSelected] = useState<(string | number)[]>([]);
const [openDrawer, setOpenDrawer] = useState(false);
const [nameGroup, setNameGroup] = useState("");
const [openModal, setOpenModal] = useState(false);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
onLoadData();
}, [id]);
const onLoadData = async () => {
try {
setLoadingGetData(true);
const response = await apiCollaborationGetParticipants({
category: "list",
id: id as string,
});
// console.log("response :", JSON.stringify(response.data, null, 2));
if (response.success) {
setListData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingGetData(false);
}
};
const handlerCreateGroup = async () => {
if (_.isEmpty(nameGroup)) {
Toast.show({
type: "error",
text1: "Nama grup tidak boleh kosong",
});
return;
}
try {
setIsLoading(true);
const response = await apiCollaborationCreateGroup({
id: id as string,
data: {
authorId: user?.id,
nameGroup: nameGroup,
listSelect: selected,
},
});
if (response.success) {
Toast.show({
type: "success",
text1: "Grup berhasil dibuat",
});
router.push("/(application)/(user)/collaboration/(tabs)/group");
} else {
Toast.show({
type: "error",
text1: "Gagal membuat grup",
});
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoading(false);
}
};
const handlerSubmit = () => {
return (
<>
<BoxButtonOnFooter>
<ButtonCustom
disabled={_.isEmpty(selected)}
onPress={() => {
setOpenModal(true);
setNameGroup("");
}}
>
Buat Grup
</ButtonCustom>
</BoxButtonOnFooter>
</>
);
};
return (
<>
<ViewWrapper
footerComponent={_.isEmpty(listData) ? null : handlerSubmit()}
>
{loadingGetData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Tidak ada partisipan</TextCustom>
) : (
<StackCustom>
<TextCustom size="default" color="red" bold>
*{" "}
<TextCustom size="small" semiBold>
Pilih user yang akan menjadi tim proyek anda
</TextCustom>
</TextCustom>
<CheckboxGroup
value={selected}
onChange={(val: any) => {
console.log("val :", val);
setSelected(val);
}}
>
{listData?.map((item: any, index: any) => (
<View key={index}>
<Grid key={index}>
<Grid.Col
span={2}
style={{ alignItems: "center", justifyContent: "center" }}
>
<CheckboxCustom valueKey={item?.User?.id} />
</Grid.Col>
<Grid.Col span={2} style={{ alignItems: "center" }}>
<AvatarComp
size="base"
fileId={item?.User?.Profile?.imageId}
/>
</Grid.Col>
<Grid.Col span={6} style={{ justifyContent: "center" }}>
<TextCustom bold truncate>
{item?.User?.username}
</TextCustom>
</Grid.Col>
<Grid.Col
span={2}
style={{ alignItems: "center", justifyContent: "center" }}
>
<MaterialIcons
name="notes"
size={ICON_SIZE_SMALL}
color="white"
onPress={() => {
setOpenDrawer(true);
setDescription(item?.deskripsi_diri);
}}
/>
</Grid.Col>
</Grid>
</View>
))}
</CheckboxGroup>
</StackCustom>
)}
</ViewWrapper>
<ModalCustom isVisible={openModal}>
<StackCustom gap={0}>
<TextAreaCustom
placeholder="Nama Grup"
value={nameGroup}
onChangeText={(val) => setNameGroup(val)}
/>
<Grid>
<Grid.Col span={6} style={{ paddingRight: 10 }}>
<ButtonCustom
backgroundColor="gray"
onPress={() => {
setOpenModal(false);
}}
>
Batal
</ButtonCustom>
</Grid.Col>
<Grid.Col span={6} style={{ paddingLeft: 10 }}>
<ButtonCustom
isLoading={isLoading}
disabled={_.isEmpty(nameGroup)}
onPress={() => {
AlertDefaultSystem({
title: "Buat Grup",
message:
"Apakah anda yakin ingin membuat grup untuk proyek ini ?",
textLeft: "Tidak",
textRight: "Ya",
onPressLeft: () => {},
onPressRight: () => {
handlerCreateGroup();
},
});
}}
>
Simpan
</ButtonCustom>
</Grid.Col>
</Grid>
</StackCustom>
</ModalCustom>
{/* Drawer */}
<DrawerCustom
isVisible={openDrawer}
closeDrawer={() => setOpenDrawer(false)}
>
<StackCustom>
<TextCustom bold>Deskripsi diri</TextCustom>
<BaseBox>
<ScrollView style={{ height: "80%" }}>
<TextCustom>{description}</TextCustom>
</ScrollView>
</BaseBox>
<Spacing />
</StackCustom>
</DrawerCustom>
</>
);
}

View File

@@ -1,53 +1,174 @@
import {
ButtonCustom,
SelectCustom,
StackCustom,
TextAreaCustom,
TextInputCustom,
ViewWrapper
ButtonCustom,
LoaderCustom,
SelectCustom,
StackCustom,
TextAreaCustom,
TextInputCustom,
ViewWrapper,
} from "@/components";
import { useAuth } from "@/hooks/use-auth";
import { apiCollaborationCreate } from "@/service/api-client/api-collaboration";
import { apiMasterCollaborationType } from "@/service/api-client/api-master";
import { router } from "expo-router";
import React, { useEffect, useState } from "react";
import Toast from "react-native-toast-message";
interface CollaborationCreateProps {
title?: string;
lokasi?: string;
purpose?: string;
benefit?: string;
projectCollaborationMaster_IndustriId?: string;
userId?: string;
}
export default function CollaborationCreate() {
const { user } = useAuth();
const [listMaster, setListMaster] = useState<any>([]);
const [loadingMaster, setLoadingMaster] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [data, setData] = React.useState<CollaborationCreateProps>({
title: "",
lokasi: "",
purpose: "",
benefit: "",
projectCollaborationMaster_IndustriId: "",
userId: "",
});
useEffect(() => {
onLoadMaster();
}, []);
async function onLoadMaster() {
try {
setLoadingMaster(true);
const response = await apiMasterCollaborationType();
setListMaster(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingMaster(false);
}
}
const handlerSubmit = async () => {
if (
!data?.title ||
!data?.lokasi ||
!data?.purpose ||
!data?.benefit ||
!data?.projectCollaborationMaster_IndustriId
) {
Toast.show({
type: "error",
text1: "Gagal",
text2: "Harap isi semua data",
});
return;
}
const newData: CollaborationCreateProps = {
title: data?.title,
lokasi: data?.lokasi,
purpose: data?.purpose,
benefit: data?.benefit,
projectCollaborationMaster_IndustriId:
data?.projectCollaborationMaster_IndustriId,
userId: user?.id,
};
try {
setIsLoading(true);
const response = await apiCollaborationCreate({ data: newData });
if (response.success) {
Toast.show({
type: "success",
text1: "Berhasil",
text2: response.message,
});
router.back();
} else {
Toast.show({
type: "error",
text1: "Gagal",
text2: response.message,
});
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoading(false);
}
};
return (
<ViewWrapper>
<StackCustom gap={"xs"}>
<TextInputCustom label="Judul" placeholder="Masukan judul" required />
<TextInputCustom label="Lokasi" placeholder="Masukan lokasi" required />
<SelectCustom
label="Pilih Industri"
data={[
{ label: "Industri 1", value: "industri-1" },
{ label: "Industri 2", value: "industri-2" },
{ label: "Industri 3", value: "industri-3" },
]}
onChange={(value) => console.log(value)}
/>
{loadingMaster ? (
<LoaderCustom />
) : (
<StackCustom gap={"xs"}>
<TextInputCustom
label="Judul"
placeholder="Masukan judul"
required
value={data?.title}
onChangeText={(value: any) => setData({ ...data, title: value })}
/>
<TextAreaCustom
required
label="Tujuan Proyek"
placeholder="Masukan tujuan proyek"
showCount
maxLength={1000}
/>
<TextInputCustom
label="Lokasi"
placeholder="Masukan lokasi"
required
value={data?.lokasi}
onChangeText={(value: any) => setData({ ...data, lokasi: value })}
/>
<TextAreaCustom
required
label="Keuntungan Proyek"
placeholder="Masukan keuntungan proyek"
showCount
maxLength={1000}
/>
<SelectCustom
label="Pilih Industri"
data={listMaster?.map((item: any) => ({
label: item.name,
value: item.id,
}))}
value={data?.projectCollaborationMaster_IndustriId}
onChange={(value: any) => {
console.log(value);
setData({
...data,
projectCollaborationMaster_IndustriId: value,
});
}}
/>
<ButtonCustom
title="Simpan"
onPress={() => {
console.log("Simpan proyek");
router.back();
}}
/>
</StackCustom>
<TextAreaCustom
required
label="Tujuan Proyek"
placeholder="Masukan tujuan proyek"
showCount
maxLength={1000}
value={data?.purpose}
onChangeText={(value: any) => setData({ ...data, purpose: value })}
/>
<TextAreaCustom
required
label="Keuntungan Proyek"
placeholder="Masukan keuntungan proyek"
showCount
maxLength={1000}
value={data?.benefit}
onChangeText={(value: any) => setData({ ...data, benefit: value })}
/>
<ButtonCustom
isLoading={isLoading}
title="Simpan"
onPress={() => handlerSubmit()}
/>
</StackCustom>
)}
</ViewWrapper>
);
}

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,35 +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>
@@ -37,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)}
>
@@ -72,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

@@ -4,18 +4,47 @@ import {
TextAreaCustom,
ViewWrapper,
} from "@/components";
import { useAuth } from "@/hooks/use-auth";
import { apiForumCreate } from "@/service/api-client/api-forum";
import { router } from "expo-router";
import { useState } from "react";
import Toast from "react-native-toast-message";
export default function ForumCreate() {
const { user } = useAuth();
const [text, setText] = useState("");
const [isLoading, setIsLoading] = useState(false);
const handlerSubmit = async () => {
const newData = {
diskusi: text,
authorId: user?.id,
};
try {
setIsLoading(true);
const response = await apiForumCreate({ data: newData });
if (response.success) {
Toast.show({
type: "success",
text1: "Posting berhasil",
});
setText("");
router.back();
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoading(false);
}
};
const buttonFooter = (
<BoxButtonOnFooter>
<ButtonCustom
isLoading={isLoading}
onPress={() => {
console.log("Posting", text);
router.back();
handlerSubmit();
}}
>
Posting

View File

@@ -1,25 +1,58 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
AlertCustom,
AvatarCustom,
AvatarComp,
BackButton,
DrawerCustom,
LoaderCustom,
SearchInput,
TextCustom,
ViewWrapper,
} from "@/components";
import FloatingButton from "@/components/Button/FloatingButton";
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 { router, Stack } 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, Stack, useFocusEffect } from "expo-router";
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 [alertStatus, setAlertStatus] = useState(false);
const [deleteAlert, setDeleteAlert] = useState(false);
const { user } = useAuth();
const [dataUser, setDataUser] = useState<any>();
const [listData, setListData] = useState<any[]>();
const [loadingGetList, setLoadingGetList] = useState(false);
const [search, setSearch] = useState("");
const [dataId, setDataId] = useState("");
const [authorId, setAuthorId] = useState("");
useFocusEffect(
useCallback(() => {
onLoadData();
onLoadDataProfile(user?.id as string);
}, [user?.id, search])
);
const onLoadDataProfile = async (id: string) => {
const response = await apiUser(id);
setDataUser(response.data);
};
const onLoadData = async () => {
try {
setLoadingGetList(true);
const response = await apiForumGetAll({ search: search });
setListData(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingGetList(false);
}
};
return (
<>
@@ -27,12 +60,23 @@ export default function Forum() {
options={{
title: "Forum",
headerLeft: () => <BackButton />,
headerRight: () => <AvatarCustom href={`/forum/${id}/forumku`} />,
headerRight: () => (
<AvatarComp
fileId={dataUser?.Profile?.imageId}
size="base"
href={`/forum/${user?.id}/forumku`}
/>
),
}}
/>
<ViewWrapper
headerComponent={<SearchInput placeholder="Cari topik diskusi" />}
headerComponent={
<SearchInput
placeholder="Cari topik diskusi"
onChangeText={(e) => setSearch(e)}
/>
}
floatingButton={
<FloatingButton
onPress={() =>
@@ -41,73 +85,45 @@ export default function Forum() {
/>
}
>
{listDummyDiscussionForum.map((e, i) => (
<Forum_BoxDetailSection
key={i}
data={e}
setOpenDrawer={setOpenDrawer}
setStatus={setStatus}
isTruncate={true}
href={`/forum/${id}`}
/>
))}
{loadingGetList ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center" color="gray">
Tidak ada diskusi
</TextCustom>
) : (
listData?.map((e: any, i: number) => (
<Forum_BoxDetailSection
key={i}
data={e}
onSetData={() => {
setDataId(e.id);
setOpenDrawer(true);
setStatus(e.ForumMaster_StatusPosting?.status);
setAuthorId(e.Author?.id);
}}
isTruncate={true}
href={`/forum/${e.id}`}
isRightComponent={false}
/>
))
)}
</ViewWrapper>
<DrawerCustom
height={350}
height={"auto"}
isVisible={openDrawer}
closeDrawer={() => setOpenDrawer(false)}
>
<Forum_MenuDrawerBerandaSection
id={id}
id={dataId}
authorId={authorId}
status={status}
setIsDrawerOpen={() => {
setOpenDrawer(false);
}}
setShowDeleteAlert={setDeleteAlert}
setShowAlertStatus={setAlertStatus}
/>
</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

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

@@ -1,3 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BackButton,
DotButton,
@@ -12,16 +13,42 @@ import { ICON_SIZE_MEDIUM } from "@/constants/constans-value";
import Investment_ButtonInvestasiSection from "@/screens/Invesment/ButtonInvestasiSection";
import Invesment_ComponentBoxOnBottomDetail from "@/screens/Invesment/ComponentBoxOnBottomDetail";
import Invesment_DetailDataPublishSection from "@/screens/Invesment/DetailDataPublishSection";
import { apiInvestmentGetById } from "@/service/api-client/api-investment";
import { AntDesign, MaterialIcons } from "@expo/vector-icons";
import { router, Stack, useLocalSearchParams } from "expo-router";
import {
router,
Stack,
useFocusEffect,
useLocalSearchParams,
} from "expo-router";
import _ from "lodash";
import { useState } from "react";
import { useCallback, useState } from "react";
export default function InvestmentDetailStatus() {
const { id, status } = useLocalSearchParams();
const [openDrawerDraft, setOpenDrawerDraft] = useState(false);
const [openDrawerPublish, setOpenDrawerPublish] = useState(false);
const [data, setData] = useState<any>(null);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id, status])
);
const onLoadData = async () => {
try {
const response = await apiInvestmentGetById({
id: id as string,
});
// console.log("[DATA]", JSON.stringify(response.data, null, 2));
setData(response.data);
} catch (error) {
console.log("[ERROR]", error);
}
};
const handlePressDraft = (item: IMenuDrawerItem) => {
console.log("PATH >> ", item.path);
router.navigate(item.path as any);
@@ -63,6 +90,7 @@ export default function InvestmentDetailStatus() {
<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 {
ButtonCenteredOnly,
ButtonCustom,
@@ -7,34 +8,192 @@ import {
Spacing,
StackCustom,
TextInputCustom,
ViewWrapper
ViewWrapper,
} from "@/components";
import API_STRORAGE from "@/constants/base-url-api-strorage";
import DIRECTORY_ID from "@/constants/directory-id";
import dummyPembagianDeviden from "@/lib/dummy-data/investment/pembagian-deviden";
import dummyListPencarianInvestor from "@/lib/dummy-data/investment/pencarian-investor";
import dummyPeriodeDeviden from "@/lib/dummy-data/investment/periode-deviden";
import { router } from "expo-router";
import { useState } from "react";
import {
apiInvestmentGetById,
apiInvestmentUpdateData,
} from "@/service/api-client/api-investment";
import {
deleteImageService,
uploadImageService,
} from "@/service/upload-service";
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
import pickFile from "@/utils/pickFile";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, useState } from "react";
import Toast from "react-native-toast-message";
interface IInvestment {
title?: string;
targetDana?: string;
hargaLembar?: string;
totalLembar?: string;
roi?: string;
masterPencarianInvestorId?: string;
masterPeriodeDevidenId?: string;
masterPembagianDevidenId?: string;
authorId?: string;
imageId?: string;
prospektusFileId?: string;
}
export default function InvestmentEdit() {
const [data, setData] = useState({
const { id } = useLocalSearchParams();
const [data, setData] = useState<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 displayTargetDana = formatCurrencyDisplay(data?.targetDana);
const displayHargaPerLembar = formatCurrencyDisplay(data?.hargaLembar);
const displayTotalLembar = formatCurrencyDisplay(
Number(data?.targetDana) / Number(data?.hargaLembar)
);
const handleChangeCurrency = (field: keyof typeof data) => (text: string) => {
const numeric = text.replace(/\D/g, "");
setData((prev) => ({ ...prev, [field]: numeric }));
};
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id])
);
const onLoadData = async () => {
try {
const response = await apiInvestmentGetById({
id: id as string,
});
// console.log("[DATA]", JSON.stringify(response.data, null, 2));
setData(response.data);
} catch (error) {
console.log("[ERROR]", error);
}
};
const handleSubmitUpdate = async () => {
let newData = {
...data,
};
if (
newData?.title === "" ||
newData?.targetDana === "" ||
newData?.hargaLembar === "" ||
newData?.totalLembar === "" ||
newData?.roi === "" ||
newData?.masterPencarianInvestorId === "" ||
newData?.masterPeriodeDevidenId === "" ||
newData?.masterPembagianDevidenId === ""
) {
Toast.show({
type: "info",
text1: "Harap isi semua data",
});
return;
}
try {
setIsLoading(true);
if (image) {
const responseUploadImage = await uploadImageService({
imageUri: image,
dirId: DIRECTORY_ID.investasi_image,
});
if (!responseUploadImage.success) {
Toast.show({
type: "error",
text1: "Gagal mengunggah gambar",
});
return;
}
const deletePrevImage = await deleteImageService({
id: data?.imageId as any,
});
if (!deletePrevImage.success) {
Toast.show({
type: "error",
text1: "Gagal menghapus gambar",
});
return;
}
newData = {
...newData,
imageId: responseUploadImage.data.id,
};
}
const responseUpdate = await apiInvestmentUpdateData({
id: id as string,
data: newData,
});
console.log("[RESPONSE UPDATE]", JSON.parse(JSON.stringify(responseUpdate)));
if (responseUpdate.success) {
Toast.show({
type: "success",
text1: "Data berhasil diupdate",
});
router.back();
} else {
Toast.show({
type: "error",
text1: responseUpdate.message,
});
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoading(false);
}
};
return (
<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 }) => {
console.log("URI IMAGE", uri);
setImage(uri);
},
allowedType: "image",
});
}}
>
Upload
</ButtonCenteredOnly>
@@ -47,7 +206,7 @@ export default function InvestmentEdit() {
required
placeholder="Judul"
label="Judul"
value={data.title}
value={data?.title}
onChangeText={(value) => setData({ ...data, title: value })}
/>
@@ -57,22 +216,8 @@ export default function InvestmentEdit() {
placeholder="0"
label="Target Dana"
keyboardType="numeric"
onChangeText={(value) =>
setData({ ...data, targetDana: Number(value) })
}
value={data.targetDana === 0 ? "" : data.targetDana.toString()}
/>
<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 +226,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 +235,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,12 +245,8 @@ 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
@@ -121,9 +258,9 @@ export default function InvestmentEdit() {
value: item.id,
}))}
onChange={(value) =>
setData({ ...data, pencarianInvestor: value as any })
setData({ ...data, masterPencarianInvestorId: value as any })
}
value={data.pencarianInvestor}
value={data.masterPencarianInvestorId}
/>
<SelectCustom
@@ -135,9 +272,9 @@ export default function InvestmentEdit() {
value: item.id,
}))}
onChange={(value) =>
setData({ ...data, periodeDeviden: value as any })
setData({ ...data, masterPeriodeDevidenId: value as any })
}
value={data.periodeDeviden}
value={data.masterPeriodeDevidenId}
/>
<SelectCustom
@@ -149,12 +286,12 @@ export default function InvestmentEdit() {
value: item.id,
}))}
onChange={(value) =>
setData({ ...data, pembagianDeviden: value as any })
setData({ ...data, masterPembagianDevidenId: value as any })
}
value={data.pembagianDeviden}
value={data.masterPembagianDevidenId}
/>
<Spacing />
<ButtonCustom onPress={() => router.replace("/investment/portofolio")}>
<ButtonCustom isLoading={isLoading} onPress={handleSubmitUpdate}>
Simpan
</ButtonCustom>
</StackCustom>

View File

@@ -1,76 +1,203 @@
import {
BaseBox,
ButtonCenteredOnly,
ButtonCustom,
CenterCustom,
InformationBox,
LandscapeFrameUploaded,
SelectCustom,
Spacing,
StackCustom,
TextInputCustom,
ViewWrapper,
BaseBox,
ButtonCenteredOnly,
ButtonCustom,
CenterCustom,
InformationBox,
LandscapeFrameUploaded,
SelectCustom,
Spacing,
StackCustom,
TextCustom,
TextInputCustom,
ViewWrapper,
} from "@/components";
import { MainColor } from "@/constants/color-palet";
import DIRECTORY_ID from "@/constants/directory-id";
import { useAuth } from "@/hooks/use-auth";
import dummyPembagianDeviden from "@/lib/dummy-data/investment/pembagian-deviden";
import dummyListPencarianInvestor from "@/lib/dummy-data/investment/pencarian-investor";
import dummyPeriodeDeviden from "@/lib/dummy-data/investment/periode-deviden";
import { apiInvestmentCreate } from "@/service/api-client/api-investment";
import { uploadImageService } from "@/service/upload-service";
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
import pickFile, { IFileData } from "@/utils/pickFile";
import { FontAwesome5 } from "@expo/vector-icons";
import { router } from "expo-router";
import { useState } from "react";
import Toast from "react-native-toast-message";
export default function InvestmentCreate() {
const { user } = useAuth();
const [data, setData] = useState({
title: "",
targetDana: 0,
hargaPerLembar: 0,
totalLembar: 0,
rasioKeuntungan: 0,
targetDana: "",
hargaPerLembar: "",
totalLembar: "",
rasioKeuntungan: "",
pencarianInvestor: "",
periodeDeviden: "",
pembagianDeviden: "",
authorId: "",
imageId: "",
prospektusFileId: "",
});
const [image, setImage] = useState<string | null>(null);
const [pdf, setPdf] = useState<IFileData | null>(null);
const [isLoading, setIsLoading] = useState(false);
// const [coba, setCoba] = useState("");
const displayTargetDana = formatCurrencyDisplay(data.targetDana);
const displayHargaPerLembar = formatCurrencyDisplay(data.hargaPerLembar);
const displayTotalLembar = formatCurrencyDisplay(
Number(data.targetDana) / Number(data.hargaPerLembar)
);
const handleChangeCurrency = (field: keyof typeof data) => (text: string) => {
const numeric = text.replace(/\D/g, "");
setData((prev) => ({ ...prev, [field]: numeric }));
};
const handleSubmit = async () => {
if (!image || !pdf) {
Toast.show({
type: "error",
text1: "Harap pilih gambar dan file PDF",
});
return;
}
if (
!data.title ||
!data.targetDana ||
!data.hargaPerLembar ||
!data.rasioKeuntungan ||
!data.pencarianInvestor ||
!data.periodeDeviden ||
!data.pembagianDeviden
) {
Toast.show({
type: "error",
text1: "Harap isi semua data",
});
return;
}
try {
setIsLoading(true);
const responseUploadImage = await uploadImageService({
imageUri: image,
dirId: DIRECTORY_ID.investasi_image,
});
if (!responseUploadImage.success) {
Toast.show({
type: "error",
text1: "Gagal mengunggah gambar",
});
return;
}
const imageId = responseUploadImage.data.id;
const responseUploadPdf = await uploadImageService({
imageUri: pdf.uri,
dirId: DIRECTORY_ID.investasi_prospektus,
});
if (!responseUploadPdf.success) {
Toast.show({
type: "error",
text1: "Gagal mengunggah file PDF",
});
return;
}
const pdfId = responseUploadPdf.data.id;
const newData = {
title: data.title,
targetDana: data.targetDana,
hargaLembar: data.hargaPerLembar,
totalLembar: displayTotalLembar,
roi: data.rasioKeuntungan,
masterPencarianInvestorId: data.pencarianInvestor,
masterPembagianDevidenId: data.pembagianDeviden,
masterPeriodeDevidenId: data.periodeDeviden,
authorId: user?.id,
imageId: imageId,
prospektusFileId: pdfId,
};
const response = await apiInvestmentCreate({ data: newData });
console.log("[RESPONSE]", JSON.stringify(response, null, 2));
if (response.success) {
Toast.show({
type: "success",
text1: "Berhasil",
text2: response.message,
});
router.replace("/investment/portofolio");
} else {
Toast.show({
type: "error",
text1: "Info",
text2: response.message,
});
}
} catch (error) {
console.log("error", error);
} finally {
setIsLoading(false);
}
};
// const [coba, setCoba] = useState("");
return (
<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 }) => {
console.log("URI IMAGE", 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 }) => {
console.log("URI PDF", JSON.stringify(uri, null, 2));
setPdf({ uri, name, size });
},
allowedType: "pdf",
});
}}
>
Upload File
</ButtonCenteredOnly>
@@ -90,22 +217,8 @@ export default function InvestmentCreate() {
placeholder="0"
label="Target Dana"
keyboardType="numeric"
onChangeText={(value) =>
setData({ ...data, targetDana: Number(value) })
}
value={data.targetDana === 0 ? "" : data.targetDana.toString()}
/>
<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 +227,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,11 +252,9 @@ 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()
}
/>
@@ -187,7 +300,7 @@ export default function InvestmentCreate() {
value={data.pembagianDeviden}
/>
<Spacing />
<ButtonCustom onPress={() => router.replace("/investment/portofolio")}>
<ButtonCustom isLoading={isLoading} onPress={() => handleSubmit()}>
Simpan
</ButtonCustom>
</StackCustom>

View File

@@ -1,16 +1,57 @@
import { BaseBox, TextCustom, ViewWrapper } from "@/components";
import { jobDataDummy } from "@/screens/Job/listDataDummy";
/* eslint-disable react-hooks/exhaustive-deps */
import { BaseBox, LoaderCustom, TextCustom, ViewWrapper } from "@/components";
import { useAuth } from "@/hooks/use-auth";
import { apiJobGetAll } from "@/service/api-client/api-job";
import { useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
export default function JobArchive() {
const { user } = useAuth();
const [listData, setListData] = useState<any[]>([]);
const [isLoadData, setIsLoadData] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [user?.id])
);
const onLoadData = async () => {
try {
setIsLoadData(true);
const response = await apiJobGetAll({
category: "archive",
authorId: user?.id,
});
setListData(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoadData(false);
}
};
return (
<ViewWrapper hideFooter>
{jobDataDummy.map((e, i) => (
<BaseBox key={i} paddingTop={20} paddingBottom={20}>
<TextCustom align="center" bold truncate size="large">
{e.posisi}
</TextCustom>
</BaseBox>
))}
{isLoadData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Anda tidak memiliki arsip</TextCustom>
) : (
listData.map((item, index) => (
<BaseBox
key={index}
paddingTop={20}
paddingBottom={20}
href={`/job/${item.id}/archive`}
>
<TextCustom align="center" bold truncate size="large">
{item?.title || "-"}
</TextCustom>
</BaseBox>
))
)}
</ViewWrapper>
);
}

View File

@@ -28,7 +28,7 @@ export default function JobBeranda() {
const onLoadData = async (search: string) => {
try {
setIsLoadData(true);
const response = await apiJobGetAll({ search });
const response = await apiJobGetAll({ search, category: "beranda" });
setListData(response.data);
} catch (error) {
console.log("[ERROR]", error);

View File

@@ -77,6 +77,7 @@ export default function JobDetailStatus() {
status={status as string}
isLoading={isLoading}
onSetLoading={setIsLoading}
isArchive={true}
/>
</StackCustom>
<Spacing />

View File

@@ -0,0 +1,100 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
ButtonCustom,
LoaderCustom,
Spacing,
StackCustom,
ViewWrapper,
} from "@/components";
import Job_BoxDetailSection from "@/screens/Job/BoxDetailSection";
import { apiJobGetOne, apiJobUpdateData } from "@/service/api-client/api-job";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, useState } from "react";
import Toast from "react-native-toast-message";
export default function JobDetailArchive() {
const { id } = useLocalSearchParams();
const [data, setData] = useState<any>(null);
const [isLoading, setIsLoading] = useState(false);
const [isLoadData, setIsLoadData] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id])
);
const onLoadData = async () => {
try {
setIsLoadData(true);
const response = await apiJobGetOne({ id: id as string });
setData(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoadData(false);
}
};
const handleArchive = async () => {
try {
setIsLoading(true);
const response = await apiJobUpdateData({
id: id as string,
data: false,
category: "archive",
});
if (response.success) {
Toast.show({
type: "success",
text1: response.message,
});
router.back();
} else {
Toast.show({
type: "info",
text1: "Info",
text2: response.message,
});
router.back();
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoading(false);
}
};
return (
<>
{isLoadData ? (
<LoaderCustom />
) : (
<ViewWrapper>
<>
<StackCustom>
<Job_BoxDetailSection data={data} />
<ButtonCustom
isLoading={isLoading}
onPress={() => {
handleArchive();
}}
>
Publish kembali
</ButtonCustom>
{/* <Job_ButtonStatusSection
id={id as string}
status={status as string}
isLoading={isLoading}
onSetLoading={setIsLoading}
isArchive={true}
/> */}
</StackCustom>
<Spacing />
</>
</ViewWrapper>
)}
</>
);
}

View File

@@ -99,6 +99,7 @@ export default function JobEdit() {
const response = await apiJobUpdateData({
id: id as string,
data: newData,
category: "edit",
});
if (response.success) {

View File

@@ -1,9 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { ButtonCustom, LoaderCustom, Spacing, ViewWrapper } from "@/components";
import { ButtonCustom, LoaderCustom, Spacing, StackCustom, ViewWrapper } from "@/components";
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}`;
@@ -90,9 +92,11 @@ export default function JobDetail() {
) : (
<>
<Job_BoxDetailSection data={data} />
<OpenLinkButton id={id as string} />
<StackCustom>
<OpenLinkButton id={id as string} />
<CopyLinkButton id={id as string} />
</StackCustom>
<Spacing />
<CopyLinkButton id={id as string} />
</>
)}
</ViewWrapper>

View File

@@ -1,15 +1,57 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
LoaderCustom,
TextCustom,
ViewWrapper
} from "@/components";
import { useAuth } from "@/hooks/use-auth";
import Voting_BoxPublishSection from "@/screens/Voting/BoxPublishSection";
import { apiVotingGetAll } from "@/service/api-client/api-voting";
import { useFocusEffect } from "expo-router";
import _ from "lodash";
import { useState, useCallback } from "react";
export default function VotingContribution() {
const { user } = useAuth();
const [listData, setListData] = useState<any>([]);
const [loadingGetData, setLoadingGetData] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [])
);
const onLoadData = async () => {
try {
setLoadingGetData(true);
const response = await apiVotingGetAll({
category: "contribution",
authorId: user?.id as string,
});
if (response.success) {
setListData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingGetData(false);
}
};
return (
<ViewWrapper hideFooter>
{Array.from({ length: 5 }).map((_, index) => (
{loadingGetData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Tidak ada kontribusi</TextCustom>
) : listData.map((item: any, index: number) => (
<Voting_BoxPublishSection
data={item}
key={index}
href={`/voting/${index}/contribution`}
href={`/voting/${item.id}/contribution`}
/>
))}
</ViewWrapper>

View File

@@ -1,11 +1,44 @@
import { ViewWrapper } from "@/components";
/* eslint-disable react-hooks/exhaustive-deps */
import { LoaderCustom, TextCustom, ViewWrapper } from "@/components";
import TabsTwoButtonCustom from "@/components/_ShareComponent/TabsTwoHeaderCustom";
import Voting_BoxPublishSection from "@/screens/Voting/BoxPublishSection";
import { useState } from "react";
import { useAuth } from "@/hooks/use-auth";
import { useCallback, useState } from "react";
import { apiVotingGetAll } from "@/service/api-client/api-voting";
import { useFocusEffect } from "expo-router";
import _ from "lodash";
export default function VotingHistory() {
const { user } = useAuth();
const [activeCategory, setActiveCategory] = useState<string | null>("all");
const [listData, setListData] = useState<any>([]);
const [loadingGetData, setLoadingGetData] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [activeCategory])
);
const onLoadData = async () => {
try {
setLoadingGetData(true);
const response = await apiVotingGetAll({
category: activeCategory === "all" ? "all-history" : "my-history",
authorId: user?.id as string,
});
if (response.success) {
setListData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingGetData(false);
}
};
const handlePress = (item: any) => {
setActiveCategory(item);
// tambahkan logika lain seperti filter dsb.
@@ -25,13 +58,20 @@ export default function VotingHistory() {
/>
}
>
{Array.from({ length: 10 }).map((_, index) => (
<Voting_BoxPublishSection
key={index}
id={activeCategory as any}
href={`/voting/${index}/history`}
/>
))}
{loadingGetData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Tidak ada riwayat</TextCustom>
) : (
listData.map((item: any, index: number) => (
<Voting_BoxPublishSection
key={index}
id={item.id}
data={item}
href={`/voting/${item.id}/history`}
/>
))
)}
</ViewWrapper>
);
}

View File

@@ -1,23 +1,68 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
FloatingButton,
LoaderCustom,
SearchInput,
ViewWrapper
TextCustom,
ViewWrapper,
} from "@/components";
import Voting_BoxPublishSection from "@/screens/Voting/BoxPublishSection";
import { router } from "expo-router";
import { apiVotingGetAll } from "@/service/api-client/api-voting";
import { router, useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
export default function VotingBeranda() {
const [listData, setListData] = useState<any>([]);
const [loadingGetData, setLoadingGetData] = useState(false);
const [search, setSearch] = useState("");
useFocusEffect(
useCallback(() => {
onLoadData();
}, [search])
);
const onLoadData = async () => {
try {
setLoadingGetData(true);
const response = await apiVotingGetAll({
search,
category: "beranda",
});
if (response.success) {
setListData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingGetData(false);
}
};
return (
<ViewWrapper
hideFooter
floatingButton={
<FloatingButton onPress={() => router.push("/voting/create")} />
}
headerComponent={<SearchInput placeholder="Cari voting" />}
headerComponent={
<SearchInput placeholder="Cari voting" onChangeText={setSearch} />
}
>
{Array.from({ length: 5 }).map((_, index) => (
<Voting_BoxPublishSection key={index} href={`/voting/${index}`} />
))}
{loadingGetData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Tidak ada data</TextCustom>
) : (
listData.map((item: any, index: number) => (
<Voting_BoxPublishSection
data={item}
key={index}
href={`/voting/${item.id}`}
/>
))
)}
</ViewWrapper>
);
}

View File

@@ -1,20 +1,52 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BadgeCustom,
BaseBox,
LoaderCustom,
ScrollableCustom,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import { useAuth } from "@/hooks/use-auth";
import { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
import dayjs from "dayjs";
import { useState } from "react";
import { apiVotingGetByStatus } from "@/service/api-client/api-voting";
import { dateTimeView } from "@/utils/dateTimeView";
import { useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
export default function VotingStatus() {
const { user } = useAuth();
const id = user?.id || "";
const [activeCategory, setActiveCategory] = useState<string | null>(
"publish"
);
const [listData, setListData] = useState([]);
const [loadingGetData, setLoadingGetData] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [activeCategory, id])
);
async function onLoadData() {
try {
setLoadingGetData(true);
const response = await apiVotingGetByStatus({
id: id as string,
status: activeCategory!,
});
setListData(response.data);
} catch (error) {
console.log(error);
} finally {
setLoadingGetData(false);
}
}
const handlePress = (item: any) => {
setActiveCategory(item.value);
// tambahkan logika lain seperti filter dsb.
@@ -34,27 +66,33 @@ export default function VotingStatus() {
return (
<ViewWrapper headerComponent={scrollComponent} hideFooter>
{Array.from({ length: 10 }).map((_, i) => (
<BaseBox
key={i}
paddingTop={20}
paddingBottom={20}
href={`/voting/${i}/${activeCategory}/detail`}
>
<StackCustom>
<TextCustom align="center" bold truncate size="large">
Lorem ipsum dolor sit {activeCategory}
</TextCustom>
<BadgeCustom
style={{ width: "70%", alignSelf: "center" }}
variant="light"
>
{dayjs().format("DD/MM/YYYY")} -{" "}
{dayjs().add(1, "day").format("DD/MM/YYYY")}
</BadgeCustom>
</StackCustom>
</BaseBox>
))}
{loadingGetData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Tidak ada data {activeCategory}</TextCustom>
) : (
listData.map((item: any, i: number) => (
<BaseBox
key={i}
paddingTop={20}
paddingBottom={20}
href={`/voting/${item.id}/${activeCategory}/detail`}
>
<StackCustom>
<TextCustom align="center" bold truncate={2} size="large">
{item?.title || ""}
</TextCustom>
<BadgeCustom
style={{ width: "70%", alignSelf: "center" }}
variant="light"
>
{item?.awalVote && dateTimeView({date: item?.awalVote, withoutTime: true})} -{" "}
{item?.akhirVote && dateTimeView({date: item?.akhirVote, withoutTime: true})}
</BadgeCustom>
</StackCustom>
</BaseBox>
))
)}
</ViewWrapper>
);
}

View File

@@ -1,23 +1,63 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
AlertDefaultSystem,
BackButton,
BaseBox,
DotButton,
DrawerCustom,
LoaderCustom,
MenuDrawerDynamicGrid,
Spacing,
TextCustom,
ViewWrapper,
} from "@/components";
import { IconArchive, IconContribution, IconEdit } from "@/components/_Icon";
import { IMenuDrawerItem } from "@/components/_Interface/types";
import Voting_BoxDetailHasilVotingSection from "@/screens/Voting/BoxDetailHasilVotingSection";
import { Voting_BoxDetailSection } from "@/screens/Voting/BoxDetailSection";
import Voting_ButtonStatusSection from "@/screens/Voting/ButtonStatusSection";
import { router, Stack, useLocalSearchParams } from "expo-router";
import { useState } from "react";
import {
apiVotingGetOne,
apiVotingUpdateData,
} from "@/service/api-client/api-voting";
import {
router,
Stack,
useFocusEffect,
useLocalSearchParams,
} from "expo-router";
import { useCallback, useState } from "react";
import Toast from "react-native-toast-message";
export default function VotingDetailStatus() {
const { id, status } = useLocalSearchParams();
const [openDrawerDraft, setOpenDrawerDraft] = useState(false);
const [openDrawerPublish, setOpenDrawerPublish] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [loadingGetData, setLoadingGetData] = useState(false);
const [data, setData] = useState<any>(null);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id])
);
const onLoadData = async () => {
try {
setLoadingGetData(true);
const response = await apiVotingGetOne({ id: id as string });
if (response.success) {
setData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingGetData(false);
}
};
const handlePressDraft = (item: IMenuDrawerItem) => {
console.log("PATH >> ", item.path);
@@ -32,9 +72,24 @@ export default function VotingDetailStatus() {
message: "Apakah Anda yakin ingin mengarsipkan voting ini?",
textLeft: "Batal",
textRight: "Ya",
onPressRight: () => {
console.log("Hapus");
router.back();
onPressRight: async () => {
try {
const response = await apiVotingUpdateData({
id: id as string,
data: data.isArsip ? false : true,
category: "archive",
});
if (response.success) {
Toast.show({
type: "success",
text1: response.message,
});
router.back();
}
} catch (error) {
console.log("[ERROR]", error);
}
},
});
}
@@ -46,7 +101,7 @@ export default function VotingDetailStatus() {
<>
<Stack.Screen
options={{
title: `Detail ${status}`,
title: `Detail`,
headerLeft: () => <BackButton />,
headerRight: () =>
status === "draft" ? (
@@ -57,9 +112,37 @@ export default function VotingDetailStatus() {
}}
/>
<ViewWrapper>
<Voting_BoxDetailSection />
<Voting_ButtonStatusSection status={status as string} />
<Spacing />
{loadingGetData ? (
<LoaderCustom />
) : (
<>
{status === "publish" && (
<BaseBox>
<TextCustom bold>
Status:{" "}
<TextCustom color={data?.isArsip ? "red" : "green"}>
{data?.isArsip ? "Arsip" : "Publish"}
</TextCustom>
</TextCustom>
</BaseBox>
)}
<Spacing height={0} />
<Voting_BoxDetailSection data={data as any} />
{status === "publish" ? (
<Voting_BoxDetailHasilVotingSection
listData={data?.Voting_DaftarNamaVote}
/>
) : (
<Voting_ButtonStatusSection
isLoading={isLoading}
onSetLoading={setIsLoading}
id={id as string}
status={status as string}
/>
)}
<Spacing />
</>
)}
</ViewWrapper>
{/* ========= Draft Drawer ========= */}

View File

@@ -1,22 +1,76 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
AvatarUsernameAndOtherComponent,
BackButton,
DotButton,
DrawerCustom,
MenuDrawerDynamicGrid,
Spacing,
ViewWrapper,
AvatarUsernameAndOtherComponent,
BackButton,
DotButton,
DrawerCustom,
LoaderCustom,
MenuDrawerDynamicGrid,
Spacing,
ViewWrapper,
} from "@/components";
import { IconContribution } from "@/components/_Icon";
import { IMenuDrawerItem } from "@/components/_Interface/types";
import { useAuth } from "@/hooks/use-auth";
import { Voting_BoxDetailContributionSection } from "@/screens/Voting/BoxDetailContribution";
import Voting_BoxDetailHasilVotingSection from "@/screens/Voting/BoxDetailHasilVotingSection";
import {
apiVotingContribution,
apiVotingGetOne,
} from "@/service/api-client/api-voting";
import { router, Stack, useLocalSearchParams } from "expo-router";
import { useState } from "react";
import { useEffect, useState } from "react";
export default function VotingDetailContribution() {
const { user } = useAuth();
const { id } = useLocalSearchParams();
const [openDrawerPublish, setOpenDrawerPublish] = useState(false);
const [data, setData] = useState<any>(null);
const [loadingGetData, setLoadingGetData] = useState(false);
const [nameChoice, setNameChoice] = useState("");
useEffect(() => {
handlerLoadData();
}, [id, user?.id]);
async function handlerLoadData() {
try {
setLoadingGetData(true);
await onLoadData();
await onLoadCheckContribution();
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingGetData(false);
}
}
const onLoadData = async () => {
try {
const response = await apiVotingGetOne({ id: id as string });
if (response.success) {
setData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
}
};
const onLoadCheckContribution = async () => {
try {
const response = await apiVotingContribution({
id: id as string,
authorId: user?.id as string,
category: "checked",
});
if (response.success) {
setNameChoice(response.data.nameChoice);
}
} catch (error) {
console.log("[ERROR]", error);
}
};
const handlePressPublish = (item: IMenuDrawerItem) => {
router.navigate(item.path as any);
@@ -36,11 +90,27 @@ export default function VotingDetailContribution() {
/>
<ViewWrapper>
<Voting_BoxDetailContributionSection
headerAvatar={<AvatarUsernameAndOtherComponent />}
/>
<Voting_BoxDetailHasilVotingSection />
<Spacing />
{loadingGetData ? (
<LoaderCustom />
) : (
<>
<Voting_BoxDetailContributionSection
data={data}
nameChoice={nameChoice}
headerAvatar={
<AvatarUsernameAndOtherComponent
avatar={data?.Author?.Profile?.imageId || ""}
name={data?.Author?.username || "Username"}
avatarHref={`/profile/${data?.Author?.Profile?.id}`}
/>
}
/>
<Voting_BoxDetailHasilVotingSection
listData={data?.Voting_DaftarNamaVote}
/>
<Spacing />
</>
)}
</ViewWrapper>
{/* ========= Publish Drawer ========= */}

View File

@@ -1,29 +1,184 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
ActionIcon,
BoxButtonOnFooter,
ButtonCenteredOnly,
ButtonCustom,
Grid,
CenterCustom,
LoaderCustom,
Spacing,
StackCustom,
TextAreaCustom,
TextCustom,
TextInputCustom,
ViewWrapper,
} from "@/components";
import DateTimePickerCustom from "@/components/DateInput/DateTimePickerCustom";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_XLARGE } from "@/constants/constans-value";
import {
apiVotingGetOne,
apiVotingUpdateData,
} from "@/service/api-client/api-voting";
import { Ionicons } from "@expo/vector-icons";
import { router } from "expo-router";
import { TouchableOpacity } from "react-native";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import { View } from "react-native";
import Toast from "react-native-toast-message";
interface IEditData {
title?: string;
deskripsi?: string;
awalVote?: string;
akhirVote?: string;
Voting_DaftarNamaVote?: [
{
value?: string;
}
];
}
export default function VotingEdit() {
const { id } = useLocalSearchParams();
const [loadingGetData, setLoadingGetData] = useState(false);
const [data, setData] = useState<IEditData>();
const [isLoading, setIsLoading] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id])
);
const onLoadData = async () => {
try {
setLoadingGetData(true);
const response = await apiVotingGetOne({ id: id as string });
if (response.success) {
const data = response.data;
setData({
title: data.title,
deskripsi: data.deskripsi,
awalVote: data.awalVote,
akhirVote: data.akhirVote,
Voting_DaftarNamaVote: data.Voting_DaftarNamaVote,
});
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingGetData(false);
}
};
const validateDateRange = ({
selectedStratDate,
selectedEndDate,
}: {
selectedStratDate: string | Date;
selectedEndDate: string | Date;
}): { isValid: boolean; error?: string } => {
const startDate = new Date(selectedStratDate);
const endDate = new Date(selectedEndDate);
// Cek apakah tanggal valid
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
return {
isValid: false,
error: "Invalid date provided",
};
}
if (startDate >= endDate) {
return {
isValid: false,
error: "Ubah tanggal berakhirnya event",
};
}
return {
isValid: true,
error: undefined,
};
};
const validateForm = async () => {
if (!data?.title || !data?.deskripsi) {
Toast.show({
type: "info",
text1: "Lengkapi semua data",
});
return false;
}
if (data?.Voting_DaftarNamaVote?.some((item: any) => item.value === "")) {
Toast.show({
type: "info",
text1: "Isi semua data pilihan",
});
return false;
}
const startDate = new Date(data?.awalVote as any);
const endDate = new Date(data?.akhirVote as any);
if (startDate >= endDate) {
Toast.show({
type: "info",
text1: "Ubah tanggal berakhirnya event",
});
return false;
}
return true;
};
const handlerUpdateSubmit = async () => {
const isValid = await validateForm();
if (!isValid) return;
try {
setIsLoading(true);
const newData = {
...data,
awalVote: new Date(data?.awalVote as any).toISOString(),
akhirVote: new Date(data?.akhirVote as any).toISOString(),
listVote: data?.Voting_DaftarNamaVote?.map((item: any) => item.value),
};
const response = await apiVotingUpdateData({
id: id as string,
data: newData,
category: "edit",
});
if (response.success) {
Toast.show({
type: "success",
text1: response.message,
});
return router.back();
} else {
Toast.show({
type: "error",
text1: response.message,
});
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoading(false);
}
};
const buttonSubmit = () => {
return (
<>
<BoxButtonOnFooter>
<ButtonCustom
onPress={() =>
router.back()
}
isLoading={isLoading}
onPress={() => handlerUpdateSubmit()}
>
Update
</ButtonCustom>
@@ -34,45 +189,144 @@ export default function VotingEdit() {
return (
<ViewWrapper footerComponent={buttonSubmit()}>
<StackCustom gap={"xs"}>
<TextInputCustom
label="Judul Voting"
placeholder="MasukanJudul Voting"
required
/>
<TextAreaCustom
label="Deskripsi"
placeholder="Masukan Deskripsi"
required
showCount
maxLength={1000}
/>
<DateTimePickerCustom label="Mulai Voting" required />
<DateTimePickerCustom label="Voting Berakhir" required />
{loadingGetData ? (
<LoaderCustom />
) : (
<StackCustom gap={"xs"}>
<TextInputCustom
label="Judul Voting"
placeholder="MasukanJudul Voting"
required
value={data?.title}
onChangeText={(text) => setData({ ...data, title: text })}
/>
<TextAreaCustom
label="Deskripsi"
placeholder="Masukan Deskripsi"
required
showCount
maxLength={1000}
value={data?.deskripsi}
onChangeText={(text) => setData({ ...data, deskripsi: text })}
/>
<Grid>
<Grid.Col span={10}>
<Spacing />
<DateTimePickerCustom
minimumDate={new Date(Date.now())}
label="Mulai Voting"
required
value={new Date(data?.awalVote as any)}
onChange={(date: any) => {
setData({ ...data, awalVote: date });
}}
/>
<StackCustom gap={0}>
<DateTimePickerCustom
minimumDate={new Date(data?.awalVote as any)}
label="Voting Berakhir"
required
value={new Date(data?.akhirVote as any)}
onChange={(date: any) => {
setData({ ...data, akhirVote: date });
}}
/>
{validateDateRange({
selectedStratDate: data?.awalVote as any,
selectedEndDate: data?.akhirVote as any,
}).isValid ? (
<TextCustom style={{ color: "green" }}>
{
validateDateRange({
selectedStratDate: data?.awalVote as any,
selectedEndDate: data?.akhirVote as any,
}).error
}
</TextCustom>
) : (
<TextCustom style={{ color: "red" }}>
{
validateDateRange({
selectedStratDate: data?.awalVote as any,
selectedEndDate: data?.akhirVote as any,
}).error
}
</TextCustom>
)}
<Spacing />
</StackCustom>
{data?.Voting_DaftarNamaVote?.map((item: any, index: number) => (
<TextInputCustom
key={index}
label="Pilihan"
placeholder="Masukan Pilihan"
required
value={item.value}
onChangeText={(value: any) =>
setData({
...(data as any),
Voting_DaftarNamaVote: data?.Voting_DaftarNamaVote?.map(
(item: any, i: any) =>
i === index ? { ...item, value } : item
),
})
}
/>
</Grid.Col>
<Grid.Col
span={2}
style={{ alignItems: "center", justifyContent: "center" }}
>
<TouchableOpacity onPress={() => console.log("delete")}>
<Ionicons name="trash" size={24} color={MainColor.red} />
</TouchableOpacity>
</Grid.Col>
</Grid>
))}
<ButtonCenteredOnly onPress={() => console.log("add")}>
Tambah Pilihan
</ButtonCenteredOnly>
<Spacing />
</StackCustom>
<CenterCustom>
<View
style={{ flexDirection: "row", alignItems: "center", gap: 10 }}
>
<ActionIcon
disabled={(data as any)?.Voting_DaftarNamaVote?.length >= 4}
onPress={() => {
setData({
...(data as any),
Voting_DaftarNamaVote: [
...(data as any)?.Voting_DaftarNamaVote,
{ value: "" },
],
});
}}
icon={
<Ionicons
name="add-circle-outline"
size={ICON_SIZE_XLARGE}
color={MainColor.black}
/>
}
size="xl"
/>
<ActionIcon
disabled={
((data as any)?.Voting_DaftarNamaVote?.length as any) <= 2
}
onPress={() => {
const list = _.clone((data as any)?.Voting_DaftarNamaVote);
list.pop();
setData({
...(data as any),
Voting_DaftarNamaVote: list,
});
}}
icon={
<Ionicons
name="remove-circle-outline"
size={ICON_SIZE_XLARGE}
color={MainColor.black}
/>
}
size="xl"
/>
</View>
</CenterCustom>
<Spacing />
</StackCustom>
)}
</ViewWrapper>
);
}

View File

@@ -1,23 +1,78 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
AvatarUsernameAndOtherComponent,
BackButton,
DotButton,
DrawerCustom,
MenuDrawerDynamicGrid,
Spacing,
ViewWrapper,
AvatarUsernameAndOtherComponent,
BackButton,
DotButton,
DrawerCustom,
LoaderCustom,
MenuDrawerDynamicGrid,
Spacing,
ViewWrapper,
} from "@/components";
import { IconContribution } from "@/components/_Icon";
import { IMenuDrawerItem } from "@/components/_Interface/types";
import { useAuth } from "@/hooks/use-auth";
import Voting_BoxDetailHasilVotingSection from "@/screens/Voting/BoxDetailHasilVotingSection";
import { Voting_BoxDetailHistorySection } from "@/screens/Voting/BoxDetailHistorySection";
import {
apiVotingContribution,
apiVotingGetOne,
} from "@/service/api-client/api-voting";
import { router, Stack, useLocalSearchParams } from "expo-router";
import { useState } from "react";
import { useEffect, useState } from "react";
export default function VotingDetailHistory() {
const { id } = useLocalSearchParams();
const { user } = useAuth();
const [openDrawerPublish, setOpenDrawerPublish] = useState(false);
const [data, setData] = useState<any>(null);
const [loadingGetData, setLoadingGetData] = useState(false);
const [nameChoice, setNameChoice] = useState("");
useEffect(() => {
handlerLoadData();
}, [id, user?.id]);
async function handlerLoadData() {
try {
setLoadingGetData(true);
await onLoadData();
await onLoadCheckContribution();
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingGetData(false);
}
}
const onLoadData = async () => {
try {
const response = await apiVotingGetOne({ id: id as string });
if (response.success) {
setData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
}
};
const onLoadCheckContribution = async () => {
try {
const response = await apiVotingContribution({
id: id as string,
authorId: user?.id as string,
category: "checked",
});
if (response.success) {
setNameChoice(response.data.nameChoice);
}
} catch (error) {
console.log("[ERROR]", error);
}
};
const handlePressPublish = (item: IMenuDrawerItem) => {
router.navigate(item.path as any);
setOpenDrawerPublish(false);
@@ -35,11 +90,27 @@ export default function VotingDetailHistory() {
}}
/>
<ViewWrapper>
<Voting_BoxDetailHistorySection
headerAvatar={<AvatarUsernameAndOtherComponent />}
/>
<Voting_BoxDetailHasilVotingSection />
<Spacing />
{loadingGetData ? (
<LoaderCustom />
) : (
<>
<Voting_BoxDetailHistorySection
data={data}
nameChoice={nameChoice}
headerAvatar={
<AvatarUsernameAndOtherComponent
avatar={data?.Author?.Profile?.imageId || ""}
name={data?.Author?.username || "Username"}
avatarHref={`/profile/${data?.Author?.Profile?.id}`}
/>
}
/>
<Voting_BoxDetailHasilVotingSection
listData={data?.Voting_DaftarNamaVote}
/>
<Spacing />
</>
)}
</ViewWrapper>
{/* ========= Publish Drawer ========= */}

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
AlertDefaultSystem,
AvatarUsernameAndOtherComponent,
@@ -5,20 +6,87 @@ import {
DotButton,
DrawerCustom,
InformationBox,
LoaderCustom,
MenuDrawerDynamicGrid,
StackCustom,
ViewWrapper,
} from "@/components";
import { IconArchive, IconContribution } from "@/components/_Icon";
import { IMenuDrawerItem } from "@/components/_Interface/types";
import { useAuth } from "@/hooks/use-auth";
import Voting_BoxDetailHasilVotingSection from "@/screens/Voting/BoxDetailHasilVotingSection";
import { Voting_BoxDetailPublishSection } from "@/screens/Voting/BoxDetailPublishSection";
import { router, Stack, useLocalSearchParams } from "expo-router";
import React, { useState } from "react";
import {
apiVotingContribution,
apiVotingGetOne,
apiVotingUpdateData,
} from "@/service/api-client/api-voting";
import { today } from "@/utils/dateTimeView";
import {
router,
Stack,
useFocusEffect,
useLocalSearchParams,
} from "expo-router";
import React, { useCallback, useState } from "react";
import Toast from "react-native-toast-message";
export default function VotingDetail() {
const { id } = useLocalSearchParams();
const { user } = useAuth();
const [openDrawerPublish, setOpenDrawerPublish] = useState(false);
const [data, setData] = useState<any>(null);
const [loadingGetData, setLoadingGetData] = useState(false);
const [isContribution, setIsContribution] = useState(false);
const [nameChoice, setNameChoice] = useState("");
useFocusEffect(
useCallback(() => {
handlerLoadData();
}, [id, user?.id])
);
async function handlerLoadData() {
try {
setLoadingGetData(true);
await onLoadData();
await onLoadCheckContribution();
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingGetData(false);
}
}
const onLoadData = async () => {
try {
const response = await apiVotingGetOne({ id: id as string });
if (response.success) {
setData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
}
};
const onLoadCheckContribution = async () => {
try {
const response = await apiVotingContribution({
id: id as string,
authorId: user?.id as string,
category: "checked",
});
if (response.success) {
setIsContribution(response.data.isContribution);
setNameChoice(response.data.nameChoice);
}
} catch (error) {
console.log("[ERROR]", error);
}
};
const handlePressPublish = (item: IMenuDrawerItem) => {
if (item.path === "") {
AlertDefaultSystem({
@@ -26,9 +94,24 @@ export default function VotingDetail() {
message: "Apakah Anda yakin ingin mengarsipkan voting ini?",
textLeft: "Batal",
textRight: "Ya",
onPressRight: () => {
console.log("Hapus");
router.back();
onPressRight: async () => {
try {
const response = await apiVotingUpdateData({
id: id as string,
data: data.isArsip ? false : true,
category: "archive",
});
if (response.success) {
Toast.show({
type: "success",
text1: response.message,
});
router.back();
}
} catch (error) {
console.log("[ERROR]", error);
}
},
});
}
@@ -49,15 +132,32 @@ export default function VotingDetail() {
/>
<ViewWrapper>
<StackCustom>
<InformationBox text="Untuk sementara voting ini belum di buka. Voting akan dimulai sesuai dengan tanggal awal pemilihan, dan akan ditutup sesuai dengan tanggal akhir pemilihan." />
{loadingGetData ? (
<LoaderCustom />
) : (
<StackCustom gap={"xs"}>
{today.getDate() < new Date(data?.awalVote).getDate() && (
<InformationBox text="Untuk sementara voting tidak dapat dilakukan. Voting dapat dimulai sesuai dengan tanggal awal pemilihan, dan akan ditutup sesuai dengan tanggal akhir pemilihan." />
)}
<Voting_BoxDetailPublishSection
data={data}
userId={user?.id as string}
isContribution={isContribution}
nameChoice={nameChoice}
headerAvatar={
<AvatarUsernameAndOtherComponent
avatar={data?.Author?.Profile?.imageId || ""}
name={data?.Author?.username || "Username"}
avatarHref={`/profile/${data?.Author?.Profile?.id}`}
/>
}
/>
<Voting_BoxDetailPublishSection
headerAvatar={<AvatarUsernameAndOtherComponent />}
/>
<Voting_BoxDetailHasilVotingSection />
</StackCustom>
<Voting_BoxDetailHasilVotingSection
listData={data?.Voting_DaftarNamaVote}
/>
</StackCustom>
)}
</ViewWrapper>
{/* ========= Publish Drawer ========= */}
@@ -67,18 +167,28 @@ export default function VotingDetail() {
height={"auto"}
>
<MenuDrawerDynamicGrid
data={[
{
icon: <IconContribution />,
label: "Daftar Kontributor",
path: `/voting/${id}/list-of-contributor`,
},
{
icon: <IconArchive />,
label: "Update Arsip",
path: "" as any,
},
]}
data={
user?.id === data?.Author?.id
? [
{
icon: <IconContribution />,
label: "Daftar Kontributor",
path: `/voting/${id}/list-of-contributor`,
},
{
icon: <IconArchive />,
label: "Update Arsip",
path: "" as any,
},
]
: [
{
icon: <IconContribution />,
label: "Daftar Kontributor",
path: `/voting/${id}/list-of-contributor`,
},
]
}
onPressItem={handlePressPublish as any}
/>
</DrawerCustom>

View File

@@ -1,26 +1,69 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
AvatarUsernameAndOtherComponent,
BadgeCustom,
BaseBox,
LoaderCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import { apiVotingContribution } from "@/service/api-client/api-voting";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
export default function Voting_ListOfContributor() {
const { id } = useLocalSearchParams();
const [listData, setListData] = useState<any>([]);
const [isLoadData, setIsLoadData] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadList();
}, [id])
);
const onLoadList = async () => {
try {
setIsLoadData(true);
const response = await apiVotingContribution({
id: id as string,
authorId: "",
category: "list",
});
if (response.success) {
setListData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoadData(false);
}
};
return (
<ViewWrapper>
{Array.from({ length: 10 }).map((_, index) => (
<BaseBox paddingTop={5} paddingBottom={5} key={index.toString()}>
<AvatarUsernameAndOtherComponent
rightComponent={
<BadgeCustom
style={{alignSelf: "flex-end" }}
>
Pilihan {index + 1}
</BadgeCustom>
}
/>
</BaseBox>
))}
{isLoadData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Tidak ada kontributor</TextCustom>
) : (
listData.map((item: any, index: number) => (
<BaseBox paddingTop={5} paddingBottom={5} key={index.toString()}>
<AvatarUsernameAndOtherComponent
avatar={item?.Author?.Profile?.imageId || ""}
name={item?.Author?.username || "Username"}
avatarHref={`/profile/${item?.Author?.Profile?.id}`}
rightComponent={
<BadgeCustom style={{ alignSelf: "flex-end" }}>
{item?.Voting_DaftarNamaVote?.value}
</BadgeCustom>
}
/>
</BaseBox>
))
)}
</ViewWrapper>
);
}

View File

@@ -1,30 +1,103 @@
import {
ActionIcon,
BoxButtonOnFooter,
ButtonCenteredOnly,
ButtonCustom,
Grid,
CenterCustom,
Spacing,
StackCustom,
TextAreaCustom,
TextInputCustom,
ViewWrapper,
ViewWrapper
} from "@/components";
import DateTimePickerCustom from "@/components/DateInput/DateTimePickerCustom";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_XLARGE } from "@/constants/constans-value";
import { useAuth } from "@/hooks/use-auth";
import { apiVotingCreate } from "@/service/api-client/api-voting";
import { Ionicons } from "@expo/vector-icons";
import { router } from "expo-router";
import { TouchableOpacity } from "react-native";
import _ from "lodash";
import { useState } from "react";
import { View } from "react-native";
import Toast from "react-native-toast-message";
export default function VotingCreate() {
const { user } = useAuth();
const [isLoading, setIsLoading] = useState(false);
const [data, setData] = useState({
authorId: "",
title: "",
deskripsi: "",
awalVote: "",
akhirVote: "",
listVote: [],
});
const [listVote, setListVote] = useState([
{
name: "Nama Pilihan",
value: "",
},
{
name: "Nama Pilihan",
value: "",
},
]);
const handlerSubmit = async () => {
if (!data.title || !data.deskripsi || !data.awalVote || !data.akhirVote) {
Toast.show({
type: "info",
text1: "Lengkapi semua data",
});
return;
}
if (listVote.some((item: any) => item.value === "")) {
Toast.show({
type: "info",
text1: "Lengkapi semua data pilihan",
});
return;
}
try {
setIsLoading(true);
const newData = {
...data,
authorId: user?.id,
awalVote: new Date(data.awalVote as any).toISOString(),
akhirVote: new Date(data.akhirVote as any).toISOString(),
listVote: listVote,
};
const response = await apiVotingCreate(newData);
// console.log("[RESPONSE]", JSON.stringify(response, null, 2));
if (response.success) {
Toast.show({
type: "success",
text1: "Data berhasil disimpan",
});
router.replace("/(application)/(user)/voting/(tabs)/status");
} else {
Toast.show({
type: "error",
text1: "Data gagal disimpan",
});
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoading(false);
}
};
const buttonSubmit = () => {
return (
<>
<BoxButtonOnFooter>
<ButtonCustom
onPress={() =>
router.replace("/(application)/(user)/voting/(tabs)/status")
}
>
<ButtonCustom isLoading={isLoading} onPress={() => handlerSubmit()}>
Simpan
</ButtonCustom>
</BoxButtonOnFooter>
@@ -39,6 +112,8 @@ export default function VotingCreate() {
label="Judul Voting"
placeholder="MasukanJudul Voting"
required
value={data.title}
onChangeText={(value: any) => setData({ ...data, title: value })}
/>
<TextAreaCustom
label="Deskripsi"
@@ -46,31 +121,81 @@ export default function VotingCreate() {
required
showCount
maxLength={1000}
value={data.deskripsi}
onChangeText={(value: any) => setData({ ...data, deskripsi: value })}
/>
<DateTimePickerCustom
label="Mulai Voting"
required
// value={data.awalVote ? new Date(data.awalVote) : null}
onChange={(value: any) => setData({ ...data, awalVote: value })}
minimumDate={new Date(Date.now())}
/>
<DateTimePickerCustom
disabled={!data.awalVote}
label="Voting Berakhir"
required
// value={data.akhirVote ? new Date(data.akhirVote) : null}
onChange={(value: any) => setData({ ...data, akhirVote: value })}
minimumDate={
data.awalVote ? new Date(data.awalVote) : new Date(Date.now())
}
/>
<DateTimePickerCustom label="Mulai Voting" required />
<DateTimePickerCustom label="Voting Berakhir" required />
<Grid>
<Grid.Col span={10}>
<TextInputCustom
label="Pilihan"
placeholder="Masukan Pilihan"
required
{listVote.map((item, index) => (
<TextInputCustom
key={index}
label="Pilihan"
placeholder="Masukan Pilihan"
required
value={item.value}
onChangeText={(value: any) =>
setListVote(
listVote.map((item, i) =>
i === index ? { ...item, value } : item
)
)
}
/>
))}
<CenterCustom>
<View style={{ flexDirection: "row", alignItems: "center", gap: 10 }}>
<ActionIcon
disabled={listVote.length >= 4}
onPress={() => {
setListVote([...listVote, { name: "Nama Pilihan", value: "" }]);
}}
icon={
<Ionicons
name="add-circle-outline"
size={ICON_SIZE_XLARGE}
color={MainColor.black}
/>
}
size="xl"
/>
</Grid.Col>
<Grid.Col
span={2}
style={{ alignItems: "center", justifyContent: "center" }}
>
<TouchableOpacity onPress={() => console.log("delete")}>
<Ionicons name="trash" size={24} color={MainColor.red} />
</TouchableOpacity>
</Grid.Col>
</Grid>
<ActionIcon
disabled={listVote.length <= 2}
onPress={() => {
const list = _.clone(listVote);
list.pop();
setListVote(list);
}}
icon={
<Ionicons
name="remove-circle-outline"
size={ICON_SIZE_XLARGE}
color={MainColor.black}
/>
}
size="xl"
/>
</View>
</CenterCustom>
<Spacing />
<ButtonCenteredOnly onPress={() => console.log("add")}>
Tambah Pilihan
</ButtonCenteredOnly>
<Spacing />
</StackCustom>
</ViewWrapper>

View File

@@ -42,6 +42,7 @@
"react-native-dotenv": "^3.4.11",
"react-native-gesture-handler": "~2.28.0",
"react-native-international-phone-number": "^0.9.3",
"react-native-keyboard-controller": "^1.18.6",
"react-native-maps": "1.20.1",
"react-native-otp-entry": "^1.8.5",
"react-native-pager-view": "6.9.1",
@@ -1876,6 +1877,8 @@
"react-native-is-edge-to-edge": ["react-native-is-edge-to-edge@1.2.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q=="],
"react-native-keyboard-controller": ["react-native-keyboard-controller@1.18.6", "", { "dependencies": { "react-native-is-edge-to-edge": "^1.2.1" }, "peerDependencies": { "react": "*", "react-native": "*", "react-native-reanimated": ">=3.0.0" } }, "sha512-K/RMw3MdtuykkACFN5d9RTapAcO0v4T34gmSyHkEraU5UsX+fxEHd6j4MvL7KUihvmLLod0NV/mQC0nL4cOurw=="],
"react-native-maps": ["react-native-maps@1.20.1", "", { "dependencies": { "@types/geojson": "^7946.0.13" }, "peerDependencies": { "react": ">= 17.0.1", "react-native": ">= 0.64.3", "react-native-web": ">= 0.11" }, "optionalPeers": ["react-native-web"] }, "sha512-NZI3B5Z6kxAb8gzb2Wxzu/+P2SlFIg1waHGIpQmazDSCRkNoHNY4g96g+xS0QPSaG/9xRBbDNnd2f2/OW6t6LQ=="],
"react-native-otp-entry": ["react-native-otp-entry@1.8.5", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-TZNkIuUzZKAAWrC8X/A22ZHJdycLysxUNysrGf0yTmDLRUyf4zLXwVFcDYUcRNe763Hjaf5qvtKGILb6lDGzoA=="],

View File

@@ -0,0 +1,92 @@
import { AccentColor, MainColor } from "@/constants/color-palet";
import { TEXT_SIZE_LARGE } from "@/constants/constans-value";
import React from "react";
import {
Keyboard,
StyleSheet,
TouchableWithoutFeedback,
View,
} from "react-native";
interface AlertCustomProps {
children: React.ReactNode;
isVisible: boolean;
}
export default function ModalCustom({
children,
isVisible,
}: AlertCustomProps) {
if (!isVisible) return null;
return (
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<View style={styles.overlay}>
<View style={styles.alertBox}>
<View style={{ width: "100%" }}>{children}</View>
</View>
</View>
</TouchableWithoutFeedback>
);
}
const styles = StyleSheet.create({
overlay: {
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: "rgba(0,0,0,0.5)",
justifyContent: "center",
alignItems: "center",
zIndex: 999,
paddingVertical: 20,
},
alertBox: {
width: "90%",
backgroundColor: MainColor.darkblue,
borderColor: AccentColor.blue,
borderWidth: 1,
borderRadius: 10,
padding: 20,
alignItems: "center",
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
elevation: 5,
},
alertTitle: {
fontSize: TEXT_SIZE_LARGE,
fontWeight: "bold",
marginBottom: 20,
color: MainColor.white_gray,
},
alertMessage: {
textAlign: "center",
marginBottom: 20,
color: MainColor.white_gray,
},
alertButtons: {
flexDirection: "row",
justifyContent: "space-between",
width: "100%",
},
alertButton: {
flex: 1,
padding: 10,
borderRadius: 50,
marginHorizontal: 5,
alignItems: "center",
},
leftButton: {
backgroundColor: "gray",
},
rightButton: {
backgroundColor: MainColor.green,
},
buttonText: {
color: "white",
fontWeight: "bold",
},
});

View File

@@ -1,3 +1,4 @@
import { AccentColor } from "@/constants/color-palet";
import Divider from "../Divider/Divider";
import Grid from "../Grid/GridCustom";
import AvatarComp from "../Image/AvatarComp";
@@ -39,7 +40,7 @@ const AvatarUsernameAndOtherComponent = ({
</Grid.Col>
)}
</Grid>
{withBottomLine && <Divider marginTop={0} />}
{withBottomLine && <Divider color={AccentColor.blue} marginTop={0} />}
</>
);
};

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: data?.targetDana,
},
{
label: "Harga Per Lembar",
value: "Rp. 2.400",
value: data?.hargaPerLembar,
},
{
label: "Return Of Investment (ROI)",
value: "3 %",
value: data?.roi + " %",
},
{
label: "Total Lembar",
value: "1.200",
value: data?.totalLembar,
},
{
label: "Sisa Lembar",
value: "600",
value: data?.sisaLembar,
},
{
label: "Jadwal Pembagian",
value: "Rp. 2.880.000",
value: data?.jadwalPembagian,
},
{
label: "Pembagian Deviden",
value: "Selamanya",
value: data?.pembagianDeviden,
},
{
label: "Pencarian Investor",
value: "30 Hari",
value: data?.pencarianInvestor,
},
];
];

View File

@@ -49,6 +49,7 @@
"react-native-dotenv": "^3.4.11",
"react-native-gesture-handler": "~2.28.0",
"react-native-international-phone-number": "^0.9.3",
"react-native-keyboard-controller": "^1.18.6",
"react-native-maps": "1.20.1",
"react-native-otp-entry": "^1.8.5",
"react-native-pager-view": "6.9.1",

View File

@@ -1,22 +1,33 @@
import {
AvatarUsernameAndOtherComponent,
BoxWithHeaderSection,
Grid,
StackCustom,
TextCustom
AvatarUsernameAndOtherComponent,
BoxWithHeaderSection,
Grid,
Spacing,
StackCustom,
TextCustom,
} from "@/components";
export default function Collaboration_BoxDetailSection({ id }: { id: string }) {
export default function Collaboration_BoxDetailSection({
data,
}: {
data: any;
}) {
return (
<>
<BoxWithHeaderSection>
<AvatarUsernameAndOtherComponent
avatar={data?.Author?.Profile?.imageId}
name={data?.Author?.username}
avatarHref={`/profile/${data?.Author?.Profile?.id}`}
withBottomLine
/>
<Spacing height={10}/>
<StackCustom>
<AvatarUsernameAndOtherComponent />
<TextCustom align="center" bold size="large">
Judul Proyek {id}
{data?.title || ""}
</TextCustom>
{listData.map((item, index) => (
{listData(data).map((item, index) => (
<Grid key={index}>
<Grid.Col span={4}>
<TextCustom bold>{item.title}</TextCustom>
@@ -32,23 +43,21 @@ export default function Collaboration_BoxDetailSection({ id }: { id: string }) {
);
}
const listData = [
const listData = (data: any) => [
{
title: "Industri",
value: "Pilihan Industri",
value: data?.ProjectCollaborationMaster_Industri?.name || "-",
},
{
title: "Deskripsi",
value: "Deskripsi Proyek",
title: "Lokasi",
value: data?.lokasi || "-",
},
{
title: "Tujuan Proyek",
value:
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
value: data?.purpose || "-",
},
{
title: "Keuntungan Proyek",
value:
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
value: data?.benefit || "-",
},
];

View File

@@ -7,24 +7,12 @@ import {
import { Href } from "expo-router";
function Collaboration_BoxPublishSection({
id,
title,
username,
description,
href,
// Avatar
sourceAvatar,
data,
rightComponentAvatar,
}: {
id: string;
title?: string;
username?: string;
description?: string;
href: Href;
// Avatar
sourceAvatar?: string;
data: any;
rightComponentAvatar?: React.ReactNode;
}) {
return (
@@ -32,21 +20,18 @@ function Collaboration_BoxPublishSection({
<BoxWithHeaderSection href={href}>
<StackCustom gap={0}>
<AvatarUsernameAndOtherComponent
avatarHref={`/profile/${id}`}
name={username || "Username"}
avatarHref={`/profile/${data?.Author?.id}`}
name={data?.Author?.username || "Username"}
rightComponent={rightComponentAvatar}
avatar={sourceAvatar as any}
avatar={data?.Author?.Profile?.imageId}
withBottomLine
/>
<StackCustom>
<TextCustom truncate={2} size="large" bold align="center">
{title || "Lorem ipsum dolor sit"}
</TextCustom>
<TextCustom truncate={2}>
{description ||
"Lorem ipsum dolor sit amet consectetur adipisicing elit. Porro sed doloremque tempora soluta. Dolorem ex quidem ipsum tempora, ipsa, obcaecati quia suscipit numquam, voluptates commodi porro impedit natus quos doloremque!"}
<TextCustom truncate size="large" bold align="center">
{data?.title || "-"}
</TextCustom>
<TextCustom truncate={2}>{data?.purpose || "-"}</TextCustom>
{/* <TextCustom bold size="small" >
2 Partisipan
</TextCustom> */}

View File

@@ -0,0 +1,222 @@
// ChatScreen.tsx
import React, { useEffect, useRef, useState } from "react";
import {
Dimensions,
Keyboard,
Platform,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from "react-native";
import {
SafeAreaView
} from "react-native-safe-area-context";
type Message = {
id: string;
text: string;
sender: "me" | "other";
timestamp: Date;
};
const { height: SCREEN_HEIGHT } = Dimensions.get("window");
const ChatScreen: React.FC = () => {
const [messages, setMessages] = useState<Message[]>([
{
id: "1",
text: "Hai!",
sender: "other",
timestamp: new Date(Date.now() - 300000),
},
{
id: "2",
text: "Halo juga!",
sender: "me",
timestamp: new Date(Date.now() - 240000),
},
{
id: "3",
text: "Apa kabar?",
sender: "other",
timestamp: new Date(Date.now() - 180000),
},
]);
const [inputText, setInputText] = useState<string>("");
const [keyboardHeight, setKeyboardHeight] = useState<number>(0);
const scrollViewRef = useRef<ScrollView>(null);
useEffect(() => {
const show = Keyboard.addListener("keyboardDidShow", (e) => {
let kbHeight = e.endCoordinates.height;
// Di Android dengan edge-to-edge, kadang tinggi termasuk navigation bar
if (Platform.OS === "android") {
// Batasi maksimal 60% layar
kbHeight = Math.min(kbHeight, SCREEN_HEIGHT * 2);
}
setKeyboardHeight(kbHeight);
});
const hide = Keyboard.addListener("keyboardDidHide", () => {
setKeyboardHeight(0);
});
return () => {
show.remove();
hide.remove();
};
}, []);
useEffect(() => {
// Scroll ke bawah setelah pesan baru atau keyboard muncul
const timer = setTimeout(() => {
scrollViewRef.current?.scrollToEnd({ animated: true });
}, 100); // delay kecil untuk pastikan layout stabil
return () => clearTimeout(timer);
}, [messages, keyboardHeight]);
const handleSend = () => {
if (!inputText.trim()) return;
setMessages((prev) => [
...prev,
{
id: Date.now().toString(),
text: inputText.trim(),
sender: "me",
timestamp: new Date(),
},
]);
setInputText("");
};
return (
<SafeAreaView style={styles.safeArea}>
{/* Kontainer utama dengan padding bottom = tinggi keyboard */}
<View style={[styles.container, { paddingBottom: keyboardHeight }]}>
<ScrollView
ref={scrollViewRef}
style={styles.messagesContainer}
contentContainerStyle={styles.messagesContent}
keyboardShouldPersistTaps="handled"
>
{messages.map((msg) => (
<View
key={msg.id}
style={[
styles.messageBubble,
msg.sender === "me" ? styles.myMessage : styles.otherMessage,
]}
>
<Text style={styles.messageText}>{msg.text}</Text>
<Text style={styles.timestamp}>
{msg.timestamp.toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}
</Text>
</View>
))}
</ScrollView>
<View style={[styles.inputContainer, { paddingBottom: 10 }]}>
<TextInput
value={inputText}
onChangeText={setInputText}
placeholder="Ketik pesan..."
style={styles.textInput}
multiline
blurOnSubmit={false}
scrollEnabled={Platform.OS === "ios"} // ✅ Aktifkan scroll hanya di iOS
textAlignVertical="top"
/>
<TouchableOpacity
style={styles.sendButton}
onPress={handleSend}
disabled={!inputText.trim()}
>
<Text style={styles.sendButtonText}>Kirim</Text>
</TouchableOpacity>
</View>
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
safeArea: {
flex: 1,
backgroundColor: "#e5ddd5",
},
container: {
flex: 1,
backgroundColor: "#e5ddd5",
},
messagesContainer: {
flex: 1,
paddingHorizontal: 10,
},
messagesContent: {
paddingBottom: 10,
},
messageBubble: {
maxWidth: "80%",
padding: 10,
marginVertical: 4,
borderRadius: 12,
},
myMessage: {
alignSelf: "flex-end",
backgroundColor: "#dcf8c6",
},
otherMessage: {
alignSelf: "flex-start",
backgroundColor: "#ffffff",
},
messageText: {
fontSize: 16,
color: "#000",
},
timestamp: {
fontSize: 10,
color: "#666",
textAlign: "right",
marginTop: 4,
},
inputContainer: {
flexDirection: "row",
alignItems: "flex-end",
paddingHorizontal: 10,
paddingTop: 8,
backgroundColor: "#f0f0f0",
borderTopWidth: 1,
borderTopColor: "#ddd",
},
textInput: {
flex: 1,
borderWidth: 1,
borderColor: "#ccc",
borderRadius: 20,
paddingHorizontal: 12,
paddingVertical: 8,
fontSize: 16,
backgroundColor: "#fff",
maxHeight: 100,
},
sendButton: {
marginLeft: 10,
backgroundColor: "#34b7f1",
paddingHorizontal: 16,
paddingVertical: 10,
borderRadius: 20,
},
sendButtonText: {
color: "#fff",
fontWeight: "bold",
},
});
export default ChatScreen;

View File

@@ -0,0 +1,292 @@
/* eslint-disable react-hooks/exhaustive-deps */
// ChatScreen.tsx
import { LoaderCustom } from "@/components";
import { AccentColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import { useAuth } from "@/hooks/use-auth";
import {
apiCollaborationGroupMessage,
apiCollaborationGroupMessageCreate,
} from "@/service/api-client/api-collaboration";
import { formatChatTime } from "@/utils/formatChatTime";
import { AntDesign } from "@expo/vector-icons";
import dayjs from "dayjs";
import { useFocusEffect } from "expo-router";
import _ from "lodash";
import React, { useCallback, useEffect, useRef, useState } from "react";
import {
Dimensions,
Keyboard,
Platform,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from "react-native";
import { ActivityIndicator } from "react-native-paper";
import { SafeAreaView } from "react-native-safe-area-context";
type IMessage = {
id: string;
createdAt: Date;
isActive: boolean;
message: string;
isFile: boolean;
userId: string;
User: {
select: {
id: true;
username: true;
};
};
};
const { height: SCREEN_HEIGHT } = Dimensions.get("window");
const ChatScreen: React.FC<{ id: string }> = ({ id }) => {
const { user } = useAuth();
const [messages, setMessages] = useState<IMessage[] | null>(null);
const [loadingMessage, setLoadingMessage] = useState<boolean>(false);
const [inputText, setInputText] = useState<string>("");
const [keyboardHeight, setKeyboardHeight] = useState<number>(0);
const scrollViewRef = useRef<ScrollView>(null);
useFocusEffect(
useCallback(() => {
onLoadMessage();
}, [id])
);
const onLoadMessage = async () => {
try {
setLoadingMessage(true);
const response = await apiCollaborationGroupMessage({ id: id as string });
if (response.success) {
setMessages(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingMessage(false);
}
};
useEffect(() => {
const show = Keyboard.addListener("keyboardDidShow", (e) => {
let kbHeight = e.endCoordinates.height;
// Di Android dengan edge-to-edge, kadang tinggi termasuk navigation bar
if (Platform.OS === "android") {
// Batasi maksimal 60% layar
kbHeight = Math.min(kbHeight, SCREEN_HEIGHT * 0.6);
}
setKeyboardHeight(kbHeight);
});
const hide = Keyboard.addListener("keyboardDidHide", () => {
setKeyboardHeight(0);
});
return () => {
show.remove();
hide.remove();
};
}, []);
useEffect(() => {
// Scroll ke bawah setelah pesan baru atau keyboard muncul
const timer = setTimeout(() => {
scrollViewRef.current?.scrollToEnd({ animated: true });
}, 100); // delay kecil untuk pastikan layout stabil
return () => clearTimeout(timer);
}, [messages, keyboardHeight]);
const handleSend = async () => {
if (!inputText.trim()) return;
const newData = {
userId: user?.id,
message: inputText.trim(),
};
try {
const response = await apiCollaborationGroupMessageCreate({
id: id as string,
data: newData,
});
if (response.success) {
setMessages((prev: IMessage | any) => [
...prev,
{
id: Date.now().toString(),
createdAt: new Date(),
isActive: true,
message: inputText.trim(),
isFile: false,
userId: user?.id,
User: {
username: user?.username,
},
},
]);
setInputText("");
}
} catch (error) {
console.log("[ERROR]", error);
}
};
return (
<SafeAreaView style={styles.safeArea} edges={["bottom"]}>
{/* Kontainer utama dengan padding bottom = tinggi keyboard */}
<View style={[styles.container, { paddingBottom: keyboardHeight }]}>
<ScrollView
ref={scrollViewRef}
style={styles.messagesContainer}
contentContainerStyle={styles.messagesContent}
keyboardShouldPersistTaps="handled"
>
{loadingMessage ? (
<ActivityIndicator color="black" size={"large"} />
) : _.isEmpty(messages) ? (
<Text style={styles.isEmptyMessage}>
Belum ada pesan
</Text>
) : (
messages?.map((item: any) => (
<View
key={item.id}
style={[
styles.messageBubble,
item.userId === user?.id
? styles.myMessage
: styles.otherMessage,
]}
>
<Text style={styles.name}>{item?.User?.username}</Text>
<Text style={styles.messageText}>{item.message}</Text>
<Text style={styles.timestamp}>
{formatChatTime(item.createdAt)}
</Text>
</View>
))
)}
</ScrollView>
<View style={[styles.inputContainer, { paddingBottom: 10 }]}>
<TextInput
value={inputText}
onChangeText={setInputText}
placeholder="Ketik pesan..."
style={styles.textInput}
multiline
blurOnSubmit={false}
scrollEnabled={Platform.OS === "ios"} // ✅ Aktifkan scroll hanya di iOS
textAlignVertical="top"
/>
<TouchableOpacity
activeOpacity={0.7}
style={styles.sendButton}
onPress={handleSend}
disabled={!inputText.trim()}
>
<AntDesign name="send" size={ICON_SIZE_SMALL} color="white"/>
{/* <Text style={styles.sendButtonText}>Kirim</Text> */}
</TouchableOpacity>
</View>
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
safeArea: {
flex: 1,
backgroundColor: "#e5ddd5",
},
container: {
flex: 1,
backgroundColor: "#e5ddd5",
},
isEmptyMessage: {
alignSelf: "center",
color: "#666",
marginTop: 20,
},
messagesContainer: {
flex: 1,
paddingHorizontal: 10,
},
messagesContent: {
paddingBottom: 10,
},
messageBubble: {
maxWidth: "80%",
padding: 10,
marginVertical: 4,
borderRadius: 12,
},
name: {
fontSize: 12,
color: "#666",
marginBottom: 4,
},
myMessage: {
alignSelf: "flex-end",
backgroundColor: "#dcf8c6",
},
otherMessage: {
alignSelf: "flex-start",
backgroundColor: "#ffffff",
},
messageText: {
fontSize: 16,
color: "#000",
},
timestamp: {
fontSize: 10,
color: "#666",
textAlign: "right",
marginTop: 4,
},
inputContainer: {
flexDirection: "row",
alignItems: "flex-end",
paddingHorizontal: 10,
paddingTop: 8,
backgroundColor: "#f0f0f0",
borderTopWidth: 1,
borderTopColor: "#ddd",
},
textInput: {
flex: 1,
borderWidth: 1,
borderColor: "#ccc",
borderRadius: 20,
paddingHorizontal: 12,
paddingVertical: 8,
fontSize: 16,
backgroundColor: "#fff",
maxHeight: 100,
},
sendButton: {
marginLeft: 10,
backgroundColor: AccentColor.blue,
// paddingHorizontal: 16,
// paddingVertical: 10,
borderRadius: "100%",
width: 40,
height: 40,
justifyContent: "center",
alignContent: "center",
alignItems: "center",
},
sendButtonText: {
color: "#fff",
fontWeight: "bold",
},
});
export default ChatScreen;

View File

@@ -15,10 +15,12 @@ export default function Collaboration_ProjectMainSelectedSection({
selected,
setSelected,
setOpenDrawerParticipant,
listData,
}: {
selected: (string | number)[];
setSelected: (value: (string | number)[]) => void;
setOpenDrawerParticipant: (value: boolean) => void;
listData: any[];
}) {
return (
<BaseBox style={{ height: 500 }}>
@@ -31,7 +33,7 @@ export default function Collaboration_ProjectMainSelectedSection({
</TextCustom>
<CheckboxGroup value={selected} onChange={setSelected}>
{Array.from({ length: 5 }).map((_, index) => (
{listData?.map((item: any, index: any) => (
<View key={index}>
<Grid key={index}>
<Grid.Col

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

@@ -1,5 +1,5 @@
import {
AvatarCustom,
AvatarComp,
BaseBox,
ClickableCustom,
Grid,
@@ -8,6 +8,8 @@ import {
} 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,28 +17,32 @@ 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,
}}
>
<View style={GStyles.forumBox}>
{isTruncate ? (
<TextCustom truncate={2}>{data.deskripsi}</TextCustom>
<TextCustom truncate={2}>{data?.diskusi}</TextCustom>
) : (
<TextCustom>{data.deskripsi}</TextCustom>
<TextCustom>{data?.diskusi}</TextCustom>
)}
</View>
);
@@ -47,42 +53,53 @@ export default function Forum_BoxDetailSection({
<View>
<Grid>
<Grid.Col span={2}>
<AvatarCustom href={`/profile/${data.id}`} />
<AvatarComp
fileId={data?.Author?.Profile?.imageId}
href={`/forum/${data?.Author?.id}/forumku`}
size={"base"}
/>
</Grid.Col>
<Grid.Col span={8}>
<TextCustom>{data.name}</TextCustom>
{data.status === "Open" ? (
<Grid.Col span={6}>
<TextCustom truncate>{data?.Author?.username}</TextCustom>
{data?.ForumMaster_StatusPosting?.status === "Open" ? (
<TextCustom bold size="small" color="green">
{data.status}
{data?.ForumMaster_StatusPosting?.status}
</TextCustom>
) : (
<TextCustom bold size="small" color="red">
{data.status}
{data?.ForumMaster_StatusPosting?.status}
</TextCustom>
)}
</Grid.Col>
<Grid.Col
span={2}
span={4}
style={{
justifyContent: "center",
justifyContent: "flex-start",
alignItems: "flex-end",
}}
>
<ClickableCustom
onPress={() => {
setOpenDrawer(true);
setStatus(data.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>
@@ -110,11 +127,13 @@ export default function Forum_BoxDetailSection({
size={ICON_SIZE_SMALL}
color={MainColor.white}
/>
<TextCustom>{data.jumlahBalas}</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,26 +1,67 @@
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") {
setShowDeleteAlert(true);
AlertDefaultSystem({
title: "Hapus diskusi",
message: "Apakah Anda yakin ingin menghapus diskusi ini?",
textLeft: "Batal",
textRight: "Hapus",
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") {
setShowAlertStatus(true);
AlertDefaultSystem({
title: "Ubah Status",
message: "Apakah Anda yakin ingin mengubah status forum ini?",
textLeft: "Batal",
textRight: "Ubah",
onPressRight: () => {
handlerUpdateStatus?.(item.label === "Buka forum" ? "Open" : "Closed");
},
});
} else {
router.push(item.path as any);
}
@@ -32,9 +73,13 @@ 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}
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

@@ -10,10 +10,12 @@ import { View } from "react-native";
export default function Invesment_BoxDetailDataSection({
title,
imageId,
data,
bottomSection,
}: {
title?: string;
imageId?: string;
data: any;
bottomSection?: React.ReactNode;
}) {
@@ -21,14 +23,14 @@ export default function Invesment_BoxDetailDataSection({
<>
<BaseBox paddingBottom={0}>
<StackCustom gap={"xs"}>
<DummyLandscapeImage />
<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>

View File

@@ -1,22 +1,54 @@
import { AlertDefaultSystem, ButtonCustom, Grid } from "@/components";
import {
apiInvestmentDelete,
apiInvestmentUpdateStatus,
} from "@/service/api-client/api-investment";
import { deleteImageService } from "@/service/upload-service";
import { router } from "expo-router";
import { useState } from "react";
import Toast from "react-native-toast-message";
export default function Investment_ButtonStatusSection({
id,
status,
buttonPublish
buttonPublish,
}: {
id: string;
status: string;
buttonPublish?: React.ReactNode;
}) {
const [isLoading, setIsLoading] = useState(false);
const handleBatalkanReview = () => {
AlertDefaultSystem({
title: "Batalkan Review",
message: "Apakah Anda yakin ingin batalkan review ini?",
textLeft: "Batal",
textRight: "Ya",
onPressRight: () => {
console.log("Hapus");
router.back();
onPressRight: async () => {
try {
setIsLoading(true);
const response = await apiInvestmentUpdateStatus({
id: id as string,
status: "draft",
});
console.log("[RESPONSE]", JSON.stringify(response, null, 2));
if (response.success) {
Toast.show({
type: "success",
text1: "Berhasil Batalkan Review",
});
router.back();
} else {
Toast.show({
type: "error",
text1: "Gagal Batalkan Review",
});
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoading(false);
}
},
});
};
@@ -27,9 +59,31 @@ export default function Investment_ButtonStatusSection({
message: "Apakah Anda yakin ingin ajukan review ini?",
textLeft: "Batal",
textRight: "Ya",
onPressRight: () => {
console.log("Hapus");
router.back();
onPressRight: async () => {
try {
setIsLoading(true);
const response = await apiInvestmentUpdateStatus({
id: id as string,
status: "review",
});
console.log("[RESPONSE]", JSON.stringify(response, null, 2));
if (response.success) {
Toast.show({
type: "success",
text1: "Berhasil Ajukan Review",
});
router.back();
} else {
Toast.show({
type: "error",
text1: "Gagal Ajukan Review",
});
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoading(false);
}
},
});
};
@@ -40,9 +94,31 @@ export default function Investment_ButtonStatusSection({
message: "Apakah Anda yakin ingin edit kembali ini?",
textLeft: "Batal",
textRight: "Ya",
onPressRight: () => {
console.log("Hapus");
router.back();
onPressRight: async () => {
try {
setIsLoading(true);
const response = await apiInvestmentUpdateStatus({
id: id as string,
status: "draft",
});
console.log("[RESPONSE]", JSON.stringify(response, null, 2));
if (response.success) {
Toast.show({
type: "success",
text1: "Berhasil Update Status",
});
router.back();
} else {
Toast.show({
type: "error",
text1: "Gagal Update Status",
});
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoading(false);
}
},
});
};
@@ -53,9 +129,55 @@ export default function Investment_ButtonStatusSection({
message: "Apakah Anda yakin ingin menghapus data ini?",
textLeft: "Batal",
textRight: "Hapus",
onPressRight: () => {
console.log("Hapus");
router.back();
onPressRight: async () => {
try {
setIsLoading(true);
const response = await apiInvestmentDelete({
id: id as string,
});
console.log("[RESPONSE]", JSON.stringify(response, null, 2));
if (response.success) {
const deleteImage = await deleteImageService({
id: response?.data?.imageId as string,
});
if (!deleteImage.success) {
Toast.show({
type: "error",
text1: "Gagal Hapus Data",
});
return;
}
const deleteFile = await deleteImageService({
id: response?.data?.prospektusFileId as string,
});
if (!deleteFile.success) {
Toast.show({
type: "error",
text1: "Gagal Hapus Data",
});
return;
}
Toast.show({
type: "success",
text1: "Berhasil Hapus Data",
});
router.back();
} else {
Toast.show({
type: "error",
text1: "Gagal Hapus Data",
});
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoading(false);
}
},
});
};
@@ -64,6 +186,7 @@ export default function Investment_ButtonStatusSection({
return (
<>
<ButtonCustom
isLoading={isLoading}
backgroundColor="red"
textColor="white"
onPress={handleOpenDeleteAlert}
@@ -76,13 +199,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 +213,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 +229,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

@@ -10,26 +10,32 @@ import Investment_ButtonStatusSection from "./ButtonStatusSection";
export default function Invesment_DetailDataPublishSection({
status,
data,
bottomSection,
buttonSection,
}: {
status: string;
data: any;
bottomSection?: React.ReactNode;
buttonSection?: React.ReactNode;
}) {
// console.log("[DATA DETAIL]", JSON.stringify(data, null, 2));
return (
<>
<StackCustom gap={"sm"}>
<Invesment_BoxProgressSection status={status as string} />
<Invesment_BoxDetailDataSection
title={data?.title}
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,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,5 +1,9 @@
import { AlertDefaultSystem, ButtonCustom, Grid } from "@/components";
import { apiJobDelete, apiJobUpdateStatus } from "@/service/api-client/api-job";
import {
apiJobDelete,
apiJobUpdateData,
apiJobUpdateStatus,
} from "@/service/api-client/api-job";
import { router } from "expo-router";
import Toast from "react-native-toast-message";
@@ -8,11 +12,13 @@ export default function Job_ButtonStatusSection({
status,
isLoading,
onSetLoading,
isArchive,
}: {
id: string;
status: string;
isLoading: boolean;
onSetLoading: (value: boolean) => void;
isArchive?: boolean;
}) {
const handleBatalkanReview = () => {
AlertDefaultSystem({
@@ -27,7 +33,7 @@ export default function Job_ButtonStatusSection({
id: id,
status: "draft",
});
console.log("[RESPONSE]", JSON.stringify(response, null, 2));
if (response.success) {
Toast.show({
type: "success",
@@ -64,7 +70,7 @@ export default function Job_ButtonStatusSection({
id: id,
status: "review",
});
console.log("[RESPONSE]", JSON.stringify(response, null, 2));
if (response.success) {
Toast.show({
type: "success",
@@ -101,7 +107,7 @@ export default function Job_ButtonStatusSection({
id: id,
status: "draft",
});
console.log("[RESPONSE]", JSON.stringify(response, null, 2));
if (response.success) {
Toast.show({
type: "success",
@@ -137,7 +143,7 @@ export default function Job_ButtonStatusSection({
const response = await apiJobDelete({
id: id,
});
console.log("[RESPONSE]", JSON.stringify(response, null, 2));
if (response.success) {
Toast.show({
type: "success",
@@ -161,6 +167,45 @@ export default function Job_ButtonStatusSection({
});
};
const handleArchive = () => {
AlertDefaultSystem({
title: "Arsipkan",
message: "Apakah Anda yakin ingin mengarsipkan data ini?",
textLeft: "Batal",
textRight: "Arsipkan",
onPressRight: async () => {
try {
onSetLoading(true);
const response = await apiJobUpdateData({
id: id,
data: isArchive,
category: "archive",
});
if (response.success) {
Toast.show({
type: "success",
text1: response.message,
});
// router.back();
router.replace("/(application)/(user)/job/(tabs)/archive");
} else {
Toast.show({
type: "info",
text1: "Info",
text2: response.message,
});
router.back();
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
onSetLoading(false);
}
},
});
};
const DeleteButton = () => {
return (
<>
@@ -181,9 +226,9 @@ export default function Job_ButtonStatusSection({
return (
<>
<ButtonCustom
isLoading={isLoading}
onPress={() => {
console.log("Arsipkan");
router.replace("/(application)/(user)/job/(tabs)/archive");
handleArchive();
}}
>
Arsipkan

View File

@@ -9,23 +9,32 @@ import { GStyles } from "@/styles/global-styles";
import { Voting_ComponentDetailDataSection } from "./ComponentDetailDataSection";
export function Voting_BoxDetailContributionSection({
data,
headerAvatar,
nameChoice,
}: {
data: any;
headerAvatar?: React.ReactNode;
nameChoice?: string;
}) {
return (
<>
<BoxWithHeaderSection>
{headerAvatar ? headerAvatar : <Spacing />}
{headerAvatar && (
<>
{headerAvatar}
<Spacing />
</>
)}
<StackCustom gap={"lg"}>
<Voting_ComponentDetailDataSection />
<Voting_ComponentDetailDataSection data={data} />
<StackCustom gap={"xs"}>
<StackCustom gap={"sm"}>
<TextCustom bold size="small" align="center">
Pilihan Anda
</TextCustom>
<BadgeCustom style={[GStyles.alignSelfCenter]}>
Pilihan 1
<BadgeCustom variant="light" size="lg" style={[GStyles.alignSelfCenter]}>
{nameChoice || "-"}
</BadgeCustom>
</StackCustom>
</StackCustom>

View File

@@ -6,7 +6,11 @@ import {
CircleContainer,
} from "@/components";
export default function Voting_BoxDetailHasilVotingSection() {
export default function Voting_BoxDetailHasilVotingSection({
listData,
}: {
listData: any[];
}) {
return (
<>
<BaseBox>
@@ -16,10 +20,12 @@ export default function Voting_BoxDetailHasilVotingSection() {
</TextCustom>
<Grid>
{Array.from({ length: 4 }).map((_, i) => (
<Grid.Col span={3} style={{ alignItems: "center" }} key={i}>
<CircleContainer value={9 % (i + 4)} />
<TextCustom size="small">Pilihan {i + 1}</TextCustom>
{listData?.map((item: any, i: number) => (
<Grid.Col span={12 / listData?.length} style={{ alignItems: "center" }} key={i}>
<StackCustom>
<CircleContainer value={item?.jumlah} />
<TextCustom align="center" size="small">{item?.value}</TextCustom>
</StackCustom>
</Grid.Col>
))}
</Grid>

View File

@@ -1,21 +1,36 @@
import {
BadgeCustom,
BoxWithHeaderSection,
Spacing,
StackCustom
StackCustom,
TextCustom
} from "@/components";
import { Voting_ComponentDetailDataSection } from "./ComponentDetailDataSection";
import { GStyles } from "@/styles/global-styles";
export function Voting_BoxDetailHistorySection({
headerAvatar,
data,
nameChoice,
}: {
headerAvatar?: React.ReactNode;
data: any;
nameChoice: string;
}) {
return (
<>
<BoxWithHeaderSection>
{headerAvatar ? headerAvatar : <Spacing />}
<StackCustom>
<Voting_ComponentDetailDataSection />
<Voting_ComponentDetailDataSection data={data} />
<StackCustom gap={"sm"}>
<TextCustom bold size="small" align="center">
Pilihan Anda
</TextCustom>
<BadgeCustom variant="light" size="lg" style={[GStyles.alignSelfCenter]}>
{nameChoice || "-"}
</BadgeCustom>
</StackCustom>
</StackCustom>
</BoxWithHeaderSection>
</>

View File

@@ -1,4 +1,6 @@
import {
AlertDefaultSystem,
BadgeCustom,
BoxWithHeaderSection,
ButtonCustom,
Spacing,
@@ -6,40 +8,109 @@ import {
TextCustom
} from "@/components";
import { RadioCustom, RadioGroup } from "@/components/Radio/RadioCustom";
import { apiVotingVote } from "@/service/api-client/api-voting";
import { today } from "@/utils/dateTimeView";
import { router } from "expo-router";
import { useState } from "react";
import { View } from "react-native";
import { Voting_ComponentDetailDataSection } from "./ComponentDetailDataSection";
export function Voting_BoxDetailPublishSection({
headerAvatar,
data,
userId,
isContribution,
nameChoice,
}: {
headerAvatar?: React.ReactNode;
data?: any;
userId: string;
isContribution?: boolean;
nameChoice?: string;
}) {
const [value, setValue] = useState<any | number>("");
const handlerSubmitVote = async () => {
const newData = {
chooseId: value,
userId: userId,
};
try {
const response = await apiVotingVote({
id: data?.id,
data: newData,
});
if (response.success) {
router.push(`/voting/${data?.id}/list-of-contributor`);
}
} catch (error) {
console.log("[ERROR]", error);
}
};
return (
<>
<BoxWithHeaderSection>
{headerAvatar ? headerAvatar : <Spacing />}
{headerAvatar && (
<>
{headerAvatar}
<Spacing />
</>
)}
<StackCustom gap={"lg"}>
<Voting_ComponentDetailDataSection />
<Voting_ComponentDetailDataSection data={data} />
<View>
<TextCustom bold size="small">
Pilihan :
</TextCustom>
<RadioGroup value={value} onChange={setValue}>
{Array.from({ length: 4 }).map((_, i) => (
<View key={i}>
<RadioCustom
label={`Pilihan ${i + 1}`}
value={`Pilihan ${i + 1}`}
/>
</View>
))}
</RadioGroup>
</View>
{isContribution ? (
<StackCustom gap={"sm"}>
<TextCustom align="center" size="small" bold>
Pilihan Anda :
</TextCustom>
<View style={{ alignSelf: "center" }}>
<BadgeCustom variant="light" size="lg">
{nameChoice || "-"}
</BadgeCustom>
</View>
</StackCustom>
) : (
<>
<StackCustom>
<TextCustom bold size="small">
Pilihan :
</TextCustom>
<RadioGroup value={value} onChange={setValue}>
{data?.Voting_DaftarNamaVote?.map((item: any, i: number) => (
<View key={i}>
<RadioCustom
disabled={
today.getDate() < new Date(data?.awalVote).getDate()
}
label={item?.value}
value={item?.id}
/>
</View>
))}
</RadioGroup>
</StackCustom>
<ButtonCustom onPress={() => console.log("vote")}>Vote</ButtonCustom>
<ButtonCustom
disabled={value === ""}
onPress={() => {
AlertDefaultSystem({
title: "Anda melaukan voting",
message: "Yakin dengan pilihan anda ini ?",
textLeft: "Batal",
textRight: "Ya",
onPressRight: () => handlerSubmitVote(),
});
}}
>
Vote
</ButtonCustom>
</>
)}
</StackCustom>
</BoxWithHeaderSection>
</>

View File

@@ -2,35 +2,38 @@ import {
BoxWithHeaderSection,
Spacing,
StackCustom,
TextCustom
TextCustom,
} from "@/components";
import { View } from "react-native";
import { Voting_ComponentDetailDataSection } from "./ComponentDetailDataSection";
import { Ionicons } from "@expo/vector-icons";
export function Voting_BoxDetailSection({
headerAvatar,
data,
}: {
headerAvatar?: React.ReactNode;
data?: any;
}) {
return (
<>
<BoxWithHeaderSection>
{headerAvatar ? headerAvatar : <Spacing />}
<StackCustom>
<Voting_ComponentDetailDataSection/>
<Spacing/>
<Voting_ComponentDetailDataSection data={data} />
<Spacing height={0} />
<View>
<StackCustom>
<TextCustom bold size="small">
Pilihan :
</TextCustom>
{Array.from({ length: 3 }).map((_, i) => (
<View key={i}>
<TextCustom>Nama Pilihan {i + 1}</TextCustom>
<Spacing />
</View>
{data?.Voting_DaftarNamaVote?.map((item: any, i: number) => (
<StackCustom key={i}>
<TextCustom>
<Ionicons name="caret-forward" size={14} /> {item?.value}
</TextCustom>
</StackCustom>
))}
</View>
</StackCustom>
</StackCustom>
</BoxWithHeaderSection>
</>

View File

@@ -15,37 +15,43 @@ export default function Voting_BoxPublishSection({
href,
id,
bottomComponent,
data,
}: {
href?: Href
id?: string
href?: Href;
id?: string;
bottomComponent?: React.ReactNode;
data?: any;
}) {
return (
<>
<BoxWithHeaderSection href={href}>
<AvatarUsernameAndOtherComponent avatarHref="/profile/1" />
<Spacing />
<StackCustom>
<AvatarUsernameAndOtherComponent
avatar={data?.Author?.Profile?.imageId || ""}
name={data?.Author?.username || "Username"}
avatarHref="/profile/1"
/>
<Spacing height={0} />
<StackCustom gap={"lg"}>
<TextCustom align="center" bold truncate size="large">
Voting Title {id}
{data?.title || "-"}
</TextCustom>
<BadgeCustom
style={{ width: "70%", alignSelf: "center" }}
variant="light"
>
{dayjs().format("DD/MM/YYYY")} -{" "}
{dayjs().add(1, "day").format("DD/MM/YYYY")}
{dayjs(data?.awalVote).format("DD/MM/YYYY")} -{" "}
{dayjs(data?.akhirVote).format("DD/MM/YYYY")}
</BadgeCustom>
<Grid>
{/* <Grid>
{Array.from({ length: 4 }).map((_, i) => (
<Grid.Col span={3} style={{ alignItems: "center" }} key={i}>
<CircleContainer value={9 % (i + 4)} />
<TextCustom size="small">Pilihan {i + 1}</TextCustom>
</Grid.Col>
))}
</Grid>
</Grid> */}
{bottomComponent}
</StackCustom>
</BoxWithHeaderSection>

View File

@@ -1,11 +1,21 @@
import { AlertDefaultSystem, ButtonCustom, Grid } from "@/components";
import {
apiVotingDelete,
apiVotingUpdateStatus,
} from "@/service/api-client/api-voting";
import { router } from "expo-router";
import { View } from "react-native";
import Toast from "react-native-toast-message";
export default function Voting_ButtonStatusSection({
id,
status,
isLoading,
onSetLoading,
}: {
id: string;
status: string;
isLoading: boolean;
onSetLoading: (value: boolean) => void;
}) {
const handleBatalkanReview = () => {
AlertDefaultSystem({
@@ -13,9 +23,33 @@ export default function Voting_ButtonStatusSection({
message: "Apakah Anda yakin ingin batalkan review ini?",
textLeft: "Batal",
textRight: "Ya",
onPressRight: () => {
console.log("Hapus");
router.back();
onPressRight: async () => {
try {
onSetLoading(true);
const response = await apiVotingUpdateStatus({
id: id as string,
status: "draft",
});
if (response?.success) {
Toast.show({
type: "success",
text1: response.message,
});
router.back();
} else {
Toast.show({
type: "info",
text1: "Info",
text2: response.message,
});
router.back();
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
onSetLoading(false);
}
},
});
};
@@ -26,9 +60,33 @@ export default function Voting_ButtonStatusSection({
message: "Apakah Anda yakin ingin ajukan review ini?",
textLeft: "Batal",
textRight: "Ya",
onPressRight: () => {
console.log("Hapus");
router.back();
onPressRight: async () => {
try {
onSetLoading(true);
const response = await apiVotingUpdateStatus({
id: id as string,
status: "review",
});
if (response?.success) {
Toast.show({
type: "success",
text1: response.message,
});
router.back();
} else {
Toast.show({
type: "info",
text1: "Info",
text2: response.message,
});
router.back();
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
onSetLoading(false);
}
},
});
};
@@ -39,9 +97,33 @@ export default function Voting_ButtonStatusSection({
message: "Apakah Anda yakin ingin edit kembali ini?",
textLeft: "Batal",
textRight: "Ya",
onPressRight: () => {
console.log("Hapus");
router.back();
onPressRight: async () => {
try {
onSetLoading(true);
const response = await apiVotingUpdateStatus({
id: id as string,
status: "draft",
});
if (response?.success) {
Toast.show({
type: "success",
text1: response.message,
});
router.back();
} else {
Toast.show({
type: "info",
text1: "Info",
text2: response.message,
});
router.back();
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
onSetLoading(false);
}
},
});
};
@@ -52,9 +134,32 @@ export default function Voting_ButtonStatusSection({
message: "Apakah Anda yakin ingin menghapus data ini?",
textLeft: "Batal",
textRight: "Hapus",
onPressRight: () => {
console.log("Hapus");
router.back();
onPressRight: async () => {
try {
onSetLoading(true);
const response = await apiVotingDelete({
id: id as string,
});
if (response?.success) {
Toast.show({
type: "success",
text1: response.message,
});
router.back();
} else {
Toast.show({
type: "info",
text1: "Info",
text2: response.message,
});
router.back();
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
onSetLoading(false);
}
},
});
};
@@ -63,6 +168,7 @@ export default function Voting_ButtonStatusSection({
return (
<>
<ButtonCustom
isLoading={isLoading}
backgroundColor="red"
textColor="white"
onPress={handleOpenDeleteAlert}
@@ -79,7 +185,7 @@ export default function Voting_ButtonStatusSection({
case "review":
return (
<ButtonCustom onPress={handleBatalkanReview}>
<ButtonCustom isLoading={isLoading} onPress={handleBatalkanReview}>
Batalkan Review
</ButtonCustom>
);
@@ -88,15 +194,14 @@ export default function Voting_ButtonStatusSection({
return (
<>
<Grid>
<Grid.Col span={5}>
<ButtonCustom onPress={handleAjukanReview}>
<Grid.Col span={6} style={{ paddingRight: 10 }}>
<ButtonCustom isLoading={isLoading} onPress={handleAjukanReview}>
Ajukan Review
</ButtonCustom>
</Grid.Col>
<Grid.Col span={2}>
<View />
<Grid.Col span={6} style={{ paddingRight: 10 }}>
{DeleteButton()}
</Grid.Col>
<Grid.Col span={5}>{DeleteButton()}</Grid.Col>
</Grid>
</>
);
@@ -105,15 +210,14 @@ export default function Voting_ButtonStatusSection({
return (
<>
<Grid>
<Grid.Col span={5}>
<ButtonCustom onPress={handleEditKembali}>
<Grid.Col span={6} style={{ paddingRight: 10 }}>
<ButtonCustom isLoading={isLoading} onPress={handleEditKembali}>
Edit Kembali
</ButtonCustom>
</Grid.Col>
<Grid.Col span={2}>
<View />
<Grid.Col span={6} style={{ paddingLeft: 10 }}>
{DeleteButton()}
</Grid.Col>
<Grid.Col span={5}>{DeleteButton()}</Grid.Col>
</Grid>
</>
);

View File

@@ -1,21 +1,15 @@
import { BadgeCustom, TextCustom } from "@/components";
import { BadgeCustom, StackCustom, TextCustom } from "@/components";
import { GStyles } from "@/styles/global-styles";
import dayjs from "dayjs";
import { View } from "react-native";
import { dateTimeView } from "@/utils/dateTimeView";
export function Voting_ComponentDetailDataSection() {
export function Voting_ComponentDetailDataSection({ data }: { data?: any }) {
return (
<>
<TextCustom align="center" bold size="large">
Title of Voting Here
{data?.title || "-"}
</TextCustom>
<TextCustom>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Perspiciatis
corporis blanditiis est provident corrupti facilis iste cum voluptate.
Natus eum aut quos consequatur doloribus fugiat sit ullam minima non
enim?
</TextCustom>
<View>
<TextCustom>{data?.deskripsi || "-"}</TextCustom>
<StackCustom gap={"sm"}>
<TextCustom bold size="small" align="center">
Batas Voting
</TextCustom>
@@ -23,10 +17,13 @@ export function Voting_ComponentDetailDataSection() {
style={[GStyles.alignSelfCenter, { width: "70%" }]}
variant="light"
>
{dayjs().format("DD/MM/YYYY")} -{" "}
{dayjs().add(1, "day").format("DD/MM/YYYY")}
{data?.awalVote &&
dateTimeView({ date: data?.awalVote, withoutTime: true })}{" "}
-{" "}
{data?.akhirVote &&
dateTimeView({ date: data?.akhirVote, withoutTime: true })}
</BadgeCustom>
</View>
</StackCustom>
</>
);
}

View File

@@ -0,0 +1,151 @@
import { apiConfig } from "../api-config";
export async function apiCollaborationCreate({ data }: { data: any }) {
try {
const response = await apiConfig.post(`/mobile/collaboration`, {
data: data,
});
return response.data;
} catch (error) {
throw error;
}
}
export async function apiCollaborationGetAll({
category,
authorId,
}: {
category: "beranda" | "participant" | "my-project" | "group";
authorId?: string;
}) {
try {
const authorQuery = authorId ? `&authorId=${authorId}` : "";
const response = await apiConfig.get(
`/mobile/collaboration?category=${category}${authorQuery}`
);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiCollaborationGetOne({ id }: { id: string }) {
try {
const response = await apiConfig.get(`/mobile/collaboration/${id}`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiCollaborationCreatePartisipasi({
id,
data,
}: {
id: string;
data: any;
}) {
try {
const response = await apiConfig.post(
`/mobile/collaboration/${id}/participants`,
{
data: data,
}
);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiCollaborationGetParticipants({
id,
category,
authorId,
}: {
id: string;
category: "list" | "check-participant";
authorId?: string;
}) {
try {
const authorQuery = authorId ? `&authorId=${authorId}` : "";
const response = await apiConfig.get(
`/mobile/collaboration/${id}/participants?category=${category}${authorQuery}`
);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiCollaborationCreateGroup({
id,
data,
}: {
id: string;
data: any;
}) {
try {
const response = await apiConfig.post(`/mobile/collaboration/${id}`, {
data: data,
});
return response.data;
} catch (error) {
throw error;
}
}
export async function apiCollaborationEditData({
id,
data,
}: {
id: string;
data: any;
}) {
try {
const response = await apiConfig.put(`/mobile/collaboration/${id}`, {
data: data,
});
return response.data;
} catch (error) {
throw error;
}
}
export async function apiCollaborationGroup({ id }: { id: string }) {
try {
const response = await apiConfig.get(`/mobile/collaboration/${id}/group`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiCollaborationGroupMessage({ id }: { id: string }) {
try {
const response = await apiConfig.get(`/mobile/collaboration/${id}/message`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiCollaborationGroupMessageCreate({
id,
data,
}: {
id: string;
data: any;
}) {
try {
const response = await apiConfig.post(
`/mobile/collaboration/${id}/message`,
{
data: data,
}
);
return response.data;
} catch (error) {
throw error;
}
}

View File

@@ -0,0 +1,112 @@
import { apiConfig } from "../api-config";
export async function apiForumCreate({ data }: { data: any }) {
try {
const response = await apiConfig.post(`/mobile/forum`, {
data: data,
});
return response.data;
} catch (error) {
throw error;
}
}
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${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,80 @@
import { apiConfig } from "../api-config";
export async function apiInvestmentCreate({ data }: { data: any }) {
try {
const response = await apiConfig.post(`/mobile/investment`, {
data: data,
});
return response.data;
} catch (error) {
throw error;
}
}
export async function apiInvestmentGetByStatus({
authorId,
status,
}: {
authorId: string;
status: string;
}) {
try {
const response = await apiConfig.get(
`/mobile/investment/${authorId}/${status}`
);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiInvestmentGetById({ id }: { id: string }) {
try {
const response = await apiConfig.get(`/mobile/investment/${id}`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiInvestmentUpdateStatus({
id,
status,
}: {
id: string;
status: "publish" | "draft" | "review" | "reject";
}) {
console.log("[DATA FETCH]", JSON.stringify({ id, status }, null, 2));
try {
const response = await apiConfig.put(`/mobile/investment/${id}/${status}`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiInvestmentDelete({ id }: { id: string }) {
try {
const response = await apiConfig.delete(`/mobile/investment/${id}`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiInvestmentUpdateData({
id,
data,
}: {
id: string;
data: any;
}) {
try {
const response = await apiConfig.put(`/mobile/investment/${id}`, {
data: data,
});
return response.data;
} catch (error) {
throw error;
}
}

View File

@@ -59,10 +59,24 @@ export async function apiJobDelete({ id }: { id: string }) {
}
}
export async function apiJobGetAll({ search }: { search?: string }) {
export async function apiJobGetAll({
search,
category,
authorId,
}: {
search?: string;
category: "archive" | "beranda";
authorId?: string;
}) {
try {
const searchText = search ? `?search=${search}` : "";
const response = await apiConfig.get(`/mobile/job${searchText}`);
let categoryText = category ? `?category=${category}` : "";
if (category === "archive") {
categoryText = `?category=${category}&authorId=${authorId}`;
}
const searchText = search ? `&search=${search}` : "";
const response = await apiConfig.get(
`/mobile/job${categoryText}${searchText}`
);
return response.data;
} catch (error) {
throw error;
@@ -72,12 +86,15 @@ export async function apiJobGetAll({ search }: { search?: string }) {
export async function apiJobUpdateData({
id,
data,
category,
}: {
id: string;
data: any;
category: "edit" | "archive";
}) {
try {
const response = await apiConfig.put(`/mobile/job/${id}`, {
const categoryJob = category ? `?category=${category}` : "";
const response = await apiConfig.put(`/mobile/job/${id}${categoryJob}`, {
data: data,
});
return response.data;

View File

@@ -28,4 +28,59 @@ export async function apiMasterEventType() {
} catch (error) {
throw error;
}
}
}
export async function apiMasterCollaborationType() {
try {
const response = await apiConfig.get(
`/mobile/master/collaboration-industry`
);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiMasterForumReportList() {
try {
const response = await apiConfig.get(`/mobile/master/forum-report`);
return response.data;
} catch (error) {
throw error;
}
}
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;
}
}

View File

@@ -0,0 +1,126 @@
import { apiConfig } from "../api-config";
export async function apiVotingCreate(data: any) {
try {
const response = await apiConfig.post(`/mobile/voting`, {
data: data,
});
return response.data;
} catch (error) {
throw error;
}
}
export async function apiVotingGetByStatus({
id,
status,
}: {
id: string;
status: string;
}) {
try {
const response = await apiConfig.get(`/mobile/voting/${id}/${status}`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiVotingGetOne({ id }: { id: string }) {
try {
const response = await apiConfig.get(`/mobile/voting/${id}`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiVotingUpdateStatus({
id,
status,
}: {
id: string;
status: "draft" | "review" | "publish" | "reject";
}) {
try {
const response = await apiConfig.put(`/mobile/voting/${id}/${status}`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiVotingDelete({ id }: { id: string }) {
try {
const response = await apiConfig.delete(`/mobile/voting/${id}`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiVotingUpdateData({
id,
data,
category,
}: {
id: string;
data: any;
category: "edit" | "archive";
}) {
const categoryQuery = `?category=${category}`;
try {
const response = await apiConfig.put(`/mobile/voting/${id}${categoryQuery}`, {
data: data,
});
return response.data;
} catch (error) {
throw error;
}
}
export async function apiVotingGetAll({ search, category, authorId }: { search?: string, category: "beranda" | "contribution" | "all-history" | "my-history", authorId?: string }) {
try {
const categoryQuery = category ? `?category=${category}` : "";
const searchQuery = search ? `&search=${search}` : "";
const authorIdQuery = authorId ? `&authorId=${authorId}` : "";
const response = await apiConfig.get(`/mobile/voting${categoryQuery}${searchQuery}${authorIdQuery}`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiVotingVote({ id, data }: { id: string; data: any }) {
try {
const response = await apiConfig.post(`/mobile/voting/${id}`, {
data: data,
});
return response.data;
} catch (error) {
throw error;
}
}
export async function apiVotingContribution({
id,
authorId,
category,
}: {
id: string;
authorId: string;
category: "list" | "checked";
}) {
const query =
category === "list"
? "?category=list"
: `?category=checked&authorId=${authorId}`;
try {
const response = await apiConfig.get(
`/mobile/voting/${id}/contribution${query}`
);
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

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

@@ -1,5 +1,6 @@
import dayjs from "dayjs";
export const today = new Date();
export const dateTimeView = ({
date,
withoutTime = false,

35
utils/formatChatTime.ts Normal file
View File

@@ -0,0 +1,35 @@
// utils/formatChatTime.ts
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import 'dayjs/locale/id';
dayjs.extend(relativeTime);
dayjs.locale('id');
/**
* Format waktu pesan untuk tampilan chat
* @param date ISO string atau Date object
* @returns string formatted time
*/
export const formatChatTime = (date: string | Date): string => {
const messageDate = dayjs(date);
const now = dayjs();
// Jika hari ini
if (messageDate.isSame(now, 'day')) {
return messageDate.format('HH.mm'); // contoh: "14.30"
}
// Jika kemarin
if (messageDate.isSame(now.subtract(1, 'day'), 'day')) {
return messageDate.format('dddd HH:mm');
}
// Jika dalam 7 hari terakhir (tapi bukan kemarin/ hari ini)
if (now.diff(messageDate, 'day') < 7) {
return messageDate.format('dddd HH:mm'); // contoh: "Senin 14:30"
}
// Lebih dari seminggu lalu → tampilkan tanggal
return messageDate.format('D MMM YYYY'); // contoh: "12 Mei 2024"
};

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