414 lines
12 KiB
JavaScript
414 lines
12 KiB
JavaScript
import React, { useRef, useState, useEffect } from 'react';
|
|
import styles from './Dashboard.module.css';
|
|
import { useNavigate } from 'react-router-dom';
|
|
|
|
import Modal from './Modal';
|
|
import Conversations from './Conversations';
|
|
import DiscussedTopics from './DiscussedTopics';
|
|
|
|
import Chart from 'chart.js/auto';
|
|
import NotificationPrompt from './NotificationPrompt';
|
|
|
|
const Dashboard = () => {
|
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
|
|
|
const chartRef = useRef(null);
|
|
const chartInstanceRef = useRef(null);
|
|
const [conversations, setConversations] = useState([]);
|
|
const [discussedTopics, setDiscussedTopics] = useState([]);
|
|
const [modalContent, setModalContent] = useState(null);
|
|
const [rawData, setRawData] = useState([]);
|
|
const [loading, setLoading] = useState(true); // ⬅️ Tambahkan state loading
|
|
const [checkOnce, setCheckOnce] = useState(false); // ⬅️ Tambahkan state loading
|
|
|
|
const [stats, setStats] = useState({
|
|
totalChats: 0,
|
|
userMessages: 0,
|
|
botMessages: 0,
|
|
});
|
|
|
|
const [isDragging, setIsDragging] = useState(false);
|
|
const [selectedFile, setSelectedFile] = useState(null);
|
|
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
|
|
|
const navigate = useNavigate();
|
|
|
|
const handleFile = (file) => {
|
|
if (file) {
|
|
setSelectedFile(file);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
const token = localStorage.getItem('token');
|
|
setIsLoggedIn(!!token);
|
|
}, []);
|
|
|
|
const handleLogout = () => {
|
|
localStorage.removeItem('token');
|
|
localStorage.removeItem('user');
|
|
|
|
navigator.serviceWorker.ready.then(function (registration) {
|
|
registration.pushManager.getSubscription().then(function (subscription) {
|
|
if (subscription) {
|
|
subscription.unsubscribe().then(function (successful) {
|
|
console.log('Push subscription unsubscribed on logout:', successful);
|
|
// Optional: also notify backend to clear the token
|
|
fetch('/api/clear-subscription', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ endpoint: subscription.endpoint }),
|
|
});
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
window.location.reload();
|
|
};
|
|
|
|
const menuRef = useRef(null);
|
|
|
|
// Close dropdown if click outside
|
|
useEffect(() => {
|
|
const handleClickOutside = (event) => {
|
|
if (menuRef.current && !menuRef.current.contains(event.target)) {
|
|
setIsMenuOpen(false);
|
|
}
|
|
};
|
|
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
return () => {
|
|
document.removeEventListener('mousedown', handleClickOutside);
|
|
};
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const fetchData = async () => {
|
|
const token = localStorage.getItem('token');
|
|
|
|
try {
|
|
const response = await fetch('https://bot.kediritechnopark.com/webhook/profile', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${token}`,
|
|
},
|
|
});
|
|
|
|
if (response.status === 401 || response.status === 403) {
|
|
handleLogout();
|
|
return;
|
|
}
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Fetch gagal dengan status: ' + response.status);
|
|
}
|
|
|
|
const data = await response.json();
|
|
console.log(data);
|
|
setDiscussedTopics(data?.result?.topics)
|
|
|
|
const graphObj = data.result.graph;
|
|
const rawDataArray = Object.entries(graphObj).map(([hour, sesi]) => ({
|
|
hour,
|
|
sesi,
|
|
}));
|
|
setRawData(rawDataArray);
|
|
let totalSessions = new Set();
|
|
let userMessages = 0;
|
|
let botMessages = 0;
|
|
|
|
rawDataArray.forEach(({ sesi }) => {
|
|
Object.values(sesi).forEach(messages => {
|
|
messages.forEach(msg => {
|
|
totalSessions.add(msg.session_id);
|
|
if (msg.message.type === 'human') userMessages++;
|
|
if (msg.message.type === 'ai') botMessages++;
|
|
});
|
|
});
|
|
});
|
|
|
|
setStats({
|
|
totalChats: totalSessions.size,
|
|
userMessages,
|
|
botMessages,
|
|
});
|
|
|
|
setLoading(false); // ⬅️ Setelah berhasil, hilangkan loading
|
|
} catch (error) {
|
|
console.error('Error:', error);
|
|
navigate('/login');
|
|
}
|
|
};
|
|
|
|
if (!checkOnce && 'serviceWorker' in navigator) {
|
|
navigator.serviceWorker.ready.then(function (registration) {
|
|
registration.pushManager.getSubscription().then(function (subscription) {
|
|
setCheckOnce(false);
|
|
if (subscription === null) {
|
|
// Not subscribed yet — show modal asking user to subscribe
|
|
setModalContent(<NotificationPrompt onAllow={subscribeUser} onDismiss={null} />);
|
|
} else {
|
|
// Already subscribed
|
|
setModalContent('')
|
|
console.log('User is already subscribed.');
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
fetchData(); // Jalankan langsung saat komponen di-mount
|
|
const interval = setInterval(fetchData, 30000); // Jalankan setiap 30 detik
|
|
return () => clearInterval(interval); // Bersihkan interval saat komponen unmount
|
|
|
|
}, [navigate]);
|
|
|
|
const subscribeUser = async () => {
|
|
const registration = await navigator.serviceWorker.register('/sw.js', {
|
|
scope: '/',
|
|
});
|
|
|
|
const subscription = await registration.pushManager.subscribe({
|
|
userVisibleOnly: true,
|
|
applicationServerKey: urlBase64ToUint8Array('BPT-ypQB0Z7HndmeFhRR7AMjDujCLSbOQ21VoVHLQg9MOfWhEZ7SKH5cMjLqkXHl2sTuxdY2rjHDOAxhRK2G2K4'),
|
|
});
|
|
|
|
const token = localStorage.getItem('token');
|
|
|
|
await fetch('https://bot.kediritechnopark.com/webhook/subscribe', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
subscription, // ← push subscription object
|
|
}),
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${token}`,
|
|
},
|
|
});
|
|
|
|
setModalContent('')
|
|
};
|
|
|
|
function urlBase64ToUint8Array(base64String) {
|
|
const padding = '='.repeat((4 - base64String.length % 4) % 4);
|
|
const base64 = (base64String + padding).replace(/\-/g, '+').replace(/_/g, '/');
|
|
const rawData = atob(base64);
|
|
return Uint8Array.from([...rawData].map(char => char.charCodeAt(0)));
|
|
}
|
|
|
|
const openConversationsModal = () => {
|
|
setModalContent(<Conversations conversations={conversations} />);
|
|
};
|
|
|
|
const openTopicsModal = () => {
|
|
setModalContent(<DiscussedTopics topics={discussedTopics} />);
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (!rawData.length) return;
|
|
|
|
const ctx = chartRef.current?.getContext('2d');
|
|
if (!ctx) return;
|
|
|
|
if (chartInstanceRef.current) {
|
|
chartInstanceRef.current.destroy();
|
|
}
|
|
|
|
const prefixLabelMap = {
|
|
WEB: 'Web App',
|
|
TGG: 'Telegram',
|
|
WGG: 'Whatsapp',
|
|
};
|
|
|
|
const prefixColors = {
|
|
WEB: { border: '#4285F4', background: 'rgba(66, 133, 244, 0.2)' },
|
|
TGG: { border: '#25D366', background: 'rgba(37, 211, 102, 0.2)' },
|
|
WGG: { border: '#AA00FF', background: 'rgba(170, 0, 255, 0.2)' },
|
|
};
|
|
|
|
const prefixes = Object.keys(prefixLabelMap);
|
|
const hours = rawData.map(d => d.hour.split(' ')[1]).sort((a, b) => {
|
|
// Sort berdasarkan jam dan menit
|
|
const [h1, m1] = a.split(':').map(Number);
|
|
const [h2, m2] = b.split(':').map(Number);
|
|
return h1 !== h2 ? h1 - h2 : m1 - m2;
|
|
});
|
|
|
|
const counts = {};
|
|
prefixes.forEach(prefix => {
|
|
counts[prefix] = hours.map(() => 0);
|
|
});
|
|
|
|
rawData.forEach(({ sesi }, index) => {
|
|
prefixes.forEach(prefix => {
|
|
if (Array.isArray(sesi[prefix])) {
|
|
counts[prefix][index] = sesi[prefix].length;
|
|
}
|
|
});
|
|
});
|
|
|
|
const datasets = prefixes.map(prefix => ({
|
|
label: prefixLabelMap[prefix],
|
|
data: counts[prefix],
|
|
borderColor: prefixColors[prefix].border,
|
|
backgroundColor: prefixColors[prefix].background,
|
|
fill: true,
|
|
tension: 0.3,
|
|
}));
|
|
|
|
chartInstanceRef.current = new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: hours,
|
|
datasets,
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
plugins: {
|
|
legend: {
|
|
display: true,
|
|
position: 'bottom',
|
|
},
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
title: {
|
|
display: true,
|
|
text: 'Jumlah Pesan',
|
|
},
|
|
},
|
|
x: {
|
|
title: {
|
|
display: true,
|
|
text: 'Jam',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
}, [rawData]);
|
|
|
|
// ⬇️ Jika masih loading, tampilkan full white screen
|
|
if (loading) {
|
|
return <div style={{ backgroundColor: 'white', width: '100vw', height: '100vh' }} />;
|
|
}
|
|
|
|
return (
|
|
<div className={styles.dashboardContainer}>
|
|
<div className={styles.dashboardHeader}>
|
|
{isLoggedIn ? (
|
|
|
|
<div className={styles.dropdownContainer} ref={menuRef}> {/* ✅ Pindahkan ref ke sini */}
|
|
<button
|
|
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
|
className={styles.dropdownToggle}
|
|
>
|
|
☰ Menu
|
|
</button>
|
|
|
|
{isMenuOpen && (
|
|
<div className={styles.dropdownMenu}>
|
|
<button onClick={() => navigate('/reset-password')} className={styles.dropdownItem}>
|
|
Ganti Password
|
|
</button>
|
|
<button onClick={handleLogout} className={styles.dropdownItem}>
|
|
Logout
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
) : (
|
|
<a href="/login" className={styles.loginButton}>Login</a>
|
|
)}
|
|
<img src="/dermalounge.jpg" alt="Bot Avatar" />
|
|
<div>
|
|
<h1 className={styles.h1}>Dermalounge AI Admin Dashboard</h1>
|
|
</div>
|
|
</div>
|
|
|
|
<div className={styles.statsGrid}>
|
|
<div className={styles.statCard} onClick={openConversationsModal}>
|
|
<h2>{stats.totalChats}</h2>
|
|
<p>Total Percakapan selama 24 jam</p>
|
|
</div>
|
|
<div className={styles.statCard}>
|
|
<h2>{stats.userMessages}</h2>
|
|
<p>Pesan dari Pengguna</p>
|
|
</div>
|
|
<div className={styles.statCard}>
|
|
<h2>{stats.botMessages}</h2>
|
|
<p>Respons Bot</p>
|
|
</div>
|
|
<div className={styles.statCard} onClick={openTopicsModal}>
|
|
<h2 style={{ fontSize: '17px' }}>{discussedTopics[0]?.topic}</h2>
|
|
<p>Paling sering ditanyakan</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className={styles.chartSection}>
|
|
<h2 className={styles.chartTitle}>Grafik Interaksi</h2>
|
|
<canvas ref={chartRef}></canvas>
|
|
</div>
|
|
|
|
<div className={styles.chartSection}>
|
|
<h2 className={styles.chartTitle}>Update data</h2>
|
|
|
|
<div
|
|
className={`${styles.uploadContainer} ${isDragging ? styles.dragActive : ""}`}
|
|
onClick={() => selectedFile ? null : document.getElementById("fileInput").click()}
|
|
onDragOver={(e) => {
|
|
e.preventDefault();
|
|
setIsDragging(true);
|
|
}}
|
|
onDragLeave={() => setIsDragging(false)}
|
|
onDrop={(e) => {
|
|
e.preventDefault();
|
|
setIsDragging(false);
|
|
const file = e.dataTransfer.files[0];
|
|
handleFile(file);
|
|
}}
|
|
>
|
|
<p className={styles.desktopText}>
|
|
Seret file ke sini, atau <span className={styles.uploadLink}>Klik untuk unggah</span>
|
|
</p>
|
|
<p className={styles.mobileText}>Klik untuk unggah</p>
|
|
|
|
{selectedFile && (
|
|
<>
|
|
<div className={styles.fileInfo}>
|
|
<strong>{selectedFile.name}</strong>
|
|
</div>
|
|
<div className={styles.fileInfoClose} onClick={() => setSelectedFile(null)}>
|
|
X
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
<input
|
|
id="fileInput"
|
|
type="file"
|
|
style={{ display: "none" }}
|
|
onChange={(e) => {
|
|
const file = e.target.files[0];
|
|
handleFile(file);
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className={styles.footer}>
|
|
© 2025 Kediri Technopark
|
|
</div>
|
|
|
|
{modalContent && <Modal onClose={() => setModalContent(null)}>{modalContent}</Modal>}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Dashboard;
|