Compare commits

..

13 Commits

Author SHA1 Message Date
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
60b0befa60 API Job
Add:
- api-client/api-job: kumpulan fetch api

Fix:
- UI beranda , status sudah terintergrasi dengan API
- UI detail status, detail utama sudah terintergrasi dengan API
- Search pada beranda sudah terintegrasi
- Edit sudah terintergrasi

### No Issue
2025-09-16 17:27:58 +08:00
3287f4c287 Event
Fix:
-  user)/event/(tabs)/contribution: tampilan creator yang di depan
- /Event/ButtonStatusSection: return message

### No Issuue
2025-09-16 11:17:36 +08:00
76fb14ed0c Event
Fix: Intergrasi pada UI
- Beranda, Kontibusi & Riwayat
- Detail beranda, detail kontribusi & detail riwayat
- List partisipan

### No issue
2025-09-15 17:17:50 +08:00
1d2153b253 Event
Fix: Intergrasi tampilan ke API

Package.json
Fix: Pem baharuan SDK53 -> SDK54

### No Issue
2025-09-12 15:57:30 +08:00
005b798688 Event
Fix:
- UI: status, detail status, delete button, detail utama, tampilan utama
- Semua terintergrasi ke API mobile

### No Issue
2025-09-12 14:14:37 +08:00
b6d4c0e6a6 API Event
Add:
- api-client/api-event : fetch get one, update data, update status, create event

Fix:
- UI : create , detail dan status: untuk menyambungkan ke API

### No Issue
2025-09-11 17:34:08 +08:00
3854db9330 Event
Fix:
- Create event
- api master tipe event

## NO Issue
2025-09-10 17:43:01 +08:00
fc181bda7c API
User search:
Fix:
-  api get all user
- searching by username

Portofolio:
Fix:
- dot button hanya muncul jika user yang memiliki portofolio tersebut yang melihat

Profile:
Fix:
- dot button muncul hanya untuk user yang memiliki akunnya

### No Issue
2025-09-10 16:33:39 +08:00
fb822d20b6 Portofolio
Fix: API edit detail data

### No Issue
2025-09-10 12:12:11 +08:00
0e708dde0f API Portofolio
Fix:
- edit select sub bidang

### NO Issue
2025-09-03 17:42:17 +08:00
9a915c55d2 Portofolio
Fix:
- Sub bidang bisnis

### No Issu
2025-09-02 18:29:28 +08:00
66 changed files with 4211 additions and 19173 deletions

View File

@@ -36,6 +36,7 @@ export default {
plugins: [
'expo-router',
'expo-web-browser',
[
'expo-splash-screen',
{

View File

@@ -509,6 +509,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,43 +1,115 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
AvatarCustom,
AvatarUsernameAndOtherComponent,
BoxWithHeaderSection,
Grid,
LoaderCustom,
Spacing,
StackCustom,
TextCustom,
ViewWrapper
} from "@/components";
import React from "react";
import { useAuth } from "@/hooks/use-auth";
import {
apiEventGetAll
} from "@/service/api-client/api-event";
import { dateTimeView } from "@/utils/dateTimeView";
import { useFocusEffect } from "expo-router";
import _ from "lodash";
import React, { useCallback, useState } from "react";
export default function EventContribution() {
const { user } = useAuth();
const [listData, setListData] = useState<any>([]);
const [isLoadList, setIsLoadList] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [user?.id])
);
async function onLoadData() {
try {
setIsLoadList(true);
const response = await apiEventGetAll({
category: "contribution",
userId: user?.id,
});
console.log("[DATA] ", JSON.stringify(response.data, null, 2));
if (response.success) {
setListData(response.data);
// const responseListParticipants = await apiEventListOfParticipants({
// id: response?.data?.Event?.id,
// });
// console.log(
// "[LIST PARTICIPANTS]",
// JSON.stringify(responseListParticipants.data, null, 2)
// );
// if (responseListParticipants.success) {
// setListParticipants(responseListParticipants.data);
// }
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoadList(false);
}
}
return (
<ViewWrapper hideFooter>
{Array.from({ length: 10 }).map((_, index) => (
<BoxWithHeaderSection key={index} href={`/event/${index}/contribution`}>
<StackCustom>
<AvatarUsernameAndOtherComponent
avatarHref={`/profile/${index}`}
rightComponent={
<TextCustom truncate>
{new Date().toLocaleDateString()}
</TextCustom>
}
/>
{isLoadList ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Belum ada kontribusi</TextCustom>
) : (
listData.map((item: any, index: number) => (
<BoxWithHeaderSection
key={index}
href={`/event/${item?.Event?.id}/contribution`}
>
<StackCustom>
<AvatarUsernameAndOtherComponent
avatar={item?.Event?.Author?.Profile?.imageId}
avatarHref={`/profile/${item?.Event?.Author?.Profile?.id}`}
name={item?.Event?.Author?.username}
rightComponent={
<TextCustom truncate>
{dateTimeView({
date: item?.Event?.tanggal,
withoutTime: true,
})}
</TextCustom>
}
/>
<TextCustom bold align="center" size="xlarge">
Judul Event Disini
</TextCustom>
<TextCustom bold align="center" size="xlarge" truncate={2}>
{item?.Event?.title}
</TextCustom>
<Spacing height={0} />
<Grid>
{Array.from({ length: 4 }).map((_, index2) => (
<Grid.Col span={3} key={index2}>
<AvatarCustom size="sm" href={`/profile/${index2}`} />
</Grid.Col>
))}
</Grid>
</StackCustom>
</BoxWithHeaderSection>
))}
{/* <Grid>
{item?.Event?.Event_Peserta?.map(
(item2: any, index2: number) => (
<Grid.Col
style={{ alignItems: "center" }}
span={12 / item?.Event?.Event_Peserta?.length}
key={index2}
>
<AvatarComp
size="base"
href={`/profile/${item2?.User?.Profile?.id}`}
fileId={item2?.User?.Profile?.imageId}
/>
</Grid.Col>
)
)}
</Grid> */}
</StackCustom>
</BoxWithHeaderSection>
))
)}
</ViewWrapper>
);
}

View File

@@ -1,12 +1,41 @@
import { ButtonCustom, Spacing, TextCustom } 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 Event_BoxPublishSection from "@/screens/Event/BoxPublishSection";
import { useState } from "react";
import { apiEventGetAll } from "@/service/api-client/api-event";
import { dateTimeView } from "@/utils/dateTimeView";
import _ from "lodash";
import { useEffect, useState } from "react";
import { View } from "react-native";
export default function EventHistory() {
const [activeCategory, setActiveCategory] = useState<string | null>("all");
const { user } = useAuth();
const [listData, setListData] = useState<any>([]);
const [isLoadList, setIsLoadList] = useState(false);
useEffect(() => {
onLoadData({ userId: user?.id });
}, [user?.id, activeCategory]);
async function onLoadData({ userId }: { userId?: string }) {
try {
setIsLoadList(true);
const response = await apiEventGetAll({
category: activeCategory === "all" ? "all-history" : "my-history",
userId: userId,
});
if (response.success) {
setListData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoadList(false);
}
}
const handlePress = (item: any) => {
setActiveCategory(item);
@@ -52,17 +81,24 @@ export default function EventHistory() {
return (
<ViewWrapper headerComponent={headerComponent} hideFooter>
{Array.from({ length: 10 }).map((_, index) => (
<Event_BoxPublishSection
key={index.toString()}
id={index.toString()}
username={`Riwayat ${activeCategory === "main" ? "Saya" : "Semua"}`}
rightComponentAvatar={
<TextCustom>{new Date().toLocaleDateString()}</TextCustom>
}
href={`/event/${index}/history`}
/>
))}
{isLoadList ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Belum ada riwayat</TextCustom>
) : (
listData.map((item: any, index: number) => (
<Event_BoxPublishSection
key={index.toString()}
data={item}
rightComponentAvatar={
<TextCustom>
{dateTimeView({ date: item?.tanggal, withoutTime: true })}
</TextCustom>
}
href={`/event/${item.id}/history`}
/>
))
)}
</ViewWrapper>
);
}

View File

@@ -1,9 +1,36 @@
import { LoaderCustom, TextCustom } from "@/components";
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
import FloatingButton from "@/components/Button/FloatingButton";
import Event_BoxPublishSection from "@/screens/Event/BoxPublishSection";
import { router } from "expo-router";
import { apiEventGetAll } from "@/service/api-client/api-event";
import { dateTimeView } from "@/utils/dateTimeView";
import { router, useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
export default function EventBeranda() {
const [listData, setListData] = useState([]);
const [isLoadData, setIsLoadData] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [])
);
const onLoadData = async () => {
try {
setIsLoadData(true);
const response = await apiEventGetAll({category: "beranda"});
// console.log("Response", JSON.stringify(response.data, null, 2));
setListData(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoadData(false);
}
};
return (
<ViewWrapper
hideFooter
@@ -11,13 +38,24 @@ export default function EventBeranda() {
<FloatingButton onPress={() => router.push("/event/create")} />
}
>
{Array.from({ length: 10 }).map((_, index) => (
<Event_BoxPublishSection
key={index}
id={index.toString()}
href={`/event/${index}/publish`}
/>
))}
{isLoadData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Belum ada event</TextCustom>
) : (
listData.map((item: any, index) => (
<Event_BoxPublishSection
key={index}
href={`/event/${item.id}/publish`}
data={item}
rightComponentAvatar={
<TextCustom>
{dateTimeView({ date: item?.tanggal, withoutTime: true })}
</TextCustom>
}
/>
))
)}
</ViewWrapper>
);
}

View File

@@ -1,27 +1,57 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BoxWithHeaderSection,
Grid,
ScrollableCustom,
StackCustom,
TextCustom
BoxWithHeaderSection,
Grid,
LoaderCustom,
ScrollableCustom,
StackCustom,
TextCustom,
} from "@/components";
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
import { useAuth } from "@/hooks/use-auth";
import { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
import { useState } from "react";
import { apiEventGetByStatus } from "@/service/api-client/api-event";
import { useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
export default function EventStatus() {
const id = "test-id-event";
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 apiEventGetByStatus({
id: id!,
status: activeCategory!,
});
// console.log("Response", JSON.stringify(response.data, null, 2));
setListData(response.data);
} catch (error) {
console.log(error);
} finally {
setLoadingGetData(false);
}
}
const handlePress = (item: any) => {
setActiveCategory(item.value);
// tambahkan logika lain seperti filter dsb.
};
const scrollComponent = (
const tabsComponent = (
<ScrollableCustom
data={dummyMasterStatus.map((e, i) => ({
id: i,
@@ -34,30 +64,36 @@ export default function EventStatus() {
);
return (
<ViewWrapper headerComponent={scrollComponent}>
<BoxWithHeaderSection href={`/event/${id}/${activeCategory}/detail-event`}>
<StackCustom gap={"xs"}>
<Grid>
<Grid.Col span={8}>
<TextCustom truncate bold>
Lorem ipsum,{" "}
<TextCustom color="green">{activeCategory}</TextCustom> dolor
sit amet consectetur adipisicing elit.
</TextCustom>
</Grid.Col>
<Grid.Col span={4} style={{ alignItems: "flex-end" }}>
<TextCustom>{new Date().toLocaleDateString()}</TextCustom>
</Grid.Col>
</Grid>
<ViewWrapper headerComponent={tabsComponent}>
{loadingGetData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Tidak ada data {activeCategory}</TextCustom>
) : (
listData.map((item: any, i) => (
<BoxWithHeaderSection
key={i}
href={`/event/${item.id }/${activeCategory}/detail-event`}
>
<StackCustom gap={"xs"}>
<Grid>
<Grid.Col span={8}>
<TextCustom truncate bold>
{item?.title}
</TextCustom>
</Grid.Col>
<Grid.Col span={4} style={{ alignItems: "flex-end" }}>
<TextCustom>
{new Date(item?.tanggal).toLocaleDateString()}
</TextCustom>
</Grid.Col>
</Grid>
<TextCustom truncate={2}>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Consectetur
eveniet ab eum ducimus tempore a quia deserunt quisquam. Tempora,
atque. Aperiam minima asperiores dicta perferendis quis adipisci,
dolore optio porro!
</TextCustom>
</StackCustom>
</BoxWithHeaderSection>
<TextCustom truncate={2}>{item?.deskripsi}</TextCustom>
</StackCustom>
</BoxWithHeaderSection>
))
)}
</ViewWrapper>
);
}

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BaseBox,
DotButton,
@@ -11,17 +12,64 @@ import {
} from "@/components";
import { IMenuDrawerItem } from "@/components/_Interface/types";
import LeftButtonCustom from "@/components/Button/BackButton";
import Event_AlertButtonStatusSection from "@/screens/Event/AlertButtonStatusSection";
import Event_ButtonStatusSection from "@/screens/Event/ButtonStatusSection";
import { menuDrawerDraftEvent } from "@/screens/Event/menuDrawerDraft";
import { router, Stack, useLocalSearchParams } from "expo-router";
import { useState } from "react";
import { apiEventGetOne } from "@/service/api-client/api-event";
import { dateTimeView } from "@/utils/dateTimeView";
import {
router,
Stack,
useFocusEffect,
useLocalSearchParams,
} from "expo-router";
import { useCallback, useState } from "react";
export default function EventDetailStatus() {
const { id, status } = useLocalSearchParams();
const [openDrawer, setOpenDrawer] = useState(false);
const [openAlert, setOpenAlert] = useState(false);
const [openDeleteAlert, setOpenDeleteAlert] = useState(false);
// const [openAlert, setOpenAlert] = useState(false);
const [data, setData] = useState<any>();
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id])
);
async function onLoadData() {
try {
const response = await apiEventGetOne({ id: id as string });
if (response.success) {
setData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
}
}
const listData = [
{
title: "Lokasi",
value: data?.lokasi || "-",
},
{
title: "Tipe Acara",
value: data?.EventMaster_TipeAcara?.name || "-",
},
{
title: "Tanggal Mulai",
value: dateTimeView({ date: data?.tanggal }) || "-",
},
{
title: "Tanggal Berakhir",
value: dateTimeView({ date: data?.tanggalSelesai }) || "-",
},
{
title: "Deskripsi",
value: data?.deskripsi || "-",
},
];
const handlePress = (item: IMenuDrawerItem) => {
console.log("PATH >> ", item.path);
@@ -45,7 +93,7 @@ export default function EventDetailStatus() {
<BaseBox>
<StackCustom>
<TextCustom bold align="center" size="xlarge">
Judul event {status}
{data?.title || "-"}
</TextCustom>
{listData.map((item, index) => (
<Grid key={index}>
@@ -60,9 +108,8 @@ export default function EventDetailStatus() {
</StackCustom>
</BaseBox>
<Event_ButtonStatusSection
id={id as string}
status={status as string}
onOpenAlert={setOpenAlert}
onOpenDeleteAlert={setOpenDeleteAlert}
/>
<Spacing />
</ViewWrapper>
@@ -70,7 +117,7 @@ export default function EventDetailStatus() {
<DrawerCustom
isVisible={openDrawer}
closeDrawer={() => setOpenDrawer(false)}
height={250}
height={"auto"}
>
<MenuDrawerDynamicGrid
data={menuDrawerDraftEvent({ id: id as string }) as any}
@@ -78,40 +125,6 @@ export default function EventDetailStatus() {
onPressItem={handlePress as any}
/>
</DrawerCustom>
<Event_AlertButtonStatusSection
id={id as string}
status={status as string}
openAlert={openAlert}
setOpenAlert={setOpenAlert}
openDeleteAlert={openDeleteAlert}
setOpenDeleteAlert={setOpenDeleteAlert}
/>
</>
);
}
const listData = [
{
title: "Lokasi",
value:
"Lorem ipsum dolor sit amet consectetur adipisicing elit. Consectetur eveniet ab eum ducimus tempore a quia deserunt quisquam. Tempora, atque. Aperiam minima asperiores dicta perferendis quis adipisci, dolore optio porro!",
},
{
title: "Tipe Acara",
value: "Workshop",
},
{
title: "Tanggal Mulai",
value: "Senin, 18 Juli 2025, 10:00 WIB",
},
{
title: "Tanggal Berakhir",
value: "Selasa, 19 Juli 2025, 12:00 WIB",
},
{
title: "Deskripsi",
value:
"Lorem ipsum dolor sit amet consectetur adipisicing elit. Consectetur eveniet ab eum ducimus tempore a quia deserunt quisquam. Tempora, atque. Aperiam minima asperiores dicta perferendis quis adipisci, dolore optio porro!",
},
];

View File

@@ -1,20 +1,43 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
DotButton,
DrawerCustom,
LoaderCustom,
MenuDrawerDynamicGrid,
ViewWrapper,
Spacing,
ViewWrapper,
} from "@/components";
import { IMenuDrawerItem } from "@/components/_Interface/types";
import LeftButtonCustom from "@/components/Button/BackButton";
import Event_BoxDetailPublishSection from "@/screens/Event/BoxDetailPublishSection";
import { menuDrawerPublishEvent } from "@/screens/Event/menuDrawerPublish";
import { apiEventGetOne } from "@/service/api-client/api-event";
import { router, Stack, useLocalSearchParams } from "expo-router";
import { useState } from "react";
import { useEffect, useState } from "react";
export default function EventDetailContribution() {
const { id } = useLocalSearchParams();
const [openDrawer, setOpenDrawer] = useState(false);
const [data, setData] = useState<any>();
const [isLoadData, setIsLoadData] = useState(false);
useEffect(() => {
onLoadData();
}, [id]);
const onLoadData = async () => {
try {
setIsLoadData(true);
const response = await apiEventGetOne({ id: id as string });
if (response.success) {
setData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoadData(false);
}
};
const handlePress = (item: IMenuDrawerItem) => {
console.log("PATH ", item.path);
@@ -32,18 +55,22 @@ export default function EventDetailContribution() {
}}
/>
<ViewWrapper>
<Event_BoxDetailPublishSection />
{isLoadData ? (
<LoaderCustom />
) : (
<Event_BoxDetailPublishSection data={data} />
)}
<Spacing />
</ViewWrapper>
<DrawerCustom
isVisible={openDrawer}
closeDrawer={() => setOpenDrawer(false)}
height={250}
height={"auto"}
>
<MenuDrawerDynamicGrid
data={menuDrawerPublishEvent({ id: id as string })}
columns={4}
onPressItem={handlePress}
onPressItem={handlePress as any}
/>
</DrawerCustom>
</>

View File

@@ -1,106 +1,271 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
ButtonCustom,
LoaderCustom,
SelectCustom,
Spacing,
StackCustom,
TextAreaCustom,
TextCustom,
TextInputCustom,
ViewWrapper,
} from "@/components";
import DateTimePickerCustom from "@/components/DateInput/DateTimePickerCustom";
import { masterTypeEvent } from "@/lib/dummy-data/event/master-type-event";
import {
apiEventGetOne,
apiEventUpdateData,
} from "@/service/api-client/api-event";
import { apiMasterEventType } from "@/service/api-client/api-master";
import { DateTimePickerEvent } from "@react-native-community/datetimepicker";
import { router } from "expo-router";
import React, { useState } from "react";
import { Platform } from "react-native";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import React, { useCallback, useEffect, useState } from "react";
import Toast from "react-native-toast-message";
export default function EventEdit() {
const { id } = useLocalSearchParams();
const [data, setData] = useState<any>();
// {
// title: "",
// lokasi: "",
// deskripsi: "",
// eventMaster_TipeAcaraId: "",
// tanggal: "",
// tanggalSelesai: "",
// authorId: "",
// }
const [listTypeEvent, setListTypeEvent] = useState([]);
const [selectedDate, setSelectedDate] = useState<
Date | DateTimePickerEvent | null
>(null);
>();
const [selectedEndDate, setSelectedEndDate] = useState<
Date | DateTimePickerEvent | null
>(null);
>();
const handlerSubmit = () => {
const [isLoading, setIsLoading] = useState(false);
const [isLoadData, setIsLoadData] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id])
);
async function onLoadData() {
try {
if (selectedDate) {
console.log("Tanggal yang dipilih:", selectedDate);
console.log(`ISO Format ${Platform.OS}:`, selectedDate.toString());
// Kirim ke API atau proses lanjutan
} else {
console.log("Tanggal belum dipilih");
setIsLoadData(true);
const response = await apiEventGetOne({ id: id as string });
if (response.success) {
setData(response.data);
setSelectedDate(new Date(response.data.tanggal));
setSelectedEndDate(new Date(response.data.tanggalSelesai));
}
if (selectedEndDate) {
console.log("Tanggal yang dipilih:", selectedEndDate);
console.log(`ISO Format ${Platform.OS}:`, selectedEndDate.toString());
// Kirim ke API atau proses lanjutan
} else {
console.log("Tanggal berakhir belum dipilih");
}
console.log("Data berhasil terupdate");
router.back()
} catch (error) {
console.log(error);
console.log("[ERROR]", error);
} finally {
setIsLoadData(false);
}
}
useEffect(() => {
onLoadMasterEventType();
}, []);
const onLoadMasterEventType = async () => {
try {
const response = await apiMasterEventType();
setListTypeEvent(response.data);
} catch (error) {
console.log("Error onLoadMasterEventType", error);
}
};
const buttonSubmit = (
<ButtonCustom title="Update" onPress={handlerSubmit} />
);
const validateDate = async () => {
if (
data?.title === "" ||
data?.lokasi === "" ||
data?.deskripsi === "" ||
data?.eventMaster_TipeAcaraId === ""
) {
Toast.show({
type: "info",
text1: "Info",
text2: "Lengkapi semua data",
});
return false;
}
const startDate = new Date(selectedDate as any);
const endDate = new Date(selectedEndDate as any);
if (startDate >= endDate) {
Toast.show({
type: "info",
text1: "Info",
text2: "Ubah tanggal berakhirnya event",
});
return false;
}
return true;
};
const handlerSubmit = async () => {
const isValid = await validateDate();
if (!isValid) return;
try {
setIsLoading(true);
const newData = {
...data,
tanggal: new Date(selectedDate as any).toISOString(),
tanggalSelesai: new Date(selectedEndDate as any).toISOString(),
};
const response = await apiEventUpdateData({
id: id as string,
data: newData,
});
if (response.success) {
Toast.show({
type: "success",
text1: response.message,
});
return router.back();
}
} catch (error) {
console.log(error);
} finally {
setIsLoading(false);
}
};
const validateDateRange = (
selectedDate: string | Date,
selectedEndDate: string | Date
): { isValid: boolean; error?: string } => {
const startDate = new Date(selectedDate);
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,
};
};
return (
<>
<ViewWrapper>
<StackCustom gap={"xs"}>
<TextInputCustom
placeholder="Masukkan nama event"
label="Nama Event"
required
/>
<SelectCustom
label="Tipe Event"
placeholder="Pilih tipe event"
data={masterTypeEvent}
onChange={(value) => console.log(value)}
/>
<TextInputCustom
label="Lokasi"
placeholder="Masukkan lokasi event"
required
/>
{isLoadData ? (
<LoaderCustom />
) : (
<StackCustom gap={"xs"}>
<TextInputCustom
placeholder="Masukkan nama event"
label="Nama Event"
required
value={data?.title}
onChangeText={(value) => setData({ ...data, title: value })}
/>
<SelectCustom
label="Tipe Event"
placeholder="Pilih tipe event"
data={listTypeEvent.map((item: any) => ({
label: item.name,
value: item.id,
}))}
value={data?.eventMaster_TipeAcaraId || ""}
onChange={(value) => {
console.log(value);
setData({ ...data, eventMaster_TipeAcaraId: value });
}}
/>
<TextInputCustom
label="Lokasi"
placeholder="Masukkan lokasi event"
required
value={data?.lokasi}
onChangeText={(value) => setData({ ...data, lokasi: value })}
/>
<DateTimePickerCustom
minimumDate={new Date(Date.now())}
label="Tanggal & Waktu Mulai"
required
value={selectedDate as any}
onChange={(date: any) => {
setSelectedDate(date as any);
}}
/>
<StackCustom gap={0}>
<DateTimePickerCustom
minimumDate={selectedDate as any}
label="Tanggal & Waktu Berakhir"
required
value={selectedEndDate as any}
onChange={(date: any) => {
setSelectedEndDate(date as any);
}}
/>
<DateTimePickerCustom
label="Tanggal & Waktu Mulai"
required
onChange={(date: Date) => {
setSelectedDate(date as any);
}}
value={selectedDate as any}
minimumDate={new Date(Date.now())}
/>
{/* Muncul */}
{validateDateRange(selectedDate as any, selectedEndDate as any)
.isValid ? (
<TextCustom style={{ color: "green" }}>
{
validateDateRange(
selectedDate as any,
selectedEndDate as any
).error
}
</TextCustom>
) : (
<TextCustom style={{ color: "red" }}>
{
validateDateRange(
selectedDate as any,
selectedEndDate as any
).error
}
</TextCustom>
)}
<Spacing />
</StackCustom>
<DateTimePickerCustom
label="Tanggal & Waktu Berakhir"
required
onChange={(date: Date) => {
setSelectedEndDate(date as any);
}}
value={selectedEndDate as any}
/>
<TextAreaCustom
label="Deskripsi"
placeholder="Masukkan deskripsi event"
required
showCount
maxLength={100}
value={data?.deskripsi}
onChangeText={(value) => setData({ ...data, deskripsi: value })}
/>
<TextAreaCustom
label="Deskripsi"
placeholder="Masukkan deskripsi event"
required
showCount
maxLength={100}
/>
{buttonSubmit}
</StackCustom>
<ButtonCustom
isLoading={isLoading}
title="Update"
onPress={handlerSubmit}
/>
</StackCustom>
)}
</ViewWrapper>
</>
);

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
DotButton,
DrawerCustom,
@@ -9,12 +10,29 @@ import { IMenuDrawerItem } from "@/components/_Interface/types";
import LeftButtonCustom from "@/components/Button/BackButton";
import Event_BoxDetailPublishSection from "@/screens/Event/BoxDetailPublishSection";
import { menuDrawerPublishEvent } from "@/screens/Event/menuDrawerPublish";
import { apiEventGetOne } from "@/service/api-client/api-event";
import { router, Stack, useLocalSearchParams } from "expo-router";
import { useState } from "react";
import { useEffect, useState } from "react";
export default function EventDetailHistory() {
const { id } = useLocalSearchParams();
const [openDrawer, setOpenDrawer] = useState(false);
const [data, setData] = useState();
useEffect(() => {
onLoadData();
}, [id]);
const onLoadData = async () => {
try {
const response = await apiEventGetOne({ id: id as string });
if (response.success) {
setData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
}
};
const handlePress = (item: IMenuDrawerItem) => {
console.log("PATH ", item.path);
@@ -32,7 +50,7 @@ export default function EventDetailHistory() {
}}
/>
<ViewWrapper>
<Event_BoxDetailPublishSection />
<Event_BoxDetailPublishSection data={data} />
<Spacing />
</ViewWrapper>
<DrawerCustom
@@ -43,7 +61,7 @@ export default function EventDetailHistory() {
<MenuDrawerDynamicGrid
data={menuDrawerPublishEvent({ id: id as string })}
columns={4}
onPressItem={handlePress}
onPressItem={handlePress as any}
/>
</DrawerCustom>
</>

View File

@@ -1,17 +1,102 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
AvatarUsernameAndOtherComponent,
BadgeCustom,
BaseBox,
LoaderCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import {
apiEventGetOne,
apiEventListOfParticipants,
} from "@/service/api-client/api-event";
import { useLocalSearchParams } from "expo-router";
import { useEffect, useState } from "react";
import { View } from "react-native";
export default function EventListOfParticipants() {
const { id } = useLocalSearchParams();
const [startDate, setStartDate] = useState();
const [listData, setListData] = useState([]);
const [isLoadData, setIsLoadData] = useState(false);
useEffect(() => {
handlerLoadData();
}, [id]);
const handlerLoadData = () => {
try {
setIsLoadData(true);
onLoadData();
onLoadList();
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoadData(false);
}
};
const onLoadData = async () => {
try {
const response = await apiEventGetOne({ id: id as string });
if (response.success) {
setStartDate(response.data.tanggal);
}
} catch (error) {
console.log("[ERROR]", error);
}
};
const onLoadList = async () => {
try {
const response = await apiEventListOfParticipants({ id: id as string });
if (response.success) {
setListData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
}
};
return (
<ViewWrapper>
{Array.from({ length: 10 }).map((_, index) => (
<BaseBox key={index} paddingBlock={0}>
<AvatarUsernameAndOtherComponent avatarHref={`/profile/${index}`} />
</BaseBox>
))}
{isLoadData ? (
<LoaderCustom />
) : listData.length === 0 ? (
<TextCustom align="center">Belum ada peserta</TextCustom>
) : (
listData.map((item: any, index: number) => (
<BaseBox key={index}>
<AvatarUsernameAndOtherComponent
avatar={item?.User?.Profile?.imageId}
name={item?.User?.username}
avatarHref={`/profile/${item?.User?.Profile?.id}`}
rightComponent={
new Date().getTime() > new Date(startDate as any).getTime() ? (
<View
style={{
justifyContent: "flex-end",
}}
>
<BadgeCustom color={item?.isPresent ? "green" : "red"}>
{item?.isPresent ? "Hadir" : "Tidak Hadir"}
</BadgeCustom>
</View>
) : (
<View
style={{
justifyContent: "flex-end",
}}
>
<BadgeCustom color="gray">-</BadgeCustom>
</View>
)
}
/>
</BaseBox>
))
)}
</ViewWrapper>
);
}

View File

@@ -1,22 +1,73 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
ButtonCustom,
DotButton,
DrawerCustom,
MenuDrawerDynamicGrid,
Spacing,
ViewWrapper
AlertDefaultSystem,
ButtonCustom,
DotButton,
DrawerCustom,
LoaderCustom,
MenuDrawerDynamicGrid,
ViewWrapper,
} from "@/components";
import { IMenuDrawerItem } from "@/components/_Interface/types";
import LeftButtonCustom from "@/components/Button/BackButton";
import { useAuth } from "@/hooks/use-auth";
import Event_BoxDetailPublishSection from "@/screens/Event/BoxDetailPublishSection";
import { menuDrawerPublishEvent } from "@/screens/Event/menuDrawerPublish";
import { router, Stack, useLocalSearchParams } from "expo-router";
import { useState } from "react";
import { Alert } from "react-native";
import {
apiEventCheckParticipants,
apiEventGetOne,
apiEventJoin,
} from "@/service/api-client/api-event";
import {
router,
Stack,
useFocusEffect,
useLocalSearchParams,
} from "expo-router";
import { useCallback, useState } from "react";
import Toast from "react-native-toast-message";
export default function EventDetailPublish() {
const { id } = useLocalSearchParams();
const { user } = useAuth();
const [openDrawer, setOpenDrawer] = useState(false);
const [isLoadingData, setIsLoadingData] = useState(false);
const [isLoadingJoin, setIsLoadingJoin] = useState(false);
const [data, setData] = useState();
const [isParticipant, setIsParticipant] = useState<boolean | null>(null);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [])
);
async function onLoadData() {
try {
setIsLoadingData(true);
const response = await apiEventGetOne({ id: id as string });
if (response.success) {
setData(response.data);
const responseCheckParticipants = await apiEventCheckParticipants({
id: id as string,
userId: user?.id as string,
});
if (
responseCheckParticipants.success &&
responseCheckParticipants.data
) {
setIsParticipant(true);
}
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoadingData(false);
}
}
const handlePress = (item: IMenuDrawerItem) => {
console.log("PATH ", item.path);
@@ -24,15 +75,61 @@ export default function EventDetailPublish() {
setOpenDrawer(false);
};
const footerButton = (
<ButtonCustom
backgroundColor="green"
textColor="white"
onPress={() => Alert.alert("Anda berhasil join event ini")}
>
Join
</ButtonCustom>
);
const handlerJoin = async () => {
const userId = user?.id;
if (!userId) {
return Toast.show({
type: "error",
text2: "Anda belum login",
});
}
try {
setIsLoadingJoin(true);
const response = await apiEventJoin({
id: id as string,
userId: userId as string,
});
if (response.success) {
router.navigate(
`/(application)/(user)/event/${id}/list-of-participants`
);
Toast.show({
type: "success",
text1: "Anda berhasil join",
});
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoadingJoin(false);
}
};
const footerButton = () => {
return (
<>
<ButtonCustom
disabled={isParticipant as any}
isLoading={isLoadingJoin}
backgroundColor="green"
textColor="white"
onPress={() =>
AlertDefaultSystem({
title: "Join event",
message: "Anda yakin ingin join sebagai peserta event ?",
textLeft: "Tidak",
textRight: "Ya",
onPressLeft: () => {},
onPressRight: () => handlerJoin(),
})
}
>
{isParticipant ? "Anda sudah tergabung" : "Join"}
</ButtonCustom>
</>
);
};
return (
<>
@@ -44,19 +141,25 @@ export default function EventDetailPublish() {
}}
/>
<ViewWrapper>
<Event_BoxDetailPublishSection footerButton={footerButton} />
<Spacing />
{isLoadingData ? (
<LoaderCustom />
) : (
<Event_BoxDetailPublishSection
data={data}
footerButton={footerButton()}
/>
)}
</ViewWrapper>
<DrawerCustom
isVisible={openDrawer}
closeDrawer={() => setOpenDrawer(false)}
height={250}
height={"auto"}
>
<MenuDrawerDynamicGrid
data={menuDrawerPublishEvent({ id: id as string })}
columns={4}
onPressItem={handlePress}
onPressItem={handlePress as any}
/>
</DrawerCustom>
</>

View File

@@ -1,19 +1,51 @@
import {
ButtonCustom,
SelectCustom,
Spacing,
StackCustom,
TextAreaCustom,
TextCustom,
TextInputCustom,
ViewWrapper,
} from "@/components";
import DateTimePickerCustom from "@/components/DateInput/DateTimePickerCustom";
import { masterTypeEvent } from "@/lib/dummy-data/event/master-type-event";
import { useAuth } from "@/hooks/use-auth";
import { apiEventCreate } from "@/service/api-client/api-event";
import { apiMasterEventType } from "@/service/api-client/api-master";
import { DateTimePickerEvent } from "@react-native-community/datetimepicker";
import { router } from "expo-router";
import React, { useState } from "react";
import { Platform } from "react-native";
import React, { useEffect, useState } from "react";
import Toast from "react-native-toast-message";
interface EventCreateProps {
title?: string;
lokasi?: string;
deskripsi?: string;
eventMaster_TipeAcaraId?: string;
tanggal?: string;
tanggalSelesai?: string;
authorId?: string;
}
export default function EventCreate() {
const [data, setData] = useState<EventCreateProps>();
const [listTypeEvent, setListTypeEvent] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const { user } = useAuth();
useEffect(() => {
onLoadMasterEventType();
}, []);
const onLoadMasterEventType = async () => {
try {
const response = await apiMasterEventType();
setListTypeEvent(response.data);
} catch (error) {
console.log("Error onLoadMasterEventType", error);
}
};
const [selectedDate, setSelectedDate] = useState<
Date | DateTimePickerEvent | null
>(null);
@@ -22,35 +54,76 @@ export default function EventCreate() {
Date | DateTimePickerEvent | null
>(null);
const handlerSubmit = () => {
const handlerSubmit = async () => {
if (
!data?.title ||
!data?.lokasi ||
!data?.deskripsi ||
!data?.eventMaster_TipeAcaraId
) {
Toast.show({
type: "info",
text1: "Info",
text2: "Lengkapi semua data",
});
return;
}
if (!selectedDate || !selectedEndDate) {
Toast.show({
type: "info",
text1: "Info",
text2: "Pilih tanggal mulai dan berakhir",
});
return;
}
// if (selectedDate) {
// console.log("Tanggal yang dipilih:", selectedDate);
// console.log(`ISO Format ${Platform.OS}:`, selectedDate.toString());
// // Kirim ke API atau proses lanjutan
// } else {
// console.log("Tanggal belum dipilih");
// }
// if (selectedEndDate) {
// console.log("Tanggal yang dipilih:", selectedEndDate);
// console.log(`ISO Format ${Platform.OS}:`, selectedEndDate.toString());
// // Kirim ke API atau proses lanjutan
// } else {
// console.log("Tanggal berakhir belum dipilih");
// }
try {
if (selectedDate) {
console.log("Tanggal yang dipilih:", selectedDate);
console.log(`ISO Format ${Platform.OS}:`, selectedDate.toString());
// Kirim ke API atau proses lanjutan
} else {
console.log("Tanggal belum dipilih");
}
setIsLoading(true);
if (selectedEndDate) {
console.log("Tanggal yang dipilih:", selectedEndDate);
console.log(`ISO Format ${Platform.OS}:`, selectedEndDate.toString());
// Kirim ke API atau proses lanjutan
} else {
console.log("Tanggal berakhir belum dipilih");
}
const newData = {
...data,
tanggal: new Date(selectedDate as any).toISOString(),
tanggalSelesai: new Date(selectedEndDate as any).toISOString(),
authorId: user?.id,
};
console.log("Data berhasil disimpan", JSON.stringify(newData, null, 2));
const response = await apiEventCreate(newData);
console.log("Response", JSON.stringify(response, null, 2));
console.log("Data berhasil disimpan");
router.navigate("/event/status");
} catch (error) {
console.log(error);
} finally {
setIsLoading(false);
}
};
const buttonSubmit = (
<ButtonCustom title="Simpan" onPress={handlerSubmit} />
// <BoxButtonOnFooter>
// </BoxButtonOnFooter>
<ButtonCustom
isLoading={isLoading}
title="Simpan"
onPress={handlerSubmit}
/>
);
return (
@@ -61,17 +134,27 @@ export default function EventCreate() {
placeholder="Masukkan nama event"
label="Nama Event"
required
onChangeText={(value: any) => setData({ ...data, title: value })}
/>
<SelectCustom
label="Tipe Event"
placeholder="Pilih tipe event"
data={masterTypeEvent}
onChange={(value) => console.log(value)}
data={listTypeEvent.map((item: any) => ({
label: item.name,
value: item.id,
}))}
value={data?.eventMaster_TipeAcaraId || ""}
onChange={(value: any) =>
setData({ ...data, eventMaster_TipeAcaraId: value })
}
/>
<TextInputCustom
label="Lokasi"
placeholder="Masukkan lokasi event"
required
onChangeText={(value: any) => setData({ ...data, lokasi: value })}
/>
<DateTimePickerCustom
@@ -84,14 +167,24 @@ export default function EventCreate() {
minimumDate={new Date(Date.now())}
/>
<DateTimePickerCustom
label="Tanggal & Waktu Berakhir"
required
onChange={(date: Date) => {
setSelectedEndDate(date as any);
}}
value={selectedEndDate as any}
/>
<StackCustom gap={0}>
<DateTimePickerCustom
disabled={!selectedDate}
label="Tanggal & Waktu Berakhir"
required
onChange={(date: Date) => {
setSelectedEndDate(date as any);
}}
value={selectedEndDate as any}
minimumDate={new Date(selectedDate as any)}
/>
{!selectedDate && (
<TextCustom color="gray" size={"small"}>
Note: Pilih tanggal mulai terlebih dahulu
</TextCustom>
)}
<Spacing />
</StackCustom>
<TextAreaCustom
label="Deskripsi"
@@ -99,6 +192,9 @@ export default function EventCreate() {
required
showCount
maxLength={100}
onChangeText={(value: any) =>
setData({ ...data, deskripsi: value })
}
/>
{buttonSubmit}

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable react-hooks/exhaustive-deps */
import { StackCustom, ViewWrapper } from "@/components";
import { MainColor } from "@/constants/color-palet";
@@ -8,31 +9,40 @@ import TabSection from "@/screens/Home/tabSection";
import { tabsHome } from "@/screens/Home/tabsList";
import Home_FeatureSection from "@/screens/Home/topFeatureSection";
import { apiUser } from "@/service/api-client/api-user";
import { apiVersion } from "@/service/api-config";
import { Ionicons } from "@expo/vector-icons";
import { Redirect, router, Stack } from "expo-router";
import { useEffect, useState } from "react";
export default function Application() {
const { user } = useAuth();
const [data, setData] = useState<any>({});
const { token, user } = useAuth();
const [data, setData] = useState<any>();
useEffect(() => {
onLoadData();
checkVersion();
}, []);
async function onLoadData() {
const response = await apiUser(user?.id as string);
console.log("User >>", JSON.stringify(response.data.username, null, 2));
console.log("Profile Check >>", JSON.stringify(response.data.Profile.id, null, 2));
console.log("Response profile >>", JSON.stringify(response?.data?.Profile, null, 2));
setData(response.data);
}
const checkVersion = async () => {
const response = await apiVersion();
console.log("Version >>", JSON.stringify(response.data, null, 2));
};
if (data && data?.active === false) {
console.log("User is not active");
return <Redirect href={`/waiting-room`} />;
}
if (data && data?.Profile === null) {
console.log("Profile is null");
return <Redirect href={`/profile/create`} />;
}
@@ -40,7 +50,7 @@ export default function Application() {
<>
<Stack.Screen
options={{
title: "HIPMI",
title: `HIPMI`,
headerLeft: () => (
<Ionicons
name="search"

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

@@ -1,34 +1,83 @@
import {
AvatarUsernameAndOtherComponent,
BoxWithHeaderSection,
FloatingButton,
SearchInput,
Spacing,
TextCustom,
ViewWrapper
AvatarUsernameAndOtherComponent,
BoxWithHeaderSection,
FloatingButton,
LoaderCustom,
SearchInput,
Spacing,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import { jobDataDummy } from "@/screens/Job/listDataDummy";
import { router } from "expo-router";
import { apiJobGetAll } from "@/service/api-client/api-job";
import { router, useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
export default function JobBeranda() {
const [listData, setListData] = useState<any[]>([]);
const [isLoadData, setIsLoadData] = useState(false);
const [search, setSearch] = useState("");
useFocusEffect(
useCallback(() => {
onLoadData(search);
}, [search])
);
const onLoadData = async (search: string) => {
try {
setIsLoadData(true);
const response = await apiJobGetAll({ search, category: "beranda" });
setListData(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoadData(false);
}
};
const handleSearch = (search: string) => {
setSearch(search);
onLoadData(search);
};
return (
<ViewWrapper
hideFooter
floatingButton={
<FloatingButton onPress={() => router.push("/job/create")} />
}
headerComponent={<SearchInput placeholder="Cari pekerjaan" />}
headerComponent={
<SearchInput placeholder="Cari pekerjaan" onChangeText={handleSearch} />
}
>
{jobDataDummy.map((item, index) => (
<BoxWithHeaderSection key={index} onPress={() => router.push(`/job/${item.id}`)}>
<AvatarUsernameAndOtherComponent avatarHref={`/profile/${item.id}`} />
<Spacing />
<TextCustom truncate={2} align="center" bold size="large">
{item.posisi}
</TextCustom>
<Spacing />
</BoxWithHeaderSection>
))}
{isLoadData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Belum ada lowongan</TextCustom>
) : (
listData.map((item, index) => (
<BoxWithHeaderSection
key={index}
onPress={() => router.push(`/job/${item.id}`)}
>
<StackCustom>
<AvatarUsernameAndOtherComponent
avatar={item?.Author?.Profile?.imageId}
avatarHref={`/profile/${item?.Author?.Profile?.id}`}
name={item?.Author?.username}
/>
<TextCustom truncate={2} align="center" bold size="large">
{item?.title || "-"}
</TextCustom>
</StackCustom>
<Spacing />
</BoxWithHeaderSection>
))
)}
<Spacing />
</ViewWrapper>
);
}

View File

@@ -1,17 +1,46 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BaseBox,
ScrollableCustom,
TextCustom,
ViewWrapper,
BaseBox,
LoaderCustom,
ScrollableCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import { useAuth } from "@/hooks/use-auth";
import { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
import { jobDataDummy } from "@/screens/Job/listDataDummy";
import { useState } from "react";
import { apiJobGetByStatus } from "@/service/api-client/api-job";
import { useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
export default function JobStatus() {
const { user } = useAuth();
const [activeCategory, setActiveCategory] = useState<string | null>(
"publish"
);
const [listData, setListData] = useState<any[]>([]);
const [isLoadList, setIsLoadList] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [user?.id, activeCategory])
);
const onLoadData = async () => {
try {
setIsLoadList(true);
const response = await apiJobGetByStatus({
authorId: user?.id as string,
status: activeCategory as string,
});
setListData(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoadList(false);
}
};
const handlePress = (item: any) => {
setActiveCategory(item.value);
@@ -32,19 +61,24 @@ export default function JobStatus() {
return (
<ViewWrapper headerComponent={scrollComponent} hideFooter>
{jobDataDummy.map((e, i) => (
<BaseBox
key={i}
paddingTop={20}
paddingBottom={20}
href={`/job/${e.id}/${activeCategory}/detail`}
// onPress={() => console.log("pressed")}
>
<TextCustom align="center" bold truncate size="large">
{e.posisi} {activeCategory?.toUpperCase()}
</TextCustom>
</BaseBox>
))}
{isLoadList ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Tidak ada data {activeCategory}</TextCustom>
) : (
listData.map((e, i) => (
<BaseBox
key={i}
paddingTop={20}
paddingBottom={20}
href={`/job/${e?.id}/${activeCategory}/detail`}
>
<TextCustom align="center" bold truncate size="large">
{e?.title}
</TextCustom>
</BaseBox>
))
)}
</ViewWrapper>
);
}

View File

@@ -1,23 +1,51 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BackButton,
DotButton,
DrawerCustom,
LoaderCustom,
MenuDrawerDynamicGrid,
Spacing,
StackCustom,
ViewWrapper,
} from "@/components";
import { IconEdit } from "@/components/_Icon";
import { IMenuDrawerItem } from "@/components/_Interface/types";
import Job_BoxDetailSection from "@/screens/Job/BoxDetailSection";
import Job_ButtonStatusSection from "@/screens/Job/ButtonStatusSection";
import { jobDataDummy } from "@/screens/Job/listDataDummy";
import { router, Stack, useLocalSearchParams } from "expo-router";
import { useState } from "react";
import { apiJobGetOne } from "@/service/api-client/api-job";
import {
router,
Stack,
useFocusEffect,
useLocalSearchParams,
} from "expo-router";
import { useCallback, useState } from "react";
export default function JobDetailStatus() {
const { id, status } = useLocalSearchParams();
const [openDrawer, setOpenDrawer] = useState(false);
const jobDetail = jobDataDummy.find((e) => e.id === Number(id));
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 handlePress = (item: IMenuDrawerItem) => {
console.log("PATH >> ", item.path);
@@ -38,9 +66,23 @@ export default function JobDetailStatus() {
}}
/>
<ViewWrapper>
<Job_BoxDetailSection data={jobDetail} />
<Job_ButtonStatusSection status={status as string} />
<Spacing />
{isLoadData ? (
<LoaderCustom />
) : (
<>
<StackCustom>
<Job_BoxDetailSection data={data} />
<Job_ButtonStatusSection
id={id as string}
status={status as string}
isLoading={isLoading}
onSetLoading={setIsLoading}
isArchive={true}
/>
</StackCustom>
<Spacing />
</>
)}
</ViewWrapper>
<DrawerCustom

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

@@ -1,21 +1,133 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BaseBox,
ButtonCenteredOnly,
ButtonCustom,
DummyLandscapeImage,
InformationBox,
LandscapeFrameUploaded,
LoaderCustom,
Spacing,
StackCustom,
TextAreaCustom,
TextInputCustom,
ViewWrapper,
} from "@/components";
import { router } from "expo-router";
import DIRECTORY_ID from "@/constants/directory-id";
import { apiJobGetOne, apiJobUpdateData } from "@/service/api-client/api-job";
import {
deleteImageService,
uploadImageService,
} from "@/service/upload-service";
import pickImage from "@/utils/pickImage";
import { router, useLocalSearchParams } from "expo-router";
import { useEffect, useState } from "react";
import Toast from "react-native-toast-message";
export default function JobEdit() {
const { id } = useLocalSearchParams();
const [data, setData] = useState<any>({
title: "",
content: "",
deskripsi: "",
});
const [isLoadData, setIsLoadData] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [imageUri, setImageUri] = useState<string | null>(null);
useEffect(() => {
onLoadData();
}, [id]);
const onLoadData = async () => {
try {
setIsLoadData(true);
const response = await apiJobGetOne({ id: id as string });
if (response.success) {
setData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoadData(false);
}
};
const handlerOnUpdate = async () => {
if (!data.title || !data.content || !data.deskripsi) {
Toast.show({
type: "info",
text1: "Info",
text2: "Harap isi semua data",
});
return;
}
try {
setIsLoading(true);
let newImageId = "";
if (imageUri) {
const responseUploadImage = await uploadImageService({
imageUri: imageUri,
dirId: DIRECTORY_ID.job_image,
});
if (responseUploadImage.success) {
newImageId = responseUploadImage.data.id;
}
}
if (data?.imageId) {
const responseDeleteImage = await deleteImageService({
id: data.imageId,
});
if (!responseDeleteImage.success) {
console.log("[ERROR DELETE IMAGE]", responseDeleteImage.message);
}
}
const newData = {
title: data.title,
content: data.content,
deskripsi: data.deskripsi,
imageId: newImageId,
};
const response = await apiJobUpdateData({
id: id as string,
data: newData,
category: "edit",
});
if (response.success) {
Toast.show({
type: "success",
text1: response.message,
});
router.back();
} else {
Toast.show({
type: "info",
text1: "Info",
text2: response.message,
});
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoading(false);
}
};
const buttonSubmit = () => {
return (
<>
<ButtonCustom onPress={() => router.back()}>Update</ButtonCustom>
<ButtonCustom isLoading={isLoading} onPress={() => handlerOnUpdate()}>
Update
</ButtonCustom>
<Spacing />
</>
);
@@ -23,45 +135,64 @@ export default function JobEdit() {
return (
<ViewWrapper>
<StackCustom gap={"xs"}>
<InformationBox text="Poster atau gambar lowongan kerja bersifat opsional, tidak wajib untuk dimasukkan dan upload lah gambar yang sesuai dengan deskripsi lowongan kerja." />
{isLoadData ? (
<LoaderCustom />
) : (
<StackCustom gap={"xs"}>
<InformationBox text="Poster atau gambar lowongan kerja bersifat opsional, tidak wajib untuk dimasukkan dan upload lah gambar yang sesuai dengan deskripsi lowongan kerja." />
<LandscapeFrameUploaded />
<ButtonCenteredOnly
onPress={() => {
router.push("/(application)/(image)/take-picture/123");
}}
icon="upload"
>
Upload
</ButtonCenteredOnly>
{imageUri ? (
<LandscapeFrameUploaded image={imageUri as any} />
) : (
<BaseBox>
<DummyLandscapeImage imageId={data?.imageId} />
</BaseBox>
)}
<Spacing />
<ButtonCenteredOnly
onPress={() => {
pickImage({
setImageUri,
});
}}
icon="upload"
>
Upload
</ButtonCenteredOnly>
<TextInputCustom
label="Judul Lowongan"
placeholder="Masukan Judul Lowongan Kerja"
required
/>
<Spacing />
<TextAreaCustom
label="Syarat & Kualifikasi"
placeholder="Masukan Syarat & Kualifikasi Lowongan Kerja"
required
showCount
maxLength={1000}
/>
<TextInputCustom
label="Judul Lowongan"
placeholder="Masukan Judul Lowongan Kerja"
required
value={data.title}
onChangeText={(value) => setData({ ...data, title: value })}
/>
<TextAreaCustom
label="Deskripsi Lowongan"
placeholder="Masukan Deskripsi Lowongan Kerja"
required
showCount
maxLength={1000}
/>
<TextAreaCustom
label="Syarat & Kualifikasi"
placeholder="Masukan Syarat & Kualifikasi Lowongan Kerja"
required
showCount
maxLength={1000}
value={data.content}
onChangeText={(value) => setData({ ...data, content: value })}
/>
{buttonSubmit()}
</StackCustom>
<TextAreaCustom
label="Deskripsi Lowongan"
placeholder="Masukan Deskripsi Lowongan Kerja"
required
showCount
maxLength={1000}
value={data.deskripsi}
onChangeText={(value) => setData({ ...data, deskripsi: value })}
/>
{buttonSubmit()}
</StackCustom>
)}
</ViewWrapper>
);
}

View File

@@ -1,25 +1,41 @@
import {
ButtonCustom,
Spacing,
ViewWrapper
} from "@/components";
/* eslint-disable react-hooks/exhaustive-deps */
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 { jobDataDummy } from "@/screens/Job/listDataDummy";
import { apiJobGetOne } from "@/service/api-client/api-job";
import { Ionicons } from "@expo/vector-icons";
import * as Clipboard from "expo-clipboard";
import { useLocalSearchParams } from "expo-router";
import { useEffect, useState } from "react";
import { Alert, Linking } from "react-native";
export default function JobDetail() {
const { id } = useLocalSearchParams();
const [data, setData] = useState<any>(null);
const [isLoading, setIsLoading] = useState(false);
const jobDetail = jobDataDummy.find((e) => e.id === Number(id));
useEffect(() => {
onLoadData();
}, [id]);
const OpenLinkButton = () => {
const jobUrl =
"https://stg-hipmi.wibudev.com/job-vacancy/cm6ijt9w8005zucv4twsct657";
const onLoadData = async () => {
try {
setIsLoading(true);
const response = await apiJobGetOne({ id: id as string });
setData(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoading(false);
}
};
const linkUrl = `http://192.168.1.83:3000/job-vacancy/`;
const OpenLinkButton = ({ id }: { id: string }) => {
const jobUrl = `${linkUrl}${id}`;
const openInBrowser = async () => {
const supported = await Linking.canOpenURL(jobUrl);
@@ -44,9 +60,8 @@ export default function JobDetail() {
);
};
const CopyLinkButton = () => {
const jobUrl =
"https://stg-hipmi.wibudev.com/job-vacancy/cm6ijt9w8005zucv4twsct657";
const CopyLinkButton = ({ id }: { id: string }) => {
const jobUrl = `${linkUrl}${id}`;
const copyToClipboard = async () => {
await Clipboard.setStringAsync(jobUrl);
@@ -70,10 +85,18 @@ export default function JobDetail() {
return (
<ViewWrapper>
<Job_BoxDetailSection data={jobDetail}/>
<OpenLinkButton />
<Spacing />
<CopyLinkButton />
{isLoading ? (
<LoaderCustom />
) : (
<>
<Job_BoxDetailSection data={data} />
<StackCustom>
<OpenLinkButton id={id as string} />
<CopyLinkButton id={id as string} />
</StackCustom>
<Spacing />
</>
)}
</ViewWrapper>
);
}

View File

@@ -7,19 +7,99 @@ import {
StackCustom,
TextAreaCustom,
TextInputCustom,
ViewWrapper,
ViewWrapper
} from "@/components";
import DIRECTORY_ID from "@/constants/directory-id";
import { useAuth } from "@/hooks/use-auth";
import { apiJobCreate } from "@/service/api-client/api-job";
import { uploadImageService } from "@/service/upload-service";
import pickImage from "@/utils/pickImage";
import { router } from "expo-router";
import { useState } from "react";
import Toast from "react-native-toast-message";
export default function JobCreate() {
const nextUrl = "/(application)/(user)/job/(tabs)/status";
const { user } = useAuth();
const [isLoading, setIsLoading] = useState(false);
const [image, setImage] = useState<string | null>(null);
const [data, setData] = useState({
title: "",
content: "",
deskripsi: "",
authorId: "",
});
const handlerOnSubmit = async () => {
let imageId = "";
const newData = {
title: data.title,
content: data.content,
deskripsi: data.deskripsi,
authorId: user?.id,
imageId: "",
};
if (!data.title || !data.content || !data.deskripsi || !user?.id) {
Toast.show({
type: "info",
text1: "Info",
text2: "Harap isi semua data",
});
return;
}
try {
setIsLoading(true);
if (image === null || !image) {
const response = await apiJobCreate(newData);
if (response.success) {
Toast.show({
type: "success",
text1: "Berhasil",
text2: "Lowongan berhasil dibuat",
});
router.replace(nextUrl);
}
return;
}
const responseUploadImage = await uploadImageService({
imageUri: image,
dirId: DIRECTORY_ID.job_image,
});
if (responseUploadImage.success) {
imageId = responseUploadImage.data.id;
}
const fixData = {
...newData,
imageId: imageId,
};
const response = await apiJobCreate(fixData);
if (response.success) {
Toast.show({
type: "success",
text1: "Berhasil",
text2: "Lowongan berhasil dibuat",
});
router.replace(nextUrl);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoading(false);
}
};
const buttonSubmit = () => {
return (
<>
<ButtonCustom
onPress={() =>
router.replace("/(application)/(user)/job/(tabs)/status")
}
>
<ButtonCustom isLoading={isLoading} onPress={() => handlerOnSubmit()}>
Simpan
</ButtonCustom>
<Spacing />
@@ -32,10 +112,19 @@ export default function JobCreate() {
<StackCustom gap={"xs"}>
<InformationBox text="Poster atau gambar lowongan kerja bersifat opsional, tidak wajib untuk dimasukkan dan upload lah gambar yang sesuai dengan deskripsi lowongan kerja." />
<LandscapeFrameUploaded />
{/* <BaseBox>
<Image
source={image ? { uri: image } : DUMMY_IMAGE.dummy_image}
style={{ width: "100%", height: 200 }}
/>
</BaseBox> */}
<LandscapeFrameUploaded image={image as string} />
<ButtonCenteredOnly
onPress={() => {
router.push("/(application)/(image)/take-picture/123");
// router.push("/(application)/(image)/take-picture/123");
pickImage({
setImageUri: setImage,
});
}}
icon="upload"
>
@@ -48,6 +137,8 @@ export default function JobCreate() {
label="Judul Lowongan"
placeholder="Masukan Judul Lowongan Kerja"
required
value={data.title}
onChangeText={(value) => setData({ ...data, title: value })}
/>
<TextAreaCustom
@@ -56,6 +147,8 @@ export default function JobCreate() {
required
showCount
maxLength={1000}
value={data.content}
onChangeText={(value) => setData({ ...data, content: value })}
/>
<TextAreaCustom
@@ -64,6 +157,8 @@ export default function JobCreate() {
required
showCount
maxLength={1000}
value={data.deskripsi}
onChangeText={(value) => setData({ ...data, deskripsi: value })}
/>
{buttonSubmit()}

View File

@@ -1,9 +1,11 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import {
ActionIcon,
AvatarComp,
BaseBox,
ButtonCenteredOnly,
CenterCustom,
Grid,
InformationBox,
SelectCustom,
Spacing,
@@ -13,7 +15,9 @@ import {
TextInputCustom,
ViewWrapper,
} from "@/components";
import { IconPlus } from "@/components/_Icon";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_XLARGE } from "@/constants/constans-value";
import DUMMY_IMAGE from "@/constants/dummy-image-value";
import Portofolio_ButtonCreate from "@/screens/Portofolio/ButtonCreatePortofolio";
import {
@@ -25,10 +29,12 @@ import {
IMasterSubBidangBisnis,
} from "@/types/Type-Master";
import pickImage from "@/utils/pickImage";
import { Ionicons } from "@expo/vector-icons";
import { Image } from "expo-image";
import { useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useEffect, useState } from "react";
import { Text, View } from "react-native";
import { Text, TouchableOpacity, View } from "react-native";
import PhoneInput, { ICountry } from "react-native-international-phone-number";
import { Avatar } from "react-native-paper";
@@ -46,12 +52,11 @@ export default function PortofolioCreate() {
const [imageUri, setImageUri] = useState<string | null>(null);
const [bidangBisnis, setBidangBisnis] = useState<IMasterBidangBisnis[]>([]);
const [selectBidangId, setSelectBidangId] = useState<string>("");
const [subBidangBisnis, setSubBidangBisnis] = useState<
IMasterSubBidangBisnis[]
>([]);
// const [subBidangSelected, setSubBidangSelected] = useState<string[]>([]);
const [selectedSubBidang, setSelectedSubBidang] = useState<string[]>([]);
const [listSubBidangSelected, setListSubBidangSelected] = useState([
{
id: "",
@@ -82,6 +87,7 @@ export default function PortofolioCreate() {
useEffect(() => {
onLoadMaster();
onLoadMasterSubBidangBisnis();
}, []);
const onLoadMaster = async () => {
@@ -94,15 +100,23 @@ export default function PortofolioCreate() {
}
};
const onLoadMasterSubBidangBisnis = async ({ id }: { id: string }) => {
const onLoadMasterSubBidangBisnis = async () => {
try {
const response = await apiMasterSubBidangBisnis({ id: id });
const response = await apiMasterSubBidangBisnis({});
setSubBidangBisnis(response.data);
} catch (error) {
setSubBidangBisnis([]);
console.log("Error onLoadMasterBidangBisnis", error);
}
};
const handlerSelectedSubBidang = ({ id }: { id: string }) => {
const selectedList = subBidangBisnis?.filter(
(item) => (item?.masterBidangBisnisId as any) === id
);
setSelectedSubBidang(selectedList as any[]);
};
return (
<ViewWrapper
footerComponent={
@@ -136,45 +150,100 @@ export default function PortofolioCreate() {
}))}
value={data.masterBidangBisnisId}
onChange={(value) => {
const isSameBidang = data.masterBidangBisnisId === value;
if (!isSameBidang) {
setListSubBidangSelected([{ id: "" }]);
}
setData({ ...(data as any), masterBidangBisnisId: value });
setSelectBidangId(id as string);
onLoadMasterSubBidangBisnis({ id: value as string });
handlerSelectedSubBidang({ id: value as string });
}}
/>
{/* {listSubBidangSelected.map((item, index) => (
<Grid key={index}>
<Grid.Col span={10}>
<SelectCustom
disabled={selectBidangId === ""}
label="Sub Bidang Usaha"
required
data={subBidangBisnis.map((item) => ({
label: item.name,
value: item.id,
}))}
value={data.sub_bidang_usaha}
onChange={(value) => {
setData({ ...(data as any), sub_bidang_usaha: value });
setListSubBidangSelected([
...listSubBidangSelected,
{ id: value as string },
]);
}}
/>
</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>
{listSubBidangSelected.map((item, index) => (
<SelectCustom
key={index}
disabled={data.masterBidangBisnisId === ""}
label="Sub Bidang Usaha"
required
data={_.map(selectedSubBidang as any)
.filter((option: any) => {
const selectedValues = listSubBidangSelected.map((s) => s.id);
return (
option.id === item.id || // biarkan tetap muncul kalau ini valuenya sendiri
!selectedValues.includes(option.id)
);
})
.map((e: any) => ({
value: e.id,
label: e.name,
}))}
value={item.id || null}
onChange={(value) => {
const list = _.clone(listSubBidangSelected);
list[index].id = value as any;
setListSubBidangSelected(list);
}}
/>
))}
<ButtonCenteredOnly
<CenterCustom>
<View style={{ flexDirection: "row", alignItems: "center", gap: 10 }}>
<ActionIcon
disabled={
selectedSubBidang.length === listSubBidangSelected.length
}
onPress={() => {
setListSubBidangSelected([
...listSubBidangSelected,
{ id: "" },
]);
}}
icon={
<Ionicons
name="add-circle-outline"
size={ICON_SIZE_XLARGE}
color={MainColor.black}
/>
}
size="xl"
/>
<ActionIcon
disabled={listSubBidangSelected.length <= 1}
onPress={() => {
const list = _.clone(listSubBidangSelected);
list.pop();
setListSubBidangSelected(list);
}}
icon={
<Ionicons
name="remove-circle-outline"
size={ICON_SIZE_XLARGE}
color={MainColor.black}
/>
}
size="xl"
/>
</View>
</CenterCustom>
<Spacing />
{/* <SelectCustom
label="Bidang Usaha"
required
data={bidangBisnis.map((item) => ({
label: item.name,
value: item.id,
}))}
value={null}
onChange={(value) => {
setData({ ...(data as any), masterBidangBisnisId: value });
handlerSelectedSubBidang({ id: value as string });
}}
/> */}
{/* <ButtonCenteredOnly
onPress={() => {
setListSubBidangSelected([...listSubBidangSelected, { id: "" }]);
}}

View File

@@ -0,0 +1,357 @@
import {
ActionIcon,
BoxButtonOnFooter,
ButtonCustom,
CenterCustom,
SelectCustom,
Spacing,
StackCustom,
TextAreaCustom,
TextCustom,
TextInputCustom,
ViewWrapper,
} from "@/components";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_XLARGE } from "@/constants/constans-value";
import {
apiMasterBidangBisnis,
apiMasterSubBidangBisnis,
} from "@/service/api-client/api-master";
import {
apiGetOnePortofolio,
apiUpdatePortofolio,
} from "@/service/api-client/api-portofolio";
import {
IMasterBidangBisnis,
IMasterSubBidangBisnis,
} from "@/types/Type-Master";
import { Ionicons } from "@expo/vector-icons";
import { router, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useEffect, useState } from "react";
import { Text, View } from "react-native";
import PhoneInput, { ICountry } from "react-native-international-phone-number";
import Toast from "react-native-toast-message";
interface IFormData {
id_Portofolio: string;
namaBisnis: string;
alamatKantor: string;
tlpn: string;
deskripsi: string;
masterBidangBisnisId: string;
subBidang: any[];
}
interface IListSubBidangSelected {
id: string;
MasterSubBidangBisnis: {
id: string;
name: string;
masterBidangBisnisId: string;
};
}
export default function PortofolioEdit() {
const { id } = useLocalSearchParams();
const [isLoading, setIsLoading] = useState(false);
const [data, setData] = useState<any>({});
const [selectedCountry, setSelectedCountry] = useState<null | ICountry>(null);
const [bidangBisnis, setBidangBisnis] = useState<IMasterBidangBisnis[]>([]);
const [subBidangBisnis, setSubBidangBisnis] = useState<
IMasterSubBidangBisnis[]
>([]);
const [selectedSubBidang, setSelectedSubBidang] = useState<string[]>([]);
const [listSubBidangSelected, setListSubBidangSelected] = useState<
IListSubBidangSelected[]
>([
{
id: "",
MasterSubBidangBisnis: {
id: "",
name: "",
masterBidangBisnisId: "",
},
},
]);
function handleInputValue(phoneNumber: string) {
setData({ ...data, tlpn: phoneNumber });
}
function handleSelectedCountry(country: ICountry) {
setSelectedCountry(country);
}
useEffect(() => {
onLoadData(id as string);
onLoadMasterBidang();
onLoadMasterSubBidangBisnis();
}, [id]);
const onLoadData = async (id: string) => {
const response = await apiGetOnePortofolio({ id: id });
if (response.data.tlpn && response.data.tlpn.includes("62")) {
const fixNumber = response.data.tlpn.replace("62", "");
setData({ ...response.data, tlpn: fixNumber });
}
};
const onLoadMasterBidang = async () => {
try {
const response = await apiMasterBidangBisnis();
setBidangBisnis(response.data);
} catch (error) {
setBidangBisnis([]);
console.log("Error onLoadMasterBidangBisnis", error);
}
};
async function onLoadMasterSubBidangBisnis() {
try {
const response = await apiMasterSubBidangBisnis({});
if (response.success) {
setSubBidangBisnis(response.data);
}
} catch (error) {
console.error("Error on load master sub bidang bisnis", error);
}
}
const handleSubmitUpdate = async () => {
try {
setIsLoading(true);
const callingCode = selectedCountry?.callingCode.replace(/^\+/, "") || "";
const fixNumber = data.tlpn.replace(/\s+/g, "");
const realNumber = callingCode + fixNumber;
const newData: IFormData = {
id_Portofolio: data.id_Portofolio,
namaBisnis: data.namaBisnis,
alamatKantor: data.alamatKantor,
tlpn: realNumber,
deskripsi: data.deskripsi,
masterBidangBisnisId: data.masterBidangBisnisId,
subBidang: listSubBidangSelected,
};
const response = await apiUpdatePortofolio({
id: id as string,
data: newData,
category: "detail",
});
if (!response.success) {
Toast.show({
type: "info",
text1: "Info",
text2: response.message,
});
return;
}
Toast.show({
type: "success",
text1: "Sukses",
text2: "Data terupdate",
});
router.back();
} catch (error) {
console.log("Error handleSubmitUpdate", error);
} finally {
setIsLoading(false);
}
};
const buttonUpdate = (
<BoxButtonOnFooter>
<ButtonCustom
isLoading={isLoading}
disabled={isLoading}
onPress={handleSubmitUpdate}
>
Update
</ButtonCustom>
</BoxButtonOnFooter>
);
return (
<>
<ViewWrapper footerComponent={buttonUpdate}>
<StackCustom gap={"xs"}>
<TextInputCustom
required
label="Nama Bisnis"
placeholder="Masukkan nama bisnis"
value={data.namaBisnis}
onChangeText={(value: any) =>
setData({ ...data, namaBisnis: value })
}
/>
<SelectCustom
label="Bidang Usaha"
required
data={bidangBisnis?.map((item) => ({
label: item.name,
value: item.id,
}))}
value={data.masterBidangBisnisId}
onChange={(value) => {
setData({ ...(data as any), masterBidangBisnisId: value });
}}
/>
{listSubBidangSelected.map((item, index) => (
<SelectCustom
key={index}
label="Sub Bidang Usaha"
required
data={subBidangBisnis?.map((item) => ({
label: item.name,
value: item.id,
}))}
value={item.id || null}
onChange={(value) => {
console.log("Value >>", value);
}}
/>
))}
<CenterCustom>
<View
style={{ flexDirection: "row", alignItems: "center", gap: 10 }}
>
<ActionIcon
// disabled={
// selectedSubBidang.length === listSubBidangSelected.length
// }
onPress={() => {
setListSubBidangSelected([
...listSubBidangSelected,
{
id: "",
MasterSubBidangBisnis: {
id: "",
name: "",
masterBidangBisnisId: "",
},
},
]);
}}
icon={
<Ionicons
name="add-circle-outline"
size={ICON_SIZE_XLARGE}
color={MainColor.black}
/>
}
size="xl"
/>
<ActionIcon
// disabled={listSubBidangSelected.length <= 1}
onPress={() => {
const list = _.clone(listSubBidangSelected);
list.pop();
setListSubBidangSelected(list);
}}
icon={
<Ionicons
name="remove-circle-outline"
size={ICON_SIZE_XLARGE}
color={MainColor.black}
/>
}
size="xl"
/>
</View>
</CenterCustom>
<Spacing />
{/* <Grid>
<Grid.Col span={10}>
<SelectCustom
// disabled
label="Sub Bidang Usaha"
required
data={dummyMasterSubBidangBisnis.map((item) => ({
label: item.name,
value: item.id,
}))}
value={data.masterSubBidangBisnisId}
onChange={(value) => {
setData({ ...(data as any), masterSubBidangBisnisId: value });
}}
/>
</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 /> */}
<View>
<View style={{ flexDirection: "row", alignItems: "center" }}>
<TextCustom semiBold style={{ color: MainColor.white_gray }}>
Nomor Telepon
</TextCustom>
<Text style={{ color: "red" }}> *</Text>
</View>
<Spacing height={5} />
<PhoneInput
value={data.tlpn}
onChangePhoneNumber={handleInputValue}
selectedCountry={selectedCountry}
onChangeSelectedCountry={handleSelectedCountry}
defaultCountry="ID"
placeholder="xxx-xxx-xxx"
/>
</View>
<Spacing />
<TextInputCustom
required
label="Alamat Bisnis"
placeholder="Masukkan alamat bisnis"
value={data.alamatKantor}
onChangeText={(value: any) =>
setData({ ...data, alamatKantor: value })
}
/>
<TextAreaCustom
label="Deskripsi Bisnis"
placeholder="Masukkan deskripsi bisnis"
value={data.deskripsi}
onChangeText={(value: any) =>
setData({ ...data, deskripsi: value })
}
autosize
minRows={2}
maxRows={5}
required
showCount
maxLength={1000}
/>
<Spacing />
<TextCustom>{JSON.stringify(subBidangBisnis, null, 2)}</TextCustom>
</StackCustom>
</ViewWrapper>
</>
);
}

View File

@@ -1,6 +1,9 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
ActionIcon,
BoxButtonOnFooter,
ButtonCustom,
CenterCustom,
SelectCustom,
Spacing,
StackCustom,
@@ -10,16 +13,26 @@ import {
ViewWrapper,
} from "@/components";
import { MainColor } from "@/constants/color-palet";
import { apiMasterBidangBisnis } from "@/service/api-client/api-master";
import { ICON_SIZE_XLARGE } from "@/constants/constans-value";
import {
apiMasterBidangBisnis,
apiMasterSubBidangBisnis,
} from "@/service/api-client/api-master";
import {
apiGetOnePortofolio,
apiUpdatePortofolio,
} from "@/service/api-client/api-portofolio";
import { IMasterBidangBisnis } from "@/types/Type-Master";
import {
IMasterBidangBisnis,
IMasterSubBidangBisnis,
} from "@/types/Type-Master";
import { Ionicons } from "@expo/vector-icons";
import { router, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useEffect, useState } from "react";
import { Text, View } from "react-native";
import PhoneInput, { ICountry } from "react-native-international-phone-number";
import { ActivityIndicator } from "react-native-paper";
import Toast from "react-native-toast-message";
interface IFormData {
@@ -29,15 +42,35 @@ interface IFormData {
tlpn: string;
deskripsi: string;
masterBidangBisnisId: string;
subBidang: any[];
}
interface IListSubBidangSelected {
id: string;
MasterSubBidangBisnis?: {
id?: string;
name?: string;
masterBidangBisnisId?: string;
};
}
export default function PortofolioEdit() {
const { id } = useLocalSearchParams();
const [isLoading, setIsLoading] = useState(false);
const [selectedCountry, setSelectedCountry] = useState<null | ICountry>(null);
const [bidangBisnis, setBidangBisnis] = useState<IMasterBidangBisnis[]>([]);
const [data, setData] = useState<any>({});
const [selectedCountry, setSelectedCountry] = useState<null | ICountry>(null);
const [bidangBisnis, setBidangBisnis] = useState<
IMasterBidangBisnis[] | null
>(null);
const [subBidangBisnis, setSubBidangBisnis] = useState<
IMasterSubBidangBisnis[] | null
>(null);
const [selectedSubBidang, setSelectedSubBidang] = useState<string[]>([]);
const [listSubBidangSelected, setListSubBidangSelected] = useState<
IListSubBidangSelected[]
>([]);
function handleInputValue(phoneNumber: string) {
setData({ ...data, tlpn: phoneNumber });
}
@@ -46,46 +79,200 @@ export default function PortofolioEdit() {
setSelectedCountry(country);
}
useEffect(() => {
onLoadData(id as string);
onLoadMaster();
}, [id]);
const onLoadData = async (id: string) => {
const response = await apiGetOnePortofolio({ id: id });
if (response.data.tlpn && response.data.tlpn.includes("62")) {
const fixNumber = response.data.tlpn.replace("62", "");
console.log("Fix Number >>", fixNumber);
setData({ ...response.data, tlpn: fixNumber });
}
};
const onLoadMaster = async () => {
const onLoadMasterBidang = async () => {
try {
const response = await apiMasterBidangBisnis();
setBidangBisnis(response.data);
return response.success;
} catch (error) {
setBidangBisnis([]);
console.log("Error onLoadMasterBidangBisnis", error);
}
};
async function onLoadMasterSubBidangBisnis() {
try {
const response = await apiMasterSubBidangBisnis({});
setSubBidangBisnis(response.data);
return response.success;
} catch (error) {
console.error("Error on load master sub bidang bisnis", error);
}
}
const handleLoadMaster = async (id: string) => {
const loadBidang = await onLoadMasterBidang();
const loadSubBidang = await onLoadMasterSubBidangBisnis();
if (!loadBidang || !loadSubBidang) {
return;
}
onLoadData(id);
};
useEffect(() => {
handleLoadMaster(id as any);
}, [id]);
const onLoadData = async (id: string) => {
const response = await apiGetOnePortofolio({ id: id });
if (response.success) {
const fixNumber = response.data.tlpn.replace("62", "");
setData({ ...response.data, tlpn: fixNumber });
// Cek apakah ada sub bidang bisnis yang terpilih
const prevSubBidang = response.data.Portofolio_BidangDanSubBidangBisnis;
if (prevSubBidang && prevSubBidang.length > 0) {
setListSubBidangSelected(prevSubBidang);
} else {
// Jika tidak ada sub bidang yang terpilih sebelumnya, tetap inisialisasi dengan array kosong
setListSubBidangSelected([
{
id: "",
MasterSubBidangBisnis: {
id: "",
name: "",
},
},
]);
}
const bisnisId = response.data.masterBidangBisnisId;
handleLoadSelectedSubBidang({
id: bisnisId,
});
}
};
// Handler untuk saat komponen pertama kali load
const handleLoadSelectedSubBidang = ({ id }: { id: string }) => {
if (!subBidangBisnis) return;
const filteredSubBidang: any = subBidangBisnis.filter((item) => {
return item.masterBidangBisnisId === id;
});
setSelectedSubBidang(filteredSubBidang);
};
// Handler untuk menambah sub bidang bisnis
const handleAddSubBidang = () => {
setListSubBidangSelected([
...listSubBidangSelected,
{
id: "",
MasterSubBidangBisnis: { id: "", name: "" },
},
]);
};
// Handler untuk menghapus sub bidang bisnis
const handleRemoveSubBidang = (index: number) => {
if (listSubBidangSelected.length <= 1) return;
const updatedList = [...listSubBidangSelected];
updatedList.splice(index, 1);
setListSubBidangSelected(updatedList);
};
// Handler untuk perubahan bidang bisnis
const handleBidangBisnisChange = (val: string) => {
const isSameBidang = data?.MasterBidangBisnis?.id === val;
setData({ ...(data as any), masterBidangBisnisId: val });
// Reset sub bidang jika ganti bidang
if (!isSameBidang) {
setListSubBidangSelected([
{
id: "",
MasterSubBidangBisnis: { id: "", name: "" },
},
]);
}
handleLoadSelectedSubBidang({ id: val });
};
// Handler untuk update sub bidang
const handleSubBidangChange = (value: string, index: number) => {
const select = selectedSubBidang.find((sub: any) => sub.id === value);
const list: any = _.cloneDeep(listSubBidangSelected);
list[index] = {
id: "",
MasterSubBidangBisnis: select || {
id: value,
name: "",
masterBidangBisnisId: "",
},
};
setListSubBidangSelected(list);
};
useEffect(() => {
if (subBidangBisnis?.length !== undefined && data.masterBidangBisnisId) {
handleLoadSelectedSubBidang({
id: data.masterBidangBisnisId,
});
}
}, [subBidangBisnis, data.masterBidangBisnisId]);
function validateData(data: any) {
if (
!data.namaBisnis ||
!data.alamatKantor ||
!data.tlpn ||
!data.deskripsi ||
!data.masterBidangBisnisId
) {
return false;
}
return true;
}
function validateDataSubBidang(dataArray: any[]) {
return !dataArray.some(
(item: any) =>
!item.MasterSubBidangBisnis.id ||
item.MasterSubBidangBisnis.id.trim() === ""
);
}
const handleSubmitUpdate = async () => {
const callingCode = selectedCountry?.callingCode.replace(/^\+/, "") || "";
const fixNumber = data.tlpn.replace(/\s+/g, "");
const realNumber = callingCode + fixNumber;
const newData: IFormData = {
id_Portofolio: data.id_Portofolio,
namaBisnis: data.namaBisnis,
alamatKantor: data.alamatKantor,
tlpn: realNumber,
deskripsi: data.deskripsi,
masterBidangBisnisId: data.masterBidangBisnisId,
subBidang: listSubBidangSelected,
};
if (!validateData(newData)) {
return Toast.show({
type: "error",
text1: "Harap lengkapi data",
});
}
if (!validateDataSubBidang(listSubBidangSelected as any)) {
return Toast.show({
type: "error",
text1: "Harap lengkapi sub bidang",
});
}
try {
setIsLoading(true);
const callingCode = selectedCountry?.callingCode.replace(/^\+/, "") || "";
const fixNumber = data.tlpn.replace(/\s+/g, "");
const realNumber = callingCode + fixNumber;
const newData: IFormData = {
id_Portofolio: data.id_Portofolio,
namaBisnis: data.namaBisnis,
alamatKantor: data.alamatKantor,
tlpn: realNumber,
deskripsi: data.deskripsi,
masterBidangBisnisId: data.masterBidangBisnisId,
};
const response = await apiUpdatePortofolio({
id: id as string,
@@ -100,14 +287,14 @@ export default function PortofolioEdit() {
text2: response.message,
});
return
return;
}
Toast.show({
type: "success",
text1: "Sukses",
text2: "Data terupdate",
});
Toast.show({
type: "success",
text1: "Sukses",
text2: "Data terupdate",
});
router.back();
} catch (error) {
@@ -129,6 +316,16 @@ export default function PortofolioEdit() {
</BoxButtonOnFooter>
);
if (!bidangBisnis || !subBidangBisnis) {
return (
<>
<ViewWrapper>
<ActivityIndicator size="large" color={MainColor.yellow} />
</ViewWrapper>
</>
);
}
return (
<>
<ViewWrapper footerComponent={buttonUpdate}>
@@ -151,40 +348,83 @@ export default function PortofolioEdit() {
value: item.id,
}))}
value={data.masterBidangBisnisId}
onChange={(value) => {
setData({ ...(data as any), masterBidangBisnisId: value });
onChange={(value: any) => {
handleBidangBisnisChange(value);
}}
/>
{/* <Grid>
<Grid.Col span={10}>
{listSubBidangSelected.map((item, index) => {
// Filter data untuk select sub bidang, menghilangkan yang sudah dipilih kecuali untuk item ini sendiri
const selectedIds = listSubBidangSelected
.filter((_, i) => i !== index)
.map((s) => s.MasterSubBidangBisnis?.id)
.filter((id) => id); // Filter hanya yang memiliki id (tidak kosong)
const availableSubBidangOptions = (selectedSubBidang || [])
.filter((sub: any) => {
// Tampilkan jika ini adalah opsi yang dipilih saat ini atau belum dipilih di sub bidang lainnya
return (
sub.id === item.MasterSubBidangBisnis?.id ||
!selectedIds.includes(sub.id)
);
})
.map((sub: any) => ({
value: sub.id,
label: sub.name,
}));
return (
<SelectCustom
// disabled
key={index}
label="Sub Bidang Usaha"
required
data={dummyMasterSubBidangBisnis.map((item) => ({
label: item.name,
value: item.id,
}))}
value={data.masterSubBidangBisnisId}
onChange={(value) => {
setData({ ...(data as any), masterSubBidangBisnisId: value });
data={availableSubBidangOptions}
value={item.MasterSubBidangBisnis?.id || null}
onChange={(value: any) => {
handleSubBidangChange(value, index);
}}
/>
</Grid.Col>
<Grid.Col
span={2}
style={{ alignItems: "center", justifyContent: "center" }}
);
})}
<CenterCustom>
<View
style={{ flexDirection: "row", alignItems: "center", gap: 10 }}
>
<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 /> */}
<ActionIcon
disabled={
selectedSubBidang.length === listSubBidangSelected.length
}
onPress={() => {
handleAddSubBidang();
}}
icon={
<Ionicons
name="add-circle-outline"
size={ICON_SIZE_XLARGE}
color={MainColor.black}
/>
}
size="xl"
/>
<ActionIcon
disabled={listSubBidangSelected.length <= 1}
onPress={() => {
handleRemoveSubBidang(listSubBidangSelected.length - 1);
}}
icon={
<Ionicons
name="remove-circle-outline"
size={ICON_SIZE_XLARGE}
color={MainColor.black}
/>
}
size="xl"
/>
</View>
</CenterCustom>
<Spacing />
<View>
<View style={{ flexDirection: "row", alignItems: "center" }}>

View File

@@ -1,4 +1,5 @@
import { DrawerCustom, Spacing, StackCustom } from "@/components";
/* eslint-disable react-hooks/exhaustive-deps */
import { DrawerCustom, LoaderCustom, Spacing, StackCustom } from "@/components";
import LeftButtonCustom from "@/components/Button/BackButton";
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
import { MainColor } from "@/constants/color-palet";
@@ -10,6 +11,7 @@ import { drawerItemsPortofolio } from "@/screens/Portofolio/ListPage";
import Portofolio_MenuDrawerSection from "@/screens/Portofolio/MenuDrawer";
import Portofolio_SocialMediaSection from "@/screens/Portofolio/SocialMediaSection";
import { apiGetOnePortofolio } from "@/service/api-client/api-portofolio";
import { apiUser } from "@/service/api-client/api-user";
import { GStyles } from "@/styles/global-styles";
import { Ionicons } from "@expo/vector-icons";
import { Stack, useFocusEffect, useLocalSearchParams } from "expo-router";
@@ -18,10 +20,12 @@ import { TouchableOpacity } from "react-native";
export default function Portofolio() {
const { id } = useLocalSearchParams();
const { user } = useAuth();
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const [isLoadingDelete, setIsLoadingDelete] = useState(false);
const [data, setData] = useState<any>();
const [profileId, setProfileId] = useState<any>();
const { user } = useAuth();
const openDrawer = () => {
setIsDrawerOpen(true);
@@ -33,24 +37,27 @@ export default function Portofolio() {
useFocusEffect(
useCallback(() => {
onLoadData(id as string);
onLoadUserByToken();
}, [id])
);
async function onLoadData(id: string) {
const response = await apiGetOnePortofolio({ id: id });
console.log(
"Response portofolio >>",
JSON.stringify(response.data.namaBisnis, null, 2)
"[PROFILE ID]>>",
JSON.stringify(response.data.Profile.id, null, 2)
);
setData(response.data);
}
const userId = user?.id;
const userLoginId = data?.Profile?.User?.id;
console.log("User ID >>", userId);
console.log("User Login ID >>", userLoginId);
const onLoadUserByToken = async () => {
const response = await apiUser(user?.id as string);
console.log(
"[PROFILE LOGIN]>>",
JSON.stringify(response.data?.Profile.id, null, 2)
);
setProfileId(response?.data?.Profile?.id);
};
return (
<>
@@ -59,32 +66,41 @@ export default function Portofolio() {
options={{
title: "Portofolio",
headerLeft: () => <LeftButtonCustom />,
headerRight: () => (
<TouchableOpacity onPress={openDrawer}>
<Ionicons
name="ellipsis-vertical"
size={20}
color={MainColor.yellow}
/>
</TouchableOpacity>
),
headerRight: () =>
data?.Profile?.id !== profileId ? null : (
<TouchableOpacity onPress={openDrawer}>
<Ionicons
name="ellipsis-vertical"
size={20}
color={MainColor.yellow}
/>
</TouchableOpacity>
),
headerStyle: GStyles.headerStyle,
headerTitleStyle: GStyles.headerTitleStyle,
}}
/>
<ViewWrapper>
{/* <PorfofolioSection setShowDeleteAlert={setDeleteAlert} data={data} /> */}
<StackCustom>
<Portofolio_Data data={data} />
<Portofolio_BusinessLocation />
<Portofolio_SocialMediaSection data={data?.Portofolio_MediaSosial} />
<Portofolio_ButtonDelete
id={id as string}
isLoadingDelete={isLoadingDelete}
setIsLoadingDelete={setIsLoadingDelete}
/>
<Spacing />
</StackCustom>
{!data || !profileId ? (
<LoaderCustom />
) : (
<StackCustom>
<Portofolio_Data
data={data}
listSubBidang={data?.Portofolio_BidangDanSubBidangBisnis as any[]}
/>
<Portofolio_BusinessLocation />
<Portofolio_SocialMediaSection
data={data?.Portofolio_MediaSosial}
/>
<Portofolio_ButtonDelete
id={id as string}
isLoadingDelete={isLoadingDelete}
setIsLoadingDelete={setIsLoadingDelete}
/>
<Spacing />
</StackCustom>
)}
</ViewWrapper>
{/* Drawer Komponen Eksternal */}

View File

@@ -1,3 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { LoaderCustom } from "@/components";
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
import LeftButtonCustom from "@/components/Button/BackButton";
import DrawerCustom from "@/components/Drawer/DrawerCustom";
@@ -9,6 +11,7 @@ import Profile_PortofolioSection from "@/screens/Profile/PortofolioSection";
import ProfileSection from "@/screens/Profile/ProfileSection";
import { apiGetPortofolio } from "@/service/api-client/api-portofolio";
import { apiProfile } from "@/service/api-client/api-profile";
import { apiUser } from "@/service/api-client/api-user";
import { GStyles } from "@/styles/global-styles";
import { IProfile } from "@/types/Type-Profile";
import { Ionicons } from "@expo/vector-icons";
@@ -20,9 +23,10 @@ export default function Profile() {
const { id } = useLocalSearchParams();
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const [data, setData] = useState<IProfile>();
const [dataPortofolio, setDataPortofolio] = useState<any[]>();
const [dataToken, setDataToken] = useState<IProfile>();
const [listPortofolio, setListPortofolio] = useState<any[]>();
const { logout, isAdmin } = useAuth();
const { logout, isAdmin, user } = useAuth();
const openDrawer = () => {
setIsDrawerOpen(true);
@@ -36,14 +40,28 @@ export default function Profile() {
useCallback(() => {
onLoadData(id as string);
onLoadPortofolio(id as string);
onLoadUserByToken();
isUserCheck();
}, [id])
);
const isUserCheck = () => {
const userId = id;
const userLoginId = dataToken?.id;
return userId === userLoginId;
};
const onLoadData = async (id: string) => {
const response = await apiProfile({ id: id });
setData(response.data);
};
const onLoadUserByToken = async () => {
const response = await apiUser(user?.id as string);
setDataToken(response?.data?.Profile);
};
const onLoadPortofolio = async (id: string) => {
const response = await apiGetPortofolio({ id: id });
const lastTwoByDate = response.data
@@ -52,23 +70,22 @@ export default function Profile() {
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
) // urut desc
.slice(0, 2);
setDataPortofolio(lastTwoByDate);
setListPortofolio(lastTwoByDate);
};
return (
<>
<Stack.Screen
options={{
title: "Profile",
title: `Profile`,
headerLeft: () => <LeftButtonCustom />,
headerRight: () => (
<TouchableOpacity onPress={openDrawer}>
<Ionicons
name="ellipsis-vertical"
size={20}
color={MainColor.yellow}
/>
</TouchableOpacity>
<ButtonnDot
id={id as string}
openDrawer={openDrawer}
isUserCheck={isUserCheck()}
logout={logout}
/>
),
headerStyle: GStyles.headerStyle,
headerTitleStyle: GStyles.headerTitleStyle,
@@ -76,12 +93,18 @@ export default function Profile() {
/>
{/* Main View */}
<ViewWrapper>
<ProfileSection data={data as any} />
{!data || !dataToken ? (
<LoaderCustom />
) : (
<>
<ProfileSection data={data as any} />
<Profile_PortofolioSection
data={dataPortofolio as any}
profileId={id as string}
/>
<Profile_PortofolioSection
data={listPortofolio as any}
profileId={id as string}
/>
</>
)}
</ViewWrapper>
{/* Drawer Komponen Eksternal */}
@@ -99,3 +122,44 @@ export default function Profile() {
</>
);
}
const ButtonnDot = ({
id,
openDrawer,
isUserCheck,
logout,
}: {
id: string;
openDrawer: () => void;
isUserCheck: boolean;
logout: () => Promise<void>;
}) => {
const isId = id === undefined || id === null;
console.log("ID CHECK", id);
if (isId) {
console.log("ID UNDEFINED", id);
return (
<>
<TouchableOpacity onPress={logout}>
<Ionicons name="log-out" size={20} color={MainColor.red} />
</TouchableOpacity>
</>
);
}
return (
<>
{isUserCheck && (
<TouchableOpacity onPress={openDrawer}>
<Ionicons
name="ellipsis-vertical"
size={20}
color={MainColor.yellow}
/>
</TouchableOpacity>
)}
</>
);
};

View File

@@ -1,7 +1,8 @@
import {
AvatarCustom,
AvatarComp,
ClickableCustom,
Grid,
LoaderCustom,
Spacing,
StackCustom,
TextCustom,
@@ -10,39 +11,46 @@ import {
} from "@/components";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import { apiAllUser } from "@/service/api-client/api-user";
import { Ionicons } from "@expo/vector-icons";
import { router } from "expo-router";
import _ from "lodash";
import { useEffect, useState } from "react";
export default function UserSearch() {
function generateRandomPhoneNumber(index: number) {
let prefix;
const [data, setData] = useState<any[]>([]);
const [search, setSearch] = useState<string>("");
const [isLoadList, setIsLoadList] = useState(false);
// Menentukan prefix berdasarkan index genap atau ganjil
if (index % 2 === 0) {
const evenPrefixes = ["6288", "6289", "6281"];
prefix = evenPrefixes[Math.floor(Math.random() * evenPrefixes.length)];
} else {
const oddPrefixes = ["6285", "6283"];
prefix = oddPrefixes[Math.floor(Math.random() * oddPrefixes.length)];
useEffect(() => {
onLoadData(search);
}, [search]);
const onLoadData = async (search: string) => {
try {
setIsLoadList(true);
const response = await apiAllUser({ search: search });
console.log("[DATA USER] >", JSON.stringify(response.data, null, 2));
setData(response.data);
} catch (error) {
console.log("Error fetching data", error);
} finally {
setIsLoadList(false);
}
};
// Menghitung panjang sisa nomor acak (antara 10 - 12 digit)
const remainingLength = Math.floor(Math.random() * 3) + 10; // 10, 11, atau 12
const handleSearch = (search: string) => {
setSearch(search);
onLoadData(search);
};
// Membuat sisa nomor acak
let randomNumber = "";
for (let i = 0; i < remainingLength; i++) {
randomNumber += Math.floor(Math.random() * 10); // Digit acak antara 0-9
}
// Menggabungkan prefix dan sisa nomor
return prefix + randomNumber;
}
return (
<>
<ViewWrapper
headerComponent={
<TextInputCustom
value={search}
onChangeText={handleSearch}
iconLeft={
<Ionicons
name="search"
@@ -57,41 +65,48 @@ export default function UserSearch() {
}
>
<StackCustom>
{Array.from({ length: 20 }).map((e, index) => {
return (
<Grid key={index}>
<Grid.Col span={2}>
<AvatarCustom href={`/profile/${index}`}/>
</Grid.Col>
<Grid.Col span={9}>
<TextCustom size="large">Nama user {index}</TextCustom>
<TextCustom size="small">
+{generateRandomPhoneNumber(index)}
</TextCustom>
</Grid.Col>
<Grid.Col
span={1}
style={{
justifyContent: "center",
alignItems: "flex-end",
{isLoadList ? (
<LoaderCustom />
) : !_.isEmpty(data) ? (
data?.map((e, index) => {
return (
<ClickableCustom
key={index}
onPress={() => {
console.log("Ke Profile");
router.push(`/profile/${e?.Profile?.id}`);
}}
>
<ClickableCustom
onPress={() => {
console.log("Ke Profile");
router.push(`/profile/${index}`);
}}
>
<Ionicons
name="chevron-forward"
size={ICON_SIZE_SMALL}
color={MainColor.white}
/>
</ClickableCustom>
</Grid.Col>
</Grid>
);
})}
<Grid>
<Grid.Col span={2}>
<AvatarComp fileId={e?.Profile?.imageId} size="base" />
</Grid.Col>
<Grid.Col span={9}>
<StackCustom gap={"sm"}>
<TextCustom size="large">{e?.username}</TextCustom>
<TextCustom size="small">+{e?.nomor}</TextCustom>
</StackCustom>
</Grid.Col>
<Grid.Col
span={1}
style={{
justifyContent: "center",
alignItems: "flex-end",
}}
>
<Ionicons
name="chevron-forward"
size={ICON_SIZE_SMALL}
color={MainColor.white}
/>
</Grid.Col>
</Grid>
</ClickableCustom>
);
})
) : (
<TextCustom align="center">Tidak ditemukan</TextCustom>
)}
</StackCustom>
<Spacing height={50} />
</ViewWrapper>

View File

@@ -1,20 +1,55 @@
/* 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 { apiVotingGetByStatus } from "@/service/api-client/api-voting";
import { dateTimeView } from "@/utils/dateTimeView";
import dayjs from "dayjs";
import { useState } from "react";
import { useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
export default function VotingStatus() {
const { user } = useAuth();
const id = user?.id || "";
console.log("ID >> ", 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!,
});
console.log("[RES LIST STATUS]", JSON.stringify(response.data, null, 2));
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 +69,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,3 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
AlertDefaultSystem,
BackButton,
@@ -11,14 +12,42 @@ import { IconArchive, IconContribution, IconEdit } from "@/components/_Icon";
import { IMenuDrawerItem } from "@/components/_Interface/types";
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 } from "@/service/api-client/api-voting";
import {
router,
Stack,
useFocusEffect,
useLocalSearchParams,
} from "expo-router";
import { useCallback, useState } from "react";
import { useAuth } from "@/hooks/use-auth";
export default function VotingDetailStatus() {
const {user} = useAuth()
const { id, status } = useLocalSearchParams();
console.log("ID >> ", id);
console.log("STATUS >> ", status);
const [openDrawerDraft, setOpenDrawerDraft] = useState(false);
const [openDrawerPublish, setOpenDrawerPublish] = useState(false);
const [data, setData] = useState<any>(null);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id])
);
const onLoadData = async () => {
try {
const response = await apiVotingGetOne({ id: id as string });
console.log("Response", JSON.stringify(response.data, null, 2));
setData(response.data);
} catch (error) {
console.log(error);
}
};
const handlePressDraft = (item: IMenuDrawerItem) => {
console.log("PATH >> ", item.path);
router.navigate(item.path as any);
@@ -57,8 +86,8 @@ export default function VotingDetailStatus() {
}}
/>
<ViewWrapper>
<Voting_BoxDetailSection />
<Voting_ButtonStatusSection status={status as string} />
<Voting_BoxDetailSection data={data as any}/>
<Voting_ButtonStatusSection id={id as string} status={status as string} />
<Spacing />
</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,11 +121,28 @@ 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>
<Grid.Col span={10}>
<TextInputCustom
label="Pilihan"
@@ -66,11 +158,64 @@ export default function VotingCreate() {
<Ionicons name="trash" size={24} color={MainColor.red} />
</TouchableOpacity>
</Grid.Col>
</Grid>
<ButtonCenteredOnly onPress={() => console.log("add")}>
</Grid> */}
{/* <ButtonCenteredOnly onPress={() => console.log("add")}>
Tambah Pilihan
</ButtonCenteredOnly>
</ButtonCenteredOnly> */}
{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"
/>
<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 />
<Spacing />
</StackCustom>
</ViewWrapper>

585
bun.lock

File diff suppressed because it is too large Load Diff

View File

@@ -9,11 +9,13 @@ export default function ActionIcon({
onPress,
icon,
size = "md",
disabled = false,
}: {
href?: Href;
onPress?: () => void;
icon: React.ReactNode;
size?: SizeType;
disabled?: boolean;
}) {
const sizeMap = {
xs: 22,
@@ -25,7 +27,7 @@ export default function ActionIcon({
const getSize = (size: SizeType): DimensionValue => {
if (!size) return sizeMap.md; // Default to 'md' if size is undefined
if (typeof size === 'string' && size in sizeMap) {
if (typeof size === "string" && size in sizeMap) {
return sizeMap[size as keyof typeof sizeMap];
}
return size as DimensionValue;
@@ -35,9 +37,10 @@ export default function ActionIcon({
return (
<TouchableOpacity
disabled={disabled}
activeOpacity={0.7}
style={{
backgroundColor: MainColor.yellow,
backgroundColor: disabled ? MainColor.disabled : MainColor.yellow,
padding: 5,
borderRadius: 50,
alignItems: "center",
@@ -46,6 +49,7 @@ export default function ActionIcon({
height: iconSize,
}}
onPress={() => {
if (disabled) return;
if (href) {
router.push(href);
} else {

View File

@@ -40,7 +40,7 @@ export default function BaseBox({
return (
<>
{onPress || href ? (
{onPress as any || href ? (
<TouchableOpacity
activeOpacity={0.7}
onPress={href ? () => router.navigate(href) : onPress}

View File

@@ -1,4 +1,4 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
// DateTimeInput.tsx
import { MainColor } from "@/constants/color-palet";
import { GStyles } from "@/styles/global-styles";
@@ -119,7 +119,7 @@ const DateTimeInput_Android: React.FC<DateTimeInputProps> = ({
<Ionicons
name="calendar-outline"
size={20}
color={MainColor.placeholder}
color={disabled ? MainColor.white_gray : MainColor.placeholder}
/>
</View>
@@ -131,8 +131,8 @@ const DateTimeInput_Android: React.FC<DateTimeInputProps> = ({
}}
>
<Grid.Col span={6} style={{}}>
<Pressable onPress={toggleDatePicker}>
<TextCustom color="gray">
<Pressable onPress={() => !disabled && toggleDatePicker()}>
<TextCustom color={disabled ? "default" : "gray"}>
{selectedDate ? (
<TextCustom color="black">
{selectedDate.toLocaleDateString()}
@@ -148,8 +148,8 @@ const DateTimeInput_Android: React.FC<DateTimeInputProps> = ({
</Grid.Col>
<Grid.Col span={5} style={{}}>
<Pressable onPress={toggleTimePicker}>
<TextCustom color="gray">
<Pressable onPress={() => !disabled && toggleTimePicker()}>
<TextCustom color={disabled ? "default" : "gray"}>
{selectedTime ? (
<TextCustom color="black">
{selectedTime.toLocaleTimeString("id-ID", {

View File

@@ -3,24 +3,19 @@ import { MainColor } from "@/constants/color-palet";
import { GStyles } from "@/styles/global-styles";
import { Ionicons } from "@expo/vector-icons";
import DateTimePicker, {
DateTimePickerEvent,
DateTimePickerEvent,
} from "@react-native-community/datetimepicker";
import dayjs from "dayjs";
import React, { useState } from "react";
import {
StyleProp,
Text,
View,
ViewStyle
} from "react-native";
import { StyleProp, Text, View, ViewStyle } from "react-native";
import ClickableCustom from "../Clickable/ClickableCustom";
import TextCustom from "../Text/TextCustom";
interface DateTimeInputProps {
// Main
value?: DateTimePickerEvent;
value?: DateTimePickerEvent | Date | null;
mode?: "date" | "time";
onChange: (selectedDate: DateTimePickerEvent) => void;
onChange: (selectedDate: DateTimePickerEvent | Date | null) => void;
maximumDate?: Date;
minimumDate?: Date;
// Main
@@ -74,7 +69,7 @@ const DateTimeInput_IOS: React.FC<DateTimeInputProps> = ({
<ClickableCustom
activeOpacity={0.8}
style={[GStyles.inputContainerArea, containerStyle]}
onPress={handlePress}
onPress={() => !disabled && handlePress()}
>
{label && (
<Text style={GStyles.inputLabel}>
@@ -95,11 +90,11 @@ const DateTimeInput_IOS: React.FC<DateTimeInputProps> = ({
<Ionicons
name="calendar-outline"
size={20}
color={MainColor.placeholder}
color={disabled ? MainColor.white : MainColor.placeholder}
/>
</View>
<TextCustom color="gray">
<TextCustom color={disabled ? "default" : "gray"}>
{selectedDate ? (
<TextCustom color="black">
{dayjs(selectedDate).format("DD-MM-YYYY HH:mm")}

View File

@@ -1,19 +1,17 @@
import {
DateTimePickerEvent,
} from "@react-native-community/datetimepicker";
import { DateTimePickerEvent } from "@react-native-community/datetimepicker";
import React from "react";
import { Platform } from "react-native";
import DateTimeInput_Android from "./DataTimeAndroid";
import DateTimeInput_IOS from "./DateTimeIOS";
type Props = {
value?: Date;
value?: Date | DateTimePickerEvent | null;
onChange?: (date: Date) => void;
label?: string;
required?: boolean;
maximumDate?: Date;
minimumDate?: Date;
disabled?: boolean;
};
const DateTimePickerCustom: React.FC<Props> = ({
@@ -23,18 +21,21 @@ const DateTimePickerCustom: React.FC<Props> = ({
required,
maximumDate,
minimumDate,
disabled = false,
}) => {
return (
<>
{Platform.OS === "ios" ? (
<DateTimeInput_IOS
label={label}
onChange={(date: DateTimePickerEvent) => {
onChange={(date: DateTimePickerEvent | Date | null) => {
onChange?.(date as any);
}}
required={required}
maximumDate={maximumDate}
minimumDate={minimumDate}
disabled={disabled}
value={value as DateTimePickerEvent | Date | null}
/>
) : (
<DateTimeInput_Android
@@ -45,6 +46,7 @@ const DateTimePickerCustom: React.FC<Props> = ({
required={required}
maximumDate={maximumDate}
minimumDate={minimumDate}
disabled={disabled}
/>
)}
</>

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { AccentColor, MainColor } from "@/constants/color-palet";
import { TEXT_SIZE_SMALL } from "@/constants/constans-value";
import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
@@ -5,19 +6,23 @@ import { IMenuDrawerItem } from "../_Interface/types";
import { Href } from "expo-router";
type IMenuDrawerItemProps = {
icon: React.ReactNode;
label: string;
value?: string;
path?: Href | string;
color?: string;
}
icon: React.ReactNode;
label: string;
value?: string;
path?: Href | string;
color?: string;
};
interface MenuDrawerDynamicGridProps {
data: IMenuDrawerItemProps[];
columns?: number;
onPressItem?: (item: IMenuDrawerItemProps) => void;
data: IMenuDrawerItemProps[];
columns?: number;
onPressItem?: (item: IMenuDrawerItemProps) => void;
}
const MenuDrawerDynamicGrid = ({ data, columns = 4, onPressItem }: MenuDrawerDynamicGridProps) => {
const MenuDrawerDynamicGrid = ({
data,
columns = 4,
onPressItem,
}: MenuDrawerDynamicGridProps) => {
const numColumns = columns;
return (

View File

@@ -2,7 +2,11 @@ import DUMMY_IMAGE from "@/constants/dummy-image-value";
import { Image } from "react-native";
import BaseBox from "../Box/BaseBox";
export default function LandscapeFrameUploaded() {
export default function LandscapeFrameUploaded({
image,
}: {
image?: string;
}) {
return (
<BaseBox
style={{
@@ -11,7 +15,7 @@ export default function LandscapeFrameUploaded() {
}}
>
<Image
source={DUMMY_IMAGE.background}
source={image ? { uri: image } : DUMMY_IMAGE.dummy_image}
resizeMode="cover"
style={{ width: "100%", height: "100%", borderRadius: 10 }}
/>

View File

@@ -0,0 +1,8 @@
import { MainColor } from "@/constants/color-palet";
import { ActivityIndicator } from "react-native";
export default function LoaderCustom({ size }: { size?: "small" | "large" }) {
return (
<ActivityIndicator size={size ? size : "small"} color={MainColor.yellow} />
);
}

View File

@@ -1,7 +1,6 @@
import { ImageSourcePropType } from "react-native";
import Divider from "../Divider/Divider";
import Grid from "../Grid/GridCustom";
import AvatarCustom from "../Image/AvatarCustom";
import AvatarComp from "../Image/AvatarComp";
import TextCustom from "../Text/TextCustom";
const AvatarUsernameAndOtherComponent = ({
@@ -12,7 +11,7 @@ const AvatarUsernameAndOtherComponent = ({
withBottomLine = false,
}: {
avatarHref?: string;
avatar?: ImageSourcePropType;
avatar?: string;
name?: string;
rightComponent?: React.ReactNode;
withBottomLine?: boolean;
@@ -21,7 +20,7 @@ const AvatarUsernameAndOtherComponent = ({
<>
<Grid containerStyle={{ zIndex: 10 }}>
<Grid.Col span={2}>
<AvatarCustom source={avatar} href={avatarHref as any} />
<AvatarComp fileId={avatar} href={avatarHref as any} size="base" />
</Grid.Col>
<Grid.Col
span={rightComponent ? 6 : 10}

View File

@@ -4,17 +4,33 @@ import { Image } from "expo-image";
import { StyleSheet } from "react-native";
import ClickableCustom from "../Clickable/ClickableCustom";
import { router } from "expo-router";
import API_STRORAGE from "@/constants/base-url-api-strorage";
export default function DummyLandscapeImage({height, unClickPath}: {height?: number, unClickPath?: boolean}) {
export default function DummyLandscapeImage({
height,
unClickPath,
imageId,
}: {
height?: number;
unClickPath?: boolean;
imageId?: string;
}) {
return (
<ClickableCustom
onPress={() => {
if (!unClickPath) {
router.push("/(application)/(image)/preview-image/1");
router.push(`/(application)/(image)/preview-image/${imageId}`);
}
}}
>
<Image source={DUMMY_IMAGE.background} style={[styles.backgroundImage, {height: height || 200}]} />
<Image
source={
imageId
? { uri: API_STRORAGE.GET({ fileId: imageId }) }
: DUMMY_IMAGE.dummy_image
}
style={[styles.backgroundImage, { height: height || 200 }]}
/>
</ClickableCustom>
);
}

View File

@@ -11,6 +11,7 @@ interface SearchInputProps {
iconRight?: React.ReactNode;
containerStyle?: StyleProp<ViewStyle>;
style?: StyleProp<TextStyle>;
onChangeText?: (value: string) => void;
}
export default function SearchInput({
placeholder,
@@ -19,6 +20,7 @@ export default function SearchInput({
iconRight,
containerStyle,
style,
onChangeText,
...props
}: SearchInputProps) {
return (
@@ -30,6 +32,7 @@ export default function SearchInput({
color={MainColor.placeholder}
/>
}
onChangeText={onChangeText}
placeholder={placeholder}
borderRadius={50}
containerStyle={[containerStyle, { marginBottom: 0 }]}

View File

@@ -61,6 +61,8 @@ import DummyLandscapeImage from "./_ShareComponent/DummyLandscapeImage";
import GridComponentView from "./_ShareComponent/GridSectionView";
// Progress
import ProgressCustom from "./Progress/ProgressCustom";
// Loader
import LoaderCustom from "./Loader/LoaderCustom";
export {
// ActionIcon
@@ -128,4 +130,6 @@ export {
TextInputCustom,
// ViewWrapper
ViewWrapper,
// Loader
LoaderCustom,
};

View File

@@ -92,7 +92,7 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
await AsyncStorage.setItem("authToken", token);
const responseUser = await apiConfig.get(
`/mobile/user?token=${token}`,
`/mobile?token=${token}`,
{
headers: {
Authorization: `Bearer ${token}`,

18107
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,65 +11,69 @@
"lint": "expo lint"
},
"dependencies": {
"@expo/vector-icons": "^14.1.0",
"@react-native-async-storage/async-storage": "2.1.2",
"@react-native-community/datetimepicker": "8.4.1",
"@react-navigation/bottom-tabs": "^7.4.2",
"@react-navigation/drawer": "^7.5.2",
"@expo/vector-icons": "^15.0.2",
"@react-native-async-storage/async-storage": "2.2.0",
"@react-native-community/datetimepicker": "8.4.4",
"@react-navigation/bottom-tabs": "^7.3.10",
"@react-navigation/drawer": "^7.3.9",
"@react-navigation/elements": "^2.3.8",
"@react-navigation/native": "^7.1.6",
"@react-navigation/native-stack": "^7.3.21",
"@react-navigation/native-stack": "^7.3.10",
"@types/lodash": "^4.17.20",
"@types/react-native-vector-icons": "^6.4.18",
"axios": "^1.11.0",
"dayjs": "^1.11.13",
"expo": "53.0.17",
"expo-blur": "~14.1.5",
"expo-camera": "~16.1.10",
"expo-clipboard": "~7.1.5",
"expo-constants": "~17.1.7",
"expo-dev-client": "~5.2.4",
"expo-document-picker": "~13.1.6",
"expo-file-system": "~18.1.11",
"expo-font": "~13.3.2",
"expo-haptics": "~14.1.4",
"expo-image": "~2.3.2",
"expo-image-picker": "~16.1.4",
"expo-linking": "~7.1.7",
"expo-router": "~5.1.3",
"expo-splash-screen": "~0.30.10",
"expo-status-bar": "~2.2.3",
"expo-symbols": "~0.4.5",
"expo-system-ui": "~5.0.10",
"expo-web-browser": "~14.2.0",
"expo": "^54.0.0",
"expo-blur": "~15.0.7",
"expo-camera": "~17.0.7",
"expo-clipboard": "~8.0.7",
"expo-constants": "~18.0.8",
"expo-dev-client": "~6.0.12",
"expo-document-picker": "~14.0.7",
"expo-file-system": "~19.0.12",
"expo-font": "~14.0.8",
"expo-haptics": "~15.0.7",
"expo-image": "~3.0.8",
"expo-image-picker": "~17.0.8",
"expo-linking": "~8.0.8",
"expo-router": "~6.0.1",
"expo-splash-screen": "~31.0.9",
"expo-status-bar": "~3.0.8",
"expo-symbols": "~1.0.7",
"expo-system-ui": "~6.0.7",
"expo-web-browser": "~15.0.7",
"lodash": "^4.17.21",
"react": "19.0.0",
"react-dom": "19.0.0",
"react-native": "0.79.5",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-native": "0.81.4",
"react-native-dotenv": "^3.4.11",
"react-native-gesture-handler": "~2.24.0",
"react-native-gesture-handler": "~2.28.0",
"react-native-international-phone-number": "^0.9.3",
"react-native-maps": "1.20.1",
"react-native-otp-entry": "^1.8.5",
"react-native-pager-view": "6.7.1",
"react-native-pager-view": "6.9.1",
"react-native-paper": "^5.14.5",
"react-native-qrcode-svg": "^6.3.15",
"react-native-reanimated": "~3.17.4",
"react-native-safe-area-context": "5.4.0",
"react-native-screens": "~4.11.1",
"react-native-svg": "15.11.2",
"react-native-reanimated": "~4.1.0",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0",
"react-native-svg": "15.12.1",
"react-native-toast-message": "^2.3.3",
"react-native-vector-icons": "^10.2.0",
"react-native-web": "~0.20.0",
"react-native-webview": "13.13.5"
"react-native-web": "^0.21.0",
"react-native-webview": "13.15.0",
"react-native-worklets": "^0.5.1"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@types/react": "~19.0.10",
"@types/react": "~19.1.10",
"eslint": "^9.25.0",
"eslint-config-expo": "~9.2.0",
"eslint-config-expo": "~10.0.0",
"expo-module-scripts": "^4.1.10",
"typescript": "~5.8.3"
"typescript": "~5.9.2"
},
"private": true
"private": true,
"engines": {
"bun": ">=1.0.0"
}
}

View File

@@ -14,7 +14,6 @@ import Toast from "react-native-toast-message";
export default function RegisterView() {
const { nomor } = useLocalSearchParams();
const [username, setUsername] = useState("");
// const [loading, setLoading] = useState(false);
const { registerUser, isLoading } = useAuth();
@@ -22,16 +21,22 @@ export default function RegisterView() {
if (!nomor) {
Toast.show({
type: "error",
text1: "Gagal",
text2: "Nomor tidak ditemukan",
text1: "Lengkapi nomor",
});
return false;
}
if (!username) {
Toast.show({
type: "error",
text1: "Gagal",
text2: "Username tidak boleh kosong",
text1: "Username tidak boleh kosong",
});
return false;
}
if (username.includes(" ")) {
Toast.show({
type: "info",
text1: "Username tidak boleh mengandung spasi",
});
return false;
}
@@ -42,9 +47,20 @@ export default function RegisterView() {
const isValid = validasiData();
if (!isValid) return;
if (username.length < 5) {
Toast.show({
type: "info",
text1: "Info",
text2: "Username minimal 5 karakter",
});
return;
}
const usernameLower = username.toLowerCase();
await registerUser({
nomor: nomor as string,
username: username,
username: usernameLower,
});
}
@@ -73,6 +89,11 @@ export default function RegisterView() {
placeholder="Masukkan username"
value={username}
onChangeText={(text) => setUsername(text)}
error={
username.includes(" ")
? "Username tidak boleh mengandung spasi"
: ""
}
/>
<ButtonCustom isLoading={isLoading} onPress={handleRegister}>

View File

@@ -1,21 +1,49 @@
import {
BaseBox,
Grid,
StackCustom,
TextCustom
} from "@/components";
import { AvatarUsernameAndOtherComponent, BoxWithHeaderSection, Grid, StackCustom, TextCustom } from "@/components";
import { dateTimeView } from "@/utils/dateTimeView";
export default function Event_BoxDetailPublishSection({
data,
footerButton,
}: {
data: any;
footerButton?: React.ReactNode;
}) {
const listData = [
{
title: "Lokasi",
value: data?.lokasi || "-",
},
{
title: "Tipe Acara",
value: data?.EventMaster_TipeAcara?.name || "-",
},
{
title: "Tanggal Mulai",
value: dateTimeView({ date: data?.tanggal }) || "-",
},
{
title: "Tanggal Berakhir",
value: dateTimeView({ date: data?.tanggalSelesai }) || "-",
},
{
title: "Deskripsi",
value: data?.deskripsi || "-",
},
];
return (
<>
<BaseBox>
<BoxWithHeaderSection>
<StackCustom>
<AvatarUsernameAndOtherComponent
avatarHref={`/profile/${data?.Author?.Profile?.id}`}
name={data?.Author?.username || "-"}
avatar={data?.Author?.Profile?.imageId || ""}
/>
<TextCustom bold align="center" size="xlarge">
Judul event publish
{data?.title || "-"}
</TextCustom>
{listData.map((item, index) => (
<Grid key={index}>
@@ -28,34 +56,9 @@ export default function Event_BoxDetailPublishSection({
</Grid>
))}
</StackCustom>
</BaseBox>
</BoxWithHeaderSection>
{footerButton}
</>
);
}
const listData = [
{
title: "Lokasi",
value:
"Lorem ipsum dolor sit amet consectetur adipisicing elit. Consectetur eveniet ab eum ducimus tempore a quia deserunt quisquam. Tempora, atque. Aperiam minima asperiores dicta perferendis quis adipisci, dolore optio porro!",
},
{
title: "Tipe Acara",
value: "Workshop",
},
{
title: "Tanggal Mulai",
value: "Senin, 18 Juli 2025, 10:00 WIB",
},
{
title: "Tanggal Berakhir",
value: "Selasa, 19 Juli 2025, 12:00 WIB",
},
{
title: "Deskripsi",
value:
"Lorem ipsum dolor sit amet consectetur adipisicing elit. Consectetur eveniet ab eum ducimus tempore a quia deserunt quisquam. Tempora, atque. Aperiam minima asperiores dicta perferendis quis adipisci, dolore optio porro!",
},
];

View File

@@ -7,21 +7,15 @@ import {
import { Href } from "expo-router";
export default function Event_BoxPublishSection({
id,
title,
username,
description,
href,
data,
// Avatar
sourceAvatar,
rightComponentAvatar,
}: {
id: string;
title?: string;
username?: string;
description?: string;
href: Href;
data: any;
// Avatar
sourceAvatar?: string;
@@ -32,18 +26,15 @@ export default function Event_BoxPublishSection({
<BoxWithHeaderSection href={href}>
<StackCustom gap={"xs"}>
<AvatarUsernameAndOtherComponent
avatarHref={`/profile/${id}`}
name={username || "Lorem ipsum dolor sit"}
avatarHref={`/profile/${data?.Author?.Profile?.id}`}
name={data?.Author?.username || "-"}
rightComponent={rightComponentAvatar}
avatar={sourceAvatar as any}
avatar={data?.Author?.Profile?.imageId || ""}
/>
<TextCustom truncate bold>
{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!"}
{data?.title || "-"}
</TextCustom>
<TextCustom truncate={2}>{data?.deskripsi || "-"}</TextCustom>
</StackCustom>
</BoxWithHeaderSection>
</>

View File

@@ -1,28 +1,158 @@
import { ButtonCustom, Grid } from "@/components";
import { View } from "react-native";
import { AlertDefaultSystem, ButtonCustom, Grid } from "@/components";
import {
apiEventDelete,
apiEventUpdateStatus,
} from "@/service/api-client/api-event";
import { router } from "expo-router";
import Toast from "react-native-toast-message";
export default function Event_ButtonStatusSection({
id,
status,
onOpenAlert,
onOpenDeleteAlert,
}: {
id: string;
status: string;
onOpenAlert: (value: boolean) => void;
onOpenDeleteAlert: (value: boolean) => void;
}) {
const handleBatalkanReview = () => {
AlertDefaultSystem({
title: "Batalkan Review",
message: "Apakah Anda yakin ingin batalkan review ini?",
textLeft: "Batal",
textRight: "Ya",
onPressRight: async () => {
try {
const response = await apiEventUpdateStatus({
id: id,
status: "draft",
});
const handleOpenAlert = () => {
onOpenAlert(true);
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);
}
},
});
};
const handleAjukanReview = () => {
AlertDefaultSystem({
title: "Ajukan Review",
message: "Apakah Anda yakin ingin ajukan review ini?",
textLeft: "Batal",
textRight: "Ya",
onPressRight: async () => {
try {
const response = await apiEventUpdateStatus({
id: id,
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);
}
},
});
};
const handleEditKembali = () => {
AlertDefaultSystem({
title: "Edit Kembali",
message: "Apakah Anda yakin ingin edit kembali ini?",
textLeft: "Batal",
textRight: "Ya",
onPressRight: async () => {
try {
const response = await apiEventUpdateStatus({
id: id,
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);
}
},
});
};
const handleOpenDeleteAlert = () => {
onOpenDeleteAlert(true);
AlertDefaultSystem({
title: "Hapus",
message: "Apakah Anda yakin ingin menghapus data ini?",
textLeft: "Batal",
textRight: "Hapus",
onPressRight: async () => {
try {
const response = await apiEventDelete({ id: id });
if (response.success) {
Toast.show({
type: "success",
text1: "Data dihapus",
});
router.back();
} else {
Toast.show({
type: "info",
text1: "Info",
text2: response.message,
});
router.back();
}
} catch (error) {
console.log("[ERROR]", error);
}
},
});
};
const DeleteButton = () => {
return (
<>
<ButtonCustom backgroundColor="red" textColor="white" onPress={handleOpenDeleteAlert}>
<ButtonCustom
backgroundColor="red"
textColor="white"
onPress={handleOpenDeleteAlert}
>
Hapus
</ButtonCustom>
</>
@@ -35,7 +165,7 @@ export default function Event_ButtonStatusSection({
case "review":
return (
<ButtonCustom onPress={handleOpenAlert}>
<ButtonCustom onPress={handleBatalkanReview}>
Batalkan Review
</ButtonCustom>
);
@@ -44,13 +174,14 @@ export default function Event_ButtonStatusSection({
return (
<>
<Grid>
<Grid.Col span={5}>
<ButtonCustom onPress={handleOpenAlert}>Ajukan Review</ButtonCustom>
<Grid.Col span={6} style={{ paddingRight: 10 }}>
<ButtonCustom onPress={handleAjukanReview}>
Ajukan Review
</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>
</>
);
@@ -59,13 +190,14 @@ export default function Event_ButtonStatusSection({
return (
<>
<Grid>
<Grid.Col span={5}>
<ButtonCustom onPress={handleOpenAlert}>Edit Kembali</ButtonCustom>
<Grid.Col span={6} style={{ paddingRight: 10 }}>
<ButtonCustom 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,28 +1,34 @@
import { BaseBox, StackCustom, DummyLandscapeImage, TextCustom } from "@/components";
import {
BaseBox,
StackCustom,
DummyLandscapeImage,
TextCustom,
} from "@/components";
export default function Job_BoxDetailSection({data}: {data: any}) {
return (
<>
<BaseBox>
<StackCustom gap={"lg"}>
<DummyLandscapeImage />
export default function Job_BoxDetailSection({ data }: { data: any }) {
return (
<>
<BaseBox>
<StackCustom gap={"lg"}>
{data && data.imageId && (
<DummyLandscapeImage imageId={data?.imageId} />
)}
<TextCustom align="center" bold size="large">
{data?.posisi}
</TextCustom>
<TextCustom align="center" bold size="large">
{data?.title || "-"}
</TextCustom>
<StackCustom gap={"sm"}>
<TextCustom bold>Syarat & Ketentuan :</TextCustom>
<TextCustom>{data?.syaratKetentuan}</TextCustom>
</StackCustom>
<StackCustom gap={"sm"}>
<TextCustom bold>Deskripsi :</TextCustom>
<TextCustom>{data?.deskripsi}</TextCustom>
</StackCustom>
<StackCustom gap={"sm"}>
<TextCustom bold>Syarat & Ketentuan :</TextCustom>
<TextCustom>{data?.content || "-"}</TextCustom>
</StackCustom>
</BaseBox>
</>
);
<StackCustom gap={"sm"}>
<TextCustom bold>Deskripsi :</TextCustom>
<TextCustom>{data?.deskripsi || "-"}</TextCustom>
</StackCustom>
</StackCustom>
</BaseBox>
</>
);
}

View File

@@ -1,11 +1,24 @@
import { AlertDefaultSystem, ButtonCustom, Grid } from "@/components";
import {
apiJobDelete,
apiJobUpdateData,
apiJobUpdateStatus,
} from "@/service/api-client/api-job";
import { router } from "expo-router";
import { View } from "react-native";
import Toast from "react-native-toast-message";
export default function Job_ButtonStatusSection({
id,
status,
isLoading,
onSetLoading,
isArchive,
}: {
id: string;
status: string;
isLoading: boolean;
onSetLoading: (value: boolean) => void;
isArchive?: boolean;
}) {
const handleBatalkanReview = () => {
AlertDefaultSystem({
@@ -13,9 +26,33 @@ export default function Job_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 apiJobUpdateStatus({
id: id,
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 +63,33 @@ export default function Job_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 apiJobUpdateStatus({
id: id,
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 +100,33 @@ export default function Job_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 apiJobUpdateStatus({
id: id,
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 +137,71 @@ export default function Job_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 apiJobDelete({
id: id,
});
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);
}
},
});
};
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);
}
},
});
};
@@ -63,6 +210,7 @@ export default function Job_ButtonStatusSection({
return (
<>
<ButtonCustom
isLoading={isLoading}
backgroundColor="red"
textColor="white"
onPress={handleOpenDeleteAlert}
@@ -78,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
@@ -90,7 +238,7 @@ export default function Job_ButtonStatusSection({
case "review":
return (
<ButtonCustom onPress={handleBatalkanReview}>
<ButtonCustom isLoading={isLoading} onPress={handleBatalkanReview}>
Batalkan Review
</ButtonCustom>
);
@@ -99,15 +247,14 @@ export default function Job_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={{ paddingLeft: 10 }}>
{DeleteButton()}
</Grid.Col>
<Grid.Col span={5}>{DeleteButton()}</Grid.Col>
</Grid>
</>
);
@@ -116,15 +263,15 @@ export default function Job_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={{ paddingRight: 10 }}>
{DeleteButton()}
</Grid.Col>
<Grid.Col span={5}>{DeleteButton()}</Grid.Col>
</Grid>
</>
);

View File

@@ -45,7 +45,9 @@ export default function Portofolio_ButtonCreate({
!data.masterBidangBisnisId ||
!data.alamatKantor ||
!data.tlpn ||
!data.deskripsi
!data.deskripsi ||
subBidangSelected.map((item) => item.id).includes("") ||
subBidangSelected.map((item) => item.id).includes(null)
) {
return false;
}
@@ -53,6 +55,10 @@ export default function Portofolio_ButtonCreate({
};
const handleCreatePortofolio = async () => {
console.log(
"Data sub bidang >>",
JSON.stringify(subBidangSelected, null, 2)
);
if (!validaasiData()) {
Toast.show({
type: "info",
@@ -90,18 +96,18 @@ export default function Portofolio_ButtonCreate({
youtube: dataMedsos.youtube,
subBidang: subBidangSelected,
};
try {
const response = await apiPortofolioCreate({
data: newData,
});
console.log("Response >>", JSON.stringify(response, null, 2));
// return router.replace(`/maps/create`);
// return router.push(`/maps/create`);
return router.back();
} catch (error) {
throw error;
}
const response = await apiPortofolioCreate({
data: newData,
});
console.log("Response >>", JSON.stringify(response, null, 2));
// return router.replace(`/maps/create`);
// return router.push(`/maps/create`);
Toast.show({
type: "success",
text1: "Sukses",
text2: "Data berhasil disimpan",
});
return router.back();
} catch (error) {
Toast.show({
type: "error",

View File

@@ -17,7 +17,13 @@ import { FontAwesome, Ionicons } from "@expo/vector-icons";
import { router, useLocalSearchParams } from "expo-router";
import { View } from "react-native";
export default function Portofolio_Data({ data }: { data: any }) {
export default function Portofolio_Data({
data,
listSubBidang,
}: {
data: any;
listSubBidang: any[];
}) {
const { id } = useLocalSearchParams();
const listData = [
@@ -83,6 +89,8 @@ export default function Portofolio_Data({ data }: { data: any }) {
// },
// ];
// console.log("List Sub Bidang >>", JSON.stringify(listSubBidang, null, 2));
return (
<>
<BaseBox>
@@ -137,20 +145,24 @@ export default function Portofolio_Data({ data }: { data: any }) {
</Grid.Col>
</Grid>
))}
{/* <View style={{ paddingLeft: 10 }}>
{listSubBidang.map((item, index) => (
<View style={{ paddingLeft: 10 }}>
{listSubBidang?.map((item, index) => (
<Grid key={index}>
<Grid.Col span={2} style={{ alignItems: "center" }}>
{item.icon}
<Ionicons
name="chevron-forward-outline"
size={ICON_SIZE_SMALL}
color="white"
/>
</Grid.Col>
<Grid.Col span={10}>
<TextCustom style={{ paddingLeft: 5 }}>
{item.label}
{item?.MasterSubBidangBisnis?.name || "-"}
</TextCustom>
</Grid.Col>
</Grid>
))}
</View> */}
</View>
</View>
<DividerCustom labelPosition="top" color={AccentColor.blue} />

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

@@ -1,10 +1,13 @@
import { AlertDefaultSystem, ButtonCustom, Grid } from "@/components";
import { apiVotingUpdateStatus } from "@/service/api-client/api-voting";
import { router } from "expo-router";
import { View } from "react-native";
export default function Voting_ButtonStatusSection({
id,
status,
}: {
id: string;
status: string;
}) {
const handleBatalkanReview = () => {
@@ -13,9 +16,15 @@ 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() => {
// console.log("Hapus");
// router.back();
const response = await apiVotingUpdateStatus({
id: id as string,
status: "draft",
})
console.log("[RES BATALKAN REVIEW]", JSON.stringify(response, null, 2));
// router.back();
},
});
};

View File

@@ -1,21 +1,17 @@
import { BadgeCustom, TextCustom } from "@/components";
import { BadgeCustom, StackCustom, TextCustom } from "@/components";
import { GStyles } from "@/styles/global-styles";
import { dateTimeView } from "@/utils/dateTimeView";
import dayjs from "dayjs";
import { View } from "react-native";
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 +19,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,140 @@
import { apiConfig } from "../api-config";
export async function apiEventCreate(data: any) {
try {
const response = await apiConfig.post(`/mobile/event`, {
data: data,
});
return response.data;
} catch (error) {
throw error;
}
}
export async function apiEventGetByStatus({
id,
status,
}: {
id: string;
status: string;
}) {
try {
const response = await apiConfig.get(`/mobile/event/${id}/${status}`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiEventGetOne({ id }: { id: string }) {
try {
const response = await apiConfig.get(`/mobile/event/${id}`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiEventUpdateStatus({
id,
status,
}: {
id: string;
status: "draft" | "review" | "publish" | "reject";
}) {
try {
const response = await apiConfig.put(`/mobile/event/${id}/${status}`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiEventUpdateData({
id,
data,
}: {
id: string;
data: any;
}) {
try {
const response = await apiConfig.put(`/mobile/event/${id}`, {
data: data,
});
return response.data;
} catch (error) {
throw error;
}
}
export async function apiEventDelete({ id }: { id: string }) {
try {
const response = await apiConfig.delete(`/mobile/event/${id}`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiEventGetAll({
category,
userId,
}: {
category?: "beranda" | "contribution" | "all-history" | "my-history";
userId?: string;
}) {
try {
const categoryEvent = category ? `?category=${category}` : "";
const userIdCreator = userId ? `&userId=${userId}` : "";
const response = await apiConfig.get(
`/mobile/event${categoryEvent}${userIdCreator}`
);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiEventJoin({
id,
userId,
}: {
id: string;
userId?: string;
}) {
try {
const response = await apiConfig.post(`/mobile/event/${id}/participants`, {
userId: userId,
});
return response.data;
} catch (error) {
throw error;
}
}
export async function apiEventListOfParticipants({ id }: { id?: string }) {
try {
const response = await apiConfig.get(`/mobile/event/${id}/participants`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiEventCheckParticipants({
id,
userId,
}: {
id: string;
userId?: string;
}) {
try {
const selectUserId = userId ? `?userId=${userId}` : "";
const response = await apiConfig.get(
`/mobile/event/${id}/check-participants${selectUserId}`
);
return response.data;
} catch (error) {
throw error;
}
}

View File

@@ -0,0 +1,104 @@
import { apiConfig } from "../api-config";
export async function apiJobCreate(data: any) {
try {
const response = await apiConfig.post(`/mobile/job`, {
data: data,
});
return response.data;
} catch (error) {
throw error;
}
}
export async function apiJobGetByStatus({
authorId,
status,
}: {
authorId: string;
status: string;
}) {
try {
const response = await apiConfig.get(`/mobile/job/${authorId}/${status}`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiJobGetOne({ id }: { id: string }) {
try {
const response = await apiConfig.get(`/mobile/job/${id}`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiJobUpdateStatus({
id,
status,
}: {
id: string;
status: "draft" | "review" | "publish" | "reject";
}) {
try {
const response = await apiConfig.put(`/mobile/job/${id}/${status}`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiJobDelete({ id }: { id: string }) {
try {
const response = await apiConfig.delete(`/mobile/job/${id}`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiJobGetAll({
search,
category,
authorId,
}: {
search?: string;
category: "archive" | "beranda";
authorId?: string;
}) {
try {
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;
}
}
export async function apiJobUpdateData({
id,
data,
category,
}: {
id: string;
data: any;
category: "edit" | "archive";
}) {
try {
const categoryJob = category ? `?category=${category}` : "";
const response = await apiConfig.put(`/mobile/job/${id}${categoryJob}`, {
data: data,
});
return response.data;
} catch (error) {
throw error;
}
}

View File

@@ -9,12 +9,23 @@ export async function apiMasterBidangBisnis() {
}
}
export async function apiMasterSubBidangBisnis({id}: {id: string}) {
export async function apiMasterSubBidangBisnis({ id }: { id?: string }) {
try {
const response = await apiConfig.get(`/master/sub-bidang-bisnis/${id}`);
const selectBidangId = id ? `/${id}` : "";
const response = await apiConfig.get(
`/master/sub-bidang-bisnis${selectBidangId}`
);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiMasterEventType() {
try {
const response = await apiConfig.get(`/mobile/master/event-type`);
return response.data;
} catch (error) {
throw error;
}
}

View File

@@ -1,6 +1,12 @@
import { apiConfig } from "../api-config";
export async function apiUser(id: string) {
const response = await apiConfig.get(`/user/${id}`);
const response = await apiConfig.get(`/mobile/user/${id}`);
return response.data;
}
export async function apiAllUser({ search }: { search: string }) {
const response = await apiConfig.get(`/mobile/user?search=${search}`);
return response.data;
}

View File

@@ -0,0 +1,52 @@
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;
}
}

View File

@@ -1,6 +1,6 @@
import { API_BASE_URL } from "@/service/api-config";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { apiFileUpload } from "./api-client/api-file";
import { apiFileDelete, apiFileUpload } from "./api-client/api-file";
export async function uploadImageService({
dirId,
@@ -49,3 +49,18 @@ export async function uploadImageService({
throw error;
}
}
export async function deleteImageService({ id }: { id: string }) {
const token = await AsyncStorage.getItem("authToken");
try {
const response = await apiFileDelete({
id: id,
token: token as string,
});
return response;
} catch (error) {
throw error;
}
}

View File

@@ -14,4 +14,5 @@ export interface IMasterSubBidangBisnis {
isActive: boolean;
createdAt: string;
updatedAt: string;
masterBidangBisnisId: string;
}

14
utils/dateTimeView.ts Normal file
View File

@@ -0,0 +1,14 @@
import dayjs from "dayjs";
export const dateTimeView = ({
date,
withoutTime = false,
}: {
date: any;
withoutTime?: boolean;
}) => {
const newDate = dayjs(date).format(
`DD-MM-YYYY ${withoutTime ? "" : ", HH:mm"}`
);
return newDate;
};