diff --git a/src/components/layout/horizontal/NavbarContent.tsx b/src/components/layout/horizontal/NavbarContent.tsx
index 82a48b9..540d1ba 100644
--- a/src/components/layout/horizontal/NavbarContent.tsx
+++ b/src/components/layout/horizontal/NavbarContent.tsx
@@ -4,16 +4,11 @@ import Link from 'next/link'
// Third-party Imports
import classnames from 'classnames'
-// Type Imports
-import type { ShortcutsType } from '@components/layout/shared/ShortcutsDropdown'
-import type { NotificationsType } from '@components/layout/shared/NotificationsDropdown'
-
// Component Imports
import NavToggle from './NavToggle'
import Logo from '@components/layout/shared/Logo'
import NavSearch from '@components/layout/shared/search'
import ModeDropdown from '@components/layout/shared/ModeDropdown'
-import ShortcutsDropdown from '@components/layout/shared/ShortcutsDropdown'
import NotificationsDropdown from '@components/layout/shared/NotificationsDropdown'
import UserDropdown from '@components/layout/shared/UserDropdown'
@@ -23,105 +18,13 @@ import useHorizontalNav from '@menu/hooks/useHorizontalNav'
// Util Imports
import { horizontalLayoutClasses } from '@layouts/utils/layoutClasses'
-// Vars
-const shortcuts: ShortcutsType[] = [
- {
- url: '/apps/calendar',
- icon: 'tabler-calendar',
- title: 'Calendar',
- subtitle: 'Appointments'
- },
- {
- url: '/apps/invoice/list',
- icon: 'tabler-file-dollar',
- title: 'Invoice App',
- subtitle: 'Manage Accounts'
- },
- {
- url: '/apps/user/list',
- icon: 'tabler-user',
- title: 'Users',
- subtitle: 'Manage Users'
- },
- {
- url: '/apps/roles',
- icon: 'tabler-users-group',
- title: 'Role Management',
- subtitle: 'Permissions'
- },
- {
- url: '/',
- icon: 'tabler-device-desktop-analytics',
- title: 'Dashboard',
- subtitle: 'User Dashboard'
- },
- {
- url: '/pages/account-settings',
- icon: 'tabler-settings',
- title: 'Settings',
- subtitle: 'Account Settings'
- }
-]
-
-const notifications: NotificationsType[] = [
- {
- avatarImage: '/images/avatars/8.png',
- title: 'Congratulations Flora 🎉',
- subtitle: 'Won the monthly bestseller gold badge',
- time: '1h ago',
- read: false
- },
- {
- title: 'Cecilia Becker',
- avatarColor: 'secondary',
- subtitle: 'Accepted your connection',
- time: '12h ago',
- read: false
- },
- {
- avatarImage: '/images/avatars/3.png',
- title: 'Bernard Woods',
- subtitle: 'You have new message from Bernard Woods',
- time: 'May 18, 8:26 AM',
- read: true
- },
- {
- avatarIcon: 'tabler-chart-bar',
- title: 'Monthly report generated',
- subtitle: 'July month financial report is generated',
- avatarColor: 'info',
- time: 'Apr 24, 10:30 AM',
- read: true
- },
- {
- avatarText: 'MG',
- title: 'Application has been approved 🚀',
- subtitle: 'Your Meta Gadgets project application has been approved.',
- avatarColor: 'success',
- time: 'Feb 17, 12:17 PM',
- read: true
- },
- {
- avatarIcon: 'tabler-mail',
- title: 'New message from Harry',
- subtitle: 'You have new message from Harry',
- avatarColor: 'error',
- time: 'Jan 6, 1:48 PM',
- read: true
- }
-]
-
const NavbarContent = () => {
- // Hooks
const { isBreakpointReached } = useHorizontalNav()
return (
-
+
- {/* Hide Logo on Smaller screens */}
{!isBreakpointReached && (
@@ -132,10 +35,8 @@ const NavbarContent = () => {
-
-
+
- {/* Notification Dropdown, quick access menu dropdown, user dropdown will be placed here */}
)
diff --git a/src/components/layout/shared/NotificationsDropdown.tsx b/src/components/layout/shared/NotificationsDropdown.tsx
index b616ded..d21ba5f 100644
--- a/src/components/layout/shared/NotificationsDropdown.tsx
+++ b/src/components/layout/shared/NotificationsDropdown.tsx
@@ -1,10 +1,10 @@
'use client'
-// React Imports
-import { useRef, useState, useEffect } from 'react'
-import type { MouseEvent, ReactNode } from 'react'
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
+import type { ReactNode } from 'react'
+
+import Link from 'next/link'
-// MUI Imports
import IconButton from '@mui/material/IconButton'
import Badge from '@mui/material/Badge'
import Popper from '@mui/material/Popper'
@@ -13,32 +13,26 @@ import Paper from '@mui/material/Paper'
import ClickAwayListener from '@mui/material/ClickAwayListener'
import Typography from '@mui/material/Typography'
import Chip from '@mui/material/Chip'
-import Tooltip from '@mui/material/Tooltip'
import Divider from '@mui/material/Divider'
import Avatar from '@mui/material/Avatar'
-import useMediaQuery from '@mui/material/useMediaQuery'
+import CircularProgress from '@mui/material/CircularProgress'
+import Box from '@mui/material/Box'
+import Alert from '@mui/material/Alert'
import Button from '@mui/material/Button'
+import useMediaQuery from '@mui/material/useMediaQuery'
import type { Theme } from '@mui/material/styles'
-// Third Party Components
import classnames from 'classnames'
-import PerfectScrollbar from 'react-perfect-scrollbar'
+import { formatDistanceToNow } from 'date-fns'
-// Type Imports
import type { ThemeColor } from '@core/types'
import type { CustomAvatarProps } from '@core/components/mui/Avatar'
-
-// Component Imports
import CustomAvatar from '@core/components/mui/Avatar'
-
-// Config Imports
import themeConfig from '@configs/themeConfig'
-
-// Hook Imports
import { useSettings } from '@core/hooks/useSettings'
-
-// Util Imports
import { getInitials } from '@/utils/getInitials'
+import { useFarmHub } from '@/hooks/useFarmHub'
+import { notificationsService, type NotificationItem } from '@/libs/api/services/notificationsService'
export type NotificationsType = {
title: string
@@ -69,107 +63,204 @@ export type NotificationsType = {
}
)
-const ScrollWrapper = ({ children, hidden }: { children: ReactNode; hidden: boolean }) => {
- if (hidden) {
- return
{children}
- } else {
- return (
-
- {children}
-
- )
- }
+const ScrollWrapper = ({ children }: { children: ReactNode }) => {
+ return
{children}
}
-const getAvatar = (
- params: Pick
-) => {
- const { avatarImage, avatarIcon, avatarText, title, avatarColor, avatarSkin } = params
-
+const getAvatar = ({
+ avatarImage,
+ avatarIcon,
+ avatarText,
+ title,
+ avatarColor,
+ avatarSkin
+}: Pick) => {
if (avatarImage) {
return
- } else if (avatarIcon) {
+ }
+
+ if (avatarIcon) {
return (
)
- } else {
- return (
-
- {avatarText || getInitials(title)}
-
- )
+ }
+
+ return (
+
+ {avatarText || getInitials(title)}
+
+ )
+}
+
+const getNotificationAppearance = (notification: NotificationItem): Pick => {
+ switch (notification.level) {
+ case 'critical':
+ return { avatarIcon: 'tabler-alert-triangle', avatarColor: 'error' }
+ case 'warning':
+ return { avatarIcon: 'tabler-alert-circle', avatarColor: 'warning' }
+ default:
+ return { avatarIcon: 'tabler-info-circle', avatarColor: 'info' }
}
}
-const NotificationDropdown = ({ notifications }: { notifications: NotificationsType[] }) => {
- // States
+const NotificationsDropdown = () => {
const [open, setOpen] = useState(false)
- const [notificationsState, setNotificationsState] = useState(notifications)
+ const [notifications, setNotifications] = useState([])
+ const [isLoading, setIsLoading] = useState(false)
+ const [error, setError] = useState(null)
+ const [lastSinceId, setLastSinceId] = useState(null)
+ const [lastMarkedSliceId, setLastMarkedSliceId] = useState(null)
- // Vars
- const notificationCount = notificationsState.filter(notification => !notification.read).length
- const readAll = notificationsState.every(notification => notification.read)
-
- // Refs
const anchorRef = useRef(null)
const ref = useRef(null)
+ const pollingAbortRef = useRef(false)
+ const { farmHub } = useFarmHub()
- // Hooks
- const hidden = useMediaQuery((theme: Theme) => theme.breakpoints.down('lg'))
const isSmallScreen = useMediaQuery((theme: Theme) => theme.breakpoints.down('sm'))
const { settings } = useSettings()
+ const farmUuid = farmHub?.farm_uuid ?? null
+
+ const unreadCount = useMemo(() => notifications.filter(item => !item.is_read).length, [notifications])
+ const latestUnreadSinceId = useMemo(() => {
+ return notifications.reduce((latest, item) => {
+ if (item.is_read) {
+ return latest
+ }
+
+ if (latest === null || item.since_id > latest) {
+ return item.since_id
+ }
+
+ return latest
+ }, null)
+ }, [notifications])
+
+ const markNotificationsAsRead = useCallback(
+ async (sliceId: number) => {
+ if (!farmUuid || sliceId <= 0 || lastMarkedSliceId === sliceId) {
+ return
+ }
+
+ try {
+ await notificationsService.markAsRead({ farmUuid, sliceId })
+ setNotifications(current => current.map(item => (item.since_id <= sliceId ? { ...item, is_read: true } : item)))
+ setLastMarkedSliceId(sliceId)
+ } catch (markError) {
+ console.error('Failed to mark notifications as read', markError)
+ }
+ },
+ [farmUuid, lastMarkedSliceId]
+ )
+
+ const fetchNotifications = useCallback(
+ async (timeout: number, sinceId?: number) => {
+ if (!farmUuid) {
+ setNotifications([])
+ setLastSinceId(null)
+ return [] as NotificationItem[]
+ }
+
+ const response = await notificationsService.longPoll({ farmUuid, sinceId, timeout })
+
+ if (response.length > 0 || sinceId === undefined) {
+ setNotifications(response)
+ }
+
+ if (response.length > 0) {
+ setLastSinceId(response[response.length - 1]?.since_id ?? sinceId ?? null)
+ }
+
+ return response
+ },
+ [farmUuid]
+ )
+
+ useEffect(() => {
+ pollingAbortRef.current = false
+
+ if (!farmUuid) {
+ setNotifications([])
+ setError(null)
+ setIsLoading(false)
+ setLastSinceId(null)
+ setLastMarkedSliceId(null)
+ return
+ }
+
+ const poll = async () => {
+ setIsLoading(true)
+ setError(null)
+
+ try {
+ const initialItems = await fetchNotifications(0)
+ const initialLastSinceId = initialItems[initialItems.length - 1]?.since_id ?? null
+ let nextSinceId = initialLastSinceId
+
+ if (!pollingAbortRef.current) {
+ setIsLoading(false)
+ }
+
+ while (!pollingAbortRef.current) {
+ const newItems = await notificationsService.longPoll({ farmUuid, sinceId: nextSinceId ?? undefined, timeout: 15 })
+
+ if (pollingAbortRef.current) {
+ return
+ }
+
+ if (newItems.length > 0) {
+ setNotifications(newItems)
+ nextSinceId = newItems[newItems.length - 1]?.since_id ?? nextSinceId
+ setLastSinceId(nextSinceId)
+ }
+ }
+ } catch (pollError) {
+ if (!pollingAbortRef.current) {
+ setError(pollError instanceof Error ? pollError.message : 'Unable to load notifications.')
+ setIsLoading(false)
+ }
+ }
+ }
+
+ poll()
+
+ return () => {
+ pollingAbortRef.current = true
+ }
+ }, [farmUuid, fetchNotifications])
+
+ useEffect(() => {
+ const adjustPopoverHeight = () => {
+ if (ref.current) {
+ const availableHeight = window.innerHeight - 100
+ ref.current.style.height = `${Math.min(availableHeight, 550)}px`
+ }
+ }
+
+ adjustPopoverHeight()
+ window.addEventListener('resize', adjustPopoverHeight)
+
+ return () => {
+ window.removeEventListener('resize', adjustPopoverHeight)
+ }
+ }, [])
+
+ useEffect(() => {
+ if (open && latestUnreadSinceId) {
+ void markNotificationsAsRead(latestUnreadSinceId)
+ }
+ }, [open, latestUnreadSinceId, markNotificationsAsRead])
+
const handleClose = () => {
setOpen(false)
}
const handleToggle = () => {
- setOpen(prevOpen => !prevOpen)
+ setOpen(previousOpen => !previousOpen)
}
- // Read notification when notification is clicked
- const handleReadNotification = (event: MouseEvent, value: boolean, index: number) => {
- event.stopPropagation()
- const newNotifications = [...notificationsState]
-
- newNotifications[index].read = value
- setNotificationsState(newNotifications)
- }
-
- // Remove notification when close icon is clicked
- const handleRemoveNotification = (event: MouseEvent, index: number) => {
- event.stopPropagation()
- const newNotifications = [...notificationsState]
-
- newNotifications.splice(index, 1)
- setNotificationsState(newNotifications)
- }
-
- // Read or unread all notifications when read all icon is clicked
- const readAllNotifications = () => {
- const newNotifications = [...notificationsState]
-
- newNotifications.forEach(notification => {
- notification.read = !readAll
- })
- setNotificationsState(newNotifications)
- }
-
- useEffect(() => {
- const adjustPopoverHeight = () => {
- if (ref.current) {
- // Calculate available height, subtracting any fixed UI elements' height as necessary
- const availableHeight = window.innerHeight - 100
-
- ref.current.style.height = `${Math.min(availableHeight, 550)}px`
- }
- }
-
- window.addEventListener('resize', adjustPopoverHeight)
- }, [])
-
return (
<>
@@ -178,10 +269,8 @@ const NotificationDropdown = ({ notifications }: { notifications: NotificationsT
className='cursor-pointer'
variant='dot'
overlap='circular'
- invisible={notificationCount === 0}
- sx={{
- '& .MuiBadge-dot': { top: 6, right: 5, boxShadow: 'var(--mui-palette-background-paper) 0px 0px 0px 2px' }
- }}
+ invisible={unreadCount === 0}
+ sx={{ '& .MuiBadge-dot': { top: 6, right: 5, boxShadow: 'var(--mui-palette-background-paper) 0px 0px 0px 2px' } }}
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
>
@@ -196,110 +285,69 @@ const NotificationDropdown = ({ notifications }: { notifications: NotificationsT
anchorEl={anchorRef.current}
{...(isSmallScreen
? {
- className: 'is-full !mbs-3 z-[1] max-bs-[550px] bs-[550px]',
- modifiers: [
- {
- name: 'preventOverflow',
- options: {
- padding: themeConfig.layoutPadding
- }
- }
- ]
+ className: 'is-full !mbs-4 z-[1] max-bs-[550px] bs-[calc(100dvh-theme(spacing.6))] max-is-[calc(100dvw-theme(spacing.6))]'
}
- : { className: 'is-96 !mbs-3 z-[1] max-bs-[550px] bs-[550px]' })}
+ : {
+ className: 'is-[380px] !mbs-4 z-[1] max-bs-[550px]'
+ })}
>
{({ TransitionProps, placement }) => (
-
+
-
-
- Notifications
-
- {notificationCount > 0 && (
-
- )}
-
- {notificationsState.length > 0 ? (
- readAllNotifications()} className='text-textPrimary'>
-
-
- ) : (
- <>>
- )}
-
+
+
+ Notifications
+ {unreadCount > 0 ? : null}
+
+ {isLoading ?
: null}
-
- {notificationsState.map((notification, index) => {
- const {
- title,
- subtitle,
- time,
- read,
- avatarImage,
- avatarIcon,
- avatarText,
- avatarColor,
- avatarSkin
- } = notification
+
+ {error ? (
+
+ {error}
+
+ ) : !farmUuid ? (
+
+ To see notifications, first select an active farm.
+
+ ) : notifications.length === 0 && !isLoading ? (
+
+ No notifications yet.
+
+ ) : (
+ notifications.map(notification => {
+ const appearance = getNotificationAppearance(notification)
- return (
- handleReadNotification(e, true, index)}
- >
- {getAvatar({ avatarImage, avatarIcon, title, avatarText, avatarColor, avatarSkin })}
-
-
- {title}
-
-
- {subtitle}
-
-
- {time}
-
+ return (
+
+ {getAvatar({
+ title: notification.title,
+ avatarIcon: appearance.avatarIcon,
+ avatarColor: appearance.avatarColor
+ })}
+
+
+ {notification.title}
+
+
+ {notification.message}
+
+
+ {formatDistanceToNow(new Date(notification.created_at), { addSuffix: true })}
+
+
-
- handleReadNotification(e, !read, index)}
- className={classnames('mbs-1 mie-1', {
- 'invisible group-hover:visible': read
- })}
- />
- handleRemoveNotification(e, index)}
- />
-
-
- )
- })}
+ )
+ })
+ )}
-
@@ -312,4 +360,4 @@ const NotificationDropdown = ({ notifications }: { notifications: NotificationsT
)
}
-export default NotificationDropdown
+export default NotificationsDropdown
diff --git a/src/components/layout/vertical/NavbarContent.tsx b/src/components/layout/vertical/NavbarContent.tsx
index 16d8291..4e06512 100644
--- a/src/components/layout/vertical/NavbarContent.tsx
+++ b/src/components/layout/vertical/NavbarContent.tsx
@@ -1,116 +1,19 @@
'use client'
-// Third-party Imports
import classnames from 'classnames'
import { useContext } from 'react'
-// Context Imports
import NavbarSlotContext from '@/contexts/navbarSlotContext'
-// Type Imports
-import type { ShortcutsType } from '@components/layout/shared/ShortcutsDropdown'
-import type { NotificationsType } from '@components/layout/shared/NotificationsDropdown'
-
-// Component Imports
import NavToggle from './NavToggle'
import NavSearch from '@components/layout/shared/search'
import ThemeColorDropdown from '@components/layout/shared/ThemeColorDropdown'
-import ShortcutsDropdown from '@components/layout/shared/ShortcutsDropdown'
import NotificationsDropdown from '@components/layout/shared/NotificationsDropdown'
import UserDropdown from '@components/layout/shared/UserDropdown'
-// Util Imports
import { verticalLayoutClasses } from '@layouts/utils/layoutClasses'
import ModeDropdown from '../shared/ModeDropdown'
-// Vars
-const shortcuts: ShortcutsType[] = [
- {
- url: '/apps/calendar',
- icon: 'tabler-calendar',
- title: 'Calendar',
- subtitle: 'Appointments'
- },
- {
- url: '/apps/invoice/list',
- icon: 'tabler-file-dollar',
- title: 'Invoice App',
- subtitle: 'Manage Accounts'
- },
- {
- url: '/apps/user/list',
- icon: 'tabler-user',
- title: 'Users',
- subtitle: 'Manage Users'
- },
- {
- url: '/apps/roles',
- icon: 'tabler-users-group',
- title: 'Role Management',
- subtitle: 'Permissions'
- },
- {
- url: '/',
- icon: 'tabler-device-desktop-analytics',
- title: 'Dashboard',
- subtitle: 'User Dashboard'
- },
- {
- url: '/pages/account-settings',
- icon: 'tabler-settings',
- title: 'Settings',
- subtitle: 'Account Settings'
- }
-]
-
-const notifications: NotificationsType[] = [
- {
- avatarImage: '/images/avatars/8.png',
- title: 'Congratulations Flora 🎉',
- subtitle: 'Won the monthly bestseller gold badge',
- time: '1h ago',
- read: false
- },
- {
- title: 'Cecilia Becker',
- avatarColor: 'secondary',
- subtitle: 'Accepted your connection',
- time: '12h ago',
- read: false
- },
- {
- avatarImage: '/images/avatars/3.png',
- title: 'Bernard Woods',
- subtitle: 'You have new message from Bernard Woods',
- time: 'May 18, 8:26 AM',
- read: true
- },
- {
- avatarIcon: 'tabler-chart-bar',
- title: 'Monthly report generated',
- subtitle: 'July month financial report is generated',
- avatarColor: 'info',
- time: 'Apr 24, 10:30 AM',
- read: true
- },
- {
- avatarText: 'MG',
- title: 'Application has been approved 🚀',
- subtitle: 'Your Meta Gadgets project application has been approved.',
- avatarColor: 'success',
- time: 'Feb 17, 12:17 PM',
- read: true
- },
- {
- avatarIcon: 'tabler-mail',
- title: 'New message from Harry',
- subtitle: 'You have new message from Harry',
- avatarColor: 'error',
- time: 'Jan 6, 1:48 PM',
- read: true
- }
-]
-
const NavbarContent = () => {
const { slotContent } = useContext(NavbarSlotContext)
@@ -121,12 +24,10 @@ const NavbarContent = () => {
- {slotContent}
-
-
-
+ {slotContent}
+
- {/* */}
+
diff --git a/src/libs/api/services/notificationsService.ts b/src/libs/api/services/notificationsService.ts
new file mode 100644
index 0000000..c22317d
--- /dev/null
+++ b/src/libs/api/services/notificationsService.ts
@@ -0,0 +1,79 @@
+import { apiClient } from '../client'
+
+const PREFIX = '/api/notifications'
+
+export type NotificationLevel = 'info' | 'warning' | 'critical' | string
+
+export interface NotificationItem {
+ uuid: string
+ farm_uuid: string
+ since_id: number
+ title: string
+ message: string
+ level: NotificationLevel
+ is_read: boolean
+ metadata?: Record | null
+ created_at: string
+}
+
+export interface NotificationsLongPollResponse {
+ code: number
+ msg: string
+ data: NotificationItem[]
+}
+
+export interface NotificationsListResponse {
+ count: number
+ next: string | null
+ previous: string | null
+ results?: {
+ code: number
+ msg: string
+ data: NotificationItem[]
+ }
+}
+
+export interface MarkNotificationsAsReadResponse {
+ code: number
+ msg: string
+ marked_count: number
+}
+
+export const notificationsService = {
+ async longPoll(params: { farmUuid: string; sinceId?: number; timeout?: number }): Promise {
+ const searchParams = new URLSearchParams({ farm_uuid: params.farmUuid })
+
+ if (typeof params.sinceId === 'number') {
+ searchParams.set('since_id', String(params.sinceId))
+ }
+
+ if (typeof params.timeout === 'number') {
+ searchParams.set('timeout', String(params.timeout))
+ }
+
+ const response = await apiClient.get(`${PREFIX}/long-poll/?${searchParams.toString()}`)
+
+ return Array.isArray(response.data) ? response.data : []
+ },
+
+ async list(params: { farmUuid: string; page?: number; pageSize?: number }): Promise {
+ const searchParams = new URLSearchParams({ farm_uuid: params.farmUuid })
+
+ if (typeof params.page === 'number') {
+ searchParams.set('page', String(params.page))
+ }
+
+ if (typeof params.pageSize === 'number') {
+ searchParams.set('page_size', String(params.pageSize))
+ }
+
+ return apiClient.get(`${PREFIX}/list/?${searchParams.toString()}`)
+ },
+
+ async markAsRead(payload: { farmUuid: string; sliceId: number }): Promise {
+ return apiClient.post(`${PREFIX}/mark-as-read/`, {
+ farm_uuid: payload.farmUuid,
+ slice_id: payload.sliceId
+ })
+ }
+}
diff --git a/src/views/apps/ecommerce/settings/Notifications.tsx b/src/views/apps/ecommerce/settings/Notifications.tsx
index 9a8d79b..5adf415 100644
--- a/src/views/apps/ecommerce/settings/Notifications.tsx
+++ b/src/views/apps/ecommerce/settings/Notifications.tsx
@@ -1,82 +1,128 @@
-// MUI Imports
+'use client'
+
+import { useCallback, useEffect, useMemo, useState } from 'react'
+
import Card from '@mui/material/Card'
import CardContent from '@mui/material/CardContent'
-import Checkbox from '@mui/material/Checkbox'
+import Chip from '@mui/material/Chip'
+import Stack from '@mui/material/Stack'
import Typography from '@mui/material/Typography'
+import Alert from '@mui/material/Alert'
+import CircularProgress from '@mui/material/CircularProgress'
+import Button from '@mui/material/Button'
+import Divider from '@mui/material/Divider'
-// Style Imports
-import tableStyles from '@core/styles/table.module.css'
+import { format } from 'date-fns'
-type tableData = { type: string; email: boolean; app: boolean }
-
-type CardProps = {
- title: string
- data: tableData[]
-}
-
-// Vars
-const customerData: tableData[] = [
- { type: 'New customer sign up', email: true, app: false },
- { type: 'Customer account password reset', email: false, app: true },
- { type: 'Customer account invite', email: false, app: false }
-]
-
-const shippingData: tableData[] = [
- { type: 'Picked up', email: true, app: false },
- { type: 'Shipping update ', email: false, app: true },
- { type: 'Delivered', email: false, app: false }
-]
-
-const ordersData: tableData[] = [
- { type: 'Order purchase', email: true, app: false },
- { type: 'Order cancelled', email: false, app: true },
- { type: 'Order refund request', email: false, app: false },
- { type: 'Order confirmation', email: false, app: true },
- { type: 'Payment error', email: false, app: true }
-]
-
-const TableCard = (props: CardProps) => {
- // Props
- const { title, data } = props
-
- return (
-
-
{title}
-
-
-
-
- | Type |
- Email |
- App |
-
-
-
- {data.map((data, index) => (
-
- | {data.type} |
-
-
- |
-
-
- |
-
- ))}
-
-
-
-
- )
-}
+import { useFarmHub } from '@/hooks/useFarmHub'
+import { notificationsService, type NotificationItem } from '@/libs/api/services/notificationsService'
const Notifications = () => {
+ const { farmHub } = useFarmHub()
+ const farmUuid = farmHub?.farm_uuid ?? null
+
+ const [notifications, setNotifications] = useState([])
+ const [loading, setLoading] = useState(false)
+ const [markingAll, setMarkingAll] = useState(false)
+ const [error, setError] = useState(null)
+
+ const unreadCount = useMemo(() => notifications.filter(item => !item.is_read).length, [notifications])
+ const latestSinceId = useMemo(() => notifications.reduce((latest, item) => Math.max(latest, item.since_id), 0), [notifications])
+
+ const loadNotifications = useCallback(async () => {
+ if (!farmUuid) {
+ setNotifications([])
+ return
+ }
+
+ setLoading(true)
+ setError(null)
+
+ try {
+ const items = await notificationsService.longPoll({ farmUuid, timeout: 0 })
+ setNotifications(items.sort((first, second) => new Date(second.created_at).getTime() - new Date(first.created_at).getTime()))
+ } catch (loadError) {
+ setError(loadError instanceof Error ? loadError.message : 'Unable to load notifications.')
+ } finally {
+ setLoading(false)
+ }
+ }, [farmUuid])
+
+ const markAllAsRead = useCallback(async () => {
+ if (!farmUuid || latestSinceId <= 0 || unreadCount === 0) {
+ return
+ }
+
+ setMarkingAll(true)
+
+ try {
+ await notificationsService.markAsRead({ farmUuid, sliceId: latestSinceId })
+ setNotifications(current => current.map(item => ({ ...item, is_read: true })))
+ } catch (markError) {
+ setError(markError instanceof Error ? markError.message : 'Unable to mark notifications as read.')
+ } finally {
+ setMarkingAll(false)
+ }
+ }, [farmUuid, latestSinceId, unreadCount])
+
+ useEffect(() => {
+ void loadNotifications()
+ }, [loadNotifications])
+
return (
-
-
-
+
+
+ Notifications
+ Live farm alerts for the currently selected farm appear here.
+
+
+ 0 ? 'error' : 'default'} label={`${unreadCount} unread`} />
+ void loadNotifications()} disabled={loading || !farmUuid}>
+ Refresh
+
+ void markAllAsRead()} disabled={markingAll || unreadCount === 0 || !farmUuid}>
+ Mark all as read
+
+
+
+
+
+
+ {!farmUuid ? (
+ برای دیدن نوتیفیکیشن ها، ابتدا یک مزرعه فعال انتخاب کنید.
+ ) : error ? (
+ {error}
+ ) : loading ? (
+
+
+
+ ) : notifications.length === 0 ? (
+ هنوز نوتیفیکیشنی برای این مزرعه ثبت نشده است.
+ ) : (
+
+ {notifications.map(notification => (
+
+
+
+
+ {notification.title}
+ {notification.message}
+
+
+
+
+
+
+
+ {format(new Date(notification.created_at), 'yyyy-MM-dd HH:mm')}
+
+
+
+ ))}
+
+ )}
)
diff --git a/src/views/pages/account-settings/index.tsx b/src/views/pages/account-settings/index.tsx
index c959565..6cb1d94 100644
--- a/src/views/pages/account-settings/index.tsx
+++ b/src/views/pages/account-settings/index.tsx
@@ -1,8 +1,9 @@
'use client'
// React Imports
-import { useState } from 'react'
+import { useEffect, useState } from 'react'
import { useTranslations } from 'next-intl'
+import { useSearchParams } from 'next/navigation'
import type { SyntheticEvent, ReactElement } from 'react'
// MUI Imports
@@ -16,7 +17,15 @@ import CustomTabList from '@core/components/mui/TabList'
const AccountSettings = ({ tabContentList }: { tabContentList: { [key: string]: ReactElement } }) => {
const t = useTranslations('accountSettings')
- const [activeTab, setActiveTab] = useState('account')
+ const searchParams = useSearchParams()
+ const initialTab = searchParams.get('tab') ?? 'account'
+ const [activeTab, setActiveTab] = useState(initialTab)
+
+ useEffect(() => {
+ const nextTab = searchParams.get('tab') ?? 'account'
+
+ setActiveTab(nextTab)
+ }, [searchParams])
const handleChange = (event: SyntheticEvent, value: string) => {
setActiveTab(value)
@@ -35,14 +44,15 @@ const AccountSettings = ({ tabContentList }: { tabContentList: { [key: string]:
icon={}
iconPosition='start'
value='billing-plans'
- />
+ >
+ */}
}
iconPosition='start'
value='notifications'
/>
- } iconPosition='start' value='connections' /> */}
+ {/*} iconPosition='start' value='connections' /> */}
diff --git a/src/views/pages/account-settings/notifications/index.tsx b/src/views/pages/account-settings/notifications/index.tsx
index 7e78d48..7b5015e 100644
--- a/src/views/pages/account-settings/notifications/index.tsx
+++ b/src/views/pages/account-settings/notifications/index.tsx
@@ -1,126 +1,172 @@
'use client'
-import { useTranslations } from 'next-intl'
+import { useCallback, useEffect, useMemo, useState } from 'react'
-// MUI Imports
-import Card from '@mui/material/Card'
-import CardHeader from '@mui/material/CardHeader'
-import CardContent from '@mui/material/CardContent'
-import Typography from '@mui/material/Typography'
-import Checkbox from '@mui/material/Checkbox'
-import MenuItem from '@mui/material/MenuItem'
-import Grid from '@mui/material/Grid2'
+import Alert from '@mui/material/Alert'
+import Box from '@mui/material/Box'
import Button from '@mui/material/Button'
+import Card from '@mui/material/Card'
+import CardContent from '@mui/material/CardContent'
+import CardHeader from '@mui/material/CardHeader'
+import Chip from '@mui/material/Chip'
+import CircularProgress from '@mui/material/CircularProgress'
+import Divider from '@mui/material/Divider'
+import Pagination from '@mui/material/Pagination'
+import Stack from '@mui/material/Stack'
+import Typography from '@mui/material/Typography'
-// Component Imports
-import Link from '@components/Link'
-import Form from '@components/Form'
-import CustomTextField from '@core/components/mui/TextField'
+import { format } from 'date-fns'
-// Style Imports
-import tableStyles from '@core/styles/table.module.css'
+import { useFarmHub } from '@/hooks/useFarmHub'
+import { notificationsService, type NotificationItem } from '@/libs/api/services/notificationsService'
-type TableDataType = {
- type: string
- app: boolean
- email: boolean
- browser: boolean
+const PAGE_SIZE = 10
+
+const getLevelColor = (level: NotificationItem['level']) => {
+ switch (level) {
+ case 'critical':
+ return 'error'
+ case 'warning':
+ return 'warning'
+ default:
+ return 'info'
+ }
}
-// Vars
-const tableData: TableDataType[] = [
- {
- app: true,
- email: true,
- browser: true,
- type: 'New for you'
- },
- {
- app: true,
- email: true,
- browser: true,
- type: 'Account activity'
- },
- {
- app: false,
- email: true,
- browser: true,
- type: 'A new browser used to sign in'
- },
- {
- app: false,
- email: true,
- browser: false,
- type: 'A new device is linked'
- }
-]
-
const Notifications = () => {
- const t = useTranslations('accountSettings')
- const tNotif = useTranslations('accountSettings.notifications')
+ const { farmHub } = useFarmHub()
+ const farmUuid = farmHub?.farm_uuid ?? null
+
+ const [notifications, setNotifications] = useState([])
+ const [loading, setLoading] = useState(false)
+ const [markingAll, setMarkingAll] = useState(false)
+ const [error, setError] = useState(null)
+ const [page, setPage] = useState(1)
+ const [count, setCount] = useState(0)
+
+ const unreadCount = useMemo(() => notifications.filter(item => !item.is_read).length, [notifications])
+ const latestSinceId = useMemo(() => notifications.reduce((latest, item) => Math.max(latest, item.since_id), 0), [notifications])
+ const totalPages = useMemo(() => Math.max(1, Math.ceil(count / PAGE_SIZE)), [count])
+
+ const loadNotifications = useCallback(
+ async (nextPage: number) => {
+ if (!farmUuid) {
+ setNotifications([])
+ setCount(0)
+ return
+ }
+
+ setLoading(true)
+ setError(null)
+
+ try {
+ const response = await notificationsService.list({ farmUuid, page: nextPage, pageSize: PAGE_SIZE })
+
+ setNotifications(Array.isArray(response.results?.data) ? response.results.data : [])
+ setCount(typeof response.count === 'number' ? response.count : 0)
+ } catch (loadError) {
+ setError(loadError instanceof Error ? loadError.message : 'Unable to load notifications.')
+ } finally {
+ setLoading(false)
+ }
+ },
+ [farmUuid]
+ )
+
+ const markAllAsRead = useCallback(async () => {
+ if (!farmUuid || latestSinceId <= 0 || unreadCount === 0) {
+ return
+ }
+
+ setMarkingAll(true)
+
+ try {
+ await notificationsService.markAsRead({ farmUuid, sliceId: latestSinceId })
+ setNotifications(current => current.map(item => ({ ...item, is_read: true })))
+ } catch (markError) {
+ setError(markError instanceof Error ? markError.message : 'Unable to mark notifications as read.')
+ } finally {
+ setMarkingAll(false)
+ }
+ }, [farmUuid, latestSinceId, unreadCount])
+
+ useEffect(() => {
+ setPage(1)
+ }, [farmUuid])
+
+ useEffect(() => {
+ void loadNotifications(page)
+ }, [loadNotifications, page])
return (
- {tNotif('permissionRequest')}
- {tNotif('requestPermission')}
- >
+ title='Notifications'
+ subheader='All notifications for the currently selected farm are listed here.'
+ action={
+
+ 0 ? 'error' : 'default'} label={`${unreadCount} unread`} />
+ void loadNotifications(page)} disabled={loading || !farmUuid}>
+ Refresh
+
+ void markAllAsRead()} disabled={markingAll || unreadCount === 0 || !farmUuid}>
+ Mark page as read
+
+
}
/>
-
+
+
+ {totalPages > 1 ? (
+
+ setPage(value)} />
+
+ ) : null}
+ >
+ )}
+
)
}