diff --git a/.env b/.env
index fc25010..871e4c5 100644
--- a/.env
+++ b/.env
@@ -1,6 +1,7 @@
REACT_APP_TAB_TITLE=Gepafin
REACT_APP_API_EXECUTION_ADDRESS=https://api-dev-gepafin.memento.credit/v1
REACT_APP_API_ADDRESS=https://api-dev-gepafin.memento.credit
+REACT_APP_API_ADDRESS_WS=https://api-dev-gepafin.memento.credit/wss
REACT_APP_LOGO_FILENAME=gepafin-logo.svg
REACT_APP_FAVICON_FILENAME=gepafin-favicon.ico
REACT_APP_HUB_ID=p4lk3bcx1RStqTaIVVbXs
diff --git a/environments/dev/dev.env b/environments/dev/dev.env
index 38e36ec..ae149fd 100644
--- a/environments/dev/dev.env
+++ b/environments/dev/dev.env
@@ -1,6 +1,7 @@
REACT_APP_TAB_TITLE=Gepafin
REACT_APP_API_EXECUTION_ADDRESS=https://api-dev-gepafin.memento.credit/v1
REACT_APP_API_ADDRESS=https://api-dev-gepafin.memento.credit
+REACT_APP_API_ADDRESS_WS=https://api-dev-gepafin.memento.credit/wss
REACT_APP_LOGO_FILENAME=gepafin-logo.svg
REACT_APP_FAVICON_FILENAME=gepafin-favicon.ico
REACT_APP_HUB_ID=p4lk3bcx1RStqTaIVVbXs
diff --git a/package.json b/package.json
index e58a0a4..0783782 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,7 @@
"@emotion/styled": "11.13.0",
"@number-flow/react": "0.4.2",
"@sentry/browser": "^8.42.0",
+ "@stomp/stompjs": "^7.0.0",
"@tanstack/react-table": "^8.20.5",
"@wordpress/i18n": "5.8.0",
"@wordpress/react-i18n": "4.8.0",
@@ -36,6 +37,7 @@
"react-hook-form": "7.53.0",
"react-router-dom": "6.26.2",
"react-scripts": "5.0.1",
+ "sockjs-client": "^1.6.1",
"validate.js": "0.13.1",
"zustand": "4.5.4",
"zustand-x": "3.0.4"
diff --git a/src/assets/scss/components/appPage.scss b/src/assets/scss/components/appPage.scss
index 7d782b3..4ed00a3 100644
--- a/src/assets/scss/components/appPage.scss
+++ b/src/assets/scss/components/appPage.scss
@@ -218,6 +218,7 @@
.appPageSection__pMeta {
margin-bottom: 1em;
+ break-inside: avoid;
span:nth-of-type(1) {
max-width: 30%;
@@ -435,6 +436,15 @@
}
}
+.appPageSection__emailTemplate {
+ > div {
+ max-width: 100%!important;
+ > div {
+ max-width: 100%!important;
+ }
+ }
+}
+
@media (max-width: 700px) {
.appPageSection {
&.columns {
diff --git a/src/assets/scss/components/notificationsSidebar.scss b/src/assets/scss/components/notificationsSidebar.scss
new file mode 100644
index 0000000..2bc2eb8
--- /dev/null
+++ b/src/assets/scss/components/notificationsSidebar.scss
@@ -0,0 +1,55 @@
+.notificationsIcon {
+ &:hover {
+ cursor: pointer;
+ }
+}
+
+.notificationsSidebar {
+ max-width: 360px;
+ width: 100%;
+}
+
+.notificationsSidebar__loading {
+ padding: 30px 0;
+ display: flex;
+ justify-content: center;
+ flex-direction: column;
+ align-items: center;
+ gap: 10px;
+}
+
+.notificationsSidebar__list {
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+ list-style: none;
+ padding: 0;
+}
+
+.notificationsSidebar__listItem {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 5px;
+ padding: 15px 0;
+ border-bottom: 1px solid #e7e7e7;
+
+ &:hover {
+ cursor: pointer;
+ color: var(--primary-text);
+ }
+}
+
+.notificationsSidebar__listItemContent {
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+ font-size: 14px;
+}
+
+.notificationsSidebar__listItemChosen {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 5px;
+}
diff --git a/src/assets/scss/components/statsBigBadges.scss b/src/assets/scss/components/statsBigBadges.scss
index 7de0232..1788b0c 100644
--- a/src/assets/scss/components/statsBigBadges.scss
+++ b/src/assets/scss/components/statsBigBadges.scss
@@ -5,11 +5,15 @@
display: grid;
align-items: stretch;
/*grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));*/
- grid-template-columns: repeat(2, minmax(220px,1fr));
+ grid-template-columns: repeat(2, minmax(220px, 1fr));
gap: 1rem;
width: 100%;
container-name: big-badges-grid;
container-type: inline-size;
+
+ &.grid-small {
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+ }
}
.statsBigBadges__grid .statsBigBadges__gridItem span {
diff --git a/src/assets/scss/theme.scss b/src/assets/scss/theme.scss
index 4ad1ef5..2b48f72 100644
--- a/src/assets/scss/theme.scss
+++ b/src/assets/scss/theme.scss
@@ -44,4 +44,5 @@
@import "./components/error404.scss";
@import "./components/myTable.scss";
@import "./components/evaluation.scss";
-@import "./components/fieldsRepeater.scss";
\ No newline at end of file
+@import "./components/fieldsRepeater.scss";
+@import "./components/notificationsSidebar.scss";
diff --git a/src/components/FlowBuilder/index.js b/src/components/FlowBuilder/index.js
index fdb2d7f..3bf66de 100644
--- a/src/components/FlowBuilder/index.js
+++ b/src/components/FlowBuilder/index.js
@@ -8,7 +8,7 @@ import { isEmpty } from 'ramda';
import '@xyflow/react/dist/style.css';
// store
-import { useStore, storeSet, storeGet } from '../../store';
+import { useStore, storeSet } from '../../store';
// nodes
import NodeInitialForm from './components/NodeInitialForm';
diff --git a/src/components/FormField/components/DatepickerRange/index.js b/src/components/FormField/components/DatepickerRange/index.js
index 00d5394..5c9fa75 100644
--- a/src/components/FormField/components/DatepickerRange/index.js
+++ b/src/components/FormField/components/DatepickerRange/index.js
@@ -1,6 +1,6 @@
import React from 'react';
import { classNames } from 'primereact/utils';
-import { isEmpty, isNil } from 'ramda';
+import { isNil } from 'ramda';
// components
import { Controller } from 'react-hook-form';
diff --git a/src/components/NotificationsSidebar/components/NotificationItem/index.js b/src/components/NotificationsSidebar/components/NotificationItem/index.js
new file mode 100644
index 0000000..3d2efc5
--- /dev/null
+++ b/src/components/NotificationsSidebar/components/NotificationItem/index.js
@@ -0,0 +1,22 @@
+import React from 'react';
+import getDateFromISOstring from '../../../../helpers/getDateFromISOstring';
+
+const NotificationItem = ({ item, clickFn }) => {
+ const handleClick = () => {
+ clickFn(item.id);
+ }
+
+ return (
+
+
+ {item.status === 'READ'
+ ?
{item.title}
+ :
{item.title}}
+
{getDateFromISOstring(item.createdDate)}
+
+
+
+ )
+}
+
+export default NotificationItem;
diff --git a/src/components/NotificationsSidebar/components/NotificationItemChosen/index.js b/src/components/NotificationsSidebar/components/NotificationItemChosen/index.js
new file mode 100644
index 0000000..4588de6
--- /dev/null
+++ b/src/components/NotificationsSidebar/components/NotificationItemChosen/index.js
@@ -0,0 +1,30 @@
+import React from 'react';
+import { __ } from '@wordpress/i18n';
+import { Button } from 'primereact/button';
+import getDateFromISOstring from '../../../../helpers/getDateFromISOstring';
+
+const NotificationItemChosen = ({ item, closeFn, markReadFn }) => {
+ return (
+
+
+ {item.title}
+ {getDateFromISOstring(item.createdDate)}
+ {item.message}
+
+
+ )
+}
+
+export default NotificationItemChosen;
diff --git a/src/components/NotificationsSidebar/index.js b/src/components/NotificationsSidebar/index.js
new file mode 100644
index 0000000..f44a5e5
--- /dev/null
+++ b/src/components/NotificationsSidebar/index.js
@@ -0,0 +1,277 @@
+import React, { useEffect, useRef, useState } from 'react';
+import { __ } from '@wordpress/i18n';
+import { head, isEmpty, pathOr } from 'ramda';
+import SockJS from 'sockjs-client';
+import { Stomp } from '@stomp/stompjs';
+
+// store
+import { storeGet, useStore } from '../../store';
+
+// api
+import NotificationService from '../../service/notification-service';
+
+// tools
+import set404FromErrorResponse from '../../helpers/set404FromErrorResponse';
+
+// components
+import { Badge } from 'primereact/badge';
+import { Sidebar } from 'primereact/sidebar';
+import { TabPanel, TabView } from 'primereact/tabview';
+import NotificationItem from './components/NotificationItem';
+import NotificationItemChosen from './components/NotificationItemChosen';
+
+const socketUrl = process.env.REACT_APP_API_ADDRESS_WS;
+
+const NotificationsSidebar = () => {
+ const chosenCompanyId = useStore().main.chosenCompanyId();
+ const userData = useStore().main.userData();
+ const [activeIndex, setActiveIndex] = useState(0);
+ const [loading, setLoading] = useState(false);
+ const [notificationsVisible, setNotificationsVisible] = useState(false);
+ const [notifications, setNotifications] = useState([]);
+ const [notificationsRead, setNotificationsRead] = useState([]);
+ const [chosenMsg, setChosenMsg] = useState({});
+ const socket = useRef(null);
+ const stomp = useRef(null);
+ const [currentSubscription, setCurrentSubscription] = useState(null);
+ const [isConnected, setIsConnected] = useState(false);
+
+ // Handle tab change
+ const handleTabChange = (e) => {
+ setActiveIndex(e.index);
+ fetchTabData(e.index);
+ };
+
+ const fetchTabData = (index) => {
+ setChosenMsg({});
+
+ if (0 === index) {
+ fetchMessages();
+ } else {
+ fetchMessages('READ');
+ }
+ }
+
+ const chooseNotification = (id) => {
+ const properItems = activeIndex === 0 ? notifications : notificationsRead;
+ const chosen = head(properItems.filter(o => o.id === id));
+ if (chosen) {
+ setChosenMsg(chosen);
+ }
+ }
+
+ const closeChosenMsg = () => {
+ setChosenMsg({});
+ }
+
+ const fetchMessages = (status = 'UNREAD') => {
+ const chosenCompanyId = storeGet.main.chosenCompanyId();
+ const userData = storeGet.main.userData();
+ const role = pathOr('', ['role', 'roleType'], userData);
+
+ if (currentSubscription) {
+ //console.log('UNsubscribed')
+ currentSubscription.unsubscribe();
+ setCurrentSubscription(null);
+ }
+
+ if (userData.id && chosenCompanyId !== 0 && role === 'ROLE_BENEFICIARY') {
+ setLoading(true);
+ NotificationService.getNotifications(
+ userData.id,
+ status === 'UNREAD' ? getNotifications : getNotificationsRead,
+ errGetNotifications,
+ [
+ ['status', status],
+ ['companyId', chosenCompanyId]
+ ]
+ );
+ if (isConnected && socket.current) {
+ subscribeTo(`/topic/notifications_user_${userData.id}_company_${chosenCompanyId}`)
+ }
+ } else if (userData.id && role !== 'ROLE_BENEFICIARY') {
+ setLoading(true);
+ NotificationService.getNotifications(
+ userData.id,
+ status === 'UNREAD' ? getNotifications : getNotificationsRead,
+ errGetNotifications,
+ [
+ ['status', status]
+ ]
+ );
+ if (isConnected && socket.current) {
+ subscribeTo(`/topic/notifications_user_${userData.id}`)
+ }
+ }
+ }
+
+ const getNotifications = (resp) => {
+ if (resp.status === 'SUCCESS') {
+ setNotifications(resp.data);
+ }
+ set404FromErrorResponse(resp);
+ setLoading(false);
+ }
+
+ const getNotificationsRead = (resp) => {
+ if (resp.status === 'SUCCESS') {
+ setNotificationsRead(resp.data);
+ }
+ set404FromErrorResponse(resp);
+ setLoading(false);
+ }
+
+ const errGetNotifications = (resp) => {
+ set404FromErrorResponse(resp);
+ setLoading(false);
+ }
+
+ const makeNotificationRead = (id) => {
+ NotificationService.notificationMakeRead(id, makeReadCallback, makeReadErrorCallback)
+ }
+
+ const makeReadCallback = (resp) => {
+ if (resp.status === 'SUCCESS') {
+ if (0 === activeIndex) {
+ const msgs = notifications.map(o => o.id === resp.data.id ? resp.data : o);
+ setNotifications(msgs);
+ } else {
+ const msgs = notificationsRead.map(o => o.id === resp.data.id ? resp.data : o);
+ setNotificationsRead(msgs);
+ }
+ }
+ set404FromErrorResponse(resp);
+ }
+
+ const makeReadErrorCallback = (resp) => {
+ set404FromErrorResponse(resp);
+ }
+
+ const connectWebSocket = () => {
+ socket.current = new SockJS(socketUrl);
+ stomp.current = Stomp.over(socket.current);
+
+ stomp.current.configure({
+ debug: function(str) {
+ //console.log(str);
+ },
+ reconnectDelay: 5000,
+ heartbeatIncoming: 20000,
+ heartbeatOutgoing: 20000
+ });
+
+ stomp.current.connect(
+ {},
+ () => {
+ //console.log('Websocket connected');
+ setIsConnected(true);
+ },
+ (error) => {
+ //console.error('WebSocket Connection Error:', error);
+ setIsConnected(false);
+ setTimeout(connectWebSocket, 5000);
+ }
+ );
+ };
+
+ const subscribeTo = (topic) => {
+ const subscription = stomp.current.subscribe(
+ topic,
+ (message) => {
+ try {
+ const notification = JSON.parse(message.body);
+ setNotifications(prev => [notification, ...prev]);
+ } catch (error) {
+ console.error('Error parsing notification:', error);
+ }
+ }
+ );
+
+ setCurrentSubscription(subscription);
+ }
+
+ useEffect(() => {
+ fetchMessages();
+ }, [chosenCompanyId, userData.id, isConnected]);
+
+ useEffect(() => {
+ connectWebSocket();
+
+ return () => {
+ if (currentSubscription) {
+ currentSubscription.unsubscribe();
+ setCurrentSubscription(null);
+ }
+
+ if (stomp.current) {
+ stomp.current.disconnect(() => {
+ //console.log('WebSocket Disconnected');
+ });
+ }
+ };
+ }, []);
+
+ return (
+ <>
+ setNotificationsVisible(true)}>
+ o.status === 'UNREAD').length}>
+
+ setNotificationsVisible(false)}>
+
+
+ {loading
+ ?
+
+
+ : !isEmpty(chosenMsg)
+ ?
+ : (notifications.length > 0
+ ?
+ {notifications.map(o => )}
+
+ :
+
+ {__('Vuoto', 'gepafin')}
+
)}
+
+
+ {loading
+ ?
+
+
+ : !isEmpty(chosenMsg)
+ ?
+ : (notificationsRead.length > 0
+ ?
+ {notificationsRead.map(o => )}
+
+ :
+
+
+ {__('Vuoto', 'gepafin')}
+
)}
+
+
+
+ >
+ )
+}
+
+export default NotificationsSidebar;
diff --git a/src/helpers/getStrippedHtmlBodyTags.js b/src/helpers/getStrippedHtmlBodyTags.js
new file mode 100644
index 0000000..59c4315
--- /dev/null
+++ b/src/helpers/getStrippedHtmlBodyTags.js
@@ -0,0 +1,28 @@
+import parse from 'html-react-parser';
+import DOMPurify from 'dompurify';
+
+const getEmailTemplateForSoccorso = (content = '', fallback = '') => {
+ const config = {
+ FORBID_TAGS: ['html', 'body'],
+ WHOLE_DOCUMENT: false,
+ RETURN_DOM: false,
+ RETURN_DOM_FRAGMENT: false,
+ RETURN_DOM_IMPORT: false,
+ FORCE_BODY: false,
+ ADD_TAGS: ['*'],
+ ADD_ATTR: ['*']
+ };
+ try {
+ const wrappedHtml = `${content}
`;
+ const cleaned = DOMPurify.sanitize(wrappedHtml, config);
+
+ const tempDiv = document.createElement('div');
+ tempDiv.innerHTML = cleaned;
+ return parse(tempDiv.innerHTML);
+ } catch (error) {
+ console.error('DOMPurify cleaning error:', error);
+ return fallback;
+ }
+}
+
+export default getEmailTemplateForSoccorso;
\ No newline at end of file
diff --git a/src/helpers/set404FromErrorResponse.js b/src/helpers/set404FromErrorResponse.js
index abcd70a..1830c19 100644
--- a/src/helpers/set404FromErrorResponse.js
+++ b/src/helpers/set404FromErrorResponse.js
@@ -1,4 +1,4 @@
-import { storeSet } from '../store';
+//import { storeSet } from '../store';
const set404FromErrorResponse = (data) => {
if (data && data.status === 'NOT_FOUND') {
diff --git a/src/layouts/DefaultLayout/components/AppTopbar/index.js b/src/layouts/DefaultLayout/components/AppTopbar/index.js
index af1e9a8..8bb75b4 100644
--- a/src/layouts/DefaultLayout/components/AppTopbar/index.js
+++ b/src/layouts/DefaultLayout/components/AppTopbar/index.js
@@ -8,9 +8,9 @@ import LogoIcon from '../../../../icons/LogoIcon';
import { IconField } from 'primereact/iconfield';
import { InputIcon } from 'primereact/inputicon';
import { InputText } from 'primereact/inputtext';
-import { Badge } from 'primereact/badge';
import { Button } from 'primereact/button';
import TopBarProfileMenu from '../../../../components/TopBarProfileMenu';
+import NotificationsSidebar from '../../../../components/NotificationsSidebar';
const AppTopbar = () => {
const menuLeft = useRef(null);
@@ -24,14 +24,13 @@ const AppTopbar = () => {
-
-
-
+
{/*
*/}