datastake-daf 0.6.501 → 0.6.503

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/layouts/index.css +1 -0
  2. package/dist/layouts/index.js +7137 -0
  3. package/dist/utils/index.js +287 -0
  4. package/package.json +1 -1
  5. package/rollup.config.js +25 -0
  6. package/src/@daf/hooks/usePermissions.js +20 -0
  7. package/src/@daf/layouts/AppLayout/AppLayout.stories.js +532 -0
  8. package/src/@daf/layouts/AppLayout/components/LoginPopup/index.js +116 -0
  9. package/src/@daf/layouts/AppLayout/components/LoginPopup/style.js +26 -0
  10. package/src/@daf/layouts/AppLayout/components/MobileDrawer/index.js +208 -0
  11. package/src/@daf/layouts/AppLayout/components/MobileDrawer/style.js +87 -0
  12. package/src/@daf/layouts/AppLayout/components/Notifications/Notification/index.js +31 -0
  13. package/src/@daf/layouts/AppLayout/components/Notifications/NotificationHistory/index.js +22 -0
  14. package/src/@daf/layouts/AppLayout/components/Notifications/context/index.js +79 -0
  15. package/src/@daf/layouts/AppLayout/components/Notifications/index.js +106 -0
  16. package/src/@daf/layouts/AppLayout/components/Notifications/style.js +61 -0
  17. package/src/@daf/layouts/AppLayout/components/Notifications/useNotifications.js +105 -0
  18. package/src/@daf/layouts/AppLayout/components/Sidenav/index.js +296 -0
  19. package/src/@daf/layouts/AppLayout/components/UserDropdown/UserIcon.js +96 -0
  20. package/src/@daf/layouts/AppLayout/components/UserDropdown/index.js +112 -0
  21. package/src/@daf/layouts/AppLayout/index.jsx +384 -0
  22. package/src/@daf/layouts/AppLayout/index.scss +5 -0
  23. package/src/@daf/layouts/AppLayout/styles/header.scss +257 -0
  24. package/src/@daf/layouts/AppLayout/styles/layout.scss +76 -0
  25. package/src/@daf/layouts/AppLayout/styles/responsive.scss +52 -0
  26. package/src/@daf/layouts/AppLayout/styles/sidebar.scss +79 -0
  27. package/src/@daf/layouts/AppLayout/styles/variables.scss +22 -0
  28. package/src/helpers/theme.js +304 -0
  29. package/src/helpers/user.js +4 -0
  30. package/src/layouts.js +1 -0
  31. package/src/utils.js +4 -2
@@ -0,0 +1,208 @@
1
+ /* eslint-disable react/prop-types */
2
+ import React, { useCallback, useMemo } from "react";
3
+ import SidenavMenu from "../../../../core/components/Sidenav/Menu.jsx";
4
+ import { formatClassname } from '../../../../../helpers/ClassesHelper.js';
5
+ import { renderModule } from "../Sidenav";
6
+ import { Style } from "./style";
7
+
8
+ const isCollapsed = false;
9
+ const selectedKeys = [];
10
+
11
+ export default function MobileDrawer({
12
+ mod = 'pme',
13
+ toggle = () => { },
14
+ drawerOpened = false,
15
+ isUserDropdown = false,
16
+ user,
17
+ sidenavConfig = {},
18
+ navigate,
19
+ t = (key) => key,
20
+ checkPermission = () => false,
21
+ logOut,
22
+ changeNotificationState,
23
+ matchPath,
24
+ appName = 'app',
25
+ userHelpers = {},
26
+ isDev = false,
27
+ selectedProject,
28
+ }) {
29
+ const items = useMemo(() => sidenavConfig[mod] || [], [sidenavConfig, mod]);
30
+
31
+ const canViewPartners = useMemo(() => mod === 'tazama', [mod]);
32
+ const canViewProjects = checkPermission({
33
+ permission: 'projects.canView',
34
+ permissions: user?.role?.permissions
35
+ });
36
+ const canViewUsers = checkPermission({
37
+ permission: 'users.canView',
38
+ permissions: user?.role?.permissions
39
+ });
40
+ const canViewSettings = checkPermission({
41
+ permission: 'settings.canView',
42
+ permissions: user?.role?.permissions
43
+ });
44
+
45
+ const hasMultipleApps = useMemo(() =>
46
+ Object.keys(user?.modules || {}).filter((k) => user.modules[k].status === 'approved').length > 1,
47
+ [user]
48
+ );
49
+
50
+ const handleLogOut = useCallback(() => {
51
+ const hasPrevious = (localStorage.getItem('previous'));
52
+ sessionStorage.removeItem('notifications');
53
+ if (hasPrevious) {
54
+ logOut?.({ isImpersonation: true });
55
+ return;
56
+ }
57
+ logOut?.();
58
+ }, [logOut]);
59
+
60
+ const goTo = (...props) => {
61
+ navigate?.(...props);
62
+ toggle();
63
+ };
64
+
65
+ const filteredItems = useMemo(() => {
66
+ const mapItems = (item) => {
67
+ const isDisabled = typeof item.isDisabled === 'function' ?
68
+ item.isDisabled(user, selectedProject, selectedProject)
69
+ : item.isDisabled || false;
70
+
71
+ if (item.items) {
72
+ return {
73
+ ...item,
74
+ isDisabled,
75
+ items: item.items.filter((item) => {
76
+ const res = typeof item.visible === 'function' ?
77
+ item.visible(user, selectedProject, selectedProject) : true;
78
+ return res;
79
+ }).map(mapItems)
80
+ };
81
+ }
82
+
83
+ return { ...item, isDisabled };
84
+ };
85
+
86
+ if (isUserDropdown) {
87
+ return [
88
+ {
89
+ type: 'link',
90
+ name: t('Projects'),
91
+ path: `/app/${mod}/projects`,
92
+ isDashboard: true,
93
+ visible: canViewProjects
94
+ },
95
+ {
96
+ type: 'link',
97
+ name: t('Partners'),
98
+ path: `/app/${mod}/partners`,
99
+ isDashboard: true,
100
+ visible: canViewPartners
101
+ },
102
+ {
103
+ type: 'link',
104
+ name: t('Users'),
105
+ path: `/app/users`,
106
+ isDashboard: true,
107
+ visible: canViewUsers
108
+ },
109
+ {
110
+ type: 'link',
111
+ name: t('Settings'),
112
+ path: `/app/${mod}/view/settings`,
113
+ isDashboard: true,
114
+ visible: canViewSettings
115
+ },
116
+ {
117
+ type: 'link',
118
+ name: t('Log out'),
119
+ onClick: () => handleLogOut(),
120
+ isDashboard: true,
121
+ visible: true
122
+ },
123
+ ]
124
+ .filter((v) => v.visible)
125
+ .map(mapItems);
126
+ }
127
+
128
+ return items
129
+ .filter(item => {
130
+ const res = typeof item.visible === 'function' ?
131
+ item.visible(user, selectedProject, selectedProject) : true;
132
+ return res;
133
+ })
134
+ .map(mapItems);
135
+ }, [
136
+ items,
137
+ user,
138
+ selectedProject,
139
+ t,
140
+ isUserDropdown,
141
+ mod,
142
+ handleLogOut,
143
+ hasMultipleApps,
144
+ canViewPartners,
145
+ canViewUsers,
146
+ canViewSettings,
147
+ canViewProjects
148
+ ]);
149
+
150
+ const checkOnClick = ({ event }) => {
151
+ changeNotificationState({
152
+ onYes: () => {
153
+ event();
154
+ }
155
+ });
156
+ };
157
+
158
+ return (
159
+ <Style className={formatClassname([!drawerOpened && 'closed', isUserDropdown && 'user-dropdown'])}>
160
+ <div className="drawer">
161
+ <div
162
+ className={formatClassname([
163
+ isCollapsed ? 'sidenav-sider-collapsed sidenav-sider flex-1' : 'sidenav-sider sidenav-sider-opened flex-1',
164
+ appName
165
+ ])}
166
+ style={{ width: isCollapsed ? '70px' : '250px', minWidth: isCollapsed ? 'auto' : '250px' }}
167
+ >
168
+ <div className="flex">
169
+ <div className="flex-1">
170
+ {mod === 'app' || !user ? null : renderModule({
171
+ isCollapsed,
172
+ mod: mod,
173
+ module: mod,
174
+ user,
175
+ userHelpers
176
+ })}
177
+ </div>
178
+ <div className="cursor-pointer close-icon" onClick={toggle}>
179
+ </div>
180
+ </div>
181
+ <div className="sidenav-cont">
182
+ <div className={formatClassname(['drawer', 'menus-cont', 'not-collapsed'])}>
183
+ <SidenavMenu
184
+ module={mod}
185
+ menuMode="inline"
186
+ selectedKeys={selectedKeys}
187
+ filteredItems={filteredItems}
188
+ isCollapsed={false}
189
+ setHoverItem={() => { }}
190
+ setTopHover={() => { }}
191
+ setHoverOpen={() => { }}
192
+ onMouseMove={() => { }}
193
+ user={user}
194
+ goTo={goTo}
195
+ isDev={isDev}
196
+ showSettings
197
+ t={t}
198
+ checkOnClick={checkOnClick}
199
+ project={selectedProject}
200
+ matchPath={matchPath}
201
+ />
202
+ </div>
203
+ </div>
204
+ </div>
205
+ </div>
206
+ </Style>
207
+ );
208
+ }
@@ -0,0 +1,87 @@
1
+ import styled from "styled-components";
2
+
3
+ export const Style = styled.div`
4
+ position: fixed;
5
+ background: rgba(0, 0, 0, 0.4);
6
+ width: 100dvw;
7
+ height: calc(100dvh - 64px);
8
+ top: 64px;
9
+ left: 0px;
10
+ z-index: 1001;
11
+ display: flex;
12
+ flex-direction: row;
13
+ animation: .4s fadeIn forwards;
14
+
15
+ &.user-dropdown {
16
+ justify-content: flex-end;
17
+ }
18
+
19
+ div {
20
+ line-height: 0px;
21
+ }
22
+
23
+ .sidenav-sider {
24
+ display: flex;
25
+ flex-direction: column;
26
+
27
+ .anticon {
28
+ line-height: 0px !important;
29
+ }
30
+
31
+ span[role="img"] {
32
+ padding: 0;
33
+ }
34
+
35
+ .close-icon {
36
+ margin-top: 20px;
37
+ }
38
+
39
+ .mod-name {
40
+ margin-bottom: 20px;
41
+ margin-top: 20px;
42
+ }
43
+ }
44
+
45
+ .drawer {
46
+ width: 250px;
47
+ transition: .4s margin-right;
48
+ height: 100%;
49
+ display: flex;
50
+ flex-direction: column;
51
+
52
+ .sidenav-cont {
53
+ svg {
54
+ margin-right: 0px;
55
+ }
56
+
57
+ span {
58
+ padding-left: 0px;
59
+ }
60
+
61
+ height: unset;
62
+ flex: 1;
63
+ overflow-y: auto;
64
+ max-height: calc(100dvh - 125px);
65
+
66
+ .log-out {
67
+ padding-top: var(--size-lg);
68
+ margin: 0px var(--size-lg);
69
+ cursor: pointer;
70
+
71
+ span {
72
+ padding-left: 10px;
73
+ margin-left: 10px;
74
+ }
75
+ }
76
+ }
77
+ }
78
+
79
+ @keyframes fadeIn {
80
+ from { opacity: 0; }
81
+ to { opacity: 1; }
82
+ }
83
+
84
+ &.closed {
85
+ display: none;
86
+ }
87
+ `;
@@ -0,0 +1,31 @@
1
+ /* eslint-disable react/prop-types */
2
+ import React from "react";
3
+ import { formatClassname } from "../../../../../../helpers/ClassesHelper.js";
4
+ import { useNotificationsContext } from "../context";
5
+
6
+ export const Notification = ({
7
+ n,
8
+ toggle = () => {},
9
+ t = (key) => key,
10
+ navigate,
11
+ }) => {
12
+ const { removeNotification } = useNotificationsContext();
13
+
14
+ const handleClick = () => {
15
+ if (n.data?.link) {
16
+ removeNotification(n.id);
17
+ toggle();
18
+ navigate?.(n.data.link);
19
+ }
20
+ };
21
+
22
+ return (
23
+ <div
24
+ className={formatClassname(['noti', n.data?.link && 'clickable'])}
25
+ onClick={handleClick}
26
+ >
27
+ <strong>{n.title || t('Notification')}</strong>
28
+ <div>{n.body}</div>
29
+ </div>
30
+ );
31
+ };
@@ -0,0 +1,22 @@
1
+ /* eslint-disable react/prop-types */
2
+ import React from "react";
3
+ import { Modal } from "antd";
4
+
5
+ export default function NotificationHistory({
6
+ visible,
7
+ setHistoryVisible,
8
+ t = (key) => key,
9
+ }) {
10
+ return (
11
+ <Modal
12
+ open={visible}
13
+ onCancel={() => setHistoryVisible(false)}
14
+ footer={null}
15
+ title={t('Notification History')}
16
+ >
17
+ <div style={{ padding: '20px' }}>
18
+ {t('Notification history content will appear here')}
19
+ </div>
20
+ </Modal>
21
+ );
22
+ }
@@ -0,0 +1,79 @@
1
+ /* eslint-disable react/prop-types */
2
+ import moment from "moment";
3
+ import React, { createContext, useContext, useCallback } from "react";
4
+ import useNotifications from "../useNotifications.js";
5
+
6
+ const status = 'unread';
7
+
8
+ export const NotificationsContext = createContext({
9
+ loading: false,
10
+ _notifications: [],
11
+ _fetch: () => {},
12
+ total: 0,
13
+ clearAll: () => { },
14
+ removeNotification: () => { },
15
+ });
16
+
17
+ export const NotificationsProvider = ({
18
+ children,
19
+ user,
20
+ notificationHandlers = {},
21
+ NotificationsHistoryProvider,
22
+ firebaseEnabled = true,
23
+ useFirebaseHook,
24
+ }) => {
25
+ const {
26
+ loading,
27
+ _notifications,
28
+ _fetch,
29
+ total,
30
+ addNotification,
31
+ clearAll,
32
+ removeNotification,
33
+ } = useNotifications({ status, user, ...notificationHandlers });
34
+
35
+ const value = {
36
+ loading,
37
+ _notifications,
38
+ _fetch,
39
+ total,
40
+ clearAll,
41
+ removeNotification,
42
+ };
43
+
44
+ const onMessage = useCallback((notification) => {
45
+ let data = {};
46
+
47
+ try {
48
+ data = JSON.parse(notification.data?.data);
49
+ } catch (err) {
50
+ console.log(err);
51
+ }
52
+
53
+ addNotification({
54
+ id: notification.messageId,
55
+ type: notification.data?.type,
56
+ title: notification.notification?.title,
57
+ body: notification.notification?.body,
58
+ createdAt: moment().toString(),
59
+ data: data,
60
+ read: false,
61
+ });
62
+ }, [addNotification]);
63
+
64
+ // Only use Firebase if enabled and hook provided
65
+ if (firebaseEnabled && useFirebaseHook) {
66
+ useFirebaseHook(onMessage);
67
+ }
68
+
69
+ return (
70
+ <NotificationsContext.Provider value={value}>
71
+ {children}
72
+ </NotificationsContext.Provider>
73
+ );
74
+ };
75
+
76
+ export const useNotificationsContext = () => {
77
+ const value = useContext(NotificationsContext);
78
+ return value;
79
+ };
@@ -0,0 +1,106 @@
1
+ /* eslint-disable react/prop-types */
2
+ import { Dropdown } from "antd";
3
+ import { LoadingOutlined } from "@ant-design/icons";
4
+ import React, { useState } from "react";
5
+ import CustomIcon from '../../../../core/components/Icon/CustomIcon.jsx';
6
+ import { NotificationsStyle } from './style';
7
+ import { useNotificationsContext } from "./context";
8
+ import { Notification } from "./Notification";
9
+ import NotificationHistory from "./NotificationHistory";
10
+ import { formatClassname } from "../../../../../helpers/ClassesHelper.js";
11
+
12
+ export default function Notifications({
13
+ mod = 'pme',
14
+ toggle = () => { },
15
+ t = (key) => key,
16
+ navigate,
17
+ appName = 'app',
18
+ }) {
19
+ const [historyVisible, setHistoryVisible] = useState(false);
20
+ const { loading, _notifications, _fetch, total, clearAll } = useNotificationsContext();
21
+
22
+ const items = [
23
+ {
24
+ key: "details",
25
+ icon: <CustomIcon name="CheckDone" width={13} height={13} />,
26
+ disabled: !total,
27
+ onClick: () => {
28
+ clearAll();
29
+ toggle();
30
+ },
31
+ label: t('Clear All'),
32
+ },
33
+ {
34
+ key: "publish",
35
+ icon: <CustomIcon name="NotificationText" width={13} height={13} />,
36
+ onClick: () => {
37
+ toggle();
38
+ setHistoryVisible(true);
39
+ },
40
+ label: t('View All'),
41
+ },
42
+ {
43
+ key: "dashboard",
44
+ icon: <CustomIcon name="Settings" width={13} height={13} />,
45
+ onClick: () => {
46
+ toggle();
47
+ navigate?.(`/app/${mod}/view/settings?activeForm=notifications`);
48
+ },
49
+ label: t('Settings'),
50
+ },
51
+ ];
52
+
53
+ return (
54
+ <NotificationsStyle className={appName}>
55
+ <div className="notis-title">
56
+ <h3>{t('sbg::notification')}</h3>
57
+ <Dropdown
58
+ menu={{ items }}
59
+ trigger="click"
60
+ rootClassName={formatClassname(['dark-menu', appName])}
61
+ >
62
+ <div className="cursor-pointer">
63
+ <CustomIcon name="MoreCustom" width={3} height={13} />
64
+ </div>
65
+ </Dropdown>
66
+ </div>
67
+ <div
68
+ className="notis-list"
69
+ onScroll={(e) => {
70
+ const _t = e.target;
71
+ const canFetch = (_t.scrollTop + _t.clientHeight) >= _t.scrollHeight - 200;
72
+
73
+ if (canFetch) {
74
+ _fetch();
75
+ }
76
+ }}
77
+ >
78
+ {!loading && !total ? (
79
+ <div className="no-notis">
80
+ {t('No new notifications')}
81
+ </div>
82
+ ) : null}
83
+ {_notifications.map((n, i) => (
84
+ <Notification
85
+ toggle={toggle}
86
+ key={`notifications-${i + 1}`}
87
+ n={n}
88
+ t={t}
89
+ navigate={navigate}
90
+ />
91
+ ))}
92
+ {loading && (
93
+ <div className="loading-cont">
94
+ <LoadingOutlined />
95
+ </div>
96
+ )}
97
+ </div>
98
+ <NotificationHistory
99
+ visible={historyVisible}
100
+ setHistoryVisible={setHistoryVisible}
101
+ t={t}
102
+ navigate={navigate}
103
+ />
104
+ </NotificationsStyle>
105
+ );
106
+ }
@@ -0,0 +1,61 @@
1
+ import styled from 'styled-components';
2
+
3
+ export const NotificationsStyle = styled.div`
4
+ display: flex;
5
+ flex-direction: column;
6
+
7
+ .notis-list {
8
+ max-height: 40vh;
9
+ overflow-y: auto;
10
+ padding: 0px var(--size-lg) var(--size-lg) var(--size-lg);
11
+ }
12
+
13
+ .notis-title {
14
+ display: flex;
15
+ color: white;
16
+ padding: var(--size-lg) var(--size-lg) 0 var(--size-lg);
17
+ margin-bottom: 8px;
18
+
19
+ h3 {
20
+ color: white;
21
+ font-size: 18px;
22
+ flex: 1;
23
+ margin-bottom: 0px;
24
+ }
25
+ }
26
+
27
+ .noti {
28
+ white-space: wrap;
29
+ padding: var(--size) 0px;
30
+ transition: 0.4s background;
31
+ user-select: none;
32
+ color: white;
33
+ border-bottom: 1px solid #384250;
34
+
35
+ &:last-of-type {
36
+ padding-bottom: 0px;
37
+ border-bottom: 0px;
38
+ }
39
+
40
+ strong {
41
+ color: white;
42
+ }
43
+
44
+ &.clickable {
45
+ cursor: pointer;
46
+ }
47
+ }
48
+
49
+ .no-notis {
50
+ color: white;
51
+ text-align: center;
52
+ padding: var(--size-lg) 0;
53
+ }
54
+
55
+ .loading-cont {
56
+ display: flex;
57
+ justify-content: center;
58
+ padding: var(--size-lg) 0;
59
+ color: white;
60
+ }
61
+ `;
@@ -0,0 +1,105 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+
3
+ const useNotifications = ({
4
+ status = 'unread',
5
+ user,
6
+ fetchNotifications,
7
+ markAsRead,
8
+ markAllAsRead,
9
+ }) => {
10
+ const [loading, setLoading] = useState(false);
11
+ const [notifications, setNotifications] = useState([]);
12
+ const [total, setTotal] = useState(0);
13
+ const [page, setPage] = useState(0);
14
+ const [hasMore, setHasMore] = useState(true);
15
+ const isFetching = useRef(false);
16
+
17
+ const _notifications = useMemo(() => {
18
+ return notifications.filter((n) => !n.read);
19
+ }, [notifications]);
20
+
21
+ const _fetch = useCallback(async () => {
22
+ if (!fetchNotifications || isFetching.current || !hasMore || loading) {
23
+ return;
24
+ }
25
+
26
+ try {
27
+ isFetching.current = true;
28
+ setLoading(true);
29
+
30
+ const response = await fetchNotifications({
31
+ page: page + 1,
32
+ status,
33
+ limit: 20,
34
+ });
35
+
36
+ const newNotifications = response.data || [];
37
+
38
+ if (newNotifications.length === 0) {
39
+ setHasMore(false);
40
+ } else {
41
+ setNotifications((prev) => [...prev, ...newNotifications]);
42
+ setPage((p) => p + 1);
43
+ }
44
+
45
+ setTotal(response.total || 0);
46
+ } catch (err) {
47
+ console.error('Error fetching notifications:', err);
48
+ } finally {
49
+ setLoading(false);
50
+ isFetching.current = false;
51
+ }
52
+ }, [fetchNotifications, page, status, hasMore, loading]);
53
+
54
+ const removeNotification = useCallback(async (id) => {
55
+ try {
56
+ if (markAsRead) {
57
+ await markAsRead(id);
58
+ }
59
+
60
+ setNotifications((prev) =>
61
+ prev.map((n) => n.id === id ? { ...n, read: true } : n)
62
+ );
63
+ setTotal((t) => Math.max(0, t - 1));
64
+ } catch (err) {
65
+ console.error('Error removing notification:', err);
66
+ }
67
+ }, [markAsRead]);
68
+
69
+ const clearAll = useCallback(async () => {
70
+ try {
71
+ if (markAllAsRead) {
72
+ await markAllAsRead();
73
+ }
74
+
75
+ setNotifications((prev) => prev.map((n) => ({ ...n, read: true })));
76
+ setTotal(0);
77
+ } catch (err) {
78
+ console.error('Error clearing notifications:', err);
79
+ }
80
+ }, [markAllAsRead]);
81
+
82
+ const addNotification = useCallback((notification) => {
83
+ setNotifications((prev) => [notification, ...prev]);
84
+ setTotal((t) => t + 1);
85
+ }, []);
86
+
87
+ // Initial fetch
88
+ useEffect(() => {
89
+ if (user) {
90
+ _fetch();
91
+ }
92
+ }, [user]);
93
+
94
+ return {
95
+ loading,
96
+ _notifications,
97
+ _fetch,
98
+ total,
99
+ clearAll,
100
+ removeNotification,
101
+ addNotification,
102
+ };
103
+ };
104
+
105
+ export default useNotifications;