- 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
343 lines
13 KiB
JavaScript
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;
|