Compare commits
29 Commits
qc/5-dec-2
...
notificati
| Author | SHA1 | Date | |
|---|---|---|---|
| 14c0f0e499 | |||
| 0262423c50 | |||
| c2682246d6 | |||
| 465e01015e | |||
| 3b15871ad4 | |||
| 9123e73606 | |||
| 6e2046467f | |||
| ca33dd83bb | |||
| ea3fbdc541 | |||
| 33cd47aaed | |||
| 57ac1eb45e | |||
| 145ad73616 | |||
| 7c85e35c61 | |||
| d098b8ca16 | |||
| 73a473cdc7 | |||
| 3f85f330d2 | |||
| 7743a2467c | |||
| 54611ef812 | |||
| 1503707eed | |||
| a01a9bd93f | |||
| 05c1cac10f | |||
| d27c01ed56 | |||
| 34680a4c38 | |||
| 43c8c105cf | |||
| 2c0198b1b7 | |||
| 573b525352 | |||
| 6f9481c7c9 | |||
| cccb44a835 | |||
| 0f5862ce70 |
@@ -100,7 +100,7 @@ packagingOptions {
|
||||
applicationId 'com.bip.hipmimobileapp'
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 3
|
||||
versionCode 4
|
||||
versionName "1.0.1"
|
||||
|
||||
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
|
||||
|
||||
@@ -15,7 +15,11 @@
|
||||
<data android:scheme="https"/>
|
||||
</intent>
|
||||
</queries>
|
||||
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:supportsRtl="true" android:enableOnBackInvokedCallback="false">
|
||||
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:supportsRtl="true" android:enableOnBackInvokedCallback="false" android:fullBackupContent="@xml/secure_store_backup_rules" android:dataExtractionRules="@xml/secure_store_data_extraction_rules">
|
||||
<meta-data android:name="com.google.firebase.messaging.default_notification_color" android:resource="@color/notification_icon_color" tools:replace="android:resource"/>
|
||||
<meta-data android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@drawable/notification_icon"/>
|
||||
<meta-data android:name="expo.modules.notifications.default_notification_color" android:resource="@color/notification_icon_color"/>
|
||||
<meta-data android:name="expo.modules.notifications.default_notification_icon" android:resource="@drawable/notification_icon"/>
|
||||
<meta-data android:name="expo.modules.updates.ENABLED" android:value="false"/>
|
||||
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
|
||||
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/>
|
||||
|
||||
BIN
android/app/src/main/res/drawable-hdpi/notification_icon.png
Normal file
BIN
android/app/src/main/res/drawable-hdpi/notification_icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
BIN
android/app/src/main/res/drawable-mdpi/notification_icon.png
Normal file
BIN
android/app/src/main/res/drawable-mdpi/notification_icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
BIN
android/app/src/main/res/drawable-xhdpi/notification_icon.png
Normal file
BIN
android/app/src/main/res/drawable-xhdpi/notification_icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.4 KiB |
BIN
android/app/src/main/res/drawable-xxhdpi/notification_icon.png
Normal file
BIN
android/app/src/main/res/drawable-xxhdpi/notification_icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.2 KiB |
BIN
android/app/src/main/res/drawable-xxxhdpi/notification_icon.png
Normal file
BIN
android/app/src/main/res/drawable-xxxhdpi/notification_icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.6 KiB |
@@ -3,4 +3,5 @@
|
||||
<color name="iconBackground">#ffffff</color>
|
||||
<color name="colorPrimary">#023c69</color>
|
||||
<color name="colorPrimaryDark">#ffffff</color>
|
||||
<color name="notification_icon_color">#ffffff</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<full-backup-content>
|
||||
<exclude domain="sharedpref" path="SECURESTORE"/>
|
||||
</full-backup-content>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<data-extraction-rules>
|
||||
<cloud-backup>
|
||||
<exclude domain="sharedpref" path="SECURESTORE"/>
|
||||
</cloud-backup>
|
||||
<device-transfer>
|
||||
<exclude domain="sharedpref" path="SECURESTORE"/>
|
||||
</device-transfer>
|
||||
</data-extraction-rules>
|
||||
@@ -6,7 +6,7 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.google.gms:google-services:4.4.1'
|
||||
classpath 'com.google.gms:google-services:4.4.1'
|
||||
classpath('com.android.tools.build:gradle')
|
||||
classpath('com.facebook.react:react-native-gradle-plugin')
|
||||
classpath('org.jetbrains.kotlin:kotlin-gradle-plugin')
|
||||
|
||||
@@ -14,12 +14,14 @@ export default {
|
||||
ios: {
|
||||
supportsTablet: true,
|
||||
bundleIdentifier: "com.anonymous.hipmi-mobile",
|
||||
googleServicesFile: "./ios/HIPMIBadungConnect/GoogleService-Info.plist",
|
||||
infoPlist: {
|
||||
ITSAppUsesNonExemptEncryption: false,
|
||||
"NSLocationWhenInUseUsageDescription": "Aplikasi membutuhkan akses lokasi untuk menampilkan peta.",
|
||||
NSLocationWhenInUseUsageDescription:
|
||||
"Aplikasi membutuhkan akses lokasi untuk menampilkan peta.",
|
||||
},
|
||||
associatedDomains: ["applinks:cld-dkr-staging-hipmi.wibudev.com"],
|
||||
buildNumber: "13",
|
||||
buildNumber: "19",
|
||||
},
|
||||
|
||||
android: {
|
||||
@@ -30,7 +32,7 @@ export default {
|
||||
},
|
||||
edgeToEdgeEnabled: true,
|
||||
package: "com.bip.hipmimobileapp",
|
||||
versionCode: 3,
|
||||
versionCode: 4,
|
||||
// softwareKeyboardLayoutMode: 'resize', // option: untuk mengatur keyboard pada room chst collaboration
|
||||
intentFilters: [
|
||||
{
|
||||
@@ -56,7 +58,6 @@ export default {
|
||||
|
||||
plugins: [
|
||||
"expo-router",
|
||||
"expo-notifications",
|
||||
"expo-web-browser",
|
||||
[
|
||||
"expo-splash-screen",
|
||||
@@ -77,6 +78,15 @@ export default {
|
||||
],
|
||||
"expo-font",
|
||||
"@rnmapbox/maps",
|
||||
"@react-native-firebase/app",
|
||||
[
|
||||
"expo-notifications",
|
||||
{
|
||||
icon: "./assets/images/icon.png",
|
||||
color: "#ffffff",
|
||||
iosDisplayInForeground: true,
|
||||
},
|
||||
],
|
||||
],
|
||||
|
||||
experiments: {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { BackButton } from "@/components";
|
||||
import { IconPlus } from "@/components/_Icon";
|
||||
import { IconDot } from "@/components/_Icon/IconComponent";
|
||||
import LeftButtonCustom from "@/components/Button/BackButton";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
|
||||
@@ -51,24 +53,35 @@ export default function UserLayout() {
|
||||
/>
|
||||
|
||||
{/* ========== Notification Section ========= */}
|
||||
<Stack.Screen
|
||||
|
||||
{/* DIPINDAH DI FILE NOTIFICATION USER */}
|
||||
{/* <Stack.Screen
|
||||
name="notifications/index"
|
||||
options={{
|
||||
title: "Notifikasi",
|
||||
headerLeft: () => <BackButton />,
|
||||
// headerRight: () => (
|
||||
// <IconPlus
|
||||
// color={MainColor.yellow}
|
||||
// onPress={() => router.push("/test-notifications")}
|
||||
// />
|
||||
// ),
|
||||
}}
|
||||
/>
|
||||
/> */}
|
||||
|
||||
{/* ========== Event Section ========= */}
|
||||
|
||||
<Stack.Screen
|
||||
name="event/(tabs)"
|
||||
options={{
|
||||
title: "Event",
|
||||
headerLeft: () => (
|
||||
<LeftButtonCustom path="/(application)/(user)/home" />
|
||||
),
|
||||
// NOTE: DIPINDAH DI FILE /Event/(Tabs)/_layout.tsx
|
||||
// headerLeft: () => (
|
||||
// <LeftButtonCustom path="/(application)/(user)/home" />
|
||||
// ),
|
||||
}}
|
||||
/>
|
||||
|
||||
<Stack.Screen
|
||||
name="event/create"
|
||||
options={{
|
||||
@@ -511,7 +524,8 @@ export default function UserLayout() {
|
||||
name="job/(tabs)"
|
||||
options={{
|
||||
title: "Job Vacancy",
|
||||
headerLeft: () => <BackButton path="/home" />,
|
||||
// headerLeft: () => <BackButton path="/home" />,
|
||||
// NOTE: headerLeft di pindahkan ke Tabs Layout
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
@@ -595,6 +609,27 @@ export default function UserLayout() {
|
||||
headerLeft: () => <BackButton />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="forum/terms"
|
||||
options={{
|
||||
title: "Syarat & Ketentuan Forum",
|
||||
headerLeft: () => <BackButton />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="forum/[id]/preview-report-posting"
|
||||
options={{
|
||||
title: "Laporan Postingan",
|
||||
headerLeft: () => <BackButton />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="forum/[id]/preview-report-comment"
|
||||
options={{
|
||||
title: "Laporan Komentar",
|
||||
headerLeft: () => <BackButton />,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* ========== Maps Section ========= */}
|
||||
<Stack.Screen
|
||||
|
||||
@@ -16,6 +16,7 @@ import Donation_ComponentBoxDetailData from "@/screens/Donation/ComponentBoxDeta
|
||||
import Donation_ComponentStoryFunrising from "@/screens/Donation/ComponentStoryFunrising";
|
||||
import Donation_ProgressSection from "@/screens/Donation/ProgressSection";
|
||||
import { apiDonationGetOne } from "@/service/api-client/api-donation";
|
||||
import { countDownAndCondition } from "@/utils/countDownAndCondition";
|
||||
import { FontAwesome6 } from "@expo/vector-icons";
|
||||
import {
|
||||
router,
|
||||
@@ -24,7 +25,7 @@ import {
|
||||
useLocalSearchParams,
|
||||
} from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
export default function DonasiDetailStatus() {
|
||||
const { id, status } = useLocalSearchParams();
|
||||
@@ -58,6 +59,27 @@ export default function DonasiDetailStatus() {
|
||||
setOpenDrawer(false);
|
||||
};
|
||||
|
||||
const [value, setValue] = useState({
|
||||
sisa: 0,
|
||||
reminder: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
updateCountDown();
|
||||
}, [data]);
|
||||
|
||||
const updateCountDown = () => {
|
||||
const countDown = countDownAndCondition({
|
||||
duration: data?.DonasiMaster_Durasi?.name,
|
||||
publishTime: data?.publishTime,
|
||||
});
|
||||
|
||||
setValue({
|
||||
sisa: countDown.durationDay,
|
||||
reminder: countDown.reminder,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen
|
||||
@@ -74,10 +96,15 @@ export default function DonasiDetailStatus() {
|
||||
/>
|
||||
<ViewWrapper>
|
||||
<Donation_ComponentBoxDetailData
|
||||
sisaHari={value.sisa}
|
||||
reminder={value.reminder}
|
||||
data={data}
|
||||
bottomSection={
|
||||
status === "publish" && (
|
||||
<Donation_ProgressSection id={id as string} />
|
||||
<Donation_ProgressSection
|
||||
id={id as string}
|
||||
progres={Number(data?.progres) || 0}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -4,10 +4,34 @@ import {
|
||||
IconHome,
|
||||
IconStatus,
|
||||
} from "@/components/_Icon";
|
||||
import BackButtonFromNotification from "@/components/Button/BackButtonFromNotification";
|
||||
import { TabsStyles } from "@/styles/tabs-styles";
|
||||
import { Tabs } from "expo-router";
|
||||
import { router, Tabs, useLocalSearchParams, useNavigation } from "expo-router";
|
||||
import { useLayoutEffect } from "react";
|
||||
|
||||
export default function EventTabsLayout() {
|
||||
const navigation = useNavigation();
|
||||
|
||||
const { from, category } = useLocalSearchParams<{
|
||||
from?: string;
|
||||
category?: string;
|
||||
}>();
|
||||
|
||||
console.log("from", from);
|
||||
console.log("category", category);
|
||||
|
||||
// Atur header secara dinamis
|
||||
useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerLeft: () => (
|
||||
<BackButtonFromNotification
|
||||
from={from as string}
|
||||
category={category as string}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}, [from, router, navigation]);
|
||||
|
||||
return (
|
||||
<Tabs screenOptions={TabsStyles}>
|
||||
<Tabs.Screen
|
||||
|
||||
@@ -11,15 +11,17 @@ import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
|
||||
import { apiEventGetByStatus } from "@/service/api-client/api-event";
|
||||
import { useFocusEffect } from "expo-router";
|
||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
export default function EventStatus() {
|
||||
const { user } = useAuth();
|
||||
const { status } = useLocalSearchParams<{ status?: string }>();
|
||||
|
||||
const id = user?.id || "";
|
||||
const [activeCategory, setActiveCategory] = useState<string | null>(
|
||||
"publish"
|
||||
status || "publish"
|
||||
);
|
||||
const [listData, setListData] = useState([]);
|
||||
const [loadingGetData, setLoadingGetData] = useState(false);
|
||||
@@ -73,7 +75,7 @@ export default function EventStatus() {
|
||||
listData.map((item: any, i) => (
|
||||
<BoxWithHeaderSection
|
||||
key={i}
|
||||
href={`/event/${item.id }/${activeCategory}/detail-event`}
|
||||
href={`/event/${item.id}/${activeCategory}/detail-event`}
|
||||
>
|
||||
<StackCustom gap={"xs"}>
|
||||
<Grid>
|
||||
|
||||
@@ -56,7 +56,7 @@ export default function EventDetailHistory() {
|
||||
<DrawerCustom
|
||||
isVisible={openDrawer}
|
||||
closeDrawer={() => setOpenDrawer(false)}
|
||||
height={250}
|
||||
height={"auto"}
|
||||
>
|
||||
<MenuDrawerDynamicGrid
|
||||
data={menuDrawerPublishEvent({ id: id as string })}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
AlertDefaultSystem,
|
||||
BackButton,
|
||||
ButtonCustom,
|
||||
DotButton,
|
||||
DrawerCustom,
|
||||
LoaderCustom,
|
||||
MenuDrawerDynamicGrid,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import { IMenuDrawerItem } from "@/components/_Interface/types";
|
||||
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
|
||||
import LeftButtonCustom from "@/components/Button/BackButton";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import Event_BoxDetailPublishSection from "@/screens/Event/BoxDetailPublishSection";
|
||||
@@ -18,23 +19,26 @@ import {
|
||||
apiEventGetOne,
|
||||
apiEventJoin,
|
||||
} from "@/service/api-client/api-event";
|
||||
import dayjs from "dayjs";
|
||||
import {
|
||||
Redirect,
|
||||
router,
|
||||
Stack,
|
||||
useFocusEffect,
|
||||
useLocalSearchParams,
|
||||
} from "expo-router";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import Toast from "react-native-toast-message";
|
||||
|
||||
export default function EventDetailPublish() {
|
||||
const { id } = useLocalSearchParams();
|
||||
const now = new Date().toISOString();
|
||||
const { user } = useAuth();
|
||||
const { id } = useLocalSearchParams();
|
||||
const [openDrawer, setOpenDrawer] = useState(false);
|
||||
const [isLoadingData, setIsLoadingData] = useState(false);
|
||||
const [isLoadingJoin, setIsLoadingJoin] = useState(false);
|
||||
|
||||
const [data, setData] = useState();
|
||||
const [data, setData] = useState<any>();
|
||||
const [isParticipant, setIsParticipant] = useState<boolean | null>(null);
|
||||
|
||||
useFocusEffect(
|
||||
@@ -55,8 +59,6 @@ export default function EventDetailPublish() {
|
||||
userId: user?.id as string,
|
||||
});
|
||||
|
||||
console.log("[RES CHECK PARTICIPANTS]", responseCheckParticipants);
|
||||
|
||||
if (
|
||||
responseCheckParticipants.success &&
|
||||
responseCheckParticipants.data
|
||||
@@ -71,8 +73,6 @@ export default function EventDetailPublish() {
|
||||
}
|
||||
}
|
||||
|
||||
console.log("[participans]", isParticipant);
|
||||
|
||||
const handlePress = (item: IMenuDrawerItem) => {
|
||||
console.log("PATH ", item.path);
|
||||
router.navigate(item.path as any);
|
||||
@@ -110,7 +110,24 @@ export default function EventDetailPublish() {
|
||||
}
|
||||
};
|
||||
|
||||
const footerButton = () => {
|
||||
const isEventFinished =
|
||||
id && data?.tanggalSelesai && dayjs(data.tanggalSelesai).isBefore(now);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEventFinished) {
|
||||
router.replace(`/(application)/(user)/event/${id}/history`);
|
||||
}
|
||||
}, [isEventFinished, id]);
|
||||
|
||||
if (isEventFinished) {
|
||||
return (
|
||||
<ViewWrapper>
|
||||
<CustomSkeleton />
|
||||
</ViewWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
const FooterButton = () => {
|
||||
return (
|
||||
<>
|
||||
<ButtonCustom
|
||||
@@ -139,18 +156,18 @@ export default function EventDetailPublish() {
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: `Event publish`,
|
||||
headerLeft: () => <LeftButtonCustom />,
|
||||
title: `Event Publish`,
|
||||
headerLeft: () => <BackButton onPress={() => router.back()} />,
|
||||
headerRight: () => <DotButton onPress={() => setOpenDrawer(true)} />,
|
||||
}}
|
||||
/>
|
||||
<ViewWrapper>
|
||||
{isLoadingData ? (
|
||||
<LoaderCustom />
|
||||
<CustomSkeleton height={400} />
|
||||
) : (
|
||||
<Event_BoxDetailPublishSection
|
||||
data={data}
|
||||
footerButton={footerButton()}
|
||||
footerButton={FooterButton()}
|
||||
/>
|
||||
)}
|
||||
</ViewWrapper>
|
||||
|
||||
@@ -14,7 +14,7 @@ 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, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import Toast from "react-native-toast-message";
|
||||
|
||||
interface EventCreateProps {
|
||||
@@ -78,23 +78,6 @@ export default function EventCreate() {
|
||||
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 {
|
||||
setIsLoading(true);
|
||||
|
||||
@@ -110,7 +93,7 @@ export default function EventCreate() {
|
||||
const response = await apiEventCreate(newData);
|
||||
console.log("Response", JSON.stringify(response, null, 2));
|
||||
|
||||
router.replace("/event/status");
|
||||
router.replace("/event/status?status=review");
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
@@ -145,7 +128,7 @@ export default function EventCreate() {
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
}))}
|
||||
value={data?.eventMaster_TipeAcaraId || ""}
|
||||
value={data?.eventMaster_TipeAcaraId || null}
|
||||
onChange={(value: any) =>
|
||||
setData({ ...data, eventMaster_TipeAcaraId: value })
|
||||
}
|
||||
|
||||
@@ -19,34 +19,20 @@ import {
|
||||
apiForumGetOne,
|
||||
apiForumUpdateStatus,
|
||||
} from "@/service/api-client/api-forum";
|
||||
import { TypeForum_CommentProps } from "@/types/type-forum";
|
||||
import { isBadContent } from "@/utils/badWordsIndonesia";
|
||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Alert } from "react-native";
|
||||
|
||||
interface CommentProps {
|
||||
id: string;
|
||||
isActive: boolean;
|
||||
komentar: string;
|
||||
createdAt: Date;
|
||||
authorId: string;
|
||||
Author: {
|
||||
id: string;
|
||||
username: string;
|
||||
Profile: {
|
||||
id: string;
|
||||
imageId: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export default function ForumDetail() {
|
||||
const { id } = useLocalSearchParams();
|
||||
const { user } = useAuth();
|
||||
const [openDrawer, setOpenDrawer] = useState(false);
|
||||
const [data, setData] = useState<any | null>(null);
|
||||
const [listComment, setListComment] = useState<CommentProps[] | null>(null);
|
||||
const [listComment, setListComment] = useState<TypeForum_CommentProps[] | null>(null);
|
||||
const [isLoadingComment, setLoadingComment] = useState(false);
|
||||
|
||||
// Status
|
||||
@@ -122,6 +108,7 @@ export default function ForumDetail() {
|
||||
comment: text,
|
||||
authorId: user?.id,
|
||||
};
|
||||
|
||||
try {
|
||||
setLoadingComment(true);
|
||||
const response = await apiForumCreateComment({
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from "@/components";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { apiForumCreateReportCommentar } from "@/service/api-client/api-master";
|
||||
import { apiForumCreateReportCommentar } from "@/service/api-client/api-forum";
|
||||
import { router, useLocalSearchParams } from "expo-router";
|
||||
import { useState } from "react";
|
||||
import Toast from "react-native-toast-message";
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from "@/components";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { apiForumCreateReportPosting } from "@/service/api-client/api-master";
|
||||
import { apiForumCreateReportPosting } from "@/service/api-client/api-forum";
|
||||
import { router, useLocalSearchParams } from "expo-router";
|
||||
import { useState } from "react";
|
||||
import Toast from "react-native-toast-message";
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import {
|
||||
BaseBox,
|
||||
NewWrapper,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
} from "@/components";
|
||||
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
|
||||
import NoDataText from "@/components/_ShareComponent/NoDataText";
|
||||
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
|
||||
import { apiForumGetReportComment } from "@/service/api-client/api-forum";
|
||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
export default function ForumPreviewReportComment() {
|
||||
const { id } = useLocalSearchParams();
|
||||
const [data, setData] = useState<any | null>(null);
|
||||
const [listData, setListData] = useState<any | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
// Status
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData(id as string);
|
||||
}, [id])
|
||||
);
|
||||
|
||||
const onLoadData = async (id: string) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await apiForumGetReportComment({ id });
|
||||
setData(response.data);
|
||||
setListData(response?.data?.Forum_ReportKomentar);
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<NewWrapper>
|
||||
<StackCustom>
|
||||
<TextCustom color="red" bold>
|
||||
Komentar anda telah melanggar aturan forum ! Admin mengambil
|
||||
tindakan untuk menghapus komentar anda!
|
||||
</TextCustom>
|
||||
{loading ? (
|
||||
<CustomSkeleton height={100} />
|
||||
) : (
|
||||
<BaseBox>
|
||||
<TextCustom>"{data?.komentar ? data?.komentar : "-"}"</TextCustom>
|
||||
</BaseBox>
|
||||
)}
|
||||
</StackCustom>
|
||||
|
||||
<Spacing height={10} />
|
||||
<TextCustom bold>Beberapa laporan yang telah diterima</TextCustom>
|
||||
<Spacing height={10} />
|
||||
|
||||
{loading ? (
|
||||
<ListSkeletonComponent />
|
||||
) : _.isEmpty(listData) ? (
|
||||
<NoDataText />
|
||||
) : (
|
||||
listData?.map((e: any, index: number) => (
|
||||
<BaseBox key={index}>
|
||||
{e?.deskripsi ? (
|
||||
<StackCustom gap={"sm"}>
|
||||
<TextCustom bold>Laporan Lainnya</TextCustom>
|
||||
<TextCustom>{e?.deskripsi}</TextCustom>
|
||||
</StackCustom>
|
||||
) : (
|
||||
<StackCustom gap={"sm"}>
|
||||
<TextCustom bold>
|
||||
{e?.ForumMaster_KategoriReport?.title}
|
||||
</TextCustom>
|
||||
<TextCustom>
|
||||
{e?.ForumMaster_KategoriReport?.deskripsi}
|
||||
</TextCustom>
|
||||
</StackCustom>
|
||||
)}
|
||||
</BaseBox>
|
||||
))
|
||||
)}
|
||||
</NewWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import {
|
||||
BaseBox,
|
||||
NewWrapper,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
} from "@/components";
|
||||
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
|
||||
import NoDataText from "@/components/_ShareComponent/NoDataText";
|
||||
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
|
||||
import { apiForumGetReportPosting } from "@/service/api-client/api-forum";
|
||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
export default function ForumPreviewReportPosting() {
|
||||
const { id } = useLocalSearchParams();
|
||||
const [data, setData] = useState<any | null>(null);
|
||||
const [listData, setListData] = useState<any | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
// Status
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData(id as string);
|
||||
}, [id])
|
||||
);
|
||||
|
||||
const onLoadData = async (id: string) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await apiForumGetReportPosting({ id });
|
||||
setData(response.data);
|
||||
setListData(response?.data?.Forum_ReportPosting);
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<NewWrapper>
|
||||
<StackCustom>
|
||||
<TextCustom color="red" bold>
|
||||
Postingan anda telah melanggar aturan forum ! Admin mengambil
|
||||
tindakan untuk menghapus komentar anda!
|
||||
</TextCustom>
|
||||
{loading ? (
|
||||
<CustomSkeleton height={100} />
|
||||
) : (
|
||||
<BaseBox>
|
||||
<TextCustom>"{data?.diskusi ? data?.diskusi : "-"}"</TextCustom>
|
||||
</BaseBox>
|
||||
)}
|
||||
</StackCustom>
|
||||
|
||||
<Spacing height={10} />
|
||||
<TextCustom bold>Beberapa laporan yang telah diterima</TextCustom>
|
||||
<Spacing height={10} />
|
||||
|
||||
{loading ? (
|
||||
<ListSkeletonComponent />
|
||||
) : _.isEmpty(listData) ? (
|
||||
<NoDataText />
|
||||
) : (
|
||||
listData?.map((e: any) => (
|
||||
<BaseBox key={e?.id}>
|
||||
{e?.deskripsi ? (
|
||||
<StackCustom gap={"sm"}>
|
||||
<TextCustom bold>Laporan Lainnya</TextCustom>
|
||||
<TextCustom>{e?.deskripsi}</TextCustom>
|
||||
</StackCustom>
|
||||
) : (
|
||||
<StackCustom gap={"sm"}>
|
||||
<TextCustom bold>
|
||||
{e?.ForumMaster_KategoriReport?.title}
|
||||
</TextCustom>
|
||||
<TextCustom>
|
||||
{e?.ForumMaster_KategoriReport?.deskripsi}
|
||||
</TextCustom>
|
||||
</StackCustom>
|
||||
)}
|
||||
</BaseBox>
|
||||
))
|
||||
)}
|
||||
</NewWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -8,7 +8,8 @@ import {
|
||||
import { AccentColor, MainColor } from "@/constants/color-palet";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import Forum_ReportListSection from "@/screens/Forum/ReportListSection";
|
||||
import { apiForumCreateReportCommentar, apiMasterForumReportList } from "@/service/api-client/api-master";
|
||||
import { apiForumCreateReportCommentar } from "@/service/api-client/api-forum";
|
||||
import { apiMasterForumReportList } from "@/service/api-client/api-master";
|
||||
import { router, useLocalSearchParams } from "expo-router";
|
||||
import { useState, useEffect } from "react";
|
||||
import Toast from "react-native-toast-message";
|
||||
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
import { AccentColor, MainColor } from "@/constants/color-palet";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import Forum_ReportListSection from "@/screens/Forum/ReportListSection";
|
||||
import { apiForumCreateReportPosting } from "@/service/api-client/api-forum";
|
||||
import {
|
||||
apiForumCreateReportPosting,
|
||||
apiMasterForumReportList,
|
||||
} from "@/service/api-client/api-master";
|
||||
import { router, useLocalSearchParams } from "expo-router";
|
||||
|
||||
@@ -2,15 +2,14 @@ import {
|
||||
BoxButtonOnFooter,
|
||||
ButtonCustom,
|
||||
TextAreaCustom,
|
||||
ViewWrapper
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import AlertWarning from "@/components/Alert/AlertWarning";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { apiForumCreate } from "@/service/api-client/api-forum";
|
||||
import { isBadContent } from "@/utils/badWordsIndonesia";
|
||||
import { censorText, isBadContent } from "@/utils/badWordsIndonesia";
|
||||
import { router } from "expo-router";
|
||||
import { useState } from "react";
|
||||
import { Alert } from "react-native";
|
||||
import Toast from "react-native-toast-message";
|
||||
|
||||
export default function ForumCreate() {
|
||||
@@ -19,16 +18,22 @@ export default function ForumCreate() {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handlerSubmit = async () => {
|
||||
|
||||
if (isBadContent(text)) {
|
||||
AlertWarning({})
|
||||
if (text.trim() === "") {
|
||||
AlertWarning({
|
||||
title: "Lengkapi Data",
|
||||
description: "Postingan tidak boleh kosong",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Bisa di sensor atau return dan tidak bisa di post
|
||||
const cencorContent = censorText(text)
|
||||
|
||||
const newData = {
|
||||
diskusi: text,
|
||||
diskusi: cencorContent,
|
||||
authorId: user?.id,
|
||||
};
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await apiForumCreate({ data: newData });
|
||||
@@ -50,6 +55,7 @@ export default function ForumCreate() {
|
||||
const buttonFooter = (
|
||||
<BoxButtonOnFooter>
|
||||
<ButtonCustom
|
||||
disabled={!text.trim() || isLoading}
|
||||
isLoading={isLoading}
|
||||
onPress={() => {
|
||||
handlerSubmit();
|
||||
|
||||
202
app/(application)/(user)/forum/terms.tsx
Normal file
202
app/(application)/(user)/forum/terms.tsx
Normal file
@@ -0,0 +1,202 @@
|
||||
import {
|
||||
BaseBox,
|
||||
ButtonCustom,
|
||||
CheckboxCustom,
|
||||
NewWrapper,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
} from "@/components";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { apiAcceptForumTerms } from "@/service/api-client/api-user";
|
||||
import { GStyles } from "@/styles/global-styles";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { router } from "expo-router";
|
||||
import { useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { Text } from "react-native-paper";
|
||||
import Toast from "react-native-toast-message";
|
||||
|
||||
export default function ForumSplash() {
|
||||
const { user } = useAuth();
|
||||
const [term, setTerm] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const respone = await apiAcceptForumTerms({
|
||||
category: "Forum",
|
||||
userId: user?.id as any,
|
||||
});
|
||||
|
||||
if (respone.success) {
|
||||
Toast.show({
|
||||
type: "success",
|
||||
text1: "Berhasil",
|
||||
text2: "Terima kasih telah menerima syarat & ketentuan forum ini",
|
||||
});
|
||||
|
||||
router.replace("/(application)/forum");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Toast.show({
|
||||
type: "error",
|
||||
text1: "Gagal",
|
||||
text2: "Terjadi kesalahan, silahkan coba lagi",
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<NewWrapper>
|
||||
{/* <TextCustom bold>HIPMI Badung Connect</TextCustom> . */}
|
||||
|
||||
<BaseBox>
|
||||
<StackCustom>
|
||||
<TextCustom>
|
||||
Dengan mengakses dan menggunakan Forum HIPMI Badung Connect, Anda
|
||||
secara sadar menyetujui ketentuan berikut:
|
||||
</TextCustom>
|
||||
|
||||
<TextCustom bold>
|
||||
1. Dilarang keras memposting konten yang mengandung:
|
||||
</TextCustom>
|
||||
<View style={{ paddingInline: 10 }}>
|
||||
{forumTerms1.map((term, index) => (
|
||||
<View
|
||||
key={index}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 10,
|
||||
paddingBottom: 10,
|
||||
}}
|
||||
>
|
||||
<Ionicons name="radio-button-on" color={"white"} />
|
||||
<TextCustom>{term.text}</TextCustom>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<TextCustom bold>
|
||||
2. Setiap pengguna bertanggung jawab penuh atas konten yang
|
||||
diunggah. Konten yang melanggar ketentuan ini dapat dihapus kapan
|
||||
saja tanpa pemberitahuan.
|
||||
</TextCustom>
|
||||
|
||||
<TextCustom bold>
|
||||
3. Jika Anda menemukan konten tidak pantas, segera:
|
||||
</TextCustom>
|
||||
<View style={{ paddingInline: 10 }}>
|
||||
{forumTerms2.map((term, index) => (
|
||||
<View
|
||||
key={index}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 10,
|
||||
paddingBottom: 10,
|
||||
}}
|
||||
>
|
||||
<Ionicons name="radio-button-on" color={"white"} />
|
||||
<TextCustom>{term.text}</TextCustom>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<TextCustom bold>
|
||||
4. Gunakan fitur “Blokir Pengguna” di profil pengguna terkait
|
||||
</TextCustom>
|
||||
<View style={{ paddingInline: 10 }}>
|
||||
{forumTerms3.map((term, index) => (
|
||||
<View
|
||||
key={index}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 10,
|
||||
paddingBottom: 10,
|
||||
}}
|
||||
>
|
||||
<Ionicons name="radio-button-on" color={"white"} />
|
||||
<TextCustom>{term.text}</TextCustom>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<TextCustom>
|
||||
Pelanggaran terhadap ketentuan ini berakibat{" "}
|
||||
<TextCustom bold>pencabutan akses</TextCustom> ke Forum dan/atau{" "}
|
||||
<TextCustom bold>pemblokiran akun secara permanen</TextCustom> tanpa
|
||||
pemberitahuan lebih lanjut.
|
||||
</TextCustom>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginTop: 16,
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<CheckboxCustom value={term} onChange={() => setTerm(!term)} />
|
||||
|
||||
<Text style={GStyles.textLabel}>
|
||||
Saya telah membaca dan menyetujui Syarat & Ketentuan Forum ini
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<ButtonCustom
|
||||
disabled={!term || loading}
|
||||
onPress={() => {
|
||||
handleSubmit();
|
||||
}}
|
||||
>
|
||||
Lanjut
|
||||
</ButtonCustom>
|
||||
</StackCustom>
|
||||
</BaseBox>
|
||||
</NewWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
// Data dalam format JSON (bisa juga diimpor dari file terpisah)
|
||||
interface Term {
|
||||
text: string;
|
||||
}
|
||||
|
||||
const forumTerms1: Term[] = [
|
||||
{
|
||||
text: "Ujaran kebencian, diskriminasi, atau konten SARA (Suku, Agama, Ras, Antar-golongan)",
|
||||
},
|
||||
{ text: "Kata kasar, pelecehan, ancaman, atau bullying" },
|
||||
{ text: "Pornografi, hoaks, spam, atau informasi menyesatkan" },
|
||||
{ text: "Promosi aktivitas ilegal seperti perjudian atau narkoba" },
|
||||
];
|
||||
|
||||
const forumTerms2: Term[] = [
|
||||
{
|
||||
text: "Gunakan tombol “Laporkan” di setiap postingan, atau",
|
||||
},
|
||||
{
|
||||
text: "Gunakan fitur “Blokir Pengguna” di profil pengguna terkait.",
|
||||
},
|
||||
];
|
||||
|
||||
const forumTerms3: Term[] = [
|
||||
{
|
||||
text: "Meninjau setiap laporan dalam waktu 24 jam",
|
||||
},
|
||||
{
|
||||
text: "Menghapus konten yang melanggar",
|
||||
},
|
||||
{
|
||||
text: "Memblokir akun pelanggar sesuai tingkat pelanggaran",
|
||||
},
|
||||
];
|
||||
@@ -3,7 +3,9 @@
|
||||
import { StackCustom, ViewWrapper } from "@/components";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useNotificationStore } from "@/hooks/use-notification-store";
|
||||
import Home_BottomFeatureSection from "@/screens/Home/bottomFeatureSection";
|
||||
import HeaderBell from "@/screens/Home/HeaderBell";
|
||||
import Home_ImageSection from "@/screens/Home/imageSection";
|
||||
import TabSection from "@/screens/Home/tabSection";
|
||||
import { tabsHome } from "@/screens/Home/tabsList";
|
||||
@@ -11,39 +13,51 @@ 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";
|
||||
import { Redirect, router, Stack, useFocusEffect } from "expo-router";
|
||||
import { useCallback, useState } from "react";
|
||||
import { RefreshControl } from "react-native";
|
||||
|
||||
export default function Application() {
|
||||
const { token, user } = useAuth();
|
||||
const { token, user, userData } = useAuth();
|
||||
const [data, setData] = useState<any>();
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const { syncUnreadCount } = useNotificationStore();
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
checkVersion();
|
||||
userData(token as string);
|
||||
syncUnreadCount();
|
||||
}, [user?.id, token])
|
||||
);
|
||||
|
||||
console.log("[User] >>", JSON.stringify(user?.id, null, 2));
|
||||
|
||||
useEffect(() => {
|
||||
onLoadData();
|
||||
checkVersion();
|
||||
}, []);
|
||||
|
||||
async function onLoadData() {
|
||||
const response = await apiUser(user?.id as string);
|
||||
console.log(
|
||||
"[Profile ID]>>",
|
||||
JSON.stringify(response?.data?.Profile.id, null, 2)
|
||||
JSON.stringify(response?.data?.Profile?.id, null, 2)
|
||||
);
|
||||
|
||||
|
||||
setData(response.data);
|
||||
}
|
||||
|
||||
|
||||
const checkVersion = async () => {
|
||||
const response = await apiVersion();
|
||||
console.log("[Version] >>", JSON.stringify(response.data, null, 2));
|
||||
};
|
||||
|
||||
if (user && user?.termsOfServiceAccepted === false) {
|
||||
console.log("User is not accept term service");
|
||||
return <Redirect href={`/terms-agreement`} />;
|
||||
}
|
||||
|
||||
const onRefresh = useCallback(() => {
|
||||
setRefreshing(true);
|
||||
onLoadData();
|
||||
checkVersion();
|
||||
setRefreshing(false);
|
||||
}, []);
|
||||
|
||||
// if (user && user?.termsOfServiceAccepted === false) {
|
||||
// console.log("User is not accept term service");
|
||||
// return <Redirect href={`/terms-agreement`} />;
|
||||
// }
|
||||
|
||||
if (data && data?.active === false) {
|
||||
console.log("User is not active");
|
||||
@@ -70,24 +84,27 @@ export default function Application() {
|
||||
}}
|
||||
/>
|
||||
),
|
||||
headerRight: () => (
|
||||
<Ionicons
|
||||
name="notifications"
|
||||
size={20}
|
||||
color={MainColor.yellow}
|
||||
onPress={() => {
|
||||
router.push("/notifications");
|
||||
}}
|
||||
/>
|
||||
),
|
||||
headerRight: () => <HeaderBell />,
|
||||
}}
|
||||
/>
|
||||
<ViewWrapper
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
|
||||
}
|
||||
footerComponent={
|
||||
<TabSection tabs={tabsHome(data?.Profile?.id as string)} />
|
||||
<TabSection
|
||||
tabs={tabsHome({
|
||||
acceptedForumTermsAt: data?.acceptedForumTermsAt,
|
||||
profileId: data?.Profile?.id,
|
||||
})}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<StackCustom>
|
||||
{/* <ButtonCustom onPress={() => router.push("./test-notifications")}>
|
||||
Test Notif
|
||||
</ButtonCustom> */}
|
||||
|
||||
<Home_ImageSection />
|
||||
|
||||
<Home_FeatureSection />
|
||||
|
||||
@@ -1,9 +1,33 @@
|
||||
import BackButtonFromNotification from "@/components/Button/BackButtonFromNotification";
|
||||
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
|
||||
import { TabsStyles } from "@/styles/tabs-styles";
|
||||
import { Feather, FontAwesome6, Ionicons } from "@expo/vector-icons";
|
||||
import { Tabs } from "expo-router";
|
||||
import { router, Tabs, useLocalSearchParams, useNavigation } from "expo-router";
|
||||
import { useLayoutEffect } from "react";
|
||||
|
||||
export default function InvestmentTabsLayout() {
|
||||
// const navigation = useNavigation();
|
||||
|
||||
// const { from, category } = useLocalSearchParams<{
|
||||
// from?: string;
|
||||
// category?: string;
|
||||
// }>();
|
||||
|
||||
// console.log("from", from);
|
||||
// console.log("category", category);
|
||||
|
||||
// // Atur header secara dinamis
|
||||
// useLayoutEffect(() => {
|
||||
// navigation.setOptions({
|
||||
// headerLeft: () => (
|
||||
// <BackButtonFromNotification
|
||||
// from={from as string}
|
||||
// category={category as string}
|
||||
// />
|
||||
// ),
|
||||
// });
|
||||
// }, [from, router, navigation]);
|
||||
|
||||
return (
|
||||
<Tabs screenOptions={TabsStyles}>
|
||||
<Tabs.Screen
|
||||
|
||||
@@ -11,9 +11,7 @@ import {
|
||||
} from "@/components";
|
||||
import NoDataText from "@/components/_ShareComponent/NoDataText";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import {
|
||||
apiInvestmentGetAll
|
||||
} from "@/service/api-client/api-investment";
|
||||
import { apiInvestmentGetAll } from "@/service/api-client/api-investment";
|
||||
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
|
||||
import { router, useFocusEffect } from "expo-router";
|
||||
import _ from "lodash";
|
||||
@@ -63,37 +61,20 @@ export default function InvestmentMyHolding() {
|
||||
router.push(`/investment/${item?.id}/(my-holding)/${item?.id}`)
|
||||
}
|
||||
>
|
||||
<Grid>
|
||||
<Grid.Col span={6}>
|
||||
<StackCustom gap={"xs"}>
|
||||
<TextCustom truncate={2}>{item?.title}</TextCustom>
|
||||
|
||||
<Spacing height={5} />
|
||||
<TextCustom size="small">
|
||||
Rp. {formatCurrencyDisplay(item?.nominal)}
|
||||
</TextCustom>
|
||||
<TextCustom size="small">
|
||||
{item?.lembarTerbeli} Lembar
|
||||
</TextCustom>
|
||||
</StackCustom>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={1}>
|
||||
<View />
|
||||
</Grid.Col>
|
||||
<Grid.Col
|
||||
span={5}
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<ProgressCustom
|
||||
value={item?.progress}
|
||||
label={`${item?.progress}%`}
|
||||
size="lg"
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
<StackCustom>
|
||||
<TextCustom truncate={2}>{item?.title}</TextCustom>
|
||||
<TextCustom>
|
||||
Rp. {formatCurrencyDisplay(item?.nominal)}
|
||||
</TextCustom>
|
||||
<TextCustom>{item?.lembarTerbeli} Lembar</TextCustom>
|
||||
<ProgressCustom
|
||||
label={`${item.progress}%`}
|
||||
value={Number(item.progress)}
|
||||
size="lg"
|
||||
animated
|
||||
color="primary"
|
||||
/>
|
||||
</StackCustom>
|
||||
</BaseBox>
|
||||
))
|
||||
)}
|
||||
|
||||
@@ -9,14 +9,16 @@ import { useAuth } from "@/hooks/use-auth";
|
||||
import { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
|
||||
import Investment_StatusBox from "@/screens/Invesment/StatusBox";
|
||||
import { apiInvestmentGetByStatus } from "@/service/api-client/api-investment";
|
||||
import { useFocusEffect } from "expo-router";
|
||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
export default function InvestmentPortofolio() {
|
||||
const { user } = useAuth();
|
||||
const { status } = useLocalSearchParams<{ status?: string }>();
|
||||
|
||||
const [activeCategory, setActiveCategory] = useState<string | null>(
|
||||
"publish"
|
||||
status || "publish"
|
||||
);
|
||||
|
||||
const [listData, setListData] = useState<any[]>([]);
|
||||
|
||||
@@ -15,6 +15,7 @@ import Investment_ButtonInvestasiSection from "@/screens/Invesment/ButtonInvesta
|
||||
import Invesment_ComponentBoxOnBottomDetail from "@/screens/Invesment/ComponentBoxOnBottomDetail";
|
||||
import Invesment_DetailDataPublishSection from "@/screens/Invesment/DetailDataPublishSection";
|
||||
import { apiInvestmentGetOne } from "@/service/api-client/api-investment";
|
||||
import { countDownAndCondition } from "@/utils/countDownAndCondition";
|
||||
import { AntDesign, MaterialIcons } from "@expo/vector-icons";
|
||||
import {
|
||||
router,
|
||||
@@ -23,7 +24,7 @@ import {
|
||||
useLocalSearchParams,
|
||||
} from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
export default function InvestmentDetailStatus() {
|
||||
const { user } = useAuth();
|
||||
@@ -63,6 +64,29 @@ export default function InvestmentDetailStatus() {
|
||||
setOpenDrawerPublish(false);
|
||||
};
|
||||
|
||||
const [value, setValue] = useState({
|
||||
sisa: 0,
|
||||
reminder: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
updateCountDown();
|
||||
}, [data]);
|
||||
|
||||
console.log("[DATA DETAIL]", JSON.stringify(data, null, 2));
|
||||
|
||||
const updateCountDown = () => {
|
||||
const countDown = countDownAndCondition({
|
||||
duration: data?.MasterPencarianInvestor.name,
|
||||
publishTime: data?.countDown,
|
||||
});
|
||||
|
||||
setValue({
|
||||
sisa: countDown.durationDay,
|
||||
reminder: countDown.reminder,
|
||||
});
|
||||
};
|
||||
|
||||
const bottomSection = (
|
||||
<Invesment_ComponentBoxOnBottomDetail
|
||||
id={data?.id}
|
||||
@@ -72,7 +96,11 @@ export default function InvestmentDetailStatus() {
|
||||
);
|
||||
|
||||
const buttonSection = (
|
||||
<Investment_ButtonInvestasiSection id={id as string} isMine={user?.id === data?.author?.id} />
|
||||
<Investment_ButtonInvestasiSection
|
||||
id={id as string}
|
||||
isMine={user?.id === data?.author?.id}
|
||||
reminder={value.reminder}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -63,30 +63,28 @@ export default function InvestmentDetail() {
|
||||
setOpenDrawerPublish(false);
|
||||
};
|
||||
|
||||
const [value, setValue] = useState({
|
||||
sisa: 0,
|
||||
reminder: false,
|
||||
const [value, setValue] = useState({
|
||||
sisa: 0,
|
||||
reminder: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
updateCountDown();
|
||||
}, [data]);
|
||||
|
||||
console.log("[DATA DETAIL]", JSON.stringify(data, null, 2));
|
||||
|
||||
const updateCountDown = () => {
|
||||
const countDown = countDownAndCondition({
|
||||
duration: data?.MasterPencarianInvestor.name,
|
||||
publishTime: data?.countDown,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
updateCountDown();
|
||||
}, [data]);
|
||||
|
||||
console.log("[DATA DETAIL]", JSON.stringify(data, null, 2));
|
||||
|
||||
const updateCountDown = () => {
|
||||
const countDown = countDownAndCondition({
|
||||
duration: data?.MasterPencarianInvestor.name,
|
||||
publishTime: data?.countDown,
|
||||
});
|
||||
|
||||
setValue({
|
||||
sisa: countDown.durationDay,
|
||||
reminder: countDown.reminder,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
setValue({
|
||||
sisa: countDown.durationDay,
|
||||
reminder: countDown.reminder,
|
||||
});
|
||||
};
|
||||
|
||||
const bottomSection = (
|
||||
<Invesment_ComponentBoxOnBottomDetail
|
||||
@@ -97,7 +95,11 @@ export default function InvestmentDetail() {
|
||||
);
|
||||
|
||||
const buttonSection = (
|
||||
<Investment_ButtonInvestasiSection id={id as string} isMine={user?.id === data?.author?.id} reminder={value.reminder} />
|
||||
<Investment_ButtonInvestasiSection
|
||||
id={id as string}
|
||||
isMine={user?.id === data?.author?.id}
|
||||
reminder={value.reminder}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -167,7 +167,7 @@ export default function InvestmentCreate() {
|
||||
text1: "Berhasil",
|
||||
text2: response.message,
|
||||
});
|
||||
router.replace("/investment/portofolio");
|
||||
router.replace("/investment/portofolio?status=review");
|
||||
} else {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
@@ -224,7 +224,6 @@ export default function InvestmentCreate() {
|
||||
onPress={() => {
|
||||
pickFile({
|
||||
setPdfUri: ({ uri, name, size }) => {
|
||||
|
||||
setPdf({ uri, name, size });
|
||||
},
|
||||
allowedType: "pdf",
|
||||
@@ -357,7 +356,11 @@ export default function InvestmentCreate() {
|
||||
)}
|
||||
|
||||
<Spacing />
|
||||
<ButtonCustom isLoading={isLoading} onPress={() => handleSubmit()}>
|
||||
<ButtonCustom
|
||||
disabled={isLoading}
|
||||
isLoading={isLoading}
|
||||
onPress={() => handleSubmit()}
|
||||
>
|
||||
Simpan
|
||||
</ButtonCustom>
|
||||
</StackCustom>
|
||||
|
||||
@@ -1,34 +1,61 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { BackButton } from "@/components";
|
||||
import { IconHome, IconStatus } from "@/components/_Icon";
|
||||
import BackButtonFromNotification from "@/components/Button/BackButtonFromNotification";
|
||||
import { TabsStyles } from "@/styles/tabs-styles";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { Tabs } from "expo-router";
|
||||
import {
|
||||
router,
|
||||
Tabs,
|
||||
useLocalSearchParams,
|
||||
useNavigation
|
||||
} from "expo-router";
|
||||
import { useLayoutEffect } from "react";
|
||||
|
||||
export default function JobTabsLayout() {
|
||||
const navigation = useNavigation();
|
||||
|
||||
const { from, category } = useLocalSearchParams<{
|
||||
from?: string;
|
||||
category?: string;
|
||||
}>();
|
||||
|
||||
// Atur header secara dinamis
|
||||
useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerLeft: () => (
|
||||
<BackButtonFromNotification from={from as string} category={category as string} />
|
||||
),
|
||||
});
|
||||
}, [from, router, navigation]);
|
||||
|
||||
return (
|
||||
<Tabs screenOptions={TabsStyles}>
|
||||
<Tabs.Screen
|
||||
name="index"
|
||||
options={{
|
||||
title: "Beranda",
|
||||
tabBarIcon: ({ color }) => <IconHome color={color} />,
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="status"
|
||||
options={{
|
||||
title: "Status",
|
||||
tabBarIcon: ({ color }) => <IconStatus color={color} />,
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="archive"
|
||||
options={{
|
||||
title: "Arsip",
|
||||
tabBarIcon: ({ color }) => (
|
||||
<Ionicons size={20} name="archive" color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Tabs>
|
||||
<>
|
||||
<Tabs screenOptions={TabsStyles}>
|
||||
<Tabs.Screen
|
||||
name="index"
|
||||
options={{
|
||||
title: "Beranda",
|
||||
tabBarIcon: ({ color }) => <IconHome color={color} />,
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="status"
|
||||
options={{
|
||||
title: "Status",
|
||||
tabBarIcon: ({ color }) => <IconStatus color={color} />,
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="archive"
|
||||
options={{
|
||||
title: "Arsip",
|
||||
tabBarIcon: ({ color }) => (
|
||||
<Ionicons size={20} name="archive" color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Tabs>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,14 +9,17 @@ import {
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
|
||||
import { apiJobGetByStatus } from "@/service/api-client/api-job";
|
||||
import { useFocusEffect } from "expo-router";
|
||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
export default function JobStatus() {
|
||||
const { user } = useAuth();
|
||||
const { status } = useLocalSearchParams<{ status?: string }>();
|
||||
console.log("STATUS", status);
|
||||
|
||||
const [activeCategory, setActiveCategory] = useState<string | null>(
|
||||
"publish"
|
||||
status || "publish"
|
||||
);
|
||||
const [listData, setListData] = useState<any[]>([]);
|
||||
const [isLoadList, setIsLoadList] = useState(false);
|
||||
@@ -60,25 +63,29 @@ export default function JobStatus() {
|
||||
);
|
||||
|
||||
return (
|
||||
<ViewWrapper headerComponent={scrollComponent} hideFooter>
|
||||
{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>
|
||||
<>
|
||||
<ViewWrapper headerComponent={scrollComponent} hideFooter>
|
||||
{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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ export default function JobDetailStatus() {
|
||||
<StackCustom gap={"xs"}>
|
||||
{data &&
|
||||
data?.catatan &&
|
||||
(status === "draft" || status === "rejected") && (
|
||||
(status === "draft" || status === "reject") && (
|
||||
<ReportBox text={data?.catatan} />
|
||||
)}
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ export default function JobDetail() {
|
||||
setIsLoading(true);
|
||||
const response = await apiJobGetOne({ id: id as string });
|
||||
|
||||
console.log("DATA", JSON.stringify(response.data, null,2));
|
||||
|
||||
setData(response.data);
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
|
||||
@@ -19,7 +19,7 @@ import { useState } from "react";
|
||||
import Toast from "react-native-toast-message";
|
||||
|
||||
export default function JobCreate() {
|
||||
const nextUrl = "/(application)/(user)/job/(tabs)/status";
|
||||
const nextUrl = "/(application)/(user)/job/(tabs)/status?status=review";
|
||||
const { user } = useAuth();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [image, setImage] = useState<string | null>(null);
|
||||
|
||||
@@ -1,79 +1,101 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
AlertDefaultSystem,
|
||||
BackButton,
|
||||
BaseBox,
|
||||
Grid,
|
||||
DrawerCustom,
|
||||
MenuDrawerDynamicGrid,
|
||||
NewWrapper,
|
||||
ScrollableCustom,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { useState } from "react";
|
||||
import { View } from "react-native";
|
||||
|
||||
const categories = [
|
||||
{ value: "all", label: "Semua" },
|
||||
{ value: "event", label: "Event" },
|
||||
{ value: "job", label: "Job" },
|
||||
{ value: "voting", label: "Voting" },
|
||||
{ value: "donasi", label: "Donasi" },
|
||||
{ value: "investasi", label: "Investasi" },
|
||||
{ value: "forum", label: "Forum" },
|
||||
{ value: "collaboration", label: "Collaboration" },
|
||||
];
|
||||
import { IconDot } from "@/components/_Icon/IconComponent";
|
||||
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
|
||||
import NoDataText from "@/components/_ShareComponent/NoDataText";
|
||||
import { AccentColor, MainColor } from "@/constants/color-palet";
|
||||
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useNotificationStore } from "@/hooks/use-notification-store";
|
||||
import { apiGetNotificationsById } from "@/service/api-notifications";
|
||||
import { listOfcategoriesAppNotification } from "@/types/type-notification-category";
|
||||
import { formatChatTime } from "@/utils/formatChatTime";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { router, Stack, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
import { RefreshControl, View } from "react-native";
|
||||
|
||||
const selectedCategory = (value: string) => {
|
||||
const category = categories.find((c) => c.value === value);
|
||||
const category = listOfcategoriesAppNotification.find(
|
||||
(c) => c.value === value
|
||||
);
|
||||
return category?.label;
|
||||
};
|
||||
|
||||
const fixPath = ({
|
||||
deepLink,
|
||||
categoryApp,
|
||||
}: {
|
||||
deepLink: string;
|
||||
categoryApp: string;
|
||||
}) => {
|
||||
if (categoryApp === "OTHER") {
|
||||
return deepLink;
|
||||
}
|
||||
|
||||
const separator = deepLink.includes("?") ? "&" : "?";
|
||||
|
||||
const fixedPath = `${deepLink}${separator}from=notifications&category=${_.lowerCase(
|
||||
categoryApp
|
||||
)}`;
|
||||
|
||||
console.log("Fix Path", fixedPath);
|
||||
|
||||
return fixedPath;
|
||||
};
|
||||
|
||||
const BoxNotification = ({
|
||||
index,
|
||||
data,
|
||||
activeCategory,
|
||||
}: {
|
||||
index: number;
|
||||
data: any;
|
||||
activeCategory: string | null;
|
||||
}) => {
|
||||
// console.log("DATA NOTIFICATION", JSON.stringify(data, null, 2));
|
||||
const { markAsRead } = useNotificationStore();
|
||||
return (
|
||||
<>
|
||||
<BaseBox
|
||||
onPress={() =>
|
||||
console.log(
|
||||
"Notification >",
|
||||
selectedCategory(activeCategory as string)
|
||||
)
|
||||
}
|
||||
backgroundColor={data.isRead ? AccentColor.darkblue : AccentColor.blue}
|
||||
onPress={() => {
|
||||
// console.log(
|
||||
// "Notification >",
|
||||
// selectedCategory(activeCategory as string)
|
||||
// );
|
||||
const newPath = fixPath({
|
||||
deepLink: data.deepLink,
|
||||
categoryApp: data.kategoriApp,
|
||||
});
|
||||
|
||||
router.navigate(newPath as any);
|
||||
selectedCategory(activeCategory as string);
|
||||
|
||||
if (!data.isRead) {
|
||||
markAsRead(data.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<StackCustom>
|
||||
<TextCustom bold>
|
||||
# {selectedCategory(activeCategory as string)}
|
||||
<TextCustom truncate={2} bold>
|
||||
{data.title}
|
||||
</TextCustom>
|
||||
|
||||
<View
|
||||
style={{
|
||||
borderBottomColor: MainColor.white_gray,
|
||||
borderBottomWidth: 1,
|
||||
}}
|
||||
/>
|
||||
<TextCustom truncate={2}>{data.pesan}</TextCustom>
|
||||
|
||||
<TextCustom truncate={2}>
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Sint odio
|
||||
unde quidem voluptate quam culpa sequi molestias ipsa corrupti id,
|
||||
soluta, nostrum adipisci similique, et illo asperiores deleniti eum
|
||||
labore.
|
||||
<TextCustom size="small" color="gray">
|
||||
{formatChatTime(data.createdAt)}
|
||||
</TextCustom>
|
||||
|
||||
<Grid>
|
||||
<Grid.Col span={6}>
|
||||
<TextCustom size="small" color="gray">
|
||||
{index + 1} Agustus 2025
|
||||
</TextCustom>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6} style={{ alignItems: "flex-end" }}>
|
||||
<TextCustom size="small" color="gray">
|
||||
Belum lihat
|
||||
</TextCustom>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</StackCustom>
|
||||
</BaseBox>
|
||||
</>
|
||||
@@ -81,31 +103,146 @@ const BoxNotification = ({
|
||||
};
|
||||
|
||||
export default function Notifications() {
|
||||
const [activeCategory, setActiveCategory] = useState<string | null>("all");
|
||||
const { user } = useAuth();
|
||||
const { category } = useLocalSearchParams<{ category?: string }>();
|
||||
const [activeCategory, setActiveCategory] = useState<string | null>(
|
||||
category || "event"
|
||||
);
|
||||
const [listData, setListData] = useState<any[]>([]);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [openDrawer, setOpenDrawer] = useState(false);
|
||||
|
||||
const { markAsReadAll } = useNotificationStore();
|
||||
|
||||
const handlePress = (item: any) => {
|
||||
setActiveCategory(item.value);
|
||||
// tambahkan logika lain seperti filter dsb.
|
||||
};
|
||||
return (
|
||||
<ViewWrapper
|
||||
headerComponent={
|
||||
<ScrollableCustom
|
||||
data={categories.map((e, i) => ({
|
||||
id: i,
|
||||
label: e.label,
|
||||
value: e.value,
|
||||
}))}
|
||||
onButtonPress={handlePress}
|
||||
activeId={activeCategory as string}
|
||||
/>
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
fecthData();
|
||||
}, [activeCategory])
|
||||
);
|
||||
|
||||
const fecthData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await apiGetNotificationsById({
|
||||
id: user?.id as any,
|
||||
category: activeCategory as any,
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
setListData(response.data);
|
||||
} else {
|
||||
setListData([]);
|
||||
}
|
||||
>
|
||||
{Array.from({ length: 20 }).map((e, i) => (
|
||||
<View key={i}>
|
||||
<BoxNotification index={i} activeCategory={activeCategory as any} />
|
||||
</View>
|
||||
))}
|
||||
</ViewWrapper>
|
||||
} catch (error) {
|
||||
console.log("Error Notification", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onRefresh = () => {
|
||||
setRefreshing(true);
|
||||
fecthData();
|
||||
setRefreshing(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: "Notifikasi",
|
||||
headerLeft: () => <BackButton />,
|
||||
headerRight: () => (
|
||||
<IconDot
|
||||
color={MainColor.yellow}
|
||||
onPress={() => setOpenDrawer(true)}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
<NewWrapper
|
||||
headerComponent={
|
||||
<ScrollableCustom
|
||||
data={listOfcategoriesAppNotification.map((e, i) => ({
|
||||
id: i,
|
||||
label: e.label,
|
||||
value: e.value,
|
||||
}))}
|
||||
onButtonPress={handlePress}
|
||||
activeId={activeCategory as string}
|
||||
/>
|
||||
}
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
|
||||
}
|
||||
>
|
||||
{loading ? (
|
||||
<ListSkeletonComponent />
|
||||
) : _.isEmpty(listData) ? (
|
||||
<NoDataText text="Belum ada notifikasi" />
|
||||
) : (
|
||||
listData.map((e, i) => (
|
||||
<View key={i}>
|
||||
<BoxNotification
|
||||
data={e}
|
||||
activeCategory={activeCategory as any}
|
||||
/>
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</NewWrapper>
|
||||
|
||||
<DrawerCustom
|
||||
isVisible={openDrawer}
|
||||
closeDrawer={() => setOpenDrawer(false)}
|
||||
height={"auto"}
|
||||
>
|
||||
<MenuDrawerDynamicGrid
|
||||
data={[
|
||||
{
|
||||
label: "Tandai Semua Dibaca",
|
||||
value: "read-all",
|
||||
icon: (
|
||||
<Ionicons
|
||||
name="reader-outline"
|
||||
size={ICON_SIZE_SMALL}
|
||||
color={MainColor.white}
|
||||
/>
|
||||
),
|
||||
path: "",
|
||||
},
|
||||
]}
|
||||
onPressItem={(item: any) => {
|
||||
console.log("Item", item.value);
|
||||
if (item.value === "read-all") {
|
||||
AlertDefaultSystem({
|
||||
title: "Tandai Semua Dibaca",
|
||||
message:
|
||||
"Apakah Anda yakin ingin menandai semua notifikasi dibaca?",
|
||||
textLeft: "Batal",
|
||||
textRight: "Ya",
|
||||
onPressRight: () => {
|
||||
markAsReadAll(user?.id as any);
|
||||
const data = _.cloneDeep(listData);
|
||||
data.forEach((e) => {
|
||||
e.isRead = true;
|
||||
});
|
||||
setListData(data);
|
||||
onRefresh();
|
||||
setOpenDrawer(false);
|
||||
},
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</DrawerCustom>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,9 +31,9 @@ import {
|
||||
import pickImage from "@/utils/pickImage";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { Image } from "expo-image";
|
||||
import { useLocalSearchParams } from "expo-router";
|
||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Text, TouchableOpacity, View } from "react-native";
|
||||
import PhoneInput, { ICountry } from "react-native-international-phone-number";
|
||||
import { Avatar } from "react-native-paper";
|
||||
@@ -76,7 +76,7 @@ export default function PortofolioCreate() {
|
||||
function handleInputValue(phoneNumber: string) {
|
||||
setInputValue(phoneNumber);
|
||||
const callingCode = selectedCountry?.callingCode.replace(/^\+/, "") || "";
|
||||
const fixNumber = inputValue.replace(/\s+/g, "");
|
||||
let fixNumber = inputValue.replace(/\s+/g, "").replace(/^0+/, "");
|
||||
const realNumber = callingCode + fixNumber;
|
||||
setData({ ...data, tlpn: realNumber });
|
||||
}
|
||||
@@ -85,10 +85,12 @@ export default function PortofolioCreate() {
|
||||
setSelectedCountry(country);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
onLoadMaster();
|
||||
onLoadMasterSubBidangBisnis();
|
||||
}, []);
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadMaster();
|
||||
onLoadMasterSubBidangBisnis();
|
||||
}, [])
|
||||
);
|
||||
|
||||
const onLoadMaster = async () => {
|
||||
try {
|
||||
|
||||
@@ -244,7 +244,7 @@ export default function PortofolioEdit() {
|
||||
|
||||
const handleSubmitUpdate = async () => {
|
||||
const callingCode = selectedCountry?.callingCode.replace(/^\+/, "") || "";
|
||||
const fixNumber = data.tlpn.replace(/\s+/g, "");
|
||||
let fixNumber = data.tlpn.replace(/\s+/g, "").replace(/^0+/, "");
|
||||
const realNumber = callingCode + fixNumber;
|
||||
|
||||
const newData: IFormData = {
|
||||
|
||||
@@ -139,7 +139,9 @@ const ButtonnDot = ({
|
||||
isUserCheck: boolean;
|
||||
logout: () => Promise<void>;
|
||||
}) => {
|
||||
const isId = id === undefined || id === null;
|
||||
console.log("[ID] >>", id);
|
||||
|
||||
const isId = id === undefined || id === "undefined";
|
||||
|
||||
if (isId) {
|
||||
return (
|
||||
|
||||
75
app/(application)/(user)/test-notifications.tsx
Normal file
75
app/(application)/(user)/test-notifications.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import {
|
||||
ButtonCustom,
|
||||
NewWrapper,
|
||||
StackCustom,
|
||||
TextInputCustom,
|
||||
} from "@/components";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { apiNotificationsSend } from "@/service/api-notifications";
|
||||
import { useState } from "react";
|
||||
import Toast from "react-native-toast-message";
|
||||
|
||||
export default function TestNotification() {
|
||||
const { user } = useAuth();
|
||||
const [data, setData] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
console.log("[Data Dikirim]", data);
|
||||
setLoading(true);
|
||||
const response = await apiNotificationsSend({
|
||||
data: {
|
||||
title: "Test Notification !!",
|
||||
body: data,
|
||||
userLoginId: user?.id || "",
|
||||
appId: "hipmi",
|
||||
status: "publish",
|
||||
kategoriApp: "JOB",
|
||||
type: "announcement",
|
||||
deepLink: "/job/cmhjz8u3h0005cfaxezyeilrr",
|
||||
},
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
console.log("[RES SEND NOTIF]", JSON.stringify(response, null, 2));
|
||||
Toast.show({
|
||||
type: "success",
|
||||
text1: "Notifikasi berhasil dikirim",
|
||||
});
|
||||
} else {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
text1: "Gagal mengirim notifikasi",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[ERROR SEND NOTIF]", error);
|
||||
Toast.show({
|
||||
type: "error",
|
||||
text1: "Gagal mengirim notifikasi",
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<NewWrapper>
|
||||
<StackCustom>
|
||||
<TextInputCustom
|
||||
required
|
||||
label="Pesan"
|
||||
placeholder="Masukkan pesan"
|
||||
value={data}
|
||||
onChangeText={(text) => setData(text)}
|
||||
/>
|
||||
<ButtonCustom onPress={handleSubmit} disabled={loading}>
|
||||
Kirim
|
||||
</ButtonCustom>
|
||||
</StackCustom>
|
||||
</NewWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,37 @@
|
||||
import {
|
||||
IconContribution,
|
||||
IconHistory,
|
||||
IconHome,
|
||||
IconStatus,
|
||||
IconContribution,
|
||||
IconHistory,
|
||||
IconHome,
|
||||
IconStatus,
|
||||
} from "@/components/_Icon";
|
||||
import BackButtonFromNotification from "@/components/Button/BackButtonFromNotification";
|
||||
import { TabsStyles } from "@/styles/tabs-styles";
|
||||
import { Tabs } from "expo-router";
|
||||
import { Tabs, useLocalSearchParams, useNavigation, router } from "expo-router";
|
||||
import { useLayoutEffect } from "react";
|
||||
|
||||
export default function VotingTabsLayout() {
|
||||
const navigation = useNavigation();
|
||||
|
||||
const { from, category } = useLocalSearchParams<{
|
||||
from?: string;
|
||||
category?: string;
|
||||
}>();
|
||||
|
||||
console.log("from", from);
|
||||
console.log("category", category);
|
||||
|
||||
// Atur header secara dinamis
|
||||
useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerLeft: () => (
|
||||
<BackButtonFromNotification
|
||||
from={from as string}
|
||||
category={category as string}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}, [from, router, navigation]);
|
||||
|
||||
return (
|
||||
<Tabs screenOptions={TabsStyles}>
|
||||
<Tabs.Screen
|
||||
|
||||
@@ -12,15 +12,17 @@ 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 { useFocusEffect } from "expo-router";
|
||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
export default function VotingStatus() {
|
||||
const { user } = useAuth();
|
||||
const { status } = useLocalSearchParams<{ status?: string }>();
|
||||
|
||||
const id = user?.id || "";
|
||||
const [activeCategory, setActiveCategory] = useState<string | null>(
|
||||
"publish"
|
||||
status || "publish"
|
||||
);
|
||||
|
||||
const [listData, setListData] = useState([]);
|
||||
@@ -86,8 +88,14 @@ export default function VotingStatus() {
|
||||
style={{ width: "70%", alignSelf: "center" }}
|
||||
variant="light"
|
||||
>
|
||||
{item?.awalVote && dateTimeView({date: item?.awalVote, withoutTime: true})} -{" "}
|
||||
{item?.akhirVote && dateTimeView({date: item?.akhirVote, withoutTime: true})}
|
||||
{item?.awalVote &&
|
||||
dateTimeView({
|
||||
date: item?.awalVote,
|
||||
withoutTime: true,
|
||||
})}{" "}
|
||||
-{" "}
|
||||
{item?.akhirVote &&
|
||||
dateTimeView({ date: item?.akhirVote, withoutTime: true })}
|
||||
</BadgeCustom>
|
||||
</StackCustom>
|
||||
</BaseBox>
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from "@/components";
|
||||
import { IconArchive, IconContribution } from "@/components/_Icon";
|
||||
import { IMenuDrawerItem } from "@/components/_Interface/types";
|
||||
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import Voting_BoxDetailHasilVotingSection from "@/screens/Voting/BoxDetailHasilVotingSection";
|
||||
import { Voting_BoxDetailPublishSection } from "@/screens/Voting/BoxDetailPublishSection";
|
||||
@@ -22,13 +23,14 @@ import {
|
||||
apiVotingUpdateData,
|
||||
} from "@/service/api-client/api-voting";
|
||||
import { today } from "@/utils/dateTimeView";
|
||||
import dayjs from "dayjs";
|
||||
import {
|
||||
router,
|
||||
Stack,
|
||||
useFocusEffect,
|
||||
useLocalSearchParams,
|
||||
} from "expo-router";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import Toast from "react-native-toast-message";
|
||||
|
||||
export default function VotingDetail() {
|
||||
@@ -119,6 +121,23 @@ export default function VotingDetail() {
|
||||
setOpenDrawerPublish(false);
|
||||
};
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const isEventFinished = id && data?.akhirVote && dayjs(data.akhirVote).isBefore(now);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEventFinished) {
|
||||
router.replace(`/(application)/(user)/voting/${id}/history`);
|
||||
}
|
||||
}, [isEventFinished, id]);
|
||||
|
||||
if (isEventFinished) {
|
||||
return (
|
||||
<ViewWrapper>
|
||||
<CustomSkeleton />
|
||||
</ViewWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen
|
||||
|
||||
@@ -79,7 +79,7 @@ export default function VotingCreate() {
|
||||
type: "success",
|
||||
text1: "Data berhasil disimpan",
|
||||
});
|
||||
router.replace("/(application)/(user)/voting/(tabs)/status");
|
||||
router.replace("/(application)/(user)/voting/(tabs)/status?status=review");
|
||||
} else {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
@@ -110,7 +110,7 @@ export default function VotingCreate() {
|
||||
<StackCustom gap={"xs"}>
|
||||
<TextInputCustom
|
||||
label="Judul Voting"
|
||||
placeholder="MasukanJudul Voting"
|
||||
placeholder="Masukan Judul Voting"
|
||||
required
|
||||
value={data.title}
|
||||
onChangeText={(value: any) => setData({ ...data, title: value })}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import {
|
||||
AlertDefaultSystem,
|
||||
BoxButtonOnFooter,
|
||||
ButtonCenteredOnly,
|
||||
ButtonCustom,
|
||||
InformationBox,
|
||||
NewWrapper,
|
||||
StackCustom,
|
||||
ViewWrapper,
|
||||
StackCustom
|
||||
} from "@/components";
|
||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
|
||||
@@ -1,15 +1,28 @@
|
||||
import { BackButton } from "@/components";
|
||||
import BackgroundNotificationHandler from "@/components/Notification/BackgroundNotificationHandler";
|
||||
import NotificationInitializer from "@/components/Notification/NotificationInitializer";
|
||||
import { NotificationProvider } from "@/hooks/use-notification-store";
|
||||
import { HeaderStyles } from "@/styles/header-styles";
|
||||
import { Stack } from "expo-router";
|
||||
|
||||
export default function ApplicationLayout() {
|
||||
return (
|
||||
<>
|
||||
<NotificationProvider>
|
||||
<NotificationInitializer />
|
||||
<BackgroundNotificationHandler />
|
||||
<ApplicationStack />
|
||||
</NotificationProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ApplicationStack() {
|
||||
return (
|
||||
<>
|
||||
<Stack screenOptions={HeaderStyles}>
|
||||
<Stack.Screen name="(user)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="admin" options={{ headerShown: false }} />
|
||||
|
||||
|
||||
|
||||
{/* Take Picture */}
|
||||
<Stack.Screen
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
ICON_SIZE_XLARGE,
|
||||
} from "@/constants/constans-value";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import AdminNotificationBell from "@/screens/Admin/AdminNotificationBell";
|
||||
import {
|
||||
adminListMenu,
|
||||
superAdminListMenu,
|
||||
@@ -192,11 +193,12 @@ export default function AdminLayout() {
|
||||
label: "Notifikasi",
|
||||
value: "notification",
|
||||
icon: (
|
||||
<Ionicons
|
||||
name="notifications"
|
||||
size={ICON_SIZE_SMALL}
|
||||
color={MainColor.white}
|
||||
/>
|
||||
// <Ionicons
|
||||
// name="notifications"
|
||||
// size={ICON_SIZE_SMALL}
|
||||
// color={MainColor.white}
|
||||
// />
|
||||
<AdminNotificationBell/>
|
||||
),
|
||||
path: "/admin/notification",
|
||||
},
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
BoxButtonOnFooter,
|
||||
ButtonCustom,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
TextInputCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import {
|
||||
apiAdminMasterBusinessFieldById,
|
||||
apiAdminMasterBusinessFieldUpdate,
|
||||
} from "@/service/api-admin/api-master-admin";
|
||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import { useCallback, useState } from "react";
|
||||
import { Switch } from "react-native-paper";
|
||||
import Toast from "react-native-toast-message";
|
||||
|
||||
export default function AdminAppInformation_BusinessFieldDetail() {
|
||||
const { id } = useLocalSearchParams();
|
||||
const [data, setData] = useState<any | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadDetail();
|
||||
}, [id])
|
||||
);
|
||||
|
||||
const onLoadDetail = async () => {
|
||||
try {
|
||||
const response = await apiAdminMasterBusinessFieldById({
|
||||
id: id as string,
|
||||
category: "bidang"
|
||||
});
|
||||
|
||||
setData(response.data);
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
setData(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handlerSubmit = async () => {
|
||||
if (!data.name) {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
text1: "Lengkapi Data",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await apiAdminMasterBusinessFieldUpdate({
|
||||
id: id as string,
|
||||
data: data,
|
||||
category: "bidang",
|
||||
});
|
||||
|
||||
if (!response.success) {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
text1: "Gagal update data",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Toast.show({
|
||||
type: "success",
|
||||
text1: "Data berhasil di update",
|
||||
});
|
||||
router.back();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const buttonSubmit = (
|
||||
<BoxButtonOnFooter>
|
||||
<ButtonCustom
|
||||
disabled={!data?.name}
|
||||
isLoading={isLoading}
|
||||
onPress={() => handlerSubmit()}
|
||||
>
|
||||
Update
|
||||
</ButtonCustom>
|
||||
</BoxButtonOnFooter>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper footerComponent={buttonSubmit}>
|
||||
<StackCustom>
|
||||
<AdminBackButtonAntTitle title="Update Bidang Bisnis" />
|
||||
|
||||
<TextInputCustom
|
||||
label="Nama Bidang Bisnis"
|
||||
placeholder="Masukan Nama Bidang Bisnis"
|
||||
required
|
||||
value={data?.name}
|
||||
onChangeText={(value) => setData({ ...data, name: value })}
|
||||
/>
|
||||
|
||||
<StackCustom
|
||||
gap={"sm"}
|
||||
style={{
|
||||
alignContent: "flex-start",
|
||||
}}
|
||||
>
|
||||
<TextCustom>Status</TextCustom>
|
||||
|
||||
<Switch
|
||||
style={{
|
||||
alignSelf: "flex-start",
|
||||
}}
|
||||
color={MainColor.yellow}
|
||||
value={data?.active}
|
||||
onValueChange={(value) => setData({ ...data, active: value })}
|
||||
/>
|
||||
</StackCustom>
|
||||
</StackCustom>
|
||||
</ViewWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,22 +1,22 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
BoxButtonOnFooter,
|
||||
ButtonCustom,
|
||||
ActionIcon,
|
||||
BaseBox,
|
||||
CenterCustom,
|
||||
LoaderCustom,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
TextInputCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import { IconEdit } from "@/components/_Icon";
|
||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
||||
import { GridSpan_NewComponent } from "@/components/_ShareComponent/GridSpan_NewComponent";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import {
|
||||
apiAdminMasterBusinessFieldById,
|
||||
apiAdminMasterBusinessFieldUpdate,
|
||||
} from "@/service/api-admin/api-master-admin";
|
||||
import { apiAdminMasterBusinessFieldById } from "@/service/api-admin/api-master-admin";
|
||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import { useCallback, useState } from "react";
|
||||
import { Switch } from "react-native-paper";
|
||||
import Toast from "react-native-toast-message";
|
||||
import { Divider } from "react-native-paper";
|
||||
|
||||
export default function AdminAppInformation_BusinessFieldDetail() {
|
||||
const { id } = useLocalSearchParams();
|
||||
@@ -33,8 +33,11 @@ export default function AdminAppInformation_BusinessFieldDetail() {
|
||||
try {
|
||||
const response = await apiAdminMasterBusinessFieldById({
|
||||
id: id as string,
|
||||
category: "all",
|
||||
});
|
||||
|
||||
console.log("Response >>", JSON.stringify(response, null, 2));
|
||||
|
||||
setData(response.data);
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
@@ -42,73 +45,89 @@ export default function AdminAppInformation_BusinessFieldDetail() {
|
||||
}
|
||||
};
|
||||
|
||||
const handlerSubmit = async () => {
|
||||
if (!data.name) {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
text1: "Lengkapi Data",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await apiAdminMasterBusinessFieldUpdate({
|
||||
id: id as string,
|
||||
data: data,
|
||||
});
|
||||
|
||||
if (!response.success) {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
text1: "Gagal update data",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Toast.show({
|
||||
type: "success",
|
||||
text1: "Data berhasil di update",
|
||||
});
|
||||
router.back();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const buttonSubmit = (
|
||||
<BoxButtonOnFooter>
|
||||
<ButtonCustom
|
||||
disabled={!data?.name}
|
||||
isLoading={isLoading}
|
||||
onPress={() => handlerSubmit()}
|
||||
>
|
||||
Update
|
||||
</ButtonCustom>
|
||||
</BoxButtonOnFooter>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper footerComponent={buttonSubmit}>
|
||||
<ViewWrapper>
|
||||
<StackCustom>
|
||||
<AdminBackButtonAntTitle title="Update Bidang Bisnis" />
|
||||
<AdminBackButtonAntTitle title="Detail Bidang & Sub Bidang" />
|
||||
|
||||
<TextInputCustom
|
||||
label="Nama Bidang Bisnis"
|
||||
placeholder="Masukan Nama Bidang Bisnis"
|
||||
required
|
||||
value={data?.name}
|
||||
onChangeText={(value) => setData({ ...data, name: value })}
|
||||
/>
|
||||
{!data ? (
|
||||
<LoaderCustom />
|
||||
) : (
|
||||
<StackCustom gap={"xs"}>
|
||||
<TextCustom bold>Nama Bidang</TextCustom>
|
||||
<Spacing height={5} />
|
||||
<BaseBox>
|
||||
<StackCustom gap={"xs"}>
|
||||
<TextCustom bold>
|
||||
Status: {data?.bidang?.active ? "Aktif" : "Tidak Aktif"}
|
||||
</TextCustom>
|
||||
<GridSpan_NewComponent
|
||||
span1={10}
|
||||
span2={2}
|
||||
text1={
|
||||
<TextCustom bold size={"large"}>
|
||||
{data?.bidang?.name}
|
||||
</TextCustom>
|
||||
}
|
||||
text2={
|
||||
<CenterCustom>
|
||||
<ActionIcon
|
||||
icon={<IconEdit size={16} color={MainColor.black} />}
|
||||
onPress={() =>
|
||||
router.push(
|
||||
`/admin/app-information/business-field/${id}/bidang-update`
|
||||
)
|
||||
}
|
||||
/>
|
||||
</CenterCustom>
|
||||
}
|
||||
/>
|
||||
</StackCustom>
|
||||
</BaseBox>
|
||||
{/* <Divider /> */}
|
||||
<Spacing height={5} />
|
||||
|
||||
<TextCustom>Status Aktivasi</TextCustom>
|
||||
<Switch
|
||||
color={MainColor.yellow}
|
||||
value={data?.active}
|
||||
onValueChange={(value) => setData({ ...data, active: value })}
|
||||
/>
|
||||
<TextCustom bold>Sub Bidang Bisnis</TextCustom>
|
||||
<Spacing height={5} />
|
||||
|
||||
{data?.subBidang?.map((item: any, index: number) => (
|
||||
<BaseBox key={index}>
|
||||
<StackCustom gap={0}>
|
||||
<TextCustom bold>
|
||||
Status: {item?.isActive ? "Aktif" : "Tidak Aktif"}
|
||||
</TextCustom>
|
||||
|
||||
<GridSpan_NewComponent
|
||||
span1={10}
|
||||
span2={2}
|
||||
text1={
|
||||
<TextCustom bold size={"large"}>
|
||||
{item.name}
|
||||
</TextCustom>
|
||||
}
|
||||
text2={
|
||||
<CenterCustom>
|
||||
<ActionIcon
|
||||
icon={
|
||||
<IconEdit size={16} color={MainColor.black} />
|
||||
}
|
||||
onPress={() =>
|
||||
router.push(
|
||||
`/admin/app-information/business-field/${item?.id}/sub-bidang-update`
|
||||
)
|
||||
}
|
||||
/>
|
||||
</CenterCustom>
|
||||
}
|
||||
/>
|
||||
</StackCustom>
|
||||
</BaseBox>
|
||||
))}
|
||||
</StackCustom>
|
||||
)}
|
||||
|
||||
{/* <TextCustom>{JSON.stringify(data, null, 2)}</TextCustom> */}
|
||||
</StackCustom>
|
||||
</ViewWrapper>
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
BoxButtonOnFooter,
|
||||
ButtonCustom,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
TextInputCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import {
|
||||
apiAdminMasterBusinessFieldById,
|
||||
apiAdminMasterBusinessFieldUpdate,
|
||||
} from "@/service/api-admin/api-master-admin";
|
||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import { useCallback, useState } from "react";
|
||||
import { Switch } from "react-native-paper";
|
||||
import Toast from "react-native-toast-message";
|
||||
|
||||
export default function AdminAppInformation_BusinessFieldDetail() {
|
||||
const { id } = useLocalSearchParams();
|
||||
const [data, setData] = useState<any | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadDetail();
|
||||
}, [id])
|
||||
);
|
||||
|
||||
const onLoadDetail = async () => {
|
||||
try {
|
||||
const response = await apiAdminMasterBusinessFieldById({
|
||||
id: id as string,
|
||||
category: "sub-bidang",
|
||||
subBidangId: id as string,
|
||||
});
|
||||
|
||||
console.log("Response >>", JSON.stringify(response, null, 2));
|
||||
|
||||
|
||||
|
||||
|
||||
setData(response.data);
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
setData(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handlerSubmit = async () => {
|
||||
if (!data.name) {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
text1: "Lengkapi Data",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await apiAdminMasterBusinessFieldUpdate({
|
||||
id: id as string,
|
||||
data: data,
|
||||
category: "sub-bidang",
|
||||
});
|
||||
|
||||
if (!response.success) {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
text1: "Gagal update data",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Toast.show({
|
||||
type: "success",
|
||||
text1: "Data berhasil di update",
|
||||
});
|
||||
router.back();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const buttonSubmit = (
|
||||
<BoxButtonOnFooter>
|
||||
<ButtonCustom
|
||||
disabled={!data?.name}
|
||||
isLoading={isLoading}
|
||||
onPress={() => handlerSubmit()}
|
||||
>
|
||||
Update
|
||||
</ButtonCustom>
|
||||
</BoxButtonOnFooter>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper footerComponent={buttonSubmit}>
|
||||
<StackCustom>
|
||||
<AdminBackButtonAntTitle title="Update Bidang Bisnis" />
|
||||
|
||||
<TextInputCustom
|
||||
label="Nama Bidang Bisnis"
|
||||
placeholder="Masukan Nama Bidang Bisnis"
|
||||
required
|
||||
value={data?.name}
|
||||
onChangeText={(value) => setData({ ...data, name: value })}
|
||||
/>
|
||||
|
||||
<StackCustom
|
||||
gap={"sm"}
|
||||
style={{
|
||||
alignContent: "flex-start",
|
||||
}}
|
||||
>
|
||||
<TextCustom>Status</TextCustom>
|
||||
|
||||
<Switch
|
||||
style={{
|
||||
alignSelf: "flex-start",
|
||||
}}
|
||||
color={MainColor.yellow}
|
||||
value={data?.isActive}
|
||||
onValueChange={(value) => setData({ ...data, isActive: value })}
|
||||
/>
|
||||
</StackCustom>
|
||||
</StackCustom>
|
||||
</ViewWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,40 +1,81 @@
|
||||
import {
|
||||
BoxButtonOnFooter,
|
||||
ButtonCustom,
|
||||
StackCustom,
|
||||
TextInputCustom,
|
||||
ViewWrapper,
|
||||
ActionIcon,
|
||||
BoxButtonOnFooter,
|
||||
ButtonCustom,
|
||||
CenterCustom,
|
||||
Grid,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextInputCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { ICON_SIZE_XLARGE } from "@/constants/constans-value";
|
||||
import { apiAdminMasterBusinessFieldCreate } from "@/service/api-admin/api-master-admin";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { router } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { Divider } from "react-native-paper";
|
||||
import Toast from "react-native-toast-message";
|
||||
|
||||
export default function AdminAppInformation_BusinessFieldCreate() {
|
||||
const [data, setData] = useState<any>({
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [bidang, setBidang] = useState<any>({
|
||||
name: "",
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [subBidang, setSubBidang] = useState<any[]>([
|
||||
{
|
||||
name: "",
|
||||
},
|
||||
]);
|
||||
|
||||
const handlerSubmit = async () => {
|
||||
if (!data.name) {
|
||||
if (!bidang.name) {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
text1: "Lengkapi Data",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (subBidang[0].name === "") {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
text1: "Lengkapi Sub Bidang",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await apiAdminMasterBusinessFieldCreate({ data: data });
|
||||
|
||||
const newData = {
|
||||
bidang: bidang,
|
||||
subBidang: subBidang,
|
||||
};
|
||||
|
||||
console.log("[DATA]", newData);
|
||||
|
||||
const response = await apiAdminMasterBusinessFieldCreate({
|
||||
data: newData,
|
||||
});
|
||||
|
||||
console.log("[RESPONSE]", response);
|
||||
|
||||
if (response.success) {
|
||||
Toast.show({
|
||||
type: "success",
|
||||
text1: "Data berhasil di tambah",
|
||||
});
|
||||
router.back();
|
||||
// router.back();
|
||||
} else {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
text1: "Gagal tambah data",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
@@ -50,6 +91,7 @@ export default function AdminAppInformation_BusinessFieldCreate() {
|
||||
const buttonSubmit = (
|
||||
<BoxButtonOnFooter>
|
||||
<ButtonCustom
|
||||
disabled={subBidang[0].name === ""}
|
||||
onPress={() => handlerSubmit()}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
@@ -60,16 +102,70 @@ export default function AdminAppInformation_BusinessFieldCreate() {
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper footerComponent={buttonSubmit}>
|
||||
<StackCustom>
|
||||
<StackCustom gap={"xs"}>
|
||||
<AdminBackButtonAntTitle title="Tambah Bidang Bisnis" />
|
||||
|
||||
<TextInputCustom
|
||||
label="Nama Bidang Bisnis"
|
||||
placeholder="Masukan Nama Bidang Bisnis"
|
||||
required
|
||||
value={data.name}
|
||||
onChangeText={(value) => setData({ ...data, name: value })}
|
||||
value={bidang.name}
|
||||
onChangeText={(value) => setBidang({ ...bidang, name: value })}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
<Spacing height={5} />
|
||||
|
||||
{subBidang.map((item, index) => (
|
||||
<TextInputCustom
|
||||
key={index}
|
||||
label="Nama Sub Bidang"
|
||||
placeholder="Masukan Nama Sub Bidang"
|
||||
required
|
||||
value={item.name}
|
||||
onChangeText={(value) => {
|
||||
const list = _.clone(subBidang);
|
||||
list[index].name = value;
|
||||
setSubBidang(list);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
<CenterCustom>
|
||||
<View
|
||||
style={{ flexDirection: "row", alignItems: "center", gap: 10 }}
|
||||
>
|
||||
<ActionIcon
|
||||
onPress={() => {
|
||||
setSubBidang([...subBidang, { name: "" }]);
|
||||
}}
|
||||
icon={
|
||||
<Ionicons
|
||||
name="add-circle-outline"
|
||||
size={ICON_SIZE_XLARGE}
|
||||
color={MainColor.black}
|
||||
/>
|
||||
}
|
||||
size="xl"
|
||||
/>
|
||||
<ActionIcon
|
||||
disabled={subBidang.length <= 1}
|
||||
onPress={() => {
|
||||
const list = _.clone(subBidang);
|
||||
list.pop();
|
||||
setSubBidang(list);
|
||||
}}
|
||||
icon={
|
||||
<Ionicons
|
||||
name="remove-circle-outline"
|
||||
size={ICON_SIZE_XLARGE}
|
||||
color={MainColor.black}
|
||||
/>
|
||||
}
|
||||
size="xl"
|
||||
/>
|
||||
</View>
|
||||
</CenterCustom>
|
||||
</StackCustom>
|
||||
</ViewWrapper>
|
||||
</>
|
||||
|
||||
@@ -75,7 +75,7 @@ const listPage = [
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
label: "Bidang Bisnis",
|
||||
label: "Bidang & Sub Bidang",
|
||||
value: "business",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
import {
|
||||
BaseBox,
|
||||
Grid,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
||||
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
|
||||
import { apiAdminCollaborationGetById } from "@/service/api-admin/api-admin-collaboration";
|
||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import { useCallback, useState } from "react";
|
||||
@@ -28,6 +30,8 @@ export default function AdminCollaborationGroup() {
|
||||
category: "group",
|
||||
});
|
||||
|
||||
console.log("[DATA]", JSON.stringify(response.data, null, 2));
|
||||
|
||||
if (response.success) {
|
||||
setData(response.data);
|
||||
}
|
||||
@@ -59,38 +63,33 @@ export default function AdminCollaborationGroup() {
|
||||
))}
|
||||
</StackCustom>
|
||||
</BaseBox>
|
||||
|
||||
<TextCustom bold>Anggota</TextCustom>
|
||||
<Spacing height={5}/>
|
||||
<BaseBox>
|
||||
<StackCustom>
|
||||
<TextCustom align="center">Anggota</TextCustom>
|
||||
|
||||
<Grid>
|
||||
<Grid.Col span={6} style={{ justifyContent: "center", paddingRight: 10 }}>
|
||||
<TextCustom bold>Nomor</TextCustom>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6} style={{ justifyContent: "center" }}>
|
||||
<TextCustom bold>Username</TextCustom>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
|
||||
{data?.ProjectCollaboration_AnggotaRoomChat?.map(
|
||||
(item: any, index: number) => (
|
||||
<StackCustom key={index} gap={0}>
|
||||
<Grid>
|
||||
<Grid.Col
|
||||
span={4}
|
||||
style={{ justifyContent: "center", paddingRight: 10 }}
|
||||
>
|
||||
<TextCustom bold>Nama</TextCustom>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={8} style={{ justifyContent: "center" }}>
|
||||
<TextCustom>
|
||||
{item?.User?.Profile?.name || "-"}
|
||||
</TextCustom>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<Grid.Col
|
||||
span={4}
|
||||
style={{ justifyContent: "center", paddingRight: 10 }}
|
||||
>
|
||||
<TextCustom bold>Username</TextCustom>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={8} style={{ justifyContent: "center" }}>
|
||||
<TextCustom>{item?.User?.username || "-"}</TextCustom>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</StackCustom>
|
||||
<Grid key={index}>
|
||||
<Grid.Col span={6} style={{ justifyContent: "center", paddingRight: 10 }}>
|
||||
<TextCustom bold truncate>+{item?.User?.nomor || "-"}</TextCustom>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6} style={{ justifyContent: "center" }}>
|
||||
<TextCustom bold>
|
||||
{item?.User?.username || "-"}
|
||||
</TextCustom>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
)
|
||||
)}
|
||||
</StackCustom>
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import {
|
||||
ActionIcon,
|
||||
ClickableCustom,
|
||||
LoaderCustom,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
ViewWrapper
|
||||
} from "@/components";
|
||||
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
||||
import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle";
|
||||
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
|
||||
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
|
||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
||||
import { GridSpan_NewComponent } from "@/components/_ShareComponent/GridSpan_NewComponent";
|
||||
import { apiAdminCollaboration } from "@/service/api-admin/api-admin-collaboration";
|
||||
import { Octicons } from "@expo/vector-icons";
|
||||
import { router, useFocusEffect } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
@@ -34,7 +31,7 @@ export default function AdminCollaborationGroup() {
|
||||
const response = await apiAdminCollaboration({
|
||||
category: "group",
|
||||
});
|
||||
|
||||
|
||||
if (response.success) {
|
||||
setList(response.data);
|
||||
}
|
||||
@@ -51,10 +48,19 @@ export default function AdminCollaborationGroup() {
|
||||
<StackCustom>
|
||||
<AdminComp_BoxTitle title="Group" />
|
||||
<>
|
||||
<AdminTitleTable
|
||||
title1="Aksi"
|
||||
title2="Jumlah peserta"
|
||||
title3="Nama group"
|
||||
<GridSpan_NewComponent
|
||||
span1={6}
|
||||
span2={6}
|
||||
text1={
|
||||
<TextCustom bold truncate align="center">
|
||||
Jumlah Anggota
|
||||
</TextCustom>
|
||||
}
|
||||
text2={
|
||||
<TextCustom bold truncate>
|
||||
Nama Group
|
||||
</TextCustom>
|
||||
}
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
@@ -67,31 +73,27 @@ export default function AdminCollaborationGroup() {
|
||||
) : (
|
||||
list?.map((item: any, index: number) => (
|
||||
<View key={index}>
|
||||
<AdminTableValue
|
||||
value1={
|
||||
<ActionIcon
|
||||
icon={
|
||||
<Octicons
|
||||
name="eye"
|
||||
size={ICON_SIZE_BUTTON}
|
||||
color="black"
|
||||
/>
|
||||
}
|
||||
onPress={() => {
|
||||
router.push(`/admin/collaboration/${item.id}/group`);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
value2={
|
||||
<TextCustom truncate={1}>
|
||||
{item?.ProjectCollaboration_AnggotaRoomChat?.length ||
|
||||
"-"}
|
||||
</TextCustom>
|
||||
}
|
||||
value3={
|
||||
<TextCustom truncate={2}>{item?.name || "-"}</TextCustom>
|
||||
}
|
||||
/>
|
||||
<ClickableCustom
|
||||
onPress={() => {
|
||||
router.push(`/admin/collaboration/${item.id}/group`);
|
||||
}}
|
||||
>
|
||||
<GridSpan_NewComponent
|
||||
span1={6}
|
||||
span2={6}
|
||||
text1={
|
||||
<TextCustom truncate={1} align="center">
|
||||
{item?.ProjectCollaboration_AnggotaRoomChat?.length ||
|
||||
"-"}
|
||||
</TextCustom>
|
||||
}
|
||||
text2={
|
||||
<TextCustom truncate={2}>
|
||||
{item?.name || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
/>
|
||||
</ClickableCustom>
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import {
|
||||
ActionIcon,
|
||||
ClickableCustom,
|
||||
LoaderCustom,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
ViewWrapper,
|
||||
@@ -9,6 +11,7 @@ import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage"
|
||||
import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle";
|
||||
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
|
||||
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
|
||||
import { GridSpan_NewComponent } from "@/components/_ShareComponent/GridSpan_NewComponent";
|
||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
||||
import { apiAdminCollaboration } from "@/service/api-admin/api-admin-collaboration";
|
||||
import { Octicons } from "@expo/vector-icons";
|
||||
@@ -51,11 +54,7 @@ export default function AdminCollaborationPublish() {
|
||||
<StackCustom>
|
||||
<AdminComp_BoxTitle title="Publish" />
|
||||
|
||||
<AdminTitleTable
|
||||
title1="Aksi"
|
||||
title2="Username"
|
||||
title3="Judul Proyek"
|
||||
/>
|
||||
<GridSpan_NewComponent text1={<TextCustom bold>Username</TextCustom>} text2={<TextCustom bold>Judul Proyek</TextCustom>} />
|
||||
{/* <Spacing height={10} /> */}
|
||||
<Divider />
|
||||
|
||||
@@ -68,32 +67,26 @@ export default function AdminCollaborationPublish() {
|
||||
) : (
|
||||
list?.map((item: any, index: number) => (
|
||||
<View key={index}>
|
||||
<AdminTableValue
|
||||
value1={
|
||||
<ActionIcon
|
||||
icon={
|
||||
<Octicons
|
||||
name="eye"
|
||||
size={ICON_SIZE_BUTTON}
|
||||
color="black"
|
||||
/>
|
||||
}
|
||||
onPress={() => {
|
||||
router.push(`/admin/collaboration/${item?.id}/publish`);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
value2={
|
||||
<TextCustom align="center" truncate={1}>
|
||||
{item?.Author?.username || "-"}{" "}
|
||||
</TextCustom>
|
||||
}
|
||||
value3={
|
||||
<TextCustom align="center" truncate={2}>
|
||||
{item?.title || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
/>
|
||||
<ClickableCustom
|
||||
onPress={() => {
|
||||
router.push(`/admin/collaboration/${item?.id}/publish`);
|
||||
}}
|
||||
>
|
||||
<GridSpan_NewComponent
|
||||
text1={
|
||||
<TextCustom truncate={1}>
|
||||
{item?.Author?.username || "-"}{" "}
|
||||
</TextCustom>
|
||||
}
|
||||
text2={
|
||||
<TextCustom truncate={2}>
|
||||
{item?.title || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
/>
|
||||
</ClickableCustom>
|
||||
<Spacing height={8}/>
|
||||
<Divider/>
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
|
||||
@@ -41,8 +41,8 @@ export default function AdminEventDetail() {
|
||||
const deepLinkURL = `${DEEP_LINK_URL}/event/${id}/confirmation?userId=${user?.id}`;
|
||||
const deepLinkURLDEV = `${DEEP_LINK_URL}/--/event/${id}/confirmation?userId=${user?.id}`;
|
||||
|
||||
const isDevLink = process.env.NODE_ENV === "development" ? deepLinkURLDEV : deepLinkURL;
|
||||
|
||||
const isDevLink =
|
||||
process.env.NODE_ENV === "development" ? deepLinkURLDEV : deepLinkURL;
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
@@ -126,6 +126,7 @@ export default function AdminEventDetail() {
|
||||
const response = await funUpdateStatusEvent({
|
||||
id: id as string,
|
||||
changeStatus: "publish",
|
||||
data: { catatan: "", senderId: user?.id as string },
|
||||
});
|
||||
|
||||
if (!response.success) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from "@/components";
|
||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
||||
import AdminButtonReject from "@/components/_ShareComponent/Admin/ButtonReject";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { funUpdateStatusEvent } from "@/screens/Admin/Event/funUpdateStatus";
|
||||
import { apiAdminEventById } from "@/service/api-admin/api-admin-event";
|
||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
@@ -14,9 +15,13 @@ import { useCallback, useState } from "react";
|
||||
import Toast from "react-native-toast-message";
|
||||
|
||||
export default function AdminEventRejectInput() {
|
||||
const { user } = useAuth();
|
||||
const { id, status } = useLocalSearchParams();
|
||||
|
||||
const [data, setData] = useState<any>("");
|
||||
const [data, setData] = useState<any>({
|
||||
catatan: "",
|
||||
senderId: "",
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useFocusEffect(
|
||||
@@ -45,10 +50,16 @@ export default function AdminEventRejectInput() {
|
||||
}) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const newData = {
|
||||
catatan: data,
|
||||
senderId: user?.id as string,
|
||||
};
|
||||
|
||||
const response = await funUpdateStatusEvent({
|
||||
id: id as string,
|
||||
changeStatus,
|
||||
data: data,
|
||||
data: newData,
|
||||
});
|
||||
|
||||
if (!response.success) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
ActionIcon,
|
||||
AlertDefaultSystem,
|
||||
BaseBox,
|
||||
CenterCustom,
|
||||
DrawerCustom,
|
||||
LoaderCustom,
|
||||
MenuDrawerDynamicGrid,
|
||||
@@ -14,11 +15,11 @@ import { IconDot, IconView } from "@/components/_Icon/IconComponent";
|
||||
import { IconTrash } from "@/components/_Icon/IconTrash";
|
||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
||||
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
||||
import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle";
|
||||
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
|
||||
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
|
||||
import { GridSpan_NewComponent } from "@/components/_ShareComponent/GridSpan_NewComponent";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import {
|
||||
apiAdminForumCommentById,
|
||||
apiAdminForumDeactivateComment,
|
||||
@@ -27,11 +28,13 @@ import {
|
||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { Divider } from "react-native-paper";
|
||||
import Toast from "react-native-toast-message";
|
||||
|
||||
export default function AdminForumReportComment() {
|
||||
const { id } = useLocalSearchParams();
|
||||
const { user } = useAuth();
|
||||
const [data, setData] = useState<any | null>(null);
|
||||
const [listReport, setListReport] = useState<any[] | null>(null);
|
||||
const [loadList, setLoadList] = useState(false);
|
||||
@@ -95,24 +98,28 @@ export default function AdminForumReportComment() {
|
||||
>
|
||||
<BaseBox>
|
||||
<StackCustom gap={"sm"}>
|
||||
<GridSpan_4_8
|
||||
label={<TextCustom bold>Username</TextCustom>}
|
||||
value={<TextCustom>{data?.Author?.username || "-"}</TextCustom>}
|
||||
<GridSpan_NewComponent
|
||||
text1={<TextCustom bold>Username</TextCustom>}
|
||||
text2={<TextCustom>{data?.Author?.username || "-"}</TextCustom>}
|
||||
/>
|
||||
<GridSpan_4_8
|
||||
label={<TextCustom bold>Komentar</TextCustom>}
|
||||
value={<TextCustom>{data?.komentar || "-"}</TextCustom>}
|
||||
<GridSpan_NewComponent
|
||||
text1={<TextCustom bold>Komentar</TextCustom>}
|
||||
text2={<TextCustom>{data?.komentar || "-"}</TextCustom>}
|
||||
/>
|
||||
</StackCustom>
|
||||
</BaseBox>
|
||||
|
||||
<AdminComp_BoxTitle title="Daftar Report Komentar" />
|
||||
|
||||
<StackCustom>
|
||||
<AdminTitleTable
|
||||
title1="Aksi"
|
||||
title2="Pelapor"
|
||||
title3="Kategori Report"
|
||||
<StackCustom gap={"sm"}>
|
||||
<GridSpan_NewComponent
|
||||
text1={
|
||||
<TextCustom bold align="center">
|
||||
Aksi
|
||||
</TextCustom>
|
||||
}
|
||||
text2={<TextCustom bold>Pelapor</TextCustom>}
|
||||
text3={<TextCustom bold>Kategori Report</TextCustom>}
|
||||
/>
|
||||
<Divider />
|
||||
{loadList ? (
|
||||
@@ -123,34 +130,41 @@ export default function AdminForumReportComment() {
|
||||
</TextCustom>
|
||||
) : (
|
||||
listReport?.map((item: any, index: number) => (
|
||||
<AdminTableValue
|
||||
key={index}
|
||||
value1={
|
||||
<ActionIcon
|
||||
icon={<IconView size={ICON_SIZE_BUTTON} color="black" />}
|
||||
onPress={() => {
|
||||
setOpenDrawerAction(true);
|
||||
setSelectedReport({
|
||||
id: item.id,
|
||||
username: item.User?.username,
|
||||
kategori: item.ForumMaster_KategoriReport?.title,
|
||||
keterangan: item.ForumMaster_KategoriReport?.deskripsi,
|
||||
deskripsi: item.deskripsi,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
}
|
||||
value2={
|
||||
<TextCustom truncate={1}>
|
||||
{item?.User?.username || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
value3={
|
||||
<TextCustom truncate={2} align="center">
|
||||
{item?.ForumMaster_KategoriReport?.title || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
/>
|
||||
<View key={index}>
|
||||
<GridSpan_NewComponent
|
||||
text1={
|
||||
<CenterCustom>
|
||||
<ActionIcon
|
||||
icon={
|
||||
<IconView size={ICON_SIZE_BUTTON} color="black" />
|
||||
}
|
||||
onPress={() => {
|
||||
setOpenDrawerAction(true);
|
||||
setSelectedReport({
|
||||
id: item.id,
|
||||
username: item.User?.username,
|
||||
kategori: item.ForumMaster_KategoriReport?.title,
|
||||
keterangan:
|
||||
item.ForumMaster_KategoriReport?.deskripsi,
|
||||
deskripsi: item.deskripsi,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</CenterCustom>
|
||||
}
|
||||
text2={
|
||||
<TextCustom truncate={1}>
|
||||
{item?.User?.username || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
text3={
|
||||
<TextCustom truncate={2}>
|
||||
{item?.ForumMaster_KategoriReport?.title || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
/>
|
||||
<Divider />
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</StackCustom>
|
||||
@@ -180,15 +194,18 @@ export default function AdminForumReportComment() {
|
||||
onPressRight: async () => {
|
||||
const deleteComment = await apiAdminForumDeactivateComment({
|
||||
id: id as string,
|
||||
data: {
|
||||
senderId: user?.id as string,
|
||||
},
|
||||
});
|
||||
|
||||
if (!deleteComment.success) {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
text1: "Komentar gagal dihapus",
|
||||
});
|
||||
return;
|
||||
}
|
||||
// if (!deleteComment.success) {
|
||||
// Toast.show({
|
||||
// type: "error",
|
||||
// text1: "Komentar gagal dihapus",
|
||||
// });
|
||||
// return;
|
||||
// }
|
||||
|
||||
setOpenDrawer(false);
|
||||
Toast.show({
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
AlertDefaultSystem,
|
||||
BadgeCustom,
|
||||
BaseBox,
|
||||
CenterCustom,
|
||||
DrawerCustom,
|
||||
LoaderCustom,
|
||||
MenuDrawerDynamicGrid,
|
||||
@@ -15,11 +16,11 @@ import { IconDot, IconView } from "@/components/_Icon/IconComponent";
|
||||
import { IconTrash } from "@/components/_Icon/IconTrash";
|
||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
||||
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
||||
import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle";
|
||||
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
|
||||
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
|
||||
import { GridSpan_NewComponent } from "@/components/_ShareComponent/GridSpan_NewComponent";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import {
|
||||
apiAdminForumDeactivatePosting,
|
||||
apiAdminForumListReportPostingById,
|
||||
@@ -28,10 +29,12 @@ import {
|
||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { Divider } from "react-native-paper";
|
||||
import Toast from "react-native-toast-message";
|
||||
|
||||
export default function AdminForumReportPosting() {
|
||||
const { user } = useAuth();
|
||||
const { id } = useLocalSearchParams();
|
||||
const [openDrawerPage, setOpenDrawerPage] = useState(false);
|
||||
const [openDrawerAction, setOpenDrawerAction] = useState(false);
|
||||
@@ -95,14 +98,14 @@ export default function AdminForumReportPosting() {
|
||||
>
|
||||
<BaseBox>
|
||||
<StackCustom gap={"sm"}>
|
||||
<GridSpan_4_8
|
||||
label={<TextCustom bold>Username</TextCustom>}
|
||||
value={<TextCustom>{data?.Author?.username || "-"}</TextCustom>}
|
||||
<GridSpan_NewComponent
|
||||
text1={<TextCustom bold>Username</TextCustom>}
|
||||
text2={<TextCustom>{data?.Author?.username || "-"}</TextCustom>}
|
||||
/>
|
||||
|
||||
<GridSpan_4_8
|
||||
label={<TextCustom bold>Status</TextCustom>}
|
||||
value={
|
||||
<GridSpan_NewComponent
|
||||
text1={<TextCustom bold>Status</TextCustom>}
|
||||
text2={
|
||||
data && data?.ForumMaster_StatusPosting?.status ? (
|
||||
<BadgeCustom
|
||||
color={
|
||||
@@ -121,19 +124,23 @@ export default function AdminForumReportPosting() {
|
||||
}
|
||||
/>
|
||||
|
||||
<GridSpan_4_8
|
||||
label={<TextCustom bold>Postingan</TextCustom>}
|
||||
value={<TextCustom>{data?.diskusi || "-"}</TextCustom>}
|
||||
<GridSpan_NewComponent
|
||||
text1={<TextCustom bold>Postingan</TextCustom>}
|
||||
text2={<TextCustom>{data?.diskusi || "-"}</TextCustom>}
|
||||
/>
|
||||
</StackCustom>
|
||||
</BaseBox>
|
||||
|
||||
<AdminComp_BoxTitle title="Daftar Report Posting" />
|
||||
<StackCustom gap={"sm"}>
|
||||
<AdminTitleTable
|
||||
title1="Aksi"
|
||||
title2="Pelapor"
|
||||
title3="Kategori Report"
|
||||
<GridSpan_NewComponent
|
||||
text1={
|
||||
<TextCustom bold align="center">
|
||||
Aksi
|
||||
</TextCustom>
|
||||
}
|
||||
text2={<TextCustom bold>Pelapor</TextCustom>}
|
||||
text3={<TextCustom bold>Kategori Report</TextCustom>}
|
||||
/>
|
||||
<Divider />
|
||||
{loadListReport ? (
|
||||
@@ -144,34 +151,41 @@ export default function AdminForumReportPosting() {
|
||||
</TextCustom>
|
||||
) : (
|
||||
listReport?.map((item: any, index: number) => (
|
||||
<AdminTableValue
|
||||
key={index}
|
||||
value1={
|
||||
<ActionIcon
|
||||
icon={<IconView size={ICON_SIZE_BUTTON} color="black" />}
|
||||
onPress={() => {
|
||||
setOpenDrawerAction(true);
|
||||
setSelectedReport({
|
||||
id: item?.id,
|
||||
username: item?.User?.username,
|
||||
kategori: item?.ForumMaster_KategoriReport?.title,
|
||||
keterangan: item?.ForumMaster_KategoriReport?.deskripsi,
|
||||
deskripsi: item?.deskripsi,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
}
|
||||
value2={
|
||||
<TextCustom truncate={1}>
|
||||
{item?.User?.username || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
value3={
|
||||
<TextCustom truncate={2} align="center">
|
||||
{item?.ForumMaster_KategoriReport?.title || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
/>
|
||||
<View key={index}>
|
||||
<GridSpan_NewComponent
|
||||
text1={
|
||||
<CenterCustom>
|
||||
<ActionIcon
|
||||
icon={
|
||||
<IconView size={ICON_SIZE_BUTTON} color="black" />
|
||||
}
|
||||
onPress={() => {
|
||||
setOpenDrawerAction(true);
|
||||
setSelectedReport({
|
||||
id: item?.id,
|
||||
username: item?.User?.username,
|
||||
kategori: item?.ForumMaster_KategoriReport?.title,
|
||||
keterangan:
|
||||
item?.ForumMaster_KategoriReport?.deskripsi,
|
||||
deskripsi: item?.deskripsi,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</CenterCustom>
|
||||
}
|
||||
text2={
|
||||
<TextCustom truncate>
|
||||
{item?.User?.username || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
text3={
|
||||
<TextCustom truncate={2}>
|
||||
{item?.ForumMaster_KategoriReport?.title || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
/>
|
||||
<Divider />
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</StackCustom>
|
||||
@@ -201,6 +215,9 @@ export default function AdminForumReportPosting() {
|
||||
onPressRight: async () => {
|
||||
const response = await apiAdminForumDeactivatePosting({
|
||||
id: id as string,
|
||||
data: {
|
||||
senderId: user?.id as string,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.success) {
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
ActionIcon,
|
||||
ClickableCustom,
|
||||
LoaderCustom,
|
||||
SearchInput,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import { IconView } from "@/components/_Icon/IconComponent";
|
||||
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
||||
import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle";
|
||||
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
|
||||
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
|
||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
||||
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
|
||||
import { GridSpan_NewComponent } from "@/components/_ShareComponent/GridSpan_NewComponent";
|
||||
import { apiAdminForum } from "@/service/api-admin/api-admin-forum";
|
||||
import { router, useFocusEffect } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { Divider } from "react-native-paper";
|
||||
|
||||
export default function AdminForumPosting() {
|
||||
@@ -37,7 +37,9 @@ export default function AdminForumPosting() {
|
||||
category: "posting",
|
||||
search: search,
|
||||
});
|
||||
|
||||
|
||||
console.log("DATA", JSON.stringify(response, null, 2));
|
||||
|
||||
if (response.success) {
|
||||
setList(response.data);
|
||||
}
|
||||
@@ -51,7 +53,7 @@ export default function AdminForumPosting() {
|
||||
const rightComponent = (
|
||||
<SearchInput
|
||||
containerStyle={{ width: "100%", marginBottom: 0 }}
|
||||
placeholder="Cari"
|
||||
placeholder="Cari postingan"
|
||||
value={search}
|
||||
onChangeText={setSearch}
|
||||
/>
|
||||
@@ -61,9 +63,15 @@ export default function AdminForumPosting() {
|
||||
<>
|
||||
<ViewWrapper headerComponent={<AdminTitlePage title="Forum" />}>
|
||||
<AdminComp_BoxTitle title={"Posting"} rightComponent={rightComponent} />
|
||||
<GridSpan_NewComponent
|
||||
text1={<TextCustom bold truncate>Username</TextCustom>}
|
||||
text2={<TextCustom bold truncate> Postingan</TextCustom>}
|
||||
text3={<TextCustom bold align="center" truncate> Report Posting</TextCustom>}
|
||||
text4={<TextCustom bold align="center" truncate> Komentar</TextCustom>}
|
||||
/>
|
||||
<Divider />
|
||||
<Spacing />
|
||||
<StackCustom>
|
||||
<AdminTitleTable title1="Aksi" title2="Username" title3="Postingan" />
|
||||
<Divider />
|
||||
{loadList ? (
|
||||
<LoaderCustom />
|
||||
) : _.isEmpty(list) ? (
|
||||
@@ -72,25 +80,38 @@ export default function AdminForumPosting() {
|
||||
</TextCustom>
|
||||
) : (
|
||||
list?.map((item: any, index: number) => (
|
||||
<AdminTableValue
|
||||
key={index}
|
||||
value1={
|
||||
<ActionIcon
|
||||
icon={<IconView size={ICON_SIZE_BUTTON} color="black" />}
|
||||
onPress={() => {
|
||||
router.push(`/admin/forum/${item?.id}`);
|
||||
}}
|
||||
<View key={index}>
|
||||
<ClickableCustom
|
||||
onPress={() => {
|
||||
router.push(`/admin/forum/${item.id}`);
|
||||
}}
|
||||
>
|
||||
<GridSpan_NewComponent
|
||||
text1={
|
||||
<TextCustom truncate={1}>
|
||||
{item?.Author?.username || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
text2={
|
||||
<TextCustom truncate>
|
||||
{item?.diskusi || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
text3={
|
||||
<TextCustom align="center" truncate={2}>
|
||||
{item?.reportPosting || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
text4={
|
||||
<TextCustom align="center" truncate={2}>
|
||||
{item?.komentar || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
/>
|
||||
}
|
||||
value2={
|
||||
<TextCustom truncate={1}>
|
||||
{item?.Author?.username || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
value3={
|
||||
<TextCustom truncate={2}>{item?.diskusi || "-"}</TextCustom>
|
||||
}
|
||||
/>
|
||||
|
||||
</ClickableCustom>
|
||||
<Divider />
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</StackCustom>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
ActionIcon,
|
||||
ClickableCustom,
|
||||
LoaderCustom,
|
||||
SearchInput,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
ViewWrapper,
|
||||
@@ -12,12 +14,14 @@ import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage"
|
||||
import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle";
|
||||
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
|
||||
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
|
||||
import { GridSpan_NewComponent } from "@/components/_ShareComponent/GridSpan_NewComponent";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
||||
import { apiAdminForum } from "@/service/api-admin/api-admin-forum";
|
||||
import { router, useFocusEffect } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { Divider } from "react-native-paper";
|
||||
|
||||
export default function AdminForumReportComment() {
|
||||
@@ -67,13 +71,26 @@ export default function AdminForumReportComment() {
|
||||
rightComponent={rightComponent}
|
||||
/>
|
||||
|
||||
<StackCustom gap={"sm"}>
|
||||
<AdminTitleTable
|
||||
title1="Aksi"
|
||||
title2="Pelapor"
|
||||
title3="Jenis Laporan"
|
||||
/>
|
||||
<Divider />
|
||||
<GridSpan_NewComponent
|
||||
text1={
|
||||
<TextCustom bold truncate>
|
||||
Pelapor
|
||||
</TextCustom>
|
||||
}
|
||||
text2={
|
||||
<TextCustom bold truncate>
|
||||
Komentar
|
||||
</TextCustom>
|
||||
}
|
||||
text3={
|
||||
<TextCustom bold truncate>
|
||||
Jenis Laporan
|
||||
</TextCustom>
|
||||
}
|
||||
/>
|
||||
<Divider />
|
||||
<Spacing />
|
||||
<StackCustom gap={"lg"}>
|
||||
{loadList ? (
|
||||
<LoaderCustom />
|
||||
) : _.isEmpty(listData) ? (
|
||||
@@ -82,34 +99,35 @@ export default function AdminForumReportComment() {
|
||||
</TextCustom>
|
||||
) : (
|
||||
listData?.map((item: any, index: number) => (
|
||||
<AdminTableValue
|
||||
key={index}
|
||||
value1={
|
||||
<ActionIcon
|
||||
icon={
|
||||
<IconView
|
||||
size={ICON_SIZE_BUTTON}
|
||||
color={MainColor.black}
|
||||
/>
|
||||
<View key={index}>
|
||||
<ClickableCustom
|
||||
onPress={() => {
|
||||
router.push(
|
||||
`/admin/forum/${item?.Forum_Komentar?.id}/list-report-comment`
|
||||
);
|
||||
}}
|
||||
>
|
||||
<GridSpan_NewComponent
|
||||
text1={
|
||||
<TextCustom truncate={1}>
|
||||
{item?.User?.username || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
text2={
|
||||
<TextCustom truncate={2}>
|
||||
{item?.Forum_Komentar?.komentar || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
text3={
|
||||
<TextCustom truncate={2}>
|
||||
{item?.ForumMaster_KategoriReport?.title || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
onPress={() => {
|
||||
router.push(
|
||||
`/admin/forum/${item?.Forum_Komentar?.id}/list-report-comment`
|
||||
);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
value2={
|
||||
<TextCustom truncate={1}>
|
||||
{item?.User?.username || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
value3={
|
||||
<TextCustom truncate={2} align="center">
|
||||
{item?.ForumMaster_KategoriReport?.title || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
/>
|
||||
</ClickableCustom>
|
||||
<Spacing />
|
||||
<Divider />
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</StackCustom>
|
||||
|
||||
@@ -1,24 +1,27 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
ActionIcon,
|
||||
ClickableCustom,
|
||||
Divider,
|
||||
LoaderCustom,
|
||||
SearchInput,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
ViewWrapper
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import { IconView } from "@/components/_Icon/IconComponent";
|
||||
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
||||
import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle";
|
||||
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
|
||||
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
|
||||
import { GridSpan_NewComponent } from "@/components/_ShareComponent/GridSpan_NewComponent";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
||||
import { apiAdminForum } from "@/service/api-admin/api-admin-forum";
|
||||
import { router, useFocusEffect } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
|
||||
export default function AdminForumReportPosting() {
|
||||
const [listData, setListData] = useState<any[] | null>(null);
|
||||
@@ -67,46 +70,51 @@ export default function AdminForumReportPosting() {
|
||||
rightComponent={rightComponent}
|
||||
/>
|
||||
|
||||
<StackCustom gap={"sm"}>
|
||||
<AdminTitleTable title1="Aksi" title2="Pelapor" title3="Postingan" />
|
||||
|
||||
<Divider />
|
||||
<GridSpan_NewComponent
|
||||
text1={
|
||||
<TextCustom bold truncate>
|
||||
Pelapor
|
||||
</TextCustom>
|
||||
}
|
||||
text2={
|
||||
<TextCustom bold truncate>
|
||||
Postingan
|
||||
</TextCustom>
|
||||
}
|
||||
/>
|
||||
<Divider />
|
||||
<StackCustom>
|
||||
{loadList ? (
|
||||
<LoaderCustom />
|
||||
) : _.isEmpty(listData) ? (
|
||||
<TextCustom align="center" color="gray">
|
||||
<TextCustom align="center" color="gray">
|
||||
Belum ada data
|
||||
</TextCustom>
|
||||
) : (
|
||||
listData?.map((item: any, index: number) => (
|
||||
<AdminTableValue
|
||||
key={index}
|
||||
value1={
|
||||
<ActionIcon
|
||||
icon={
|
||||
<IconView
|
||||
size={ICON_SIZE_BUTTON}
|
||||
color={MainColor.black}
|
||||
/>
|
||||
<View key={index}>
|
||||
<ClickableCustom
|
||||
onPress={() => {
|
||||
router.push(
|
||||
`/admin/forum/${item?.Forum_Posting?.id}/list-report-posting`
|
||||
);
|
||||
}}
|
||||
>
|
||||
<GridSpan_NewComponent
|
||||
text1={
|
||||
<TextCustom truncate={1}>
|
||||
{item?.User?.username || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
text2={
|
||||
<TextCustom truncate={1}>
|
||||
{item?.Forum_Posting?.diskusi || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
onPress={() => {
|
||||
router.push(
|
||||
`/admin/forum/${item?.Forum_Posting?.id}/list-report-posting`
|
||||
);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
value2={
|
||||
<TextCustom truncate={1}>
|
||||
{item?.User?.username || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
value3={
|
||||
<TextCustom truncate={2} align="center">
|
||||
{item?.Forum_Posting?.diskusi || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
/>
|
||||
</ClickableCustom>
|
||||
<Divider />
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</StackCustom>
|
||||
|
||||
@@ -7,34 +7,39 @@ import {
|
||||
} from "@/components";
|
||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
||||
import AdminButtonReject from "@/components/_ShareComponent/Admin/ButtonReject";
|
||||
import { apiAdminInvestasiUpdateByStatus, apiAdminInvestmentDetailById } from "@/service/api-admin/api-admin-investment";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import {
|
||||
apiAdminInvestasiUpdateByStatus,
|
||||
apiAdminInvestmentDetailById,
|
||||
} from "@/service/api-admin/api-admin-investment";
|
||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import { useCallback, useState } from "react";
|
||||
import Toast from "react-native-toast-message";
|
||||
|
||||
export default function AdminInvestmentRejectInput() {
|
||||
const { user } = useAuth();
|
||||
const { id, status } = useLocalSearchParams();
|
||||
console.log("[STATUS]", status);
|
||||
const [value, setValue] = useState<any | null>(null);
|
||||
const [isLoading , setLoading] = useState(false)
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [id])
|
||||
);
|
||||
|
||||
const onLoadData = async () => {
|
||||
try {
|
||||
const response = await apiAdminInvestmentDetailById({ id: id as string });
|
||||
console.log("[DATA]", JSON.stringify(response, null, 2));
|
||||
if (response.success) {
|
||||
setValue(response.data?.catatan);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [id])
|
||||
);
|
||||
|
||||
const onLoadData = async () => {
|
||||
try {
|
||||
const response = await apiAdminInvestmentDetailById({ id: id as string });
|
||||
console.log("[DATA]", JSON.stringify(response, null, 2));
|
||||
if (response.success) {
|
||||
setValue(response.data?.catatan);
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handlerSubmit = async () => {
|
||||
if (!value) {
|
||||
@@ -45,12 +50,23 @@ export default function AdminInvestmentRejectInput() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!user?.id) {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
text1: "User tidak ditemukan",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
setLoading(true);
|
||||
const response = await apiAdminInvestasiUpdateByStatus({
|
||||
id: id as string,
|
||||
status: "reject",
|
||||
data: value,
|
||||
data: {
|
||||
catatan: value,
|
||||
senderId: user?.id as string,
|
||||
},
|
||||
});
|
||||
|
||||
console.log("[RESPONSE]", JSON.stringify(response, null, 2));
|
||||
@@ -76,7 +92,7 @@ export default function AdminInvestmentRejectInput() {
|
||||
} catch (error) {
|
||||
console.error(["ERROR"], error);
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import AdminButtonReject from "@/components/_ShareComponent/Admin/ButtonReject";
|
||||
import AdminButtonReview from "@/components/_ShareComponent/Admin/ButtonReview";
|
||||
import ReportBox from "@/components/Box/ReportBox";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import funUpdateStatusJob from "@/screens/Admin/Job/funUpdateStatus";
|
||||
import { apiAdminJobGetById } from "@/service/api-admin/api-admin-job";
|
||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
@@ -23,8 +24,10 @@ import { useCallback, useState } from "react";
|
||||
import Toast from "react-native-toast-message";
|
||||
|
||||
export default function AdminJobDetailStatus() {
|
||||
const { user } = useAuth();
|
||||
const { id, status } = useLocalSearchParams();
|
||||
const [data, setData] = useState<any | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
@@ -92,6 +95,9 @@ export default function AdminJobDetailStatus() {
|
||||
const response = await funUpdateStatusJob({
|
||||
id: id as string,
|
||||
changeStatus,
|
||||
data: {
|
||||
senderId: user?.id as string,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.success) {
|
||||
@@ -142,12 +148,15 @@ export default function AdminJobDetailStatus() {
|
||||
</StackCustom>
|
||||
</BaseBox>
|
||||
|
||||
{data && data?.catatan && (status === "reject" || status === "review") && (
|
||||
<ReportBox text={data?.catatan}/>
|
||||
)}
|
||||
{data &&
|
||||
data?.catatan &&
|
||||
(status === "reject" || status === "review") && (
|
||||
<ReportBox text={data?.catatan} />
|
||||
)}
|
||||
|
||||
{status === "review" && (
|
||||
<AdminButtonReview
|
||||
isLoading={isLoading}
|
||||
onPublish={() => {
|
||||
AlertDefaultSystem({
|
||||
title: "Publish",
|
||||
@@ -156,6 +165,7 @@ export default function AdminJobDetailStatus() {
|
||||
textRight: "Ya",
|
||||
onPressRight: () => {
|
||||
handleUpdate({ changeStatus: "publish" });
|
||||
setIsLoading(true);
|
||||
},
|
||||
});
|
||||
}}
|
||||
|
||||
@@ -15,7 +15,10 @@ import Toast from "react-native-toast-message";
|
||||
|
||||
export default function AdminJobRejectInput() {
|
||||
const { id, status } = useLocalSearchParams();
|
||||
const [data, setData] = useState<any | null>(null);
|
||||
const [data, setData] = useState({
|
||||
catatan: "",
|
||||
senderId: ""
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useFocusEffect(
|
||||
@@ -48,7 +51,7 @@ export default function AdminJobRejectInput() {
|
||||
const response = await funUpdateStatusJob({
|
||||
id: id as string,
|
||||
changeStatus,
|
||||
data: data,
|
||||
data: data ,
|
||||
});
|
||||
|
||||
if (!response.success) {
|
||||
@@ -102,8 +105,8 @@ export default function AdminJobRejectInput() {
|
||||
headerComponent={<AdminBackButtonAntTitle title="Penolakan Job" />}
|
||||
>
|
||||
<TextAreaCustom
|
||||
value={data}
|
||||
onChangeText={setData}
|
||||
value={data?.catatan}
|
||||
onChangeText={(text) => setData({ ...data, catatan: text })}
|
||||
placeholder="Masukan alasan"
|
||||
required
|
||||
showCount
|
||||
|
||||
@@ -1,20 +1,213 @@
|
||||
import { BackButton, TextCustom, ViewWrapper } from "@/components";
|
||||
import { Stack } from "expo-router";
|
||||
import {
|
||||
AlertDefaultSystem,
|
||||
BackButton,
|
||||
BaseBox,
|
||||
DrawerCustom,
|
||||
MenuDrawerDynamicGrid,
|
||||
NewWrapper,
|
||||
ScrollableCustom,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
} from "@/components";
|
||||
import { IconPlus } from "@/components/_Icon";
|
||||
import { IconDot } from "@/components/_Icon/IconComponent";
|
||||
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
|
||||
import NoDataText from "@/components/_ShareComponent/NoDataText";
|
||||
import { AccentColor, MainColor } from "@/constants/color-palet";
|
||||
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useNotificationStore } from "@/hooks/use-notification-store";
|
||||
import { apiGetNotificationsById } from "@/service/api-notifications";
|
||||
import { listOfcategoriesAppNotification } from "@/types/type-notification-category";
|
||||
import { formatChatTime } from "@/utils/formatChatTime";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { router, Stack, useFocusEffect } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
import { RefreshControl, View } from "react-native";
|
||||
|
||||
const selectedCategory = (value: string) => {
|
||||
const category = listOfcategoriesAppNotification.find(
|
||||
(c) => c.value === value
|
||||
);
|
||||
return category?.label;
|
||||
};
|
||||
|
||||
const BoxNotification = ({
|
||||
data,
|
||||
activeCategory,
|
||||
}: {
|
||||
data: any;
|
||||
activeCategory: string | null;
|
||||
}) => {
|
||||
const { markAsRead } = useNotificationStore();
|
||||
return (
|
||||
<>
|
||||
<BaseBox
|
||||
backgroundColor={data.isRead ? AccentColor.darkblue : AccentColor.blue}
|
||||
onPress={() => {
|
||||
console.log(
|
||||
"Notification >",
|
||||
selectedCategory(activeCategory as string)
|
||||
);
|
||||
router.push(data.deepLink);
|
||||
markAsRead(data.id);
|
||||
}}
|
||||
>
|
||||
<StackCustom>
|
||||
<TextCustom truncate={2} bold>
|
||||
{data.title}
|
||||
</TextCustom>
|
||||
|
||||
<TextCustom truncate={2}>{data.pesan}</TextCustom>
|
||||
|
||||
<TextCustom size="small" color="gray">
|
||||
{formatChatTime(data.createdAt)}
|
||||
</TextCustom>
|
||||
</StackCustom>
|
||||
</BaseBox>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default function AdminNotification() {
|
||||
const { user } = useAuth();
|
||||
const [activeCategory, setActiveCategory] = useState<string | null>("event");
|
||||
const [listData, setListData] = useState<any[]>([]);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [openDrawer, setOpenDrawer] = useState(false);
|
||||
|
||||
const { markAsReadAll } = useNotificationStore();
|
||||
|
||||
const handlePress = (item: any) => {
|
||||
setActiveCategory(item.value);
|
||||
// tambahkan logika lain seperti filter dsb.
|
||||
};
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
fecthData();
|
||||
}, [activeCategory])
|
||||
);
|
||||
|
||||
const fecthData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await apiGetNotificationsById({
|
||||
id: user?.id as any,
|
||||
category: activeCategory as any,
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
setListData(response.data);
|
||||
} else {
|
||||
setListData([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error Notification", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onRefresh = () => {
|
||||
setRefreshing(true);
|
||||
fecthData();
|
||||
setRefreshing(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: "Admin Notifikasi",
|
||||
headerLeft: () => <BackButton />,
|
||||
headerRight: () => <></>,
|
||||
headerRight: () => (
|
||||
<IconDot
|
||||
color={MainColor.yellow}
|
||||
onPress={() => setOpenDrawer(true)}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
<ViewWrapper>
|
||||
<TextCustom>Notification</TextCustom>
|
||||
</ViewWrapper>
|
||||
<NewWrapper
|
||||
headerComponent={
|
||||
<ScrollableCustom
|
||||
data={listOfcategoriesAppNotification.map((e, i) => ({
|
||||
id: i,
|
||||
label: e.label,
|
||||
value: e.value,
|
||||
}))}
|
||||
onButtonPress={handlePress}
|
||||
activeId={activeCategory as string}
|
||||
/>
|
||||
}
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
|
||||
}
|
||||
>
|
||||
{loading ? (
|
||||
<ListSkeletonComponent />
|
||||
) : _.isEmpty(listData) ? (
|
||||
<NoDataText text="Belum ada notifikasi" />
|
||||
) : (
|
||||
listData.map((e, i) => (
|
||||
<View key={i}>
|
||||
<BoxNotification
|
||||
data={e}
|
||||
activeCategory={activeCategory as any}
|
||||
/>
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</NewWrapper>
|
||||
|
||||
<DrawerCustom
|
||||
isVisible={openDrawer}
|
||||
closeDrawer={() => setOpenDrawer(false)}
|
||||
height={"auto"}
|
||||
>
|
||||
<MenuDrawerDynamicGrid
|
||||
data={[
|
||||
{
|
||||
label: "Tandai Semua Dibaca",
|
||||
value: "read-all",
|
||||
icon: (
|
||||
<Ionicons
|
||||
name="reader-outline"
|
||||
size={ICON_SIZE_SMALL}
|
||||
color={MainColor.white}
|
||||
/>
|
||||
),
|
||||
path: "",
|
||||
},
|
||||
]}
|
||||
onPressItem={(item: any) => {
|
||||
console.log("Item", item.value);
|
||||
if (item.value === "read-all") {
|
||||
AlertDefaultSystem({
|
||||
title: "Tandai Semua Dibaca",
|
||||
message:
|
||||
"Apakah Anda yakin ingin menandai semua notifikasi dibaca?",
|
||||
textLeft: "Batal",
|
||||
textRight: "Ya",
|
||||
onPressRight: () => {
|
||||
markAsReadAll(user?.id as any);
|
||||
const data = _.cloneDeep(listData);
|
||||
data.forEach((e) => {
|
||||
e.isRead = true;
|
||||
});
|
||||
setListData(data);
|
||||
onRefresh();
|
||||
setOpenDrawer(false);
|
||||
},
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</DrawerCustom>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ export default function SuperAdminDetail() {
|
||||
const response = await apiAdminUserAccessUpdateStatus({
|
||||
id: id as string,
|
||||
role: data?.masterUserRoleId === "2" ? "user" : "admin",
|
||||
category: "role"
|
||||
});
|
||||
|
||||
if (!response.success) {
|
||||
|
||||
@@ -76,7 +76,7 @@ export default function SuperAdmin_ListUser() {
|
||||
</TextCustom>
|
||||
}
|
||||
component2={
|
||||
<TextCustom align="center" bold>
|
||||
<TextCustom bold>
|
||||
Username
|
||||
</TextCustom>
|
||||
}
|
||||
|
||||
@@ -9,15 +9,21 @@ import {
|
||||
} from "@/components";
|
||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
||||
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { routeUser } from "@/lib/routeApp";
|
||||
import {
|
||||
apiAdminUserAccessGetById,
|
||||
apiAdminUserAccessUpdateStatus,
|
||||
} from "@/service/api-admin/api-admin-user-access";
|
||||
import {
|
||||
apiNotificationsSendById
|
||||
} from "@/service/api-notifications";
|
||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import { useCallback, useState } from "react";
|
||||
import Toast from "react-native-toast-message";
|
||||
|
||||
export default function AdminUserAccessDetail() {
|
||||
const { user } = useAuth();
|
||||
const { id } = useLocalSearchParams();
|
||||
const [data, setData] = useState<any | null>(null);
|
||||
const [loadData, setLoadData] = useState(false);
|
||||
@@ -33,6 +39,7 @@ export default function AdminUserAccessDetail() {
|
||||
try {
|
||||
setLoadData(true);
|
||||
const response = await apiAdminUserAccessGetById({ id: id as string });
|
||||
console.log("[DATA]", JSON.stringify(response.data, null, 2));
|
||||
|
||||
setData(response.data);
|
||||
} catch (error) {
|
||||
@@ -48,6 +55,7 @@ export default function AdminUserAccessDetail() {
|
||||
const response = await apiAdminUserAccessUpdateStatus({
|
||||
id: id as string,
|
||||
active: !data?.active,
|
||||
category: "access",
|
||||
});
|
||||
|
||||
if (!response.success) {
|
||||
@@ -61,6 +69,21 @@ export default function AdminUserAccessDetail() {
|
||||
type: "success",
|
||||
text1: "Update aktifasi berhasil ",
|
||||
});
|
||||
|
||||
if (data.active === false) {
|
||||
await apiNotificationsSendById({
|
||||
data: {
|
||||
title: "Akun anda telah diaktifkan",
|
||||
body: "Selamat menjelajahi HIConnect",
|
||||
userLoginId: user?.id || "",
|
||||
kategoriApp: "OTHER",
|
||||
type: "announcement",
|
||||
deepLink: routeUser.home,
|
||||
},
|
||||
id: id as string,
|
||||
});
|
||||
}
|
||||
|
||||
router.back();
|
||||
} catch (error) {
|
||||
console.log("[ERROR UPDATE STATUS]", error);
|
||||
|
||||
@@ -73,11 +73,7 @@ export default function AdminUserAccess() {
|
||||
Aksi
|
||||
</TextCustom>
|
||||
}
|
||||
component2={
|
||||
<TextCustom align="center" bold>
|
||||
Username
|
||||
</TextCustom>
|
||||
}
|
||||
component2={<TextCustom bold>Username</TextCustom>}
|
||||
component3={
|
||||
<TextCustom align="center" bold>
|
||||
Status Akses
|
||||
|
||||
@@ -16,6 +16,7 @@ import AdminButtonReview from "@/components/_ShareComponent/Admin/ButtonReview";
|
||||
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
|
||||
import ReportBox from "@/components/Box/ReportBox";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import funUpdateStatusVoting from "@/screens/Admin/Voting/funUpdateStatus";
|
||||
import { apiAdminVotingById } from "@/service/api-admin/api-admin-voting";
|
||||
import { colorBadgeStatus } from "@/utils/colorBadge";
|
||||
@@ -29,6 +30,7 @@ import { List } from "react-native-paper";
|
||||
import Toast from "react-native-toast-message";
|
||||
|
||||
export default function AdminVotingDetail() {
|
||||
const { user } = useAuth();
|
||||
const { id, status } = useLocalSearchParams();
|
||||
const [data, setData] = useState<any | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -139,6 +141,10 @@ export default function AdminVotingDetail() {
|
||||
const response = await funUpdateStatusVoting({
|
||||
id: id as string,
|
||||
changeStatus,
|
||||
data: {
|
||||
senderId: user?.id as string,
|
||||
catatan: "",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.success) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from "@/components";
|
||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
||||
import AdminButtonReject from "@/components/_ShareComponent/Admin/ButtonReject";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import funUpdateStatusVoting from "@/screens/Admin/Voting/funUpdateStatus";
|
||||
import { apiAdminVotingById } from "@/service/api-admin/api-admin-voting";
|
||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
@@ -14,6 +15,7 @@ import { useCallback, useState } from "react";
|
||||
import Toast from "react-native-toast-message";
|
||||
|
||||
export default function AdminVotingRejectInput() {
|
||||
const { user } = useAuth()
|
||||
const { id, status } = useLocalSearchParams();
|
||||
const [data, setData] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -48,7 +50,10 @@ export default function AdminVotingRejectInput() {
|
||||
const response = await funUpdateStatusVoting({
|
||||
id: id as string,
|
||||
changeStatus,
|
||||
data: data,
|
||||
data: {
|
||||
catatan: data,
|
||||
senderId: user?.id as string,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.success) {
|
||||
|
||||
@@ -103,7 +103,7 @@ export default function AdminVotingStatus() {
|
||||
</TextCustom>
|
||||
}
|
||||
value3={
|
||||
<TextCustom align="center" truncate={2}>
|
||||
<TextCustom truncate={2}>
|
||||
{item?.title || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ export default function AdminVotingHistory() {
|
||||
}
|
||||
value2={<TextCustom truncate={1}>{item?.Author?.username || "-"}</TextCustom>}
|
||||
value3={
|
||||
<TextCustom align="center" truncate={2}>
|
||||
<TextCustom truncate={2}>
|
||||
{item?.title || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
import { StackCustom, TextCustom, ViewWrapper } from "@/components";
|
||||
import { BackButton, StackCustom, TextCustom, ViewWrapper } from "@/components";
|
||||
import { Stack } from "expo-router";
|
||||
|
||||
export default function NotFoundScreen() {
|
||||
return (
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{ headerShown: false, headerLeft: () => <BackButton /> }}
|
||||
/>
|
||||
<ViewWrapper>
|
||||
<StackCustom align="center" gap={0} style={{justifyContent: "center", alignItems: "center", flex: 1}}>
|
||||
<TextCustom size="large" bold style={{fontSize: 100}}>
|
||||
<StackCustom
|
||||
align="center"
|
||||
gap={0}
|
||||
style={{ justifyContent: "center", alignItems: "center", flex: 1 }}
|
||||
>
|
||||
<TextCustom size="large" bold style={{ fontSize: 100 }}>
|
||||
404
|
||||
</TextCustom>
|
||||
<TextCustom size="large" bold>
|
||||
@@ -12,5 +21,6 @@ export default function NotFoundScreen() {
|
||||
</TextCustom>
|
||||
</StackCustom>
|
||||
</ViewWrapper>
|
||||
);
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
9
app/eula.tsx
Normal file
9
app/eula.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import EULAView from "@/screens/Authentication/EULAView";
|
||||
|
||||
export default function EULA() {
|
||||
return (
|
||||
<>
|
||||
<EULAView />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -12,10 +12,12 @@ const LeftButtonCustom = ({
|
||||
path,
|
||||
icon = "arrow-back",
|
||||
iconCustom,
|
||||
onPress,
|
||||
}: {
|
||||
path?: Href;
|
||||
icon?: React.ReactNode | any;
|
||||
iconCustom?: React.ReactNode;
|
||||
onPress?: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
@@ -26,7 +28,7 @@ const LeftButtonCustom = ({
|
||||
name={icon}
|
||||
size={20}
|
||||
color={MainColor.yellow}
|
||||
onPress={() => (path ? router.replace(path) : router.back())}
|
||||
onPress={() => (onPress ? onPress() : path ? router.replace(path) : router.back())}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
29
components/Button/BackButtonFromNotification.tsx
Normal file
29
components/Button/BackButtonFromNotification.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { useRouter } from "expo-router";
|
||||
import { BackButton } from "..";
|
||||
|
||||
export default function BackButtonFromNotification({
|
||||
from,
|
||||
category,
|
||||
}: {
|
||||
from: string;
|
||||
category?: string;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
return (
|
||||
<>
|
||||
<BackButton
|
||||
onPress={() => {
|
||||
if (from === "notifications") {
|
||||
router.replace(`/notifications?category=${category}`);
|
||||
} else {
|
||||
if (from) {
|
||||
router.replace(`/${from}` as any);
|
||||
} else {
|
||||
router.navigate("/home");
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -53,6 +53,8 @@ const DateTimeInput_Android: React.FC<DateTimeInputProps> = ({
|
||||
const [selectedDate, setSelectedDate] = useState<Date>(value as any);
|
||||
const [selectedTime, setSelectedTime] = useState<Date>(value as any);
|
||||
|
||||
console.log("Date Android", value)
|
||||
|
||||
// Fungsi untuk menggabungkan tanggal dan waktu
|
||||
const combineDateAndTime = useCallback((date: Date, time: Date): Date => {
|
||||
const combined = new Date(date);
|
||||
|
||||
@@ -23,6 +23,7 @@ const DateTimePickerCustom: React.FC<Props> = ({
|
||||
minimumDate,
|
||||
disabled = false,
|
||||
}) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{Platform.OS === "ios" ? (
|
||||
@@ -47,6 +48,9 @@ const DateTimePickerCustom: React.FC<Props> = ({
|
||||
maximumDate={maximumDate}
|
||||
minimumDate={minimumDate}
|
||||
disabled={disabled}
|
||||
value={value as DateTimePickerEvent | Date | null | any}
|
||||
|
||||
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
140
components/Notification/BackgroundNotificationHandler.tsx
Normal file
140
components/Notification/BackgroundNotificationHandler.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
// src/components/BackgroundNotificationHandler.tsx
|
||||
import { useNotificationStore } from "@/hooks/use-notification-store";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import {
|
||||
FirebaseMessagingTypes,
|
||||
getInitialNotification,
|
||||
getMessaging,
|
||||
onNotificationOpenedApp,
|
||||
} from "@react-native-firebase/messaging";
|
||||
import { router } from "expo-router";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
const HANDLED_NOTIFICATIONS_KEY = "handled_notifications";
|
||||
|
||||
export default function BackgroundNotificationHandler() {
|
||||
const { addNotification, markAsRead } = useNotificationStore();
|
||||
const messaging = getMessaging();
|
||||
const unsubscribeRef = useRef<(() => void) | null>(null); // 🔑 cegah duplikasi
|
||||
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
// 1. Handle (cold start)
|
||||
const initialNotification = await getInitialNotification(messaging);
|
||||
if (initialNotification) {
|
||||
handleNotification(initialNotification);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Handle background
|
||||
if (unsubscribeRef.current) {
|
||||
unsubscribeRef.current();
|
||||
}
|
||||
|
||||
const unsubscribe = onNotificationOpenedApp(
|
||||
messaging,
|
||||
(remoteMessage) => {
|
||||
handleNotification(remoteMessage);
|
||||
}
|
||||
);
|
||||
|
||||
unsubscribeRef.current = unsubscribe;
|
||||
};
|
||||
|
||||
init();
|
||||
|
||||
// Cleanup saat komponen unmount
|
||||
return () => {
|
||||
if (unsubscribeRef.current) {
|
||||
unsubscribeRef.current();
|
||||
unsubscribeRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [addNotification, messaging]);
|
||||
|
||||
const isNotificationHandled = async (
|
||||
notificationId: string
|
||||
): Promise<boolean> => {
|
||||
const handled = await AsyncStorage.getItem(HANDLED_NOTIFICATIONS_KEY);
|
||||
const ids = handled ? JSON.parse(handled) : [];
|
||||
return ids.includes(notificationId);
|
||||
};
|
||||
|
||||
const markNotificationAsHandled = async (notificationId: string) => {
|
||||
const handled = await AsyncStorage.getItem(HANDLED_NOTIFICATIONS_KEY);
|
||||
const ids = handled ? JSON.parse(handled) : [];
|
||||
if (!ids.includes(notificationId)) {
|
||||
ids.push(notificationId);
|
||||
// Simpan maksimal 50 ID terakhir untuk hindari memori bocor
|
||||
await AsyncStorage.setItem(
|
||||
HANDLED_NOTIFICATIONS_KEY,
|
||||
JSON.stringify(ids.slice(-50))
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNotification = async (
|
||||
remoteMessage: FirebaseMessagingTypes.RemoteMessage
|
||||
) => {
|
||||
const { notification, data } = remoteMessage;
|
||||
if (!notification?.title) return;
|
||||
|
||||
console.log(
|
||||
"🚀 Notification received:",
|
||||
JSON.stringify(remoteMessage, null, 2)
|
||||
);
|
||||
|
||||
const notificationId = data?.id;
|
||||
if (!notificationId || typeof notificationId !== "string") {
|
||||
console.warn("Notification missing notificationId, skipping navigation");
|
||||
return;
|
||||
}
|
||||
|
||||
// ✅ Cek apakah sudah pernah ditangani
|
||||
if (await isNotificationHandled(notificationId)) {
|
||||
console.log("Notification already handled, skipping:", notificationId);
|
||||
return;
|
||||
}
|
||||
|
||||
// ✅ Tandai sebagai ditangani
|
||||
await markNotificationAsHandled(notificationId);
|
||||
|
||||
// ✅ Normalisasi deepLink: pastikan string
|
||||
let deepLink: string | undefined;
|
||||
if (data?.deepLink) {
|
||||
if (typeof data.deepLink === "string") {
|
||||
deepLink = data.deepLink;
|
||||
} else {
|
||||
// Jika object (jarang), coba string-kan
|
||||
deepLink = JSON.stringify(data.deepLink);
|
||||
}
|
||||
}
|
||||
|
||||
// Tambahkan ke UI state (agar muncul di daftar notifikasi & badge)
|
||||
addNotification({
|
||||
title: notification.title,
|
||||
body: notification.body || "",
|
||||
type: "announcement",
|
||||
data: data as Record<string, string>, // aman karena di-normalisasi di useNotificationStore
|
||||
});
|
||||
|
||||
markAsRead(data?.id as any);
|
||||
|
||||
// Navigasi
|
||||
if (
|
||||
data?.deepLink &&
|
||||
typeof data.deepLink === "string" &&
|
||||
data.deepLink.startsWith("/")
|
||||
) {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
router.push(data.deepLink as any);
|
||||
} catch (error) {
|
||||
console.warn("Navigation failed:", error);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
return null;
|
||||
}
|
||||
112
components/Notification/NotificationInitializer.tsx
Normal file
112
components/Notification/NotificationInitializer.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
// src/components/NotificationInitializer.tsx
|
||||
import { useEffect } from "react";
|
||||
import { useForegroundNotifications } from "@/hooks/use-foreground-notifications";
|
||||
import { useNotificationStore } from "@/hooks/use-notification-store";
|
||||
import type { FirebaseMessagingTypes } from "@react-native-firebase/messaging";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { Platform } from "react-native";
|
||||
import * as Device from "expo-device";
|
||||
import * as Application from "expo-application";
|
||||
import { apiDeviceRegisterToken } from "@/service/api-device-token";
|
||||
import messaging, {
|
||||
isSupported,
|
||||
requestPermission,
|
||||
getToken,
|
||||
AuthorizationStatus,
|
||||
} from "@react-native-firebase/messaging";
|
||||
|
||||
// ✅ Modular imports (sesuai v22+)
|
||||
|
||||
export default function NotificationInitializer() {
|
||||
// Setup handler notifikasi
|
||||
const { user, logout } = useAuth(); // dari AuthContext
|
||||
const { addNotification } = useNotificationStore();
|
||||
|
||||
// Ambil token FCM (opsional, hanya untuk log)
|
||||
useEffect(() => {
|
||||
if (!user) {
|
||||
console.log("User not available, skipping token sync");
|
||||
return;
|
||||
}
|
||||
|
||||
const registerDeviceToken = async () => {
|
||||
try {
|
||||
// ✅ Dapatkan instance messaging
|
||||
const messagingInstance = messaging();
|
||||
|
||||
// ✅ Gunakan instance sebagai argumen
|
||||
const supported = await isSupported(messagingInstance);
|
||||
if (!supported) {
|
||||
console.log("‼️ FCM tidak didukung");
|
||||
return;
|
||||
}
|
||||
|
||||
const authStatus = await requestPermission(messagingInstance);
|
||||
if (authStatus !== AuthorizationStatus.AUTHORIZED) {
|
||||
console.warn("Izin telah ditolak");
|
||||
return;
|
||||
}
|
||||
|
||||
const fcmToken = await getToken(messagingInstance);
|
||||
if (!fcmToken) {
|
||||
console.warn("Tidak bisa mendapatkan FCM token");
|
||||
// logout();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("✅ FCM Token:", fcmToken);
|
||||
|
||||
const platform = Platform.OS; // "ios" | "android"
|
||||
const model = Device.modelName || "unknown";
|
||||
const appVersion =
|
||||
(Application.nativeApplicationVersion || "unknown") +
|
||||
"-" +
|
||||
(Application.nativeBuildVersion || "unknown");
|
||||
const deviceId =
|
||||
Device.osInternalBuildId || Device.modelName || "unknown";
|
||||
|
||||
// Kirim ke backend
|
||||
await apiDeviceRegisterToken({
|
||||
data: {
|
||||
fcmToken,
|
||||
platform,
|
||||
deviceId,
|
||||
model,
|
||||
appVersion,
|
||||
userId: user?.id || "",
|
||||
},
|
||||
});
|
||||
|
||||
console.log("✅ Device token berhasil didaftarkan ke backend");
|
||||
} catch (error) {
|
||||
console.error("❌ Gagal mendaftarkan device token:", error);
|
||||
}
|
||||
};
|
||||
|
||||
registerDeviceToken();
|
||||
}, [user?.id]);
|
||||
|
||||
const handleForegroundNotification = (
|
||||
message: FirebaseMessagingTypes.RemoteMessage
|
||||
) => {
|
||||
const title = message.notification?.title || "Notifikasi";
|
||||
const body = message.notification?.body || "";
|
||||
const rawData = message.data || {};
|
||||
|
||||
const safeData: Record<string, string> = {};
|
||||
for (const key in rawData) {
|
||||
safeData[key] =
|
||||
typeof rawData[key] === "string"
|
||||
? rawData[key]
|
||||
: JSON.stringify(rawData[key]);
|
||||
}
|
||||
|
||||
console.log("📥 Menambahkan ke store:", { title, body, safeData });
|
||||
addNotification({ title, body, data: safeData, type: "announcement" });
|
||||
console.log("✅ Notifikasi ditambahkan ke state");
|
||||
};
|
||||
|
||||
useForegroundNotifications(handleForegroundNotification);
|
||||
|
||||
return null; // komponen ini tidak merender apa-apa
|
||||
}
|
||||
@@ -87,7 +87,7 @@ const SelectCustom: React.FC<SelectProps> = ({
|
||||
borderRadius,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 10,
|
||||
// paddingHorizontal: 0,
|
||||
height: 50,
|
||||
},
|
||||
|
||||
|
||||
@@ -98,13 +98,14 @@ export const IconView = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const IconDot = ({ size, color }: { size?: number; color?: string }) => {
|
||||
export const IconDot = ({ size, color, onPress }: { size?: number; color?: string , onPress?: () => void}) => {
|
||||
return (
|
||||
<>
|
||||
<Ionicons
|
||||
name="ellipsis-vertical"
|
||||
size={size || ICON_SIZE_MEDIUM}
|
||||
color={color || MainColor.darkblue}
|
||||
onPress={onPress}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -4,12 +4,21 @@ import { Octicons } from "@expo/vector-icons";
|
||||
|
||||
export { IconPlus };
|
||||
|
||||
function IconPlus({ color, size }: { color?: string; size?: number }) {
|
||||
function IconPlus({
|
||||
color,
|
||||
size,
|
||||
onPress,
|
||||
}: {
|
||||
color?: string;
|
||||
size?: number;
|
||||
onPress?: () => void;
|
||||
}) {
|
||||
return (
|
||||
<Octicons
|
||||
name="plus-circle"
|
||||
size={size || ICON_SIZE_MEDIUM}
|
||||
color={color || MainColor.white}
|
||||
onPress={onPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Grid } from "@/components";
|
||||
|
||||
export const GridSpan_4_8 = ({
|
||||
label,
|
||||
value,
|
||||
label: text1,
|
||||
value: text2,
|
||||
}: {
|
||||
label: React.ReactNode;
|
||||
value: React.ReactNode;
|
||||
@@ -17,10 +17,10 @@ export const GridSpan_4_8 = ({
|
||||
paddingLeft: 8,
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
{text1}
|
||||
</Grid.Col>
|
||||
<Grid.Col span={8} style={{ justifyContent: "center", paddingRight: 8 }}>
|
||||
{value}
|
||||
{text2}
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
);
|
||||
|
||||
54
components/_ShareComponent/GridSpan_NewComponent.tsx
Normal file
54
components/_ShareComponent/GridSpan_NewComponent.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Grid } from "@/components";
|
||||
|
||||
export const GridSpan_NewComponent = ({
|
||||
text1,
|
||||
text2,
|
||||
text3,
|
||||
text4,
|
||||
span1,
|
||||
span2,
|
||||
}: {
|
||||
text1: React.ReactNode;
|
||||
text2: React.ReactNode;
|
||||
text3?: React.ReactNode;
|
||||
text4?: React.ReactNode;
|
||||
span1?: number;
|
||||
span2?: number;
|
||||
}) => {
|
||||
return (
|
||||
<Grid>
|
||||
<Grid.Col
|
||||
span={span1 ? span1 : text4 ? 3 : 4}
|
||||
style={{
|
||||
justifyContent: "flex-start",
|
||||
paddingRight: 5,
|
||||
paddingLeft: 3,
|
||||
}}
|
||||
>
|
||||
{text1}
|
||||
</Grid.Col>
|
||||
<Grid.Col
|
||||
span={span2 ? span2 : text4 ? 3 : text3 ? 4 : 8}
|
||||
style={{ justifyContent: "flex-start", paddingRight: 5 }}
|
||||
>
|
||||
{text2}
|
||||
</Grid.Col>
|
||||
{text3 && (
|
||||
<Grid.Col
|
||||
span={text4 ? 3 : 4}
|
||||
style={{ justifyContent: "flex-start", paddingRight: 5 }}
|
||||
>
|
||||
{text3}
|
||||
</Grid.Col>
|
||||
)}
|
||||
{text4 && (
|
||||
<Grid.Col
|
||||
span={3}
|
||||
style={{ justifyContent: "flex-start", paddingRight: 5 }}
|
||||
>
|
||||
{text4}
|
||||
</Grid.Col>
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
@@ -2,10 +2,13 @@ import {
|
||||
apiConfig,
|
||||
apiLogin,
|
||||
apiRegister,
|
||||
apiUpdatedTermCondition,
|
||||
apiValidationCode,
|
||||
} from "@/service/api-config";
|
||||
import { apiDeviceTokenDeleted } from "@/service/api-device-token";
|
||||
import { IUser } from "@/types/User";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import * as Device from "expo-device";
|
||||
import { router } from "expo-router";
|
||||
import { createContext, useEffect, useState } from "react";
|
||||
import Toast from "react-native-toast-message";
|
||||
@@ -27,6 +30,7 @@ type AuthContextType = {
|
||||
termsOfServiceAccepted: boolean;
|
||||
}) => Promise<void>;
|
||||
userData: (token: string) => Promise<any>;
|
||||
acceptedTerms: (nomor: string) => Promise<any>;
|
||||
};
|
||||
|
||||
// --- Create Context ---
|
||||
@@ -72,29 +76,32 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const loginWithNomor = async (nomor: string) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
console.log("[Masuk provider]", nomor);
|
||||
const response = await apiLogin({ nomor: nomor });
|
||||
console.log("[RESPONSE AUTH]", JSON.stringify(response));
|
||||
|
||||
console.log("[RESPONSE AUTH]", JSON.stringify(response, null, 2));
|
||||
|
||||
if (response.success) {
|
||||
console.log("[Keluar provider]", nomor);
|
||||
Toast.show({
|
||||
type: "success",
|
||||
text1: "Sukses",
|
||||
text2: "Kode OTP berhasil dikirim",
|
||||
});
|
||||
if (response.isAcceptTerms) {
|
||||
Toast.show({
|
||||
type: "success",
|
||||
text1: "Sukses",
|
||||
text2: "Kode OTP berhasil dikirim",
|
||||
});
|
||||
|
||||
await AsyncStorage.setItem("kode_otp", response.kodeId);
|
||||
router.push(`/verification?nomor=${nomor}`);
|
||||
return;
|
||||
await AsyncStorage.setItem("kode_otp", response.kodeId);
|
||||
router.push(`/verification?nomor=${nomor}`);
|
||||
return;
|
||||
} else {
|
||||
router.push(`/eula?nomor=${nomor}`);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
router.push(`/register?nomor=${nomor}`);
|
||||
Toast.show({
|
||||
type: "info",
|
||||
text1: "Info",
|
||||
text2: "Silahkan mendaftar",
|
||||
});
|
||||
router.push(`/eula?nomor=${nomor}`);
|
||||
|
||||
// Toast.show({
|
||||
// type: "info",
|
||||
// text1: "Info",
|
||||
// text2: "Silahkan mendaftar",
|
||||
// });
|
||||
return;
|
||||
}
|
||||
} catch (error: any) {
|
||||
@@ -104,18 +111,6 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
}
|
||||
};
|
||||
|
||||
// const loginWithNomor = async (nomor: string) => {
|
||||
// setIsLoading(true);
|
||||
// try {
|
||||
// const response = await apiLogin({ nomor: nomor });
|
||||
// await AsyncStorage.setItem("kode_otp", response.kodeId);
|
||||
// } catch (error: any) {
|
||||
// throw new Error(error.response?.data?.message || "Gagal kirim OTP");
|
||||
// } finally {
|
||||
// setIsLoading(false);
|
||||
// }
|
||||
// };
|
||||
|
||||
// --- 2. Validasi OTP & cek user ---
|
||||
const validateOtp = async (nomor: string) => {
|
||||
try {
|
||||
@@ -139,13 +134,15 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
await AsyncStorage.setItem("userData", JSON.stringify(dataUser));
|
||||
|
||||
if (response.active) {
|
||||
if (response.roleId === "1") {
|
||||
router.replace("/(application)/(user)/home");
|
||||
return;
|
||||
} else {
|
||||
router.replace("/(application)/admin/dashboard");
|
||||
return;
|
||||
}
|
||||
// if (response.roleId === "1") {
|
||||
// router.replace("/(application)/(user)/home");
|
||||
// return;
|
||||
// } else {
|
||||
// router.replace("/(application)/admin/dashboard");
|
||||
// return;
|
||||
// }
|
||||
router.replace("/(application)/(user)/home");
|
||||
return;
|
||||
} else {
|
||||
router.replace("/(application)/(user)/waiting-room");
|
||||
return;
|
||||
@@ -206,7 +203,6 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await apiRegister({ data: userData });
|
||||
console.log("[REGISTER FETCH]", JSON.stringify(response, null, 2));
|
||||
|
||||
if (!response.success) {
|
||||
Toast.show({
|
||||
@@ -236,42 +232,6 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
// const registerUser = async (userData: {
|
||||
// username: string;
|
||||
// nomor: string;
|
||||
// termsOfServiceAccepted: boolean;
|
||||
// }) => {
|
||||
// setIsLoading(true);
|
||||
// try {
|
||||
// const response = await apiRegister({ data: userData });
|
||||
// console.log("response", response);
|
||||
|
||||
// const { token } = response;
|
||||
// if (!response.success) {
|
||||
// Toast.show({
|
||||
// type: "info",
|
||||
// text1: "Info",
|
||||
// text2: response.message,
|
||||
// });
|
||||
|
||||
// return;
|
||||
// }
|
||||
|
||||
// setToken(token);
|
||||
// await AsyncStorage.setItem("authToken", token);
|
||||
// Toast.show({
|
||||
// type: "success",
|
||||
// text1: "Sukses",
|
||||
// text2: "Anda berhasil terdaftar",
|
||||
// });
|
||||
// router.replace("/(application)/(user)/waiting-room");
|
||||
// return;
|
||||
// } catch (error: any) {
|
||||
// console.log("Error register", error);
|
||||
// } finally {
|
||||
// setIsLoading(false);
|
||||
// }
|
||||
// };
|
||||
|
||||
// --- 5. Logout ---
|
||||
|
||||
@@ -280,9 +240,13 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
setIsLoading(true);
|
||||
setToken(null);
|
||||
setUser(null);
|
||||
|
||||
const deviceId =
|
||||
Device.osInternalBuildId || Device.modelName || "unknown";
|
||||
|
||||
await AsyncStorage.removeItem("authToken");
|
||||
await AsyncStorage.removeItem("userData");
|
||||
setIsLoading(false);
|
||||
await apiDeviceTokenDeleted({ userId: user?.id as any, deviceId });
|
||||
|
||||
Toast.show({
|
||||
type: "success",
|
||||
@@ -297,6 +261,31 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const acceptedTerms = async (nomor: string) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await apiUpdatedTermCondition({ nomor: nomor });
|
||||
|
||||
if (response.success) {
|
||||
router.replace(`/verification?nomor=${nomor}`);
|
||||
} else {
|
||||
if (response.status === 404) {
|
||||
router.replace(`/register?nomor=${nomor}`);
|
||||
} else {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
text1: "Error",
|
||||
text2: response.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error accept terms", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<AuthContext.Provider
|
||||
@@ -312,6 +301,7 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
logout,
|
||||
registerUser,
|
||||
userData,
|
||||
acceptedTerms,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
2
eas.json
2
eas.json
@@ -11,7 +11,7 @@
|
||||
"preview": {
|
||||
"distribution": "internal",
|
||||
"android": {
|
||||
"buildType": "app-bundle"
|
||||
"buildType": "apk"
|
||||
},
|
||||
"ios": {
|
||||
"simulator": false
|
||||
|
||||
@@ -13,3 +13,13 @@ Exp: open ios/HIPMIBADUNG.xcworkspace
|
||||
perubahan versi : npm version patch
|
||||
ios: bunx expo prebuild --platform ios
|
||||
android: bunx expo prebuild --platform android
|
||||
|
||||
### Android
|
||||
adb devices : cek device yang terhubung
|
||||
Note: izinkan perangkat dulu agar statusnya tidak unauthorized
|
||||
|
||||
adb install android/app/build/outputs/apk/debug/app-debug.apk : install apk ke device
|
||||
Note:
|
||||
Gunakan flag -s (serial) di perintah adb untuk menentukan target
|
||||
adb -s <0G52319V261040B2 ini adalah id nya> install android/app/build/outputs/apk/debug/app-debug.apk
|
||||
|
||||
|
||||
37
hooks/use-foreground-notifications.ts
Normal file
37
hooks/use-foreground-notifications.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { useEffect } from "react";
|
||||
import {
|
||||
getMessaging,
|
||||
onMessage,
|
||||
FirebaseMessagingTypes,
|
||||
} from "@react-native-firebase/messaging";
|
||||
import { useAuth } from "./use-auth";
|
||||
|
||||
// Gunakan tipe resmi dari library
|
||||
type RemoteMessage = FirebaseMessagingTypes.RemoteMessage;
|
||||
|
||||
export function useForegroundNotifications(
|
||||
onMessageReceived: (message: RemoteMessage) => void
|
||||
) {
|
||||
const { user } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
const messaging = getMessaging();
|
||||
|
||||
const unsubscribe = onMessage(messaging, (remoteMessage) => {
|
||||
const data = remoteMessage.data;
|
||||
// console.log("DATA NOTIFIKASI DARI SERVER", data)
|
||||
if (data?.recipientId && data?.recipientId !== user?.id) {
|
||||
console.log("📵 Notification untuk user lain", data);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
"🔔 Notifikasi diterima saat app aktif:",
|
||||
JSON.stringify(data, null, 2)
|
||||
);
|
||||
onMessageReceived(remoteMessage);
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, [user?.id, onMessageReceived]);
|
||||
}
|
||||
168
hooks/use-notification-store.tsx
Normal file
168
hooks/use-notification-store.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
// hooks/useNotificationStore.ts
|
||||
import {
|
||||
apiNotificationMarkAsRead,
|
||||
apiNotificationUnreadCount,
|
||||
} from "@/service/api-notifications";
|
||||
import {
|
||||
createContext,
|
||||
ReactNode,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useAuth } from "./use-auth";
|
||||
|
||||
type AppNotification = {
|
||||
id: string;
|
||||
title: string;
|
||||
body: string;
|
||||
data?: Record<string, string>;
|
||||
isRead: boolean;
|
||||
timestamp: number;
|
||||
type: "announcement" | "trigger";
|
||||
// untuk id dari setiap kategori app
|
||||
appId?: string;
|
||||
kategoriApp?:
|
||||
| "JOB"
|
||||
| "VOTING"
|
||||
| "EVENT"
|
||||
| "DONASI"
|
||||
| "INVESTASI"
|
||||
| "COLLABORATION"
|
||||
| "FORUM"
|
||||
| "ACCESS"; // Untuk trigger akses user;
|
||||
};
|
||||
|
||||
type NotificationContextType = {
|
||||
notifications: AppNotification[];
|
||||
unreadCount: number;
|
||||
addNotification: (
|
||||
notif: Omit<AppNotification, "id" | "isRead" | "timestamp">
|
||||
) => void;
|
||||
markAsRead: (id: string) => void;
|
||||
markAsReadAll: (id: string) => void;
|
||||
syncUnreadCount: () => Promise<void>;
|
||||
};
|
||||
|
||||
const NotificationContext = createContext<NotificationContextType>({
|
||||
notifications: [],
|
||||
unreadCount: 0,
|
||||
addNotification: () => {},
|
||||
markAsRead: () => {},
|
||||
markAsReadAll: () => {},
|
||||
syncUnreadCount: async () => {},
|
||||
});
|
||||
|
||||
export const NotificationProvider = ({ children }: { children: ReactNode }) => {
|
||||
const { user } = useAuth();
|
||||
const [notifications, setNotifications] = useState<AppNotification[]>([]);
|
||||
const [unreadCount, setUnreadCount] = useState(0);
|
||||
|
||||
console.log(
|
||||
"🚀 Notifications Masuk:",
|
||||
JSON.stringify(notifications, null, 2)
|
||||
);
|
||||
|
||||
// Sync unread count dari backend saat provider di-mount
|
||||
useEffect(() => {
|
||||
fetchUnreadCount();
|
||||
}, [user?.id]);
|
||||
|
||||
const fetchUnreadCount = async () => {
|
||||
try {
|
||||
const count = await apiNotificationUnreadCount({
|
||||
id: user?.id as any,
|
||||
role: user?.masterUserRoleId as any,
|
||||
}); // ← harus return number
|
||||
const result = count.data;
|
||||
console.log("📖 Unread count:", result);
|
||||
setUnreadCount(result);
|
||||
} catch (error) {
|
||||
console.error("Gagal fetch unread count:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const addNotification = (
|
||||
notif: Omit<AppNotification, "id" | "isRead" | "timestamp">
|
||||
) => {
|
||||
setNotifications((prev) => [
|
||||
{
|
||||
...notif,
|
||||
id: Date.now().toString(),
|
||||
isRead: false,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
...prev,
|
||||
]);
|
||||
|
||||
setUnreadCount((prev) => prev + 1);
|
||||
};
|
||||
|
||||
const markAsRead = async (id: string) => {
|
||||
try {
|
||||
const response = await apiNotificationMarkAsRead({ id, category: "one" });
|
||||
console.log("🚀 Response Mark As Read:", response);
|
||||
|
||||
if (response.success) {
|
||||
const cloneNotifications = [...notifications];
|
||||
const index = cloneNotifications.findIndex((n) => n?.data?.id === id);
|
||||
if (index !== -1) {
|
||||
cloneNotifications[index].isRead = true;
|
||||
setNotifications(cloneNotifications);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Gagal mark as read:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const markAsReadAll = async (id: string) => {
|
||||
try {
|
||||
const response = await apiNotificationMarkAsRead({ id, category: "all" });
|
||||
console.log("🚀 Response Mark As Read All:", response);
|
||||
|
||||
if (response.success) {
|
||||
const cloneNotifications = [...notifications];
|
||||
const index = cloneNotifications.findIndex((n) => n?.data?.id === id);
|
||||
if (index !== -1) {
|
||||
cloneNotifications[index].isRead = true;
|
||||
setNotifications(cloneNotifications);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Gagal mark as read:", error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const syncUnreadCount = async () => {
|
||||
try {
|
||||
const count = await apiNotificationUnreadCount({
|
||||
id: user?.id as any,
|
||||
role: user?.masterUserRoleId as any,
|
||||
}); // ← harus return number
|
||||
const result = count.data;
|
||||
console.log("📖 Unread count sync:", result);
|
||||
setUnreadCount(result);
|
||||
} catch (error) {
|
||||
console.warn("⚠️ Gagal sync unread count:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<NotificationContext.Provider
|
||||
value={{
|
||||
notifications,
|
||||
addNotification,
|
||||
unreadCount,
|
||||
markAsRead,
|
||||
markAsReadAll,
|
||||
syncUnreadCount,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</NotificationContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useNotificationStore = () => useContext(NotificationContext);
|
||||
113
hooks/use-notification-store.tsx.back
Normal file
113
hooks/use-notification-store.tsx.back
Normal file
@@ -0,0 +1,113 @@
|
||||
// hooks/useNotificationStore.ts
|
||||
import { apiGetNotificationsById } from "@/service/api-notifications";
|
||||
import { createContext, ReactNode, useContext, useState, useEffect } from "react";
|
||||
import { useAuth } from "./use-auth";
|
||||
|
||||
type AppNotification = {
|
||||
id: string;
|
||||
title: string;
|
||||
body: string;
|
||||
data?: Record<string, string>;
|
||||
isRead: boolean;
|
||||
timestamp: number;
|
||||
type: "notification" | "trigger";
|
||||
appId?: string;
|
||||
kategoriApp?:
|
||||
| "JOB"
|
||||
| "VOTING"
|
||||
| "EVENT"
|
||||
| "DONASI"
|
||||
| "INVESTASI"
|
||||
| "COLLABORATION"
|
||||
| "FORUM"
|
||||
| "ACCESS";
|
||||
};
|
||||
|
||||
type NotificationContextType = {
|
||||
notifications: AppNotification[];
|
||||
unreadCount: number;
|
||||
addNotification: (
|
||||
notif: Omit<AppNotification, "id" | "isRead" | "timestamp">
|
||||
) => void;
|
||||
markAsRead: (id: string) => void;
|
||||
syncUnreadCount: () => Promise<void>;
|
||||
};
|
||||
|
||||
const NotificationContext = createContext<NotificationContextType>({
|
||||
notifications: [],
|
||||
unreadCount: 0,
|
||||
addNotification: () => {},
|
||||
markAsRead: () => {},
|
||||
syncUnreadCount: async () => {},
|
||||
});
|
||||
|
||||
export const NotificationProvider = ({ children }: { children: ReactNode }) => {
|
||||
const {user} = useAuth()
|
||||
const [notifications, setNotifications] = useState<AppNotification[]>([]);
|
||||
const [unreadCount, setUnreadCount] = useState(0);
|
||||
|
||||
// 🔔 Sync unread count dari backend saat provider di-mount
|
||||
useEffect(() => {
|
||||
const fetchUnreadCount = async () => {
|
||||
try {
|
||||
const count = await apiGetNotificationsById({
|
||||
id: user?.id as any,
|
||||
category: "count-as-unread"
|
||||
}); // ← harus return number
|
||||
const result = count.data
|
||||
setUnreadCount(result);
|
||||
} catch (error) {
|
||||
console.erro("⚠️ Gagal fetch unread count:", error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchUnreadCount();
|
||||
}, []);
|
||||
|
||||
const addNotification = (
|
||||
notif: Omit<AppNotification, "id" | "isRead" | "timestamp">
|
||||
) => {
|
||||
setNotifications((prev) => [
|
||||
{
|
||||
...notif,
|
||||
id: Date.now().toString(),
|
||||
isRead: false,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
...prev,
|
||||
]);
|
||||
// Tambahkan ke unread count (untuk notifikasi foreground)
|
||||
setUnreadCount((prev) => prev + 1);
|
||||
};
|
||||
|
||||
const markAsRead = (id: string) => {
|
||||
setNotifications((prev) =>
|
||||
prev.map((n) => (n.id === id ? { ...n, isRead: true } : n))
|
||||
);
|
||||
// Kurangi unread count
|
||||
setUnreadCount((prev) => Math.max(0, prev - 1));
|
||||
};
|
||||
|
||||
const syncUnreadCount = async () => {
|
||||
try {
|
||||
const count = await apiGetNotificationsById({
|
||||
id: user?.id as any,
|
||||
category: "count-as-unread"
|
||||
}); // ← harus return number
|
||||
const result = count.data
|
||||
setUnreadCount(result);
|
||||
} catch (error) {
|
||||
console.warn("⚠️ Gagal sync unread count:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<NotificationContext.Provider
|
||||
value={{ notifications, unreadCount, addNotification, markAsRead, syncUnreadCount }}
|
||||
>
|
||||
{children}
|
||||
</NotificationContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useNotificationStore = () => useContext(NotificationContext);
|
||||
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user