Compare commits

..

4 Commits

Author SHA1 Message Date
255c1e64c7 Merge pull request 'lavoce' (#40) from lavoce into main
Reviewed-on: #40
2026-01-20 04:32:42 +00:00
zain94rif
2b3d8ea3d2 feat(mqtt): update notification use websocket 2026-01-14 11:26:39 +07:00
zain94rif
c0fe3aaca1 fix(icon): change size & color icon svg 2026-01-13 17:04:50 +07:00
zain94rif
e8da716e8f feat(svg): add new icon svg open mail 2026-01-13 16:31:21 +07:00
2 changed files with 50 additions and 10 deletions

View File

@@ -10,7 +10,8 @@ const topics = [
'PIU_COD/COMPRESSOR/OVERVIEW', 'PIU_COD/COMPRESSOR/OVERVIEW',
'PIU_COD/COMPRESSOR/COMPRESSOR_A', 'PIU_COD/COMPRESSOR/COMPRESSOR_A',
'PIU_COD/COMPRESSOR/COMPRESSOR_B', 'PIU_COD/COMPRESSOR/COMPRESSOR_B',
'PIU_COD/COMPRESSOR/COMPRESSOR_C' 'PIU_COD/COMPRESSOR/COMPRESSOR_C',
'PIU_COD/ERROR_CODE/SIM',
]; ];
const options = { const options = {
keepalive: 30, keepalive: 30,
@@ -98,4 +99,22 @@ const setValSvg = (listenTopic, svg) => {
}); });
}; };
export { publishMessage, listenMessage, setValSvg }; // === NOTIFICATION LISTENER ===
const notifListeners = [];
const onNotifUpdate = (callback) => {
notifListeners.push(callback);
};
client.on('message', (topic, message) => {
if (topic === import.meta.env.VITE_MQTT_TOPIC_COD) {
try {
const payload = JSON.parse(message.toString());
notifListeners.forEach((cb) => cb(payload));
} catch (err) {
console.error('Invalid notif payload', err);
}
}
});
export { publishMessage, listenMessage, setValSvg, onNotifUpdate };

View File

@@ -36,7 +36,6 @@ import {
PlusOutlined, PlusOutlined,
ExclamationCircleOutlined, ExclamationCircleOutlined,
SearchOutlined, SearchOutlined,
MailFilled,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { useNavigate, Link as RouterLink } from 'react-router-dom'; import { useNavigate, Link as RouterLink } from 'react-router-dom';
import { import {
@@ -47,9 +46,20 @@ import {
resendChatAllUser, resendChatAllUser,
searchData, searchData,
} from '../../../api/notification'; } from '../../../api/notification';
import { onNotifUpdate } from '../../../components/Global/MqttConnection';
const { Text, Paragraph, Link: AntdLink } = Typography; const { Text, Paragraph, Link: AntdLink } = Typography;
const OpenMail = ({ size = 22, color = 'black' }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 640 640"
width={size}
height={size}
fill={color}
>
<path d="M576 480C576 515.3 547.5 544 512.1 544L128 544C92.6 544 64 515.3 64 480L64 228C64.1 212.5 71.8 198 84.5 189.2L270 61.3C300.1 40.6 339.8 40.6 369.9 61.3L555.5 189.2C568.3 198 575.9 212.5 576 228L576 480zM128 496L512.1 496C520.9 496 528 488.9 528 480L528 288.3L373.2 405.7C341.8 429.6 298.3 429.6 266.8 405.7L112 288.3L112 480C112 488.9 119.2 496 128 496zM527.6 228.4L342.7 100.8C329 91.4 311 91.4 297.3 100.8L112.4 228.4L295.8 367.5C310.1 378.3 329.9 378.3 344.2 367.5L527.6 228.4z" />
</svg>
);
// Transform API response to component format // Transform API response to component format
const transformNotificationData = (apiData) => { const transformNotificationData = (apiData) => {
return apiData.map((item, index) => ({ return apiData.map((item, index) => ({
@@ -92,6 +102,7 @@ const ListNotification = memo(function ListNotification(props) {
const [activeTab, setActiveTab] = useState('all'); const [activeTab, setActiveTab] = useState('all');
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [searchValue, setSearchValue] = useState(''); const [searchValue, setSearchValue] = useState('');
const [notifTrigger, setNotifTrigger] = useState(0);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [modalContent, setModalContent] = useState(null); // 'user', 'log', 'details', or null const [modalContent, setModalContent] = useState(null); // 'user', 'log', 'details', or null
const [isAddingLog, setIsAddingLog] = useState(false); const [isAddingLog, setIsAddingLog] = useState(false);
@@ -169,6 +180,12 @@ const ListNotification = memo(function ListNotification(props) {
fetchNotifications(page, pageSize, isReadFilter); fetchNotifications(page, pageSize, isReadFilter);
}; };
useEffect(() => {
onNotifUpdate(() => {
setNotifTrigger((prev) => prev + 1);
});
}, []);
useEffect(() => { useEffect(() => {
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');
if (!token) { if (!token) {
@@ -179,18 +196,18 @@ const ListNotification = memo(function ListNotification(props) {
// Fetch notifications on component mount and when tab changes // Fetch notifications on component mount and when tab changes
const isReadFilter = activeTab === 'read' ? 1 : activeTab === 'unread' ? 0 : null; const isReadFilter = activeTab === 'read' ? 1 : activeTab === 'unread' ? 0 : null;
fetchNotifications(pagination.current_page, pagination.current_limit, isReadFilter); fetchNotifications(pagination.current_page, pagination.current_limit, isReadFilter);
}, [activeTab]); }, [activeTab, notifTrigger]);
const getIconAndColor = (type) => { const getIconAndColor = (type) => {
switch (type) { switch (type) {
case 'critical': case 'critical':
return { IconComponent: MailFilled, color: '#faad14', bgColor: '#fff1f0' }; return { IconComponent: MailOutlined, color: '#faad14', bgColor: '#fff1f0' };
case 'warning': case 'warning':
return { IconComponent: MailFilled, color: '#1890ff', bgColor: '#fffbe6' }; return { IconComponent: MailOutlined, color: '#1890ff', bgColor: '#fffbe6' };
case 'resolved': case 'resolved':
return { IconComponent: MailFilled, color: '#52c41a', bgColor: '#f6ffed' }; return { IconComponent: MailOutlined, color: '#52c41a', bgColor: '#f6ffed' };
default: default:
return { IconComponent: MailFilled, color: '#1890ff', bgColor: '#e6f7ff' }; return { IconComponent: MailOutlined, color: '#1890ff', bgColor: '#e6f7ff' };
} }
}; };
@@ -407,7 +424,11 @@ const ListNotification = memo(function ListNotification(props) {
flexShrink: 0, flexShrink: 0,
}} }}
> >
<IconComponent style={{ fontSize: '22px' }} /> {notification.type === 'resolved' ? (
<OpenMail size={28.5} color={color} />
) : (
<IconComponent style={{ fontSize: '22px' }} />
)}
</div> </div>
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<Row align="top"> <Row align="top">