From 2bb6546fbeae2cd8c9baecf4e088aa2cae5d3bfc Mon Sep 17 00:00:00 2001 From: Daphne210 Date: Sun, 14 Apr 2024 08:18:34 +0300 Subject: [PATCH 1/2] U4X-532 Adding dummy data for the notifications --- src/components/navbar/navbar.component.tsx | 2 + .../notifications-menu-button.component.tsx | 41 ++++++++ .../notifications-menu-overlay.component.tsx | 88 +++++++++++++++++ .../notifications-menu-overlay.scss | 99 +++++++++++++++++++ .../notifications-menu.scss | 9 ++ src/index.ts | 1 + src/routes.json | 84 ++++++++-------- 7 files changed, 281 insertions(+), 43 deletions(-) create mode 100644 src/components/notifications-menu/notifications-menu-button.component.tsx create mode 100644 src/components/notifications-menu/notifications-menu-overlay.component.tsx create mode 100644 src/components/notifications-menu/notifications-menu-overlay.scss create mode 100644 src/components/notifications-menu/notifications-menu.scss diff --git a/src/components/navbar/navbar.component.tsx b/src/components/navbar/navbar.component.tsx index dc39de7..5f41c3d 100644 --- a/src/components/navbar/navbar.component.tsx +++ b/src/components/navbar/navbar.component.tsx @@ -25,6 +25,7 @@ import UserMenuPanel from "../navbar-header-panels/user-menu-panel.component"; import SideMenuPanel from "../navbar-header-panels/side-menu-panel.component"; import styles from "./navbar.scss"; import AppSearchLaunch from "../app-search-icon/app-search-icon.component"; +import NotificationsMenuButton from "../notifications-menu/notifications-menu-button.component"; const Navbar: React.FC = () => { const session = useSession(); @@ -141,6 +142,7 @@ const Navbar: React.FC = () => { expanded={isActivePanel("sideMenu")} /> )} + {showAppMenu && } {showUserMenu && ( diff --git a/src/components/notifications-menu/notifications-menu-button.component.tsx b/src/components/notifications-menu/notifications-menu-button.component.tsx new file mode 100644 index 0000000..6a31455 --- /dev/null +++ b/src/components/notifications-menu/notifications-menu-button.component.tsx @@ -0,0 +1,41 @@ +import React, { useState, useCallback } from "react"; +import { HeaderGlobalAction } from "@carbon/react"; +import { Notification, Close, Switcher } from "@carbon/react/icons"; +import styles from "./notifications-menu.scss"; +import { useLayoutType } from "@openmrs/esm-framework"; +import { useTranslation } from "react-i18next"; +import NotificationMenuOverlay from "./notifications-menu-overlay.component"; + +const NotificationsMenuButton: React.FC = () => { + const [isNotificationPanelOpen, setIsNotificationPanelOpen] = useState(false); + const { t } = useTranslation(); + const layout = useLayoutType(); + + const toggleNotificationPanel = useCallback(() => { + setIsNotificationPanelOpen(!isNotificationPanelOpen); + }, [isNotificationPanelOpen]); + + return ( +
+ {isNotificationPanelOpen && } + + {isNotificationPanelOpen ? ( + + ) : ( + + )} + +
+ ); +}; + +export default NotificationsMenuButton; diff --git a/src/components/notifications-menu/notifications-menu-overlay.component.tsx b/src/components/notifications-menu/notifications-menu-overlay.component.tsx new file mode 100644 index 0000000..40001ba --- /dev/null +++ b/src/components/notifications-menu/notifications-menu-overlay.component.tsx @@ -0,0 +1,88 @@ +import React, { useState } from "react"; +import styles from "./notifications-menu-overlay.scss"; +import { useTranslation } from "react-i18next"; +import { Toggle } from "@carbon/react"; + +const NotificationMenuOverlay: React.FC = () => { + const { t } = useTranslation(); + const [showOnlyUnread, setShowOnlyUnread] = useState(false); + + // Dummy data + const notifications = [ + { + id: 1, + avatar: "AM", + title: "Jaba Musa's Viral Load Test Results Now Available", + description: + "The test findings for Jaba Musa have been delivered from CPHL", + time: "1 day ago", + read: false, + }, + { + id: 2, + avatar: "DM", + title: "Daphine Mwanje's Viral Load Test Results Now Available", + description: + "The test findings for Daphine Mwanje have been delivered from CPHL", + time: "1 week ago", + read: true, + }, + { + id: 3, + avatar: "NTM", + title: "Nancy Tai Mpango's Viral Load Test Results Now Available", + description: + "The test findings for Nancy Tai Mpango have been delivered from CPHL", + time: "1 week ago", + read: false, + }, + ]; + + return ( +
+
+

{t("notifications", "Notifications")}

+
+ setShowOnlyUnread(isToggled)} + defaultToggled={showOnlyUnread} + id="toggle-1" + /> +
+
+ +
    + {notifications + .filter((notification) => + showOnlyUnread ? !notification.read : true + ) + .map((notification) => ( +
  • +
    + {!notification.read && ( + + )} +
    {notification.avatar}
    +
    +
    +
    {notification.title}
    +

    {notification.description}

    + {notification.time} +
    +
  • + ))} +
+
+ ); +}; + +export default NotificationMenuOverlay; diff --git a/src/components/notifications-menu/notifications-menu-overlay.scss b/src/components/notifications-menu/notifications-menu-overlay.scss new file mode 100644 index 0000000..bd4c4b1 --- /dev/null +++ b/src/components/notifications-menu/notifications-menu-overlay.scss @@ -0,0 +1,99 @@ +@use '@carbon/styles/scss/colors'; +@use '@carbon/styles/scss/type'; +@use '@carbon/styles/scss/spacing'; +.notificationMenuOverlay { + position: absolute; + right: 2rem; + top: 50px; + display: block; + box-sizing: border-box; + width: 30rem; + height: calc(100vh - 75pt); + margin: 16px 0 16px; + background: white; + box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2); + border-radius: 3px; + z-index: 1000; + overflow: auto; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem; + border-bottom: 1px solid #eaeaea; + h2 { + font-size: 20px; + margin: 0; + font-weight: var(--cds-label-01-font-weight, 600); + flex: 1; + } + .toggleContainer { + display: flex; + align-items: center; + gap: 0.5rem; + .bx--toggle { + flex-shrink: 0; + } + } +} + +.notificationList { + list-style: none; + padding: 0; + margin: 0; +} + +.notificationItem { + display: flex; + align-items: center; + padding: 16px; + border-bottom: 1px solid #eaeaea; + &:last-child { + border-bottom: none; + } + &.unreadNotificationItem { + background-color: #f4f4f4; + } + .notificationIndicatorWrapper { + display: flex; + align-items: center; + gap: 0.5rem; + .unreadIndicator { + width: 10px; + height: 10px; + border-radius: 50%; + background-color: blue; + display: inline-block; + margin-left: 10px; + } + .avatar { + width: 30px; + height: 30px; + background: #eee; + border-radius: 50%; + margin-right: 16px; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + } + } + .notificationContent { + flex-grow: 1; + .title { + margin: 0 0 4px 0; + font-weight: 600; + } + .description { + margin: 0; + } + .time { + display: block; + color: #999; + margin-top: 8px; + font-size: 12px; + } + } +} \ No newline at end of file diff --git a/src/components/notifications-menu/notifications-menu.scss b/src/components/notifications-menu/notifications-menu.scss new file mode 100644 index 0000000..777ea00 --- /dev/null +++ b/src/components/notifications-menu/notifications-menu.scss @@ -0,0 +1,9 @@ +@use '@carbon/styles/scss/colors'; +@use '@carbon/styles/scss/type'; +@use '@carbon/styles/scss/spacing'; +@import '~@openmrs/esm-styleguide/src/vars'; +.noficationButtonContainer { + display: flex; + justify-content: flex-end; + align-items: center; +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index ba6881b..6767773 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ import { defineConfigSchema, defineExtensionConfigSchema, + getAsyncLifecycle, getSyncLifecycle, setupOfflineSync, } from "@openmrs/esm-framework"; diff --git a/src/routes.json b/src/routes.json index 261fa6d..610105e 100644 --- a/src/routes.json +++ b/src/routes.json @@ -1,46 +1,44 @@ { - "$schema": "https://json.openmrs.org/routes.schema.json", - "backendDependencies": { - "webservices.rest": ">=2.2.0" - }, - "pages": [ - { - "component": "root", - "routeRegex": "^(?!login/?)", - "online": true, - "offline": true, - "order": 0 + "$schema": "https://json.openmrs.org/routes.schema.json", + "backendDependencies": { + "webservices.rest": ">=2.2.0" }, - { - "component": "redirect", - "routeRegex": "^$", - "online": true, - "offline": true, - "order": 0 - } - ], - "extensions": [ - { - "name": "default-user-panel", - "slot": "user-panel-slot", - "component": "userPanel", - "online": true, - "offline": true, - "order": 0 - }, - { - "name": "change-locale", - "slot": "user-panel-slot", - "component": "localeChanger", - "online": true, - "offline": true, - "order": 1 - }, - { - "name": "link", - "component": "linkComponent", - "online": true, - "offline": true - } - ] + "pages": [{ + "component": "root", + "routeRegex": "^(?!login/?)", + "online": true, + "offline": true, + "order": 0 + }, + { + "component": "redirect", + "routeRegex": "^$", + "online": true, + "offline": true, + "order": 0 + } + ], + "extensions": [{ + "name": "default-user-panel", + "slot": "user-panel-slot", + "component": "userPanel", + "online": true, + "offline": true, + "order": 0 + }, + { + "name": "change-locale", + "slot": "user-panel-slot", + "component": "localeChanger", + "online": true, + "offline": true, + "order": 1 + }, + { + "name": "link", + "component": "linkComponent", + "online": true, + "offline": true + } + ] } \ No newline at end of file From f087240bb7202c9068aae5042ba215b8d0b53542 Mon Sep 17 00:00:00 2001 From: Daphne210 Date: Sat, 20 Apr 2024 13:42:54 +0300 Subject: [PATCH 2/2] U4X-532 Replacing dummy data with the response from the API --- .../notifications-menu-button.component.tsx | 10 +- ...nu.scss => notifications-menu-button.scss} | 0 .../notifications-menu-overlay.component.tsx | 98 ++++++++----------- .../notifications-menu-overlay.scss | 15 --- .../notifications-menu.resource.ts | 53 ++++++++++ 5 files changed, 100 insertions(+), 76 deletions(-) rename src/components/notifications-menu/{notifications-menu.scss => notifications-menu-button.scss} (100%) create mode 100644 src/components/notifications-menu/notifications-menu.resource.ts diff --git a/src/components/notifications-menu/notifications-menu-button.component.tsx b/src/components/notifications-menu/notifications-menu-button.component.tsx index 6a31455..fe8daa3 100644 --- a/src/components/notifications-menu/notifications-menu-button.component.tsx +++ b/src/components/notifications-menu/notifications-menu-button.component.tsx @@ -1,20 +1,24 @@ import React, { useState, useCallback } from "react"; import { HeaderGlobalAction } from "@carbon/react"; -import { Notification, Close, Switcher } from "@carbon/react/icons"; -import styles from "./notifications-menu.scss"; +import { Close, Notification, NotificationNew } from "@carbon/react/icons"; +import styles from "./notifications-menu-button.scss"; import { useLayoutType } from "@openmrs/esm-framework"; import { useTranslation } from "react-i18next"; import NotificationMenuOverlay from "./notifications-menu-overlay.component"; +import { useGetAlerts } from "./notifications-menu.resource"; const NotificationsMenuButton: React.FC = () => { const [isNotificationPanelOpen, setIsNotificationPanelOpen] = useState(false); const { t } = useTranslation(); const layout = useLayoutType(); + const { alerts } = useGetAlerts(); const toggleNotificationPanel = useCallback(() => { setIsNotificationPanelOpen(!isNotificationPanelOpen); }, [isNotificationPanelOpen]); + const hasUnreadAlerts = alerts.some((alert) => !alert.alertRead); + return (
{isNotificationPanelOpen && } @@ -30,6 +34,8 @@ const NotificationsMenuButton: React.FC = () => { > {isNotificationPanelOpen ? ( + ) : hasUnreadAlerts ? ( + ) : ( )} diff --git a/src/components/notifications-menu/notifications-menu.scss b/src/components/notifications-menu/notifications-menu-button.scss similarity index 100% rename from src/components/notifications-menu/notifications-menu.scss rename to src/components/notifications-menu/notifications-menu-button.scss diff --git a/src/components/notifications-menu/notifications-menu-overlay.component.tsx b/src/components/notifications-menu/notifications-menu-overlay.component.tsx index 40001ba..d7a5894 100644 --- a/src/components/notifications-menu/notifications-menu-overlay.component.tsx +++ b/src/components/notifications-menu/notifications-menu-overlay.component.tsx @@ -1,42 +1,13 @@ import React, { useState } from "react"; import styles from "./notifications-menu-overlay.scss"; import { useTranslation } from "react-i18next"; -import { Toggle } from "@carbon/react"; +import { InlineLoading, Toggle } from "@carbon/react"; +import { useGetAlerts } from "./notifications-menu.resource"; const NotificationMenuOverlay: React.FC = () => { const { t } = useTranslation(); const [showOnlyUnread, setShowOnlyUnread] = useState(false); - - // Dummy data - const notifications = [ - { - id: 1, - avatar: "AM", - title: "Jaba Musa's Viral Load Test Results Now Available", - description: - "The test findings for Jaba Musa have been delivered from CPHL", - time: "1 day ago", - read: false, - }, - { - id: 2, - avatar: "DM", - title: "Daphine Mwanje's Viral Load Test Results Now Available", - description: - "The test findings for Daphine Mwanje have been delivered from CPHL", - time: "1 week ago", - read: true, - }, - { - id: 3, - avatar: "NTM", - title: "Nancy Tai Mpango's Viral Load Test Results Now Available", - description: - "The test findings for Nancy Tai Mpango have been delivered from CPHL", - time: "1 week ago", - read: false, - }, - ]; + const { alerts, isLoading, isError } = useGetAlerts(); return (
@@ -54,33 +25,42 @@ const NotificationMenuOverlay: React.FC = () => { />
- -
    - {notifications - .filter((notification) => - showOnlyUnread ? !notification.read : true - ) - .map((notification) => ( -
  • -
    - {!notification.read && ( - - )} -
    {notification.avatar}
    -
    -
    -
    {notification.title}
    -

    {notification.description}

    - {notification.time} -
    -
  • - ))} -
+ {isLoading ? ( + + ) : alerts.length === 0 ? ( +

No notifications available

+ ) : ( +
    + {alerts + .filter((notification) => + showOnlyUnread ? !notification.alertRead : true + ) + .map((notification) => ( +
  • +
    + {!notification.alertRead && ( + + )} +
    +
    +

    {notification.display}

    + + {notification.timeDifference} + +
    +
  • + ))} +
+ )} ); }; diff --git a/src/components/notifications-menu/notifications-menu-overlay.scss b/src/components/notifications-menu/notifications-menu-overlay.scss index bd4c4b1..4abc8d4 100644 --- a/src/components/notifications-menu/notifications-menu-overlay.scss +++ b/src/components/notifications-menu/notifications-menu-overlay.scss @@ -68,24 +68,9 @@ display: inline-block; margin-left: 10px; } - .avatar { - width: 30px; - height: 30px; - background: #eee; - border-radius: 50%; - margin-right: 16px; - display: flex; - align-items: center; - justify-content: center; - font-weight: bold; - } } .notificationContent { flex-grow: 1; - .title { - margin: 0 0 4px 0; - font-weight: 600; - } .description { margin: 0; } diff --git a/src/components/notifications-menu/notifications-menu.resource.ts b/src/components/notifications-menu/notifications-menu.resource.ts new file mode 100644 index 0000000..8f5681e --- /dev/null +++ b/src/components/notifications-menu/notifications-menu.resource.ts @@ -0,0 +1,53 @@ +import { openmrsFetch, restBaseUrl } from "@openmrs/esm-framework"; +import useSWR from "swr"; + +export interface Notifications { + uuid: string; + display: string; + alertId: number; + alertRead: boolean; + dateToExpire: string; + dateCreated: string; + timeDifference: string; +} + +export function useGetAlerts() { + const apiURL = `${restBaseUrl}/alert?v=full`; + const { data, error, isLoading } = useSWR< + { data: { results: Array } }, + Error + >(apiURL, openmrsFetch); + + const alerts = data?.data?.results || []; + + alerts.forEach((notification) => { + const dateCreated = new Date(notification.dateCreated); + const currentDate = new Date(); + const timeDifference = calculateTimeDifference(dateCreated, currentDate); + notification.timeDifference = timeDifference; + }); + + return { + alerts, + isLoading, + isError: error, + }; +} + +function calculateTimeDifference(date1: Date, date2: Date): string { + const timeDifferenceInMs = date2.getTime() - date1.getTime(); + const seconds = Math.floor(timeDifferenceInMs / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + + if (days > 0) { + return `${days} day${days > 1 ? "s" : ""} ago`; + } else if (hours > 0) { + return `${hours} hour${hours > 1 ? "s" : ""} ago`; + } else if (minutes > 0) { + return `${minutes} minute${minutes > 1 ? "s" : ""} ago`; + } else { + return `${seconds} second${seconds !== 1 ? "s" : ""} ago`; + } +}