diff --git a/app/(application)/division/[id]/(fitur-division)/task/[detail]/index.tsx b/app/(application)/division/[id]/(fitur-division)/task/[detail]/index.tsx index 2e9eb93..d4ac8f3 100644 --- a/app/(application)/division/[id]/(fitur-division)/task/[detail]/index.tsx +++ b/app/(application)/division/[id]/(fitur-division)/task/[detail]/index.tsx @@ -26,6 +26,7 @@ type Props = { reason: string status: number isActive: boolean + idGroup: string } export default function DetailTaskDivision() { @@ -159,7 +160,7 @@ export default function DetailTaskDivision() { } - + diff --git a/app/(application)/division/[id]/(fitur-division)/task/[detail]/tugas-file/[taskId].tsx b/app/(application)/division/[id]/(fitur-division)/task/[detail]/tugas-file/[taskId].tsx index 2112500..7a6c724 100644 --- a/app/(application)/division/[id]/(fitur-division)/task/[detail]/tugas-file/[taskId].tsx +++ b/app/(application)/division/[id]/(fitur-division)/task/[detail]/tugas-file/[taskId].tsx @@ -251,7 +251,7 @@ export default function TugasFileScreen() { disabled={loadingUpload} /> { setSelectedProjectFiles([]); setPickerModal(true); diff --git a/app/(application)/notification.tsx b/app/(application)/notification.tsx index 64322f9..6346a4b 100644 --- a/app/(application)/notification.tsx +++ b/app/(application)/notification.tsx @@ -9,12 +9,11 @@ import { pushToPage } from "@/lib/pushToPage"; import { useAuthSession } from "@/providers/AuthProvider"; import { useTheme } from "@/providers/ThemeProvider"; import { Feather } from "@expo/vector-icons"; -import { router, Stack } from "expo-router"; import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; -import { useEffect, useMemo } from "react"; +import { router, Stack } from "expo-router"; +import { useEffect, useMemo, useState } from "react"; import { FlatList, Pressable, RefreshControl, SafeAreaView, View } from "react-native"; import { useDispatch, useSelector } from "react-redux"; -import { useState } from "react"; type Props = { id: string @@ -79,6 +78,15 @@ export default function Notification() { }, [data]) const listData = useMemo(() => { + const BULAN: Record = { + 'JAN': 0, 'FEB': 1, 'MAR': 2, 'APR': 3, 'MEI': 4, 'JUN': 5, + 'JUL': 6, 'AGU': 7, 'SEP': 8, 'OKT': 9, 'NOV': 10, 'DES': 11, + } + const parseDate = (str: string) => { + const [d, m, y] = str.split(' ') + return new Date(Number(y), BULAN[m] ?? 0, Number(d)).getTime() + } + const groups: Record = {} const dateOrder: string[] = [] @@ -90,6 +98,8 @@ export default function Notification() { groups[item.createdAt].push(item) }) + dateOrder.sort((a, b) => parseDate(b) - parseDate(a)) + const result: ListRow[] = [] dateOrder.forEach((date) => { result.push({ _type: 'header', date }) @@ -137,6 +147,17 @@ export default function Notification() { } } + async function handleMarkOneRead(id: string) { + try { + const hasil = await decryptToken(String(token?.current)) + await apiReadOneNotification({ user: hasil, id: id }) + await queryClient.invalidateQueries({ queryKey: ['notifications'] }) + dispatch(setUpdateNotification(!updateNotification)) + } catch (error) { + console.error(error) + } + } + return ( - + ) : undefined } @@ -175,7 +196,8 @@ export default function Notification() { onCancel={() => setShowConfirm(false)} /> - + + {isLoading ? ( [0, 1, 2, 3, 4].map((_, i) => ) ) : flatData.length === 0 ? ( @@ -204,11 +226,11 @@ export default function Notification() { renderItem={({ item }) => { if (item._type === 'header') { return ( - - + + {item.date} - + ) } @@ -218,37 +240,20 @@ export default function Notification() { return ( handleReadNotification(item.id, item.category, item.idContent)} - style={({ pressed }) => [{ - flexDirection: 'row', - alignItems: 'center', - borderRadius: 10, - borderWidth: 1, + style={({ pressed }) => [Styles.notifItemRow, { borderColor: colors.icon + '20', backgroundColor: pressed ? colors.icon + '10' : item.isRead ? colors.icon + '10' : colors.card, - paddingHorizontal: 12, - paddingVertical: 10, - marginBottom: 6, }]} > - {/* Colored icon */} - + - {/* Content */} - + + {!item.isRead && ( + { + e.stopPropagation() + handleMarkOneRead(item.id) + }} + hitSlop={8} + style={({ pressed }) => ({ opacity: pressed ? 0.5 : 1, flexShrink: 0 })} + > + + Tandai dibaca + + + )} - + diff --git a/app/(application)/project/[id]/tugas-file/[taskId].tsx b/app/(application)/project/[id]/tugas-file/[taskId].tsx index 3f47532..a84a0e8 100644 --- a/app/(application)/project/[id]/tugas-file/[taskId].tsx +++ b/app/(application)/project/[id]/tugas-file/[taskId].tsx @@ -246,7 +246,7 @@ export default function ProjectTugasFileScreen() { disabled={loadingUpload} /> { setSelectedProjectFiles([]); setPickerModal(true); diff --git a/components/ModalRiwayatApproval.tsx b/components/ModalRiwayatApproval.tsx index 3cc8523..0dbd256 100644 --- a/components/ModalRiwayatApproval.tsx +++ b/components/ModalRiwayatApproval.tsx @@ -33,13 +33,7 @@ function ApprovalStatusBadge({ status }: { status: number }) { : { label: 'Menunggu', color: '#FFA94D' } return ( - + {config.label} @@ -79,16 +73,10 @@ export default function ModalRiwayatApproval({ isVisible, setVisible, data, load data.map((item, index) => ( {/* Status + tanggal */} - + {item.createdAt} @@ -97,15 +85,15 @@ export default function ModalRiwayatApproval({ isVisible, setVisible, data, load {/* Pengaju */} - - Diajukan Oleh: + + Diajukan Oleh: {item.submitter.name} {/* Approver */} - - Disetujui Oleh: + + Disetujui Oleh: {item.approver?.name ?? '-'} @@ -113,16 +101,11 @@ export default function ModalRiwayatApproval({ isVisible, setVisible, data, load {/* Catatan penolakan */} {item.note && ( - - + + Alasan Penolakan - + {item.note} @@ -130,7 +113,7 @@ export default function ModalRiwayatApproval({ isVisible, setVisible, data, load )) ) : ( - + Belum ada riwayat persetujuan )} diff --git a/components/home/carouselHome.tsx b/components/home/carouselHome.tsx index 80c5e20..a0aed3c 100644 --- a/components/home/carouselHome.tsx +++ b/components/home/carouselHome.tsx @@ -36,7 +36,7 @@ export default function CaraouselHome({ refreshing }: { refreshing: boolean }) { async function handleUser() { const hasil = await decryptToken(String(token?.current)) const response = await apiGetProfile({ id: hasil }) - dispatch(setEntityUser({ role: response.data.idUserRole, admin: false, isApprover: response.data.isApprover ?? false })) + dispatch(setEntityUser({ role: response.data.idUserRole, admin: false, isApprover: response.data.isApprover ?? false, idGroup: response.data.idGroup ?? '' })) } useEffect(() => { diff --git a/components/home/carouselHome2.tsx b/components/home/carouselHome2.tsx index 501385f..8381596 100644 --- a/components/home/carouselHome2.tsx +++ b/components/home/carouselHome2.tsx @@ -59,7 +59,7 @@ export default function CaraouselHome2({ refreshing }: { refreshing: boolean }) // Sync User Role to Redux useEffect(() => { if (profile) { - dispatch(setEntityUser({ role: profile.idUserRole, admin: false, isApprover: profile.isApprover ?? false })) + dispatch(setEntityUser({ role: profile.idUserRole, admin: false, isApprover: profile.isApprover ?? false, idGroup: profile.idGroup ?? '' })) } }, [profile, dispatch]) diff --git a/components/project/sectionTanggalTugas.tsx b/components/project/sectionTanggalTugas.tsx index 19483f6..21534ad 100644 --- a/components/project/sectionTanggalTugas.tsx +++ b/components/project/sectionTanggalTugas.tsx @@ -39,7 +39,7 @@ type ApprovalRecord = { createdAt: string } -export default function SectionTanggalTugasProject({ status, member, refreshing }: { status: number | undefined, member: boolean, refreshing?: boolean }) { +export default function SectionTanggalTugasProject({ status, member, refreshing, idGroup }: { status: number | undefined, member: boolean, refreshing?: boolean, idGroup: string }) { const { colors } = useTheme(); const entityUser = useSelector((state: any) => state.user) const dispatch = useDispatch() @@ -61,7 +61,7 @@ export default function SectionTanggalTugasProject({ status, member, refreshing const [tugas, setTugas] = useState({ id: '', status: 0 }) const [showDeleteModal, setShowDeleteModal] = useState(false) - const isApprover = entityUser.isApprover || ['supadmin', 'developer'].includes(entityUser.role) + const isApprover = (entityUser.isApprover && entityUser.idGroup === idGroup) || ['supadmin', 'developer'].includes(entityUser.role) const isAdmin = entityUser.role !== 'user' && entityUser.role !== 'coadmin' async function handleLoad(loading: boolean) { diff --git a/components/task/sectionTanggalTugasTask.tsx b/components/task/sectionTanggalTugasTask.tsx index 5a4e013..9783abf 100644 --- a/components/task/sectionTanggalTugasTask.tsx +++ b/components/task/sectionTanggalTugasTask.tsx @@ -38,7 +38,7 @@ type ApprovalRecord = { createdAt: string } -export default function SectionTanggalTugasTask({ refreshing, isMemberDivision, isAdminDivision, status }: { refreshing: boolean, isMemberDivision: boolean, isAdminDivision: boolean, status?: number }) { +export default function SectionTanggalTugasTask({ refreshing, isMemberDivision, isAdminDivision, status, idGroup }: { refreshing: boolean, isMemberDivision: boolean, isAdminDivision: boolean, status?: number, idGroup: string }) { const { colors } = useTheme() const dispatch = useDispatch() const entityUser = useSelector((state: any) => state.user); @@ -60,7 +60,7 @@ export default function SectionTanggalTugasTask({ refreshing, isMemberDivision, const [tugas, setTugas] = useState({ id: '', status: 0 }) const [showDeleteModal, setShowDeleteModal] = useState(false) - const isApprover = entityUser.isApprover || ['supadmin', 'developer'].includes(entityUser.role) + const isApprover = (entityUser.isApprover && entityUser.idGroup === idGroup) || ['supadmin', 'developer'].includes(entityUser.role) const isAdmin = entityUser.role !== 'user' && entityUser.role !== 'coadmin' const canTakeAction = isMemberDivision || isAdmin diff --git a/constants/styles/approval.styles.ts b/constants/styles/approval.styles.ts new file mode 100644 index 0000000..17254dc --- /dev/null +++ b/constants/styles/approval.styles.ts @@ -0,0 +1,13 @@ +import { StyleSheet } from "react-native"; + +const ApprovalStyles = StyleSheet.create({ + approvalBadge: { borderRadius: 20, paddingHorizontal: 10, paddingVertical: 3, alignSelf: 'flex-start' }, + approvalItem: { borderWidth: 1, borderRadius: 10, padding: 12, marginBottom: 10 }, + approvalItemHeader: { justifyContent: 'space-between', marginBottom: 8 }, + approvalIconMr: { marginRight: 6 }, + approvalNoteBox: { borderRadius: 8, padding: 8, marginTop: 4 }, + approvalNoteLabel: { marginBottom: 2 }, + approvalEmptyText: { textAlign: 'center' }, +}); + +export default ApprovalStyles; diff --git a/constants/styles/index.ts b/constants/styles/index.ts index dbae1b1..226f46c 100644 --- a/constants/styles/index.ts +++ b/constants/styles/index.ts @@ -9,6 +9,8 @@ import CardStyles from './card.styles'; import ModalStyles from './modal.styles'; import HeaderStyles from './header.styles'; import ComponentStyles from './component.styles'; +import NotificationStyles from './notification.styles'; +import ApprovalStyles from './approval.styles'; const Styles = StyleSheet.create({ ...SpacingStyles, @@ -21,6 +23,8 @@ const Styles = StyleSheet.create({ ...ModalStyles, ...HeaderStyles, ...ComponentStyles, + ...NotificationStyles, + ...ApprovalStyles, }); export default Styles; diff --git a/constants/styles/notification.styles.ts b/constants/styles/notification.styles.ts new file mode 100644 index 0000000..0519fbc --- /dev/null +++ b/constants/styles/notification.styles.ts @@ -0,0 +1,29 @@ +import { StyleSheet } from "react-native"; + +const NotificationStyles = StyleSheet.create({ + notifContainer: { paddingTop: 10 }, + notifHeaderRow: { marginTop: 16, marginBottom: 8 }, + notifDateSeparator: { flex: 1, height: 1, marginLeft: 8 }, + notifDateText: { fontSize: 11, fontWeight: '600', letterSpacing: 0.6, textTransform: 'uppercase' }, + notifItemRow: { + flexDirection: 'row', + alignItems: 'center', + borderRadius: 10, + borderWidth: 1, + paddingHorizontal: 12, + paddingVertical: 10, + marginBottom: 6, + }, + notifIconContainer: { + width: 42, + height: 42, + borderRadius: 21, + alignItems: 'center', + justifyContent: 'center', + flexShrink: 0, + }, + notifContent: { marginLeft: 10 }, + notifMarkReadText: { fontSize: 11, color: '#3B82F6', fontWeight: '600' }, +}); + +export default NotificationStyles;