From 6e2046467f7e571637639449cc2e7085725b4d1f Mon Sep 17 00:00:00 2001 From: bagasbanuna Date: Tue, 13 Jan 2026 17:41:30 +0800 Subject: [PATCH] Penerapan notifikasi di event Add: components/Button/BackButtonFromNotification.tsx types/type-collect-other.ts Fix: - android/app/build.gradle - app/(application)/(user)/_layout.tsx - app/(application)/(user)/event/(tabs)/_layout.tsx - app/(application)/(user)/event/(tabs)/status.tsx - app/(application)/(user)/event/create.tsx - app/(application)/(user)/job/(tabs)/_layout.tsx - app/(application)/(user)/notifications/index.tsx - app/(application)/admin/event/[id]/[status]/index.tsx - app/(application)/admin/event/[id]/reject-input.tsx - app/(application)/admin/notification/index.tsx - components/Notification/NotificationInitializer.tsx - hipmi-note.md - hooks/use-notification-store.tsx - screens/Admin/Event/funUpdateStatus.ts - service/api-notifications.ts - utils/formatChatTime.ts ### No Issue --- android/app/build.gradle | 2 +- app/(application)/(user)/_layout.tsx | 17 ++- .../(user)/event/(tabs)/_layout.tsx | 26 +++- .../(user)/event/(tabs)/status.tsx | 8 +- app/(application)/(user)/event/create.tsx | 2 +- .../(user)/job/(tabs)/_layout.tsx | 15 +- .../(user)/notifications/index.tsx | 136 ++++++++++++++---- .../admin/event/[id]/[status]/index.tsx | 1 + .../admin/event/[id]/reject-input.tsx | 15 +- .../admin/notification/index.tsx | 68 ++++++++- .../Button/BackButtonFromNotification.tsx | 29 ++++ .../Notification/NotificationInitializer.tsx | 3 +- hipmi-note.md | 10 ++ hooks/use-notification-store.tsx | 26 +++- screens/Admin/Event/funUpdateStatus.ts | 9 +- service/api-notifications.ts | 19 ++- types/type-collect-other.ts | 4 + utils/formatChatTime.ts | 18 +-- 18 files changed, 325 insertions(+), 83 deletions(-) create mode 100644 components/Button/BackButtonFromNotification.tsx create mode 100644 types/type-collect-other.ts diff --git a/android/app/build.gradle b/android/app/build.gradle index 0874adf..b761b19 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -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'}\"" diff --git a/app/(application)/(user)/_layout.tsx b/app/(application)/(user)/_layout.tsx index 1aca1e3..7d75dd1 100644 --- a/app/(application)/(user)/_layout.tsx +++ b/app/(application)/(user)/_layout.tsx @@ -53,7 +53,9 @@ export default function UserLayout() { /> {/* ========== Notification Section ========= */} - // ), }} - /> + /> */} {/* ========== Event Section ========= */} + ( - - ), + // NOTE: DIPINDAH DI FILE /Event/(Tabs)/_layout.tsx + // headerLeft: () => ( + // + // ), }} /> + , - // Note: headerLeft di pindahkan ke Tabs Layout + // NOTE: headerLeft di pindahkan ke Tabs Layout }} /> (); + + console.log("from", from); + console.log("category", category); + + // Atur header secara dinamis + useLayoutEffect(() => { + navigation.setOptions({ + headerLeft: () => ( + + ), + }); + }, [from, router, navigation]); + return ( (); + const id = user?.id || ""; const [activeCategory, setActiveCategory] = useState( - "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) => ( diff --git a/app/(application)/(user)/event/create.tsx b/app/(application)/(user)/event/create.tsx index dffebc9..3c98340 100644 --- a/app/(application)/(user)/event/create.tsx +++ b/app/(application)/(user)/event/create.tsx @@ -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 { diff --git a/app/(application)/(user)/job/(tabs)/_layout.tsx b/app/(application)/(user)/job/(tabs)/_layout.tsx index 6a63193..fedfb7c 100644 --- a/app/(application)/(user)/job/(tabs)/_layout.tsx +++ b/app/(application)/(user)/job/(tabs)/_layout.tsx @@ -1,6 +1,7 @@ /* 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 { @@ -23,19 +24,7 @@ export default function JobTabsLayout() { useLayoutEffect(() => { navigation.setOptions({ headerLeft: () => ( - { - if (from === "notifications") { - router.replace(`/notifications?category=${category}`); - } else { - if (from) { - router.replace(`/${from}` as any); - } else { - router.navigate("/home"); - } - } - }} - /> + ), }); }, [from, router, navigation]); diff --git a/app/(application)/(user)/notifications/index.tsx b/app/(application)/(user)/notifications/index.tsx index 15be826..37ff3ac 100644 --- a/app/(application)/(user)/notifications/index.tsx +++ b/app/(application)/(user)/notifications/index.tsx @@ -1,20 +1,27 @@ /* eslint-disable react-hooks/exhaustive-deps */ import { + AlertDefaultSystem, + BackButton, BaseBox, + DrawerCustom, + MenuDrawerDynamicGrid, NewWrapper, ScrollableCustom, StackCustom, TextCustom, } from "@/components"; +import { IconDot } from "@/components/_Icon/IconComponent"; import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent"; import NoDataText from "@/components/_ShareComponent/NoDataText"; -import { AccentColor } from "@/constants/color-palet"; +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 { router, useFocusEffect, useLocalSearchParams } from "expo-router"; +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"; @@ -39,8 +46,9 @@ const fixPath = ({ const separator = deepLink.includes("?") ? "&" : "?"; - const fixedPath = - `${deepLink}${separator}from=notifications&category=${_.lowerCase(categoryApp)}`; + const fixedPath = `${deepLink}${separator}from=notifications&category=${_.lowerCase( + categoryApp + )}`; console.log("Fix Path", fixedPath); @@ -103,6 +111,9 @@ export default function Notifications() { const [listData, setListData] = useState([]); 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); @@ -142,33 +153,96 @@ export default function Notifications() { }; return ( - ({ - id: i, - label: e.label, - value: e.value, - }))} - onButtonPress={handlePress} - activeId={activeCategory as string} + <> + , + headerRight: () => ( + setOpenDrawer(true)} + /> + ), + }} + /> + + ({ + id: i, + label: e.label, + value: e.value, + }))} + onButtonPress={handlePress} + activeId={activeCategory as string} + /> + } + refreshControl={ + + } + > + {loading ? ( + + ) : _.isEmpty(listData) ? ( + + ) : ( + listData.map((e, i) => ( + + + + )) + )} + + + setOpenDrawer(false)} + height={"auto"} + > + + ), + 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); + }, + }); + } + }} /> - } - refreshControl={ - - } - > - {loading ? ( - - ) : _.isEmpty(listData) ? ( - - ) : ( - listData.map((e, i) => ( - - - - )) - )} - + + ); } diff --git a/app/(application)/admin/event/[id]/[status]/index.tsx b/app/(application)/admin/event/[id]/[status]/index.tsx index a0d0c49..78608fc 100644 --- a/app/(application)/admin/event/[id]/[status]/index.tsx +++ b/app/(application)/admin/event/[id]/[status]/index.tsx @@ -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) { diff --git a/app/(application)/admin/event/[id]/reject-input.tsx b/app/(application)/admin/event/[id]/reject-input.tsx index 5e06668..ca61964 100644 --- a/app/(application)/admin/event/[id]/reject-input.tsx +++ b/app/(application)/admin/event/[id]/reject-input.tsx @@ -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(""); + const [data, setData] = useState({ + 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) { diff --git a/app/(application)/admin/notification/index.tsx b/app/(application)/admin/notification/index.tsx index 6b36a62..9875bd5 100644 --- a/app/(application)/admin/notification/index.tsx +++ b/app/(application)/admin/notification/index.tsx @@ -1,20 +1,26 @@ 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"; @@ -70,6 +76,9 @@ export default function AdminNotification() { const [listData, setListData] = useState([]); 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); @@ -89,7 +98,7 @@ export default function AdminNotification() { id: user?.id as any, category: activeCategory as any, }); - + if (response.success) { setListData(response.data); } else { @@ -114,12 +123,12 @@ export default function AdminNotification() { options={{ title: "Admin Notifikasi", headerLeft: () => , - // headerRight: () => ( - // router.push("/test-notifications")} - // /> - // ), + headerRight: () => ( + setOpenDrawer(true)} + /> + ), }} /> @@ -154,6 +163,51 @@ export default function AdminNotification() { )) )} + + setOpenDrawer(false)} + height={"auto"} + > + + ), + 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); + }, + }); + } + }} + /> + ); } diff --git a/components/Button/BackButtonFromNotification.tsx b/components/Button/BackButtonFromNotification.tsx new file mode 100644 index 0000000..93d942d --- /dev/null +++ b/components/Button/BackButtonFromNotification.tsx @@ -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 ( + <> + { + if (from === "notifications") { + router.replace(`/notifications?category=${category}`); + } else { + if (from) { + router.replace(`/${from}` as any); + } else { + router.navigate("/home"); + } + } + }} + /> + + ); +} diff --git a/components/Notification/NotificationInitializer.tsx b/components/Notification/NotificationInitializer.tsx index 2a6e7f8..f69e307 100644 --- a/components/Notification/NotificationInitializer.tsx +++ b/components/Notification/NotificationInitializer.tsx @@ -49,7 +49,8 @@ export default function NotificationInitializer() { const fcmToken = await getToken(messagingInstance); if (!fcmToken) { - logout(); + console.warn("Tidak bisa mendapatkan FCM token"); + // logout(); return; } diff --git a/hipmi-note.md b/hipmi-note.md index 2f0281d..73d454e 100644 --- a/hipmi-note.md +++ b/hipmi-note.md @@ -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 + diff --git a/hooks/use-notification-store.tsx b/hooks/use-notification-store.tsx index 0c557ad..f361612 100644 --- a/hooks/use-notification-store.tsx +++ b/hooks/use-notification-store.tsx @@ -40,6 +40,7 @@ type NotificationContextType = { notif: Omit ) => void; markAsRead: (id: string) => void; + markAsReadAll: (id: string) => void; syncUnreadCount: () => Promise; }; @@ -48,6 +49,7 @@ const NotificationContext = createContext({ unreadCount: 0, addNotification: () => {}, markAsRead: () => {}, + markAsReadAll: () => {}, syncUnreadCount: async () => {}, }); @@ -98,7 +100,7 @@ export const NotificationProvider = ({ children }: { children: ReactNode }) => { const markAsRead = async (id: string) => { try { - const response = await apiNotificationMarkAsRead({ id }); + const response = await apiNotificationMarkAsRead({ id, category: "one" }); console.log("🚀 Response Mark As Read:", response); if (response.success) { @@ -114,6 +116,25 @@ export const NotificationProvider = ({ children }: { children: ReactNode }) => { } }; + 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({ @@ -133,8 +154,9 @@ export const NotificationProvider = ({ children }: { children: ReactNode }) => { value={{ notifications, addNotification, - markAsRead, unreadCount, + markAsRead, + markAsReadAll, syncUnreadCount, }} > diff --git a/screens/Admin/Event/funUpdateStatus.ts b/screens/Admin/Event/funUpdateStatus.ts index 7a8e9ac..5e535d5 100644 --- a/screens/Admin/Event/funUpdateStatus.ts +++ b/screens/Admin/Event/funUpdateStatus.ts @@ -1,4 +1,5 @@ import { apiAdminEventUpdateStatus } from "@/service/api-admin/api-admin-event"; +import { RejectedData } from "@/types/type-collect-other"; export const funUpdateStatusEvent = async ({ id, @@ -7,17 +8,19 @@ export const funUpdateStatusEvent = async ({ }: { id: string; changeStatus: "publish" | "review" | "reject"; - data?: string; + data?: RejectedData; }) => { try { + console.log("[DATA]", data); const response = await apiAdminEventUpdateStatus({ id: id, changeStatus: changeStatus as any, - data: data, + data: data as any, }); return response; + } catch (error) { console.log("[ERROR]", error); throw error; } -}; \ No newline at end of file +}; diff --git a/service/api-notifications.ts b/service/api-notifications.ts index 04427db..a20dbeb 100644 --- a/service/api-notifications.ts +++ b/service/api-notifications.ts @@ -1,6 +1,6 @@ import { NotificationProp, - TypeNotificationCategoryApp + TypeNotificationCategoryApp, } from "@/types/type-notification-category"; import { apiConfig } from "./api-config"; @@ -78,9 +78,22 @@ export async function apiNotificationUnreadCount({ } } -export async function apiNotificationMarkAsRead({ id }: { id: string }) { +/** + * @param id | notification id atau user id + * @param category | "all" | "one" , jika "all" id yang harus di masukan adalah user id, jika "one" id yang harus di masukan adalah notification id + * @type {string} + */ +export async function apiNotificationMarkAsRead({ + id, + category, +}: { + id: string; + category: "all" | "one"; +}) { try { - const response = await apiConfig.put(`/mobile/notification/${id}`); + const response = await apiConfig.put( + `/mobile/notification/${id}?category=${category}` + ); return response.data; } catch (error) { throw error; diff --git a/types/type-collect-other.ts b/types/type-collect-other.ts new file mode 100644 index 0000000..36e3ffa --- /dev/null +++ b/types/type-collect-other.ts @@ -0,0 +1,4 @@ +export type RejectedData = { + catatan?: string; + senderId: string; +}; diff --git a/utils/formatChatTime.ts b/utils/formatChatTime.ts index 2c86136..9df79ed 100644 --- a/utils/formatChatTime.ts +++ b/utils/formatChatTime.ts @@ -15,21 +15,21 @@ export const formatChatTime = (date: string | Date): string => { const messageDate = dayjs(date); const now = dayjs(); - // Jika hari ini + // Hari ini if (messageDate.isSame(now, 'day')) { - return messageDate.format('HH:mm'); // contoh: "14.30" + return messageDate.format('HH.mm'); // "14.30" } - // Jika kemarin + // Kemarin if (messageDate.isSame(now.subtract(1, 'day'), 'day')) { - return messageDate.format('dddd HH:mm'); + return `Kemarin, ${messageDate.format('HH.mm')}`; // "Kemarin, 14.30" } - // Jika dalam 7 hari terakhir (tapi bukan kemarin/ hari ini) + // Dalam 7 hari terakhir (termasuk hari ini & kemarin sudah di-handle, jadi aman) if (now.diff(messageDate, 'day') < 7) { - return messageDate.format('dddd HH:mm'); // contoh: "Senin 14:30" + return `${messageDate.format('dddd')}, ${messageDate.format('HH.mm')}`; // "Senin, 13.00" } - // Lebih dari seminggu lalu → tampilkan tanggal - return messageDate.format('D MMM YYYY HH:mm'); // contoh: "12 Mei 2024 14:30" -}; + // Lebih dari 7 hari lalu + return messageDate.format('DD - MM - YYYY, HH.mm'); // "05 - 11 - 2025, 14.00" +}; \ No newline at end of file