Compare commits

...

3 Commits

Author SHA1 Message Date
57ac1eb45e Notifikasi admin to user
Fix:
- android/app/src/main/AndroidManifest.xml
- app/(application)/(user)/job/(tabs)/_layout.tsx
- app/(application)/(user)/job/[id]/index.tsx
- app/(application)/(user)/notifications/index.tsx
- app/(application)/(user)/profile/[id]/index.tsx
- app/(application)/admin/job/[id]/[status]/index.tsx
- app/(application)/admin/notification/index.tsx
- app/+not-found.tsx
- ios/HIPMIBadungConnect.xcodeproj/project.pbxproj
- screens/Admin/Job/funUpdateStatus.ts
- screens/Home/bottomFeatureSection.tsx

### No Issue
2026-01-08 17:48:53 +08:00
145ad73616 Fix job notifikasi
###  No Issue
2026-01-08 10:12:53 +08:00
7c85e35c61 Note:
- Fitur notifikasi ke admin dari user baru
- Notifikasi ke user bahwa akunnya telah terverifikasi

Fix:
- app/(application)/(user)/notifications/index.tsx
- app/(application)/(user)/waiting-room.tsx
- app/(application)/admin/super-admin/[id]/index.tsx
- app/(application)/admin/user-access/[id]/index.tsx
- context/AuthContext.tsx
- screens/Home/tabsList.ts
- service/api-admin/api-admin-user-access.ts
- service/api-device-token.ts
- service/api-notifications.ts
- types/type-notification-category.ts

Add:
- lib/routeApp.ts

### No Issue
2026-01-06 12:27:30 +08:00
27 changed files with 339 additions and 201 deletions

View File

@@ -1,6 +1,4 @@
<manifest <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.CAMERA"/> <uses-permission android:name="android.permission.CAMERA"/>
@@ -17,36 +15,13 @@ xmlns:tools="http://schemas.android.com/tools">
<data android:scheme="https"/> <data android:scheme="https"/>
</intent> </intent>
</queries> </queries>
<application <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">
android:name=".MainApplication" <meta-data android:name="com.google.firebase.messaging.default_notification_color" android:resource="@color/notification_icon_color" tools:replace="android:resource"/>
android:label="@string/app_name" <meta-data android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@drawable/notification_icon"/>
android:icon="@mipmap/ic_launcher" <meta-data android:name="expo.modules.notifications.default_notification_color" android:resource="@color/notification_icon_color"/>
android:roundIcon="@mipmap/ic_launcher_round" <meta-data android:name="expo.modules.notifications.default_notification_icon" android:resource="@drawable/notification_icon"/>
android:allowBackup="true" <meta-data android:name="expo.modules.updates.ENABLED" android:value="false"/>
android:theme="@style/AppTheme" <meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
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"/> <meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/>
<activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="portrait"> <activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="portrait">
<intent-filter> <intent-filter>

View File

@@ -519,7 +519,8 @@ export default function UserLayout() {
name="job/(tabs)" name="job/(tabs)"
options={{ options={{
title: "Job Vacancy", title: "Job Vacancy",
headerLeft: () => <BackButton path="/home" />, // headerLeft: () => <BackButton path="/home" />,
// Note: headerLeft di pindahkan ke Tabs Layout
}} }}
/> />
<Stack.Screen <Stack.Screen

View File

@@ -1,34 +1,72 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { BackButton } from "@/components";
import { IconHome, IconStatus } from "@/components/_Icon"; import { IconHome, IconStatus } from "@/components/_Icon";
import { TabsStyles } from "@/styles/tabs-styles"; import { TabsStyles } from "@/styles/tabs-styles";
import { Ionicons } from "@expo/vector-icons"; 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() { export default function JobTabsLayout() {
const navigation = useNavigation();
const { from, category } = useLocalSearchParams<{
from?: string;
category?: string;
}>();
// Atur header secara dinamis
useLayoutEffect(() => {
navigation.setOptions({
headerLeft: () => (
<BackButton
onPress={() => {
if (from === "notifications") {
router.replace(`/notifications?category=${category}`);
} else {
if (from) {
router.replace(`/${from}` as any);
} else {
router.navigate("/home");
}
}
}}
/>
),
});
}, [from, router, navigation]);
return ( return (
<Tabs screenOptions={TabsStyles}> <>
<Tabs.Screen <Tabs screenOptions={TabsStyles}>
name="index" <Tabs.Screen
options={{ name="index"
title: "Beranda", options={{
tabBarIcon: ({ color }) => <IconHome color={color} />, title: "Beranda",
}} tabBarIcon: ({ color }) => <IconHome color={color} />,
/> }}
<Tabs.Screen />
name="status" <Tabs.Screen
options={{ name="status"
title: "Status", options={{
tabBarIcon: ({ color }) => <IconStatus color={color} />, title: "Status",
}} tabBarIcon: ({ color }) => <IconStatus color={color} />,
/> }}
<Tabs.Screen />
name="archive" <Tabs.Screen
options={{ name="archive"
title: "Arsip", options={{
tabBarIcon: ({ color }) => ( title: "Arsip",
<Ionicons size={20} name="archive" color={color} /> tabBarIcon: ({ color }) => (
), <Ionicons size={20} name="archive" color={color} />
}} ),
/> }}
</Tabs> />
</Tabs>
</>
); );
} }

View File

@@ -9,14 +9,17 @@ import {
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import { dummyMasterStatus } from "@/lib/dummy-data/_master/status"; import { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
import { apiJobGetByStatus } from "@/service/api-client/api-job"; import { apiJobGetByStatus } from "@/service/api-client/api-job";
import { useFocusEffect } from "expo-router"; import { useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash"; import _ from "lodash";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
export default function JobStatus() { export default function JobStatus() {
const { user } = useAuth(); const { user } = useAuth();
const { status } = useLocalSearchParams<{ status?: string }>();
console.log("STATUS", status);
const [activeCategory, setActiveCategory] = useState<string | null>( const [activeCategory, setActiveCategory] = useState<string | null>(
"publish" status || "publish"
); );
const [listData, setListData] = useState<any[]>([]); const [listData, setListData] = useState<any[]>([]);
const [isLoadList, setIsLoadList] = useState(false); const [isLoadList, setIsLoadList] = useState(false);
@@ -60,25 +63,29 @@ export default function JobStatus() {
); );
return ( return (
<ViewWrapper headerComponent={scrollComponent} hideFooter> <>
{isLoadList ? ( <ViewWrapper headerComponent={scrollComponent} hideFooter>
<LoaderCustom /> {isLoadList ? (
) : _.isEmpty(listData) ? ( <LoaderCustom />
<TextCustom align="center">Tidak ada data {activeCategory}</TextCustom> ) : _.isEmpty(listData) ? (
) : ( <TextCustom align="center">
listData.map((e, i) => ( Tidak ada data {activeCategory}
<BaseBox </TextCustom>
key={i} ) : (
paddingTop={20} listData.map((e, i) => (
paddingBottom={20} <BaseBox
href={`/job/${e?.id}/${activeCategory}/detail`} key={i}
> paddingTop={20}
<TextCustom align="center" bold truncate size="large"> paddingBottom={20}
{e?.title} href={`/job/${e?.id}/${activeCategory}/detail`}
</TextCustom> >
</BaseBox> <TextCustom align="center" bold truncate size="large">
)) {e?.title}
)} </TextCustom>
</ViewWrapper> </BaseBox>
))
)}
</ViewWrapper>
</>
); );
} }

View File

@@ -74,7 +74,7 @@ export default function JobDetailStatus() {
<StackCustom gap={"xs"}> <StackCustom gap={"xs"}>
{data && {data &&
data?.catatan && data?.catatan &&
(status === "draft" || status === "rejected") && ( (status === "draft" || status === "reject") && (
<ReportBox text={data?.catatan} /> <ReportBox text={data?.catatan} />
)} )}

View File

@@ -25,6 +25,8 @@ export default function JobDetail() {
setIsLoading(true); setIsLoading(true);
const response = await apiJobGetOne({ id: id as string }); const response = await apiJobGetOne({ id: id as string });
console.log("DATA", JSON.stringify(response.data, null,2));
setData(response.data); setData(response.data);
} catch (error) { } catch (error) {
console.log("[ERROR]", error); console.log("[ERROR]", error);

View File

@@ -19,7 +19,7 @@ import { useState } from "react";
import Toast from "react-native-toast-message"; import Toast from "react-native-toast-message";
export default function JobCreate() { export default function JobCreate() {
const nextUrl = "/(application)/(user)/job/(tabs)/status"; const nextUrl = "/(application)/(user)/job/(tabs)/status?status=review";
const { user } = useAuth(); const { user } = useAuth();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [image, setImage] = useState<string | null>(null); const [image, setImage] = useState<string | null>(null);

View File

@@ -1,26 +1,52 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { import {
BaseBox, BaseBox,
NewWrapper, NewWrapper,
ScrollableCustom, ScrollableCustom,
StackCustom, StackCustom,
TextCustom TextCustom,
} from "@/components"; } from "@/components";
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent"; import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
import NoDataText from "@/components/_ShareComponent/NoDataText";
import { AccentColor } from "@/constants/color-palet"; import { AccentColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import { useNotificationStore } from "@/hooks/use-notification-store"; import { useNotificationStore } from "@/hooks/use-notification-store";
import { apiGetNotificationsById } from "@/service/api-notifications"; import { apiGetNotificationsById } from "@/service/api-notifications";
import { listOfcategoriesAppNotification } from "@/types/type-notification-category"; import { listOfcategoriesAppNotification } from "@/types/type-notification-category";
import { formatChatTime } from "@/utils/formatChatTime"; import { formatChatTime } from "@/utils/formatChatTime";
import { router, useFocusEffect } from "expo-router"; import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { RefreshControl, View } from "react-native"; import { RefreshControl, View } from "react-native";
const selectedCategory = (value: string) => { const selectedCategory = (value: string) => {
const category = listOfcategoriesAppNotification.find((c) => c.value === value); const category = listOfcategoriesAppNotification.find(
(c) => c.value === value
);
return category?.label; 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 = ({ const BoxNotification = ({
data, data,
activeCategory, activeCategory,
@@ -28,18 +54,28 @@ const BoxNotification = ({
data: any; data: any;
activeCategory: string | null; activeCategory: string | null;
}) => { }) => {
// console.log("DATA NOTIFICATION", JSON.stringify(data, null, 2));
const { markAsRead } = useNotificationStore(); const { markAsRead } = useNotificationStore();
return ( return (
<> <>
<BaseBox <BaseBox
backgroundColor={data.isRead ? AccentColor.darkblue : AccentColor.blue} backgroundColor={data.isRead ? AccentColor.darkblue : AccentColor.blue}
onPress={() => { onPress={() => {
console.log( // console.log(
"Notification >", // "Notification >",
selectedCategory(activeCategory as string) // selectedCategory(activeCategory as string)
); // );
router.push(data.deepLink); const newPath = fixPath({
markAsRead(data.id); deepLink: data.deepLink,
categoryApp: data.kategoriApp,
});
router.replace(newPath as any);
selectedCategory(activeCategory as string);
if (!data.isRead) {
markAsRead(data.id);
}
}} }}
> >
<StackCustom> <StackCustom>
@@ -60,7 +96,10 @@ const BoxNotification = ({
export default function Notifications() { export default function Notifications() {
const { user } = useAuth(); const { user } = useAuth();
const [activeCategory, setActiveCategory] = useState<string | null>("event"); const { category } = useLocalSearchParams<{ category?: string }>();
const [activeCategory, setActiveCategory] = useState<string | null>(
category || "event"
);
const [listData, setListData] = useState<any[]>([]); const [listData, setListData] = useState<any[]>([]);
const [refreshing, setRefreshing] = useState(false); const [refreshing, setRefreshing] = useState(false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -83,7 +122,7 @@ export default function Notifications() {
id: user?.id as any, id: user?.id as any,
category: activeCategory as any, category: activeCategory as any,
}); });
// console.log("Response Notification", JSON.stringify(response, null, 2));
if (response.success) { if (response.success) {
setListData(response.data); setListData(response.data);
} else { } else {
@@ -120,7 +159,9 @@ export default function Notifications() {
} }
> >
{loading ? ( {loading ? (
<ListSkeletonComponent/> <ListSkeletonComponent />
) : _.isEmpty(listData) ? (
<NoDataText text="Belum ada notifikasi" />
) : ( ) : (
listData.map((e, i) => ( listData.map((e, i) => (
<View key={i}> <View key={i}>

View File

@@ -139,7 +139,9 @@ const ButtonnDot = ({
isUserCheck: boolean; isUserCheck: boolean;
logout: () => Promise<void>; logout: () => Promise<void>;
}) => { }) => {
const isId = id === undefined || id === null; console.log("[ID] >>", id);
const isId = id === undefined || id === "undefined";
if (isId) { if (isId) {
return ( return (

View File

@@ -1,12 +1,10 @@
import { import {
AlertDefaultSystem, AlertDefaultSystem,
BoxButtonOnFooter, BoxButtonOnFooter,
ButtonCenteredOnly,
ButtonCustom, ButtonCustom,
InformationBox, InformationBox,
NewWrapper, NewWrapper,
StackCustom, StackCustom
ViewWrapper,
} from "@/components"; } from "@/components";
import { ICON_SIZE_BUTTON } from "@/constants/constans-value"; import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";

View File

@@ -15,6 +15,7 @@ import AdminButtonReject from "@/components/_ShareComponent/Admin/ButtonReject";
import AdminButtonReview from "@/components/_ShareComponent/Admin/ButtonReview"; import AdminButtonReview from "@/components/_ShareComponent/Admin/ButtonReview";
import ReportBox from "@/components/Box/ReportBox"; import ReportBox from "@/components/Box/ReportBox";
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
import funUpdateStatusJob from "@/screens/Admin/Job/funUpdateStatus"; import funUpdateStatusJob from "@/screens/Admin/Job/funUpdateStatus";
import { apiAdminJobGetById } from "@/service/api-admin/api-admin-job"; import { apiAdminJobGetById } from "@/service/api-admin/api-admin-job";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router"; import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
@@ -23,8 +24,10 @@ import { useCallback, useState } from "react";
import Toast from "react-native-toast-message"; import Toast from "react-native-toast-message";
export default function AdminJobDetailStatus() { export default function AdminJobDetailStatus() {
const { user } = useAuth();
const { id, status } = useLocalSearchParams(); const { id, status } = useLocalSearchParams();
const [data, setData] = useState<any | null>(null); const [data, setData] = useState<any | null>(null);
const [isLoading, setIsLoading] = useState(false);
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
@@ -92,6 +95,9 @@ export default function AdminJobDetailStatus() {
const response = await funUpdateStatusJob({ const response = await funUpdateStatusJob({
id: id as string, id: id as string,
changeStatus, changeStatus,
data: {
senderId: user?.id as string,
},
}); });
if (!response.success) { if (!response.success) {
@@ -142,12 +148,15 @@ export default function AdminJobDetailStatus() {
</StackCustom> </StackCustom>
</BaseBox> </BaseBox>
{data && data?.catatan && (status === "reject" || status === "review") && ( {data &&
<ReportBox text={data?.catatan}/> data?.catatan &&
)} (status === "reject" || status === "review") && (
<ReportBox text={data?.catatan} />
)}
{status === "review" && ( {status === "review" && (
<AdminButtonReview <AdminButtonReview
isLoading={isLoading}
onPublish={() => { onPublish={() => {
AlertDefaultSystem({ AlertDefaultSystem({
title: "Publish", title: "Publish",
@@ -156,6 +165,7 @@ export default function AdminJobDetailStatus() {
textRight: "Ya", textRight: "Ya",
onPressRight: () => { onPressRight: () => {
handleUpdate({ changeStatus: "publish" }); handleUpdate({ changeStatus: "publish" });
setIsLoading(true);
}, },
}); });
}} }}

View File

@@ -15,7 +15,10 @@ import Toast from "react-native-toast-message";
export default function AdminJobRejectInput() { export default function AdminJobRejectInput() {
const { id, status } = useLocalSearchParams(); const { id, status } = useLocalSearchParams();
const [data, setData] = useState<any | null>(null); const [data, setData] = useState({
catatan: "",
senderId: ""
});
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
useFocusEffect( useFocusEffect(
@@ -48,7 +51,7 @@ export default function AdminJobRejectInput() {
const response = await funUpdateStatusJob({ const response = await funUpdateStatusJob({
id: id as string, id: id as string,
changeStatus, changeStatus,
data: data, data: data ,
}); });
if (!response.success) { if (!response.success) {
@@ -102,8 +105,8 @@ export default function AdminJobRejectInput() {
headerComponent={<AdminBackButtonAntTitle title="Penolakan Job" />} headerComponent={<AdminBackButtonAntTitle title="Penolakan Job" />}
> >
<TextAreaCustom <TextAreaCustom
value={data} value={data?.catatan}
onChangeText={setData} onChangeText={(text) => setData({ ...data, catatan: text })}
placeholder="Masukan alasan" placeholder="Masukan alasan"
required required
showCount showCount

View File

@@ -7,6 +7,8 @@ import {
TextCustom, TextCustom,
} from "@/components"; } from "@/components";
import { IconPlus } from "@/components/_Icon"; import { IconPlus } from "@/components/_Icon";
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
import NoDataText from "@/components/_ShareComponent/NoDataText";
import { AccentColor, MainColor } from "@/constants/color-palet"; import { AccentColor, MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import { useNotificationStore } from "@/hooks/use-notification-store"; import { useNotificationStore } from "@/hooks/use-notification-store";
@@ -14,6 +16,7 @@ import { apiGetNotificationsById } from "@/service/api-notifications";
import { listOfcategoriesAppNotification } from "@/types/type-notification-category"; import { listOfcategoriesAppNotification } from "@/types/type-notification-category";
import { formatChatTime } from "@/utils/formatChatTime"; import { formatChatTime } from "@/utils/formatChatTime";
import { router, Stack, useFocusEffect } from "expo-router"; import { router, Stack, useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { RefreshControl, View } from "react-native"; import { RefreshControl, View } from "react-native";
@@ -66,6 +69,7 @@ export default function AdminNotification() {
const [activeCategory, setActiveCategory] = useState<string | null>("event"); const [activeCategory, setActiveCategory] = useState<string | null>("event");
const [listData, setListData] = useState<any[]>([]); const [listData, setListData] = useState<any[]>([]);
const [refreshing, setRefreshing] = useState(false); const [refreshing, setRefreshing] = useState(false);
const [loading, setLoading] = useState(false);
const handlePress = (item: any) => { const handlePress = (item: any) => {
setActiveCategory(item.value); setActiveCategory(item.value);
@@ -80,11 +84,12 @@ export default function AdminNotification() {
const fecthData = async () => { const fecthData = async () => {
try { try {
setLoading(true);
const response = await apiGetNotificationsById({ const response = await apiGetNotificationsById({
id: user?.id as any, id: user?.id as any,
category: activeCategory as any, category: activeCategory as any,
}); });
// console.log("Response Notification", JSON.stringify(response, null, 2));
if (response.success) { if (response.success) {
setListData(response.data); setListData(response.data);
} else { } else {
@@ -92,6 +97,8 @@ export default function AdminNotification() {
} }
} catch (error) { } catch (error) {
console.log("Error Notification", error); console.log("Error Notification", error);
} finally {
setLoading(false);
} }
}; };
@@ -132,11 +139,20 @@ export default function AdminNotification() {
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} /> <RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
} }
> >
{listData.map((e, i) => ( {loading ? (
<View key={i}> <ListSkeletonComponent />
<BoxNotification data={e} activeCategory={activeCategory as any} /> ) : _.isEmpty(listData) ? (
</View> <NoDataText text="Belum ada notifikasi" />
))} ) : (
listData.map((e, i) => (
<View key={i}>
<BoxNotification
data={e}
activeCategory={activeCategory as any}
/>
</View>
))
)}
</NewWrapper> </NewWrapper>
</> </>
); );

View File

@@ -48,6 +48,7 @@ export default function SuperAdminDetail() {
const response = await apiAdminUserAccessUpdateStatus({ const response = await apiAdminUserAccessUpdateStatus({
id: id as string, id: id as string,
role: data?.masterUserRoleId === "2" ? "user" : "admin", role: data?.masterUserRoleId === "2" ? "user" : "admin",
category: "role"
}); });
if (!response.success) { if (!response.success) {

View File

@@ -9,15 +9,21 @@ import {
} from "@/components"; } from "@/components";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle"; import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import GridTwoView from "@/components/_ShareComponent/GridTwoView"; import GridTwoView from "@/components/_ShareComponent/GridTwoView";
import { useAuth } from "@/hooks/use-auth";
import { routeUser } from "@/lib/routeApp";
import { import {
apiAdminUserAccessGetById, apiAdminUserAccessGetById,
apiAdminUserAccessUpdateStatus, apiAdminUserAccessUpdateStatus,
} from "@/service/api-admin/api-admin-user-access"; } from "@/service/api-admin/api-admin-user-access";
import {
apiNotificationsSendById
} from "@/service/api-notifications";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router"; import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import Toast from "react-native-toast-message"; import Toast from "react-native-toast-message";
export default function AdminUserAccessDetail() { export default function AdminUserAccessDetail() {
const { user } = useAuth();
const { id } = useLocalSearchParams(); const { id } = useLocalSearchParams();
const [data, setData] = useState<any | null>(null); const [data, setData] = useState<any | null>(null);
const [loadData, setLoadData] = useState(false); const [loadData, setLoadData] = useState(false);
@@ -33,6 +39,7 @@ export default function AdminUserAccessDetail() {
try { try {
setLoadData(true); setLoadData(true);
const response = await apiAdminUserAccessGetById({ id: id as string }); const response = await apiAdminUserAccessGetById({ id: id as string });
console.log("[DATA]", JSON.stringify(response.data, null, 2));
setData(response.data); setData(response.data);
} catch (error) { } catch (error) {
@@ -48,6 +55,7 @@ export default function AdminUserAccessDetail() {
const response = await apiAdminUserAccessUpdateStatus({ const response = await apiAdminUserAccessUpdateStatus({
id: id as string, id: id as string,
active: !data?.active, active: !data?.active,
category: "access",
}); });
if (!response.success) { if (!response.success) {
@@ -61,6 +69,21 @@ export default function AdminUserAccessDetail() {
type: "success", type: "success",
text1: "Update aktifasi berhasil ", 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(); router.back();
} catch (error) { } catch (error) {
console.log("[ERROR UPDATE STATUS]", error); console.log("[ERROR UPDATE STATUS]", error);

View File

@@ -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() { export default function NotFoundScreen() {
return ( return (
<>
<Stack.Screen
options={{ headerShown: false, headerLeft: () => <BackButton /> }}
/>
<ViewWrapper> <ViewWrapper>
<StackCustom align="center" gap={0} style={{justifyContent: "center", alignItems: "center", flex: 1}}> <StackCustom
<TextCustom size="large" bold style={{fontSize: 100}}> align="center"
gap={0}
style={{ justifyContent: "center", alignItems: "center", flex: 1 }}
>
<TextCustom size="large" bold style={{ fontSize: 100 }}>
404 404
</TextCustom> </TextCustom>
<TextCustom size="large" bold> <TextCustom size="large" bold>
@@ -12,5 +21,6 @@ export default function NotFoundScreen() {
</TextCustom> </TextCustom>
</StackCustom> </StackCustom>
</ViewWrapper> </ViewWrapper>
); </>
} );
}

View File

@@ -12,10 +12,12 @@ const LeftButtonCustom = ({
path, path,
icon = "arrow-back", icon = "arrow-back",
iconCustom, iconCustom,
onPress,
}: { }: {
path?: Href; path?: Href;
icon?: React.ReactNode | any; icon?: React.ReactNode | any;
iconCustom?: React.ReactNode; iconCustom?: React.ReactNode;
onPress?: () => void;
}) => { }) => {
return ( return (
<> <>
@@ -26,7 +28,7 @@ const LeftButtonCustom = ({
name={icon} name={icon}
size={20} size={20}
color={MainColor.yellow} color={MainColor.yellow}
onPress={() => (path ? router.replace(path) : router.back())} onPress={() => (onPress ? onPress() : path ? router.replace(path) : router.back())}
/> />
)} )}
</> </>

View File

@@ -7,10 +7,10 @@ import {
import { apiDeviceTokenDeleted } from "@/service/api-device-token"; import { apiDeviceTokenDeleted } from "@/service/api-device-token";
import { IUser } from "@/types/User"; import { IUser } from "@/types/User";
import AsyncStorage from "@react-native-async-storage/async-storage"; import AsyncStorage from "@react-native-async-storage/async-storage";
import * as Device from "expo-device";
import { router } from "expo-router"; import { router } from "expo-router";
import { createContext, useEffect, useState } from "react"; import { createContext, useEffect, useState } from "react";
import Toast from "react-native-toast-message"; import Toast from "react-native-toast-message";
import * as Device from "expo-device";
// --- Types --- // --- Types ---
type AuthContextType = { type AuthContextType = {
@@ -105,18 +105,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 --- // --- 2. Validasi OTP & cek user ---
const validateOtp = async (nomor: string) => { const validateOtp = async (nomor: string) => {
try { try {
@@ -209,7 +197,6 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
setIsLoading(true); setIsLoading(true);
try { try {
const response = await apiRegister({ data: userData }); const response = await apiRegister({ data: userData });
console.log("[REGISTER FETCH]", JSON.stringify(response, null, 2));
if (!response.success) { if (!response.success) {
Toast.show({ Toast.show({
@@ -239,42 +226,7 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
setIsLoading(false); 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 --- // --- 5. Logout ---
@@ -284,7 +236,8 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
setToken(null); setToken(null);
setUser(null); setUser(null);
const deviceId = Device.osInternalBuildId || Device.modelName || "unknown"; const deviceId =
Device.osInternalBuildId || Device.modelName || "unknown";
await AsyncStorage.removeItem("authToken"); await AsyncStorage.removeItem("authToken");
await AsyncStorage.removeItem("userData"); await AsyncStorage.removeItem("userData");

View File

@@ -458,7 +458,7 @@
); );
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
PRODUCT_BUNDLE_IDENTIFIER = "com.anonymous.hipmi-mobile"; PRODUCT_BUNDLE_IDENTIFIER = "com.anonymous.hipmi-mobile";
PRODUCT_NAME = "HIPMIBadungConnect"; PRODUCT_NAME = HIPMIBadungConnect;
SWIFT_OBJC_BRIDGING_HEADER = "HIPMIBadungConnect/HIPMIBadungConnect-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "HIPMIBadungConnect/HIPMIBadungConnect-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@@ -490,7 +490,7 @@
); );
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = "com.anonymous.hipmi-mobile"; PRODUCT_BUNDLE_IDENTIFIER = "com.anonymous.hipmi-mobile";
PRODUCT_NAME = "HIPMIBadungConnect"; PRODUCT_NAME = HIPMIBadungConnect;
SWIFT_OBJC_BRIDGING_HEADER = "HIPMIBadungConnect/HIPMIBadungConnect-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "HIPMIBadungConnect/HIPMIBadungConnect-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";

9
lib/routeApp.ts Normal file
View File

@@ -0,0 +1,9 @@
export { routeAdmin, routeUser };
const routeAdmin = {
userAccess: ({ id }: { id: string }) => `/admin/user-access/${id}`,
};
const routeUser = {
home: `/(user)/home`,
};

View File

@@ -7,13 +7,17 @@ const funUpdateStatusJob = async ({
}: { }: {
id: string; id: string;
changeStatus: "publish" | "review" | "reject"; changeStatus: "publish" | "review" | "reject";
data?: string; data: {
catatan?: string;
senderId: string;
};
}) => { }) => {
try { try {
const fixData = data;
const response = await apiAdminJobUpdate({ const response = await apiAdminJobUpdate({
id: id, id: id,
status: changeStatus as any, status: changeStatus as any,
data: data, data: fixData as any,
}); });
return response; return response;

View File

@@ -17,7 +17,12 @@ export default function Home_BottomFeatureSection() {
}); });
// console.log("[DATA JOB]", JSON.stringify(response.data, null, 2)); // console.log("[DATA JOB]", JSON.stringify(response.data, null, 2));
const result = response.data.slice(-2); const result = response.data
.sort(
(a: any, b: any) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
)
.slice(0, 2);
setListData(result); setListData(result);
} catch (error) { } catch (error) {
console.log("[ERROR]", error); console.log("[ERROR]", error);

View File

@@ -1,6 +1,13 @@
import { ITabs } from "@/components/_Interface/types"; import { ITabs } from "@/components/_Interface/types";
import { Platform } from "react-native";
export const tabsHome: any = ({acceptedForumTermsAt, profileId}: {acceptedForumTermsAt: Date, profileId: string}) => [ export const tabsHome: any = ({
acceptedForumTermsAt,
profileId,
}: {
acceptedForumTermsAt: Date;
profileId: string;
}) => [
{ {
id: "forum", id: "forum",
icon: "chatbubble-ellipses-outline", icon: "chatbubble-ellipses-outline",
@@ -25,8 +32,8 @@ export const tabsHome: any = ({acceptedForumTermsAt, profileId}: {acceptedForumT
activeIcon: "map", activeIcon: "map",
label: "Maps", label: "Maps",
path: "/maps", path: "/maps",
isActive: true, isActive: Platform.OS === "ios" ? true : false,
disabled: false, disabled: Platform.OS === "ios" ? false : true,
}, },
{ {
id: "profile", id: "profile",

View File

@@ -28,13 +28,15 @@ export const apiAdminUserAccessUpdateStatus = async ({
id, id,
active, active,
role, role,
category,
}: { }: {
id: string; id: string;
active?: boolean; active?: boolean;
role?: "user" | "admin" | "super_admin"; role?: "user" | "admin" | "super_admin";
category: "access" | "role";
}) => { }) => {
try { try {
const response = await apiConfig.put(`/mobile/admin/user/${id}`, { const response = await apiConfig.put(`/mobile/admin/user/${id}?category=${category}`, {
data: { data: {
active, active,
role, role,

View File

@@ -31,7 +31,7 @@ export async function apiDeviceTokenDeleted({ userId, deviceId }: { userId: stri
const response = await apiConfig.delete( const response = await apiConfig.delete(
`/mobile/auth/device-tokens/${userId}?deviceId=${deviceId}` `/mobile/auth/device-tokens/${userId}?deviceId=${deviceId}`
); );
console.log("Device token deleted:", response.data);
return response.data; return response.data;
} catch (error) { } catch (error) {
console.error("Failed to delete device token:", error); console.error("Failed to delete device token:", error);
@@ -42,7 +42,7 @@ export async function apiDeviceTokenDeleted({ userId, deviceId }: { userId: stri
export async function apiGetAllTokenDevice() { export async function apiGetAllTokenDevice() {
try { try {
const response = await apiConfig.get(`/mobile/auth/device-tokens`); const response = await apiConfig.get(`/mobile/auth/device-tokens`);
console.log("Device token deleted:", response.data);
return response.data; return response.data;
} catch (error) { } catch (error) {
console.error("Failed to delete device token:", error); console.error("Failed to delete device token:", error);

View File

@@ -1,17 +1,9 @@
import { TypeNotificationCategoryApp, TypeOfTilteCategoryApp } from "@/types/type-notification-category"; import {
NotificationProp,
TypeNotificationCategoryApp
} from "@/types/type-notification-category";
import { apiConfig } from "./api-config"; import { apiConfig } from "./api-config";
type NotificationProp = {
title: TypeOfTilteCategoryApp;
body: string;
userLoginId: string;
appId?: string;
status?: string;
type?: "announcement" | "trigger";
deepLink?: string;
kategoriApp?: TypeNotificationCategoryApp
};
export async function apiNotificationsSend({ export async function apiNotificationsSend({
data, data,
}: { }: {
@@ -28,12 +20,30 @@ export async function apiNotificationsSend({
} }
} }
export async function apiNotificationsSendById({
data,
id,
}: {
data: NotificationProp;
id: string;
}) {
try {
const response = await apiConfig.post(`/mobile/notification/${id}`, {
data: data,
});
return response.data;
} catch (error) {
throw error;
}
}
export async function apiGetNotificationsById({ export async function apiGetNotificationsById({
id, id,
category, category,
}: { }: {
id: string; id: string;
category: TypeNotificationCategoryApp category: TypeNotificationCategoryApp;
}) { }) {
console.log("ID", id); console.log("ID", id);
console.log("Category", category); console.log("Category", category);
@@ -49,7 +59,13 @@ export async function apiGetNotificationsById({
} }
} }
export async function apiNotificationUnreadCount({ id, role }: { id: string, role: "user" | "admin" }) { export async function apiNotificationUnreadCount({
id,
role,
}: {
id: string;
role: "user" | "admin";
}) {
try { try {
const response = await apiConfig.get( const response = await apiConfig.get(
`/mobile/notification/${id}/unread-count?role=${role}` `/mobile/notification/${id}/unread-count?role=${role}`
@@ -62,12 +78,11 @@ export async function apiNotificationUnreadCount({ id, role }: { id: string, rol
} }
} }
export async function apiNotificationMarkAsRead({ id }: { id: string }) {
export async function apiNotificationMarkAsRead({id}: {id: string}) {
try { try {
const response = await apiConfig.put(`/mobile/notification/${id}`); const response = await apiConfig.put(`/mobile/notification/${id}`);
return response.data; return response.data;
} catch (error) { } catch (error) {
throw error; throw error;
} }
} }

View File

@@ -1,3 +1,16 @@
export type NotificationProp = {
title: TypeOfTilteCategoryApp | string
body: string;
userLoginId?: string;
appId?: string;
status?: string;
type?: "announcement" | "trigger";
deepLink?: string;
kategoriApp?: TypeNotificationCategoryApp
};
export type TypeNotificationCategoryApp = export type TypeNotificationCategoryApp =
| "EVENT" | "EVENT"
| "JOB" | "JOB"
@@ -6,9 +19,9 @@ export type TypeNotificationCategoryApp =
| "INVESTASI" | "INVESTASI"
| "COLLABORATION" | "COLLABORATION"
| "FORUM" | "FORUM"
| "ACCESS"; | "OTHER";
export type TypeOfTilteCategoryApp = "Pendaftaran User Baru" | "Other" | string; export type TypeOfTilteCategoryApp = "Pendaftaran User Baru" | "Other"
export const listOfcategoriesAppNotification = [ export const listOfcategoriesAppNotification = [
{ value: "event", label: "Event" }, { value: "event", label: "Event" },
@@ -18,4 +31,5 @@ export const listOfcategoriesAppNotification = [
{ value: "investasi", label: "Investasi" }, { value: "investasi", label: "Investasi" },
{ value: "forum", label: "Forum" }, { value: "forum", label: "Forum" },
{ value: "collaboration", label: "Collaboration" }, { value: "collaboration", label: "Collaboration" },
{ value: "other", label: "Lainnya" },
]; ];