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
This commit is contained in:
2026-01-13 17:41:30 +08:00
parent ca33dd83bb
commit 6e2046467f
18 changed files with 325 additions and 83 deletions

View File

@@ -53,7 +53,9 @@ export default function UserLayout() {
/>
{/* ========== Notification Section ========= */}
<Stack.Screen
{/* DIPINDAH DI FILE NOTIFICATION USER */}
{/* <Stack.Screen
name="notifications/index"
options={{
title: "Notifikasi",
@@ -65,18 +67,21 @@ export default function UserLayout() {
// />
// ),
}}
/>
/> */}
{/* ========== 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={{
@@ -520,7 +525,7 @@ export default function UserLayout() {
options={{
title: "Job Vacancy",
// headerLeft: () => <BackButton path="/home" />,
// Note: headerLeft di pindahkan ke Tabs Layout
// NOTE: headerLeft di pindahkan ke Tabs Layout
}}
/>
<Stack.Screen

View File

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

View File

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

View File

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

View File

@@ -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: () => (
<BackButton
onPress={() => {
if (from === "notifications") {
router.replace(`/notifications?category=${category}`);
} else {
if (from) {
router.replace(`/${from}` as any);
} else {
router.navigate("/home");
}
}
}}
/>
<BackButtonFromNotification from={from as string} category={category as string} />
),
});
}, [from, router, navigation]);

View File

@@ -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<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);
@@ -142,33 +153,96 @@ export default function Notifications() {
};
return (
<NewWrapper
headerComponent={
<ScrollableCustom
data={listOfcategoriesAppNotification.map((e, i) => ({
id: i,
label: e.label,
value: e.value,
}))}
onButtonPress={handlePress}
activeId={activeCategory as string}
<>
<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);
},
});
}
}}
/>
}
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>
</>
);
}

View File

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

View File

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

View File

@@ -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<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);
@@ -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: () => <BackButton />,
// headerRight: () => (
// <IconPlus
// color={MainColor.yellow}
// onPress={() => router.push("/test-notifications")}
// />
// ),
headerRight: () => (
<IconDot
color={MainColor.yellow}
onPress={() => setOpenDrawer(true)}
/>
),
}}
/>
@@ -154,6 +163,51 @@ export default function AdminNotification() {
))
)}
</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>
</>
);
}