Files
bflows-bandi-fe/src/components/NotificationsSidebar/index.js
BFLOWS Sandbox 8888e0326d feat(rendicontazione): editor schema con form strutturato + dashboard + integrazione microservizio
- Aggiunta voce 'Rendicontazione' in AppSidebar (id 21, icon pi-receipt)
- Nuova pagina RendicontazioneHome: dashboard con tabella bandi + stato schema
  (Non creato / Bozza / Pubblicato) + azioni Crea/Modifica per ciascuno
- Nuova pagina BandoRendicontazioneSchemaEdit: form strutturato 6 sezioni
  (importi/periodo, IVA, categorie, ULA, documenti, regole gate) con
  salva bozza + pubblica, read-only dopo pubblicazione
- Nuovo service modules/rendicontazione/service/rendicontazioneService.js
  (client fetch verso rendicontazione-api, JWT dallo store Zustand)
- 2 nuove route /rendicontazione e /bandi/:id/rendicontazione-schema
  (gate su ROLE_SUPER_ADMIN)
- Bottone 'Schema rendicontazione' aggiunto in BandoEdit come shortcut
- Patch NotificationsSidebar per disabilitare WSS se REACT_APP_ENABLE_WEBSOCKET=0
  (evita errori CORS in sandbox senza RabbitMQ)

UI coerente col codebase: appPage/appPageSection/appForm/appForm__cols/
fieldsRepeater, p-fluid per width input, h1+p in header con border-left
2026-04-18 09:37:08 +02:00

343 lines
13 KiB
JavaScript

import React, { useCallback, 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, useStoreValue } 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';
import PaginatorBasic from '../PaginatorBasic';
const socketUrl = process.env.REACT_APP_API_ADDRESS_WS;
const NotificationsSidebar = () => {
const chosenCompanyId = useStoreValue('chosenCompanyId');
const userData = useStoreValue('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);
const [currentPage, setCurrentPage] = useState(1);
const [totalRecordsNum, setTotalRecordsNum] = useState(0);
const [totalPagesNum, setTotalPagesNum] = useState(0);
const perPage = 10;
// Handle tab change
const handleTabChange = (e) => {
if (e.index === activeIndex) {
return
}
setTotalRecordsNum(0);
setTotalPagesNum(0);
setChosenMsg({});
setActiveIndex(e.index);
setCurrentPage(1);
};
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 getPaginationQuery = (status = 'UNREAD', curPage = 1) => {
return {
'globalFilters': {
'page': curPage,
'limit': perPage,
'sortBy': {
'columnName': 'id',
'sortDesc': true
}
},
'status': [
status
]
}
}
const fetchMessages = useCallback((status = 'UNREAD') => {
const chosenCompanyId = storeGet('chosenCompanyId');
const userData = storeGet('userData');
const role = pathOr('', ['role', 'roleType'], userData);
const bodyParams = getPaginationQuery(status, currentPage);
if (currentSubscription) {
currentSubscription.unsubscribe();
setCurrentSubscription(null);
}
if (userData.id && chosenCompanyId !== 0 && role === 'ROLE_BENEFICIARY') {
setLoading(true);
NotificationService.getNotificationsByCompanyIdPagination(
userData.id,
chosenCompanyId,
bodyParams,
status === 'UNREAD' ? getNotificationsPagi : getNotificationsReadPagi,
errGetNotifications
);
if (isConnected && socket.current) {
subscribeTo(`/topic/notifications_user_${userData.id}_company_${chosenCompanyId}`)
}
} else if (userData.id && role !== 'ROLE_BENEFICIARY') {
setLoading(true);
NotificationService.getNotificationsPagination(
userData.id,
bodyParams,
status === 'UNREAD' ? getNotificationsPagi : getNotificationsReadPagi,
errGetNotifications
);
if (isConnected && socket.current) {
subscribeTo(`/topic/notifications_user_${userData.id}`)
}
}
}, [currentPage]);
const getNotificationsPagi = (resp) => {
if (resp.status === 'SUCCESS') {
const { body, totalRecords, currentPage, totalPages } = resp.data;
setNotifications(body);
setTotalRecordsNum(totalRecords);
setTotalPagesNum(totalPages);
if (currentPage > totalPages) {
setCurrentPage(totalPages);
}
}
set404FromErrorResponse(resp);
setLoading(false);
}
const getNotificationsReadPagi = (resp) => {
if (resp.status === 'SUCCESS') {
const { body, totalRecords, currentPage, totalPages } = resp.data;
setNotificationsRead(body);
setTotalRecordsNum(totalRecords);
setTotalPagesNum(totalPages);
if (currentPage > totalPages) {
setCurrentPage(totalPages);
}
}
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);
}
setTotalRecordsNum(totalRecordsNum - 1);
}
set404FromErrorResponse(resp);
}
const makeReadErrorCallback = (resp) => {
set404FromErrorResponse(resp);
}
const connectWebSocket = () => {
// BFLOWS: consenti di disabilitare WSS via env (sandbox senza RabbitMQ)
if (process.env.REACT_APP_ENABLE_WEBSOCKET === '0') {
return;
}
socket.current = new SockJS(socketUrl, null, {
transports: [
'websocket',
'xhr-streaming',
'xhr-polling'
]
}
);
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);
}
const onPageChange = (num) => {
setCurrentPage(num);
};
useEffect(() => {
if (userData && userData.id) {
fetchMessages();
}
}, [chosenCompanyId, userData.id, isConnected]);
useEffect(() => {
if (0 === activeIndex) {
fetchMessages();
} else {
fetchMessages('READ');
}
}, [currentPage, activeIndex]);
useEffect(() => {
connectWebSocket();
return () => {
if (currentSubscription) {
currentSubscription.unsubscribe();
setCurrentSubscription(null);
}
if (stomp.current) {
stomp.current.disconnect(() => {
//console.log('WebSocket Disconnected');
});
}
};
}, []);
return (
<>
<i className="pi pi-bell p-overlay-badge topBar__icon notificationsIcon"
onClick={() => setNotificationsVisible(true)}>
<Badge value={totalRecordsNum}></Badge>
</i>
<Sidebar
className="notificationsSidebar"
position="left"
visible={notificationsVisible}
onHide={() => setNotificationsVisible(false)}>
<TabView activeIndex={activeIndex} onTabChange={handleTabChange}>
<TabPanel header={__('Da leggere', 'gepafin')}>
{loading
? <div className="notificationsSidebar__loading">
<i className="pi pi-spin pi-spinner" style={{ fontSize: '2rem' }}></i>
</div>
: !isEmpty(chosenMsg)
? <NotificationItemChosen
item={chosenMsg}
closeFn={closeChosenMsg}
markReadFn={makeNotificationRead}/>
: (notifications.length > 0
? <>
<ul className="notificationsSidebar__list">
{notifications.map(o => <NotificationItem
key={o.id}
item={o}
clickFn={chooseNotification}/>)}
</ul>
<PaginatorBasic
totalPages={totalPagesNum}
currentPage={currentPage}
clickFn={onPageChange}
/>
</>
: <div className="notificationsSidebar__loading">
<i className="pi pi-megaphone" style={{ fontSize: '2rem' }}></i>
{__('Vuoto', 'gepafin')}
</div>)}
</TabPanel>
<TabPanel header={__('Letti', 'gepafin')}>
{loading
? <div className="notificationsSidebar__loading">
<i className="pi pi-spin pi-spinner" style={{ fontSize: '2rem' }}></i>
</div>
: !isEmpty(chosenMsg)
? <NotificationItemChosen
item={chosenMsg}
closeFn={closeChosenMsg}
markReadFn={makeNotificationRead}/>
: (notificationsRead.length > 0
? <>
<ul className="notificationsSidebar__list">
{notificationsRead.map(o => <NotificationItem
key={o.id}
item={o}
clickFn={chooseNotification}/>)}
</ul>
<PaginatorBasic
totalPages={totalPagesNum}
currentPage={currentPage}
clickFn={onPageChange}
/>
</>
:
<div className="notificationsSidebar__loading">
<i className="pi pi-megaphone" style={{ fontSize: '2rem' }}></i>
{__('Vuoto', 'gepafin')}
</div>)}
</TabPanel>
</TabView>
</Sidebar>
</>
)
}
export default NotificationsSidebar;