This commit is contained in:
Vassshhh
2025-06-30 15:52:57 +07:00
parent 17b0653184
commit bcd53de641
6 changed files with 468 additions and 85 deletions

View File

@@ -5,6 +5,7 @@ html, body, #root {
.App { .App {
height: 100%; height: 100%;
overflow: auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background: #fff; background: #fff;

View File

@@ -1,19 +1,20 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import styles from './ChatBot.module.css'; import styles from './ChatBot.module.css';
const ChatBot = ({ existingConversation, readOnly, hh }) => { const ChatBot = ({ existingConversation }) => {
const [messages, setMessages] = useState([ const [messages, setMessages] = useState([
{ {
sender: 'bot', sender: 'bot',
text: 'Hai Dermalovers! 👋 Saya siap membantu anda tampil lebih percaya diri. Ada pertanyaan seputar perawatan kulit atau kecantikan hari ini?', text: 'Hai! Saya Alle, asisten virtual Dermalounge Clinic. Ada yang bisa Alle bantu hari ini?',
time: getTime(), time: getTime(),
quickReplies: [ quickReplies: [
'Konsultasi estetik', 'Konsultasi Estetik',
'Konsultasi kulit dan kelamin' 'Konsultasi Kulit & Kelamin'
], ],
}, },
]); ]);
const [input, setInput] = useState(''); const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@@ -231,8 +232,7 @@ function formatBoldText(text) {
</div> </div>
<div <div
className={styles.quickReply} className={styles.nextButton}
style={{ color: name.length > 2 && phoneNumber.length >= 10 ? 'black' : '#ccc' }}
onClick={() => { onClick={() => {
if (name.length > 2 && phoneNumber.length >= 10) { if (name.length > 2 && phoneNumber.length >= 10) {
const sessionData = JSON.parse(localStorage.getItem('session')) || {}; const sessionData = JSON.parse(localStorage.getItem('session')) || {};

View File

@@ -152,16 +152,30 @@
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.quickReply:hover { /* .quickReply:hover {
background: #075e54; background: #075e54;
color: white; color: white;
border-color: #075e54; border-color: #075e54;
} } */
.quickReply:hover::placeholder { /* .quickReply:hover::placeholder {
color: white;
} */
.nextButton {
background: #075e54;
border: 1px solid #ccc;
padding: 8px 14px;
border-radius: 20px;
font-size: 13px;
cursor: pointer;
transition: all 0.2s ease;
color: white; color: white;
} }
.nextButton:hover::placeholder {
color: #ccc;
}
.quickReply2 { .quickReply2 {
width: 100%; width: 100%;
background: #fff; background: #fff;
@@ -174,13 +188,13 @@
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.quickReply2:hover { /* .quickReply2:hover {
background: #075e54; background: #075e54;
color: white; color: white;
border-color: #075e54; border-color: #075e54;
border: 1px solid #075e54; border: 1px solid #075e54;
} } */
.quickReply2:hover::placeholder { /* .quickReply2:hover::placeholder {
color: white; color: white;
} } */

View File

@@ -22,6 +22,9 @@ const Dashboard = () => {
const [modalContent, setModalContent] = useState(null); const [modalContent, setModalContent] = useState(null);
const [rawData, setRawData] = useState([]); const [rawData, setRawData] = useState([]);
const [loading, setLoading] = useState(true); // ⬅️ Tambahkan state loading const [loading, setLoading] = useState(true); // ⬅️ Tambahkan state loading
const [fileList, setFileList] = useState([]);
const [selectedKeys, setSelectedKeys] = useState([]);
const [stats, setStats] = useState({ const [stats, setStats] = useState({
totalChats: 0, totalChats: 0,
@@ -29,17 +32,21 @@ const Dashboard = () => {
}); });
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
const [selectedFile, setSelectedFile] = useState(null); const [selectedFiles, setSelectedFiles] = useState([]);
const navigate = useNavigate(); const navigate = useNavigate();
const handleFile = (file) => { const handleFiles = (files) => {
if (file) { const newFiles = files.filter(file => {
setSelectedFile(file); // Hindari duplikat berdasarkan nama (atau bisa pakai hash/md5 jika perlu)
} return !selectedFiles.some(f => f.name === file.name);
});
setSelectedFiles((prev) => [...prev, ...newFiles]);
}; };
const handleLogout = () => { const handleLogout = () => {
localStorage.removeItem('token'); localStorage.removeItem('token');
localStorage.removeItem('user'); localStorage.removeItem('user');
@@ -90,16 +97,17 @@ const Dashboard = () => {
return; return;
} }
if (!response.ok) { // if (!response.ok) {
throw new Error('Fetch gagal dengan status: ' + response.status); // throw new Error('Fetch gagal dengan status: ' + response.status);
} // }
const data = await response.json(); const data = await response.json();
console.log(data); console.log(data);
setDiscussedTopics(data?.result?.topics) setDiscussedTopics(data?.graph[0]?.json?.result?.topics)
setFollowUps(data?.result?.interested_users) setFollowUps(data?.graph[0]?.json?.result?.interested_users)
setFileList(data?.files)
const graphObj = data.result.graph; const graphObj = data?.graph[0]?.json?.result?.graph;
console.log(graphObj)
const rawDataArray = Object.entries(graphObj).map(([hour, sesi]) => ({ const rawDataArray = Object.entries(graphObj).map(([hour, sesi]) => ({
hour, hour,
sesi, sesi,
@@ -144,51 +152,51 @@ const Dashboard = () => {
}, [navigate]); }, [navigate]);
useEffect(() => { useEffect(() => {
if ('serviceWorker' in navigator) { if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(async (registration) => { navigator.serviceWorker.ready.then(async (registration) => {
const subscription = await registration.pushManager.getSubscription(); const subscription = await registration.pushManager.getSubscription();
if (!subscription) { if (!subscription) {
// Belum subscribe → tampilkan prompt // Belum subscribe → tampilkan prompt
setModalContent( setModalContent(
<NotificationPrompt <NotificationPrompt
onAllow={subscribeUser} onAllow={subscribeUser}
onDismiss={() => setModalContent('')} onDismiss={() => setModalContent('')}
/> />
); );
} else { } else {
// Sudah subscribe → tidak perlu panggil subscribeUser lagi // Sudah subscribe → tidak perlu panggil subscribeUser lagi
console.log('User is already subscribed.'); console.log('User is already subscribed.');
setModalContent(''); setModalContent('');
subscribeUser(); subscribeUser();
} }
});
}
}, []);
const subscribeUser = async () => {
setModalContent('');
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array('BPT-ypQB0Z7HndmeFhRR7AMjDujCLSbOQ21VoVHLQg9MOfWhEZ7SKH5cMjLqkXHl2sTuxdY2rjHDOAxhRK2G2K4'),
}); });
}
}, []);
const token = localStorage.getItem('token');
const subscribeUser = async () => { await fetch('https://bot.kediritechnopark.com/webhook/subscribe', {
setModalContent(''); method: 'POST',
const registration = await navigator.serviceWorker.ready; body: JSON.stringify({ subscription }),
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
});
const subscription = await registration.pushManager.subscribe({ setModalContent('');
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 }),
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
});
setModalContent('');
};
function urlBase64ToUint8Array(base64String) { function urlBase64ToUint8Array(base64String) {
@@ -298,6 +306,109 @@ const subscribeUser = async () => {
}); });
}, [rawData]); }, [rawData]);
const handleDeleteFile = async (key) => {
if (!window.confirm(`Yakin ingin menghapus "${key}"?`)) return;
const token = localStorage.getItem('token');
try {
await fetch("https://bot.kediritechnopark.com/webhook/files/delete", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`,
},
body: JSON.stringify({ key }),
});
// fetchFiles(); // Refresh list
} catch (error) {
console.error("Gagal menghapus file:", error);
}
};
const handleBatchDownload = async () => {
const token = localStorage.getItem('token');
for (const key of selectedKeys) {
try {
const response = await fetch(
`https://bot.kediritechnopark.com/webhook/files/download?key=${encodeURIComponent(key)}`,
{
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`
}
}
);
if (!response.ok) throw new Error('Gagal download');
const blob = await response.blob();
// Coba ambil nama file dari header Content-Disposition (jika tersedia)
let filename = key;
const disposition = response.headers.get('Content-Disposition');
if (disposition && disposition.includes('filename=')) {
const match = disposition.match(/filename="?(.+?)"?$/);
if (match) filename = match[1];
}
// Buat URL dan download
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
window.URL.revokeObjectURL(url);
} catch (err) {
console.error(`Gagal download ${key}:`, err);
}
}
};
const handleBatchUpload = async () => {
const token = localStorage.getItem('token');
for (const file of selectedFiles) {
const formData = new FormData();
formData.append('file', file); // Kirim satu per satu file
const response = await fetch('https://bot.kediritechnopark.com/webhook/files/upload', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`
// ❗Jangan set 'Content-Type' untuk FormData, biarkan browser mengaturnya
},
body: formData
});
if (!response.ok) {
console.error(`Upload gagal untuk file ${file.name}`);
}
}
alert('Upload selesai');
setSelectedFiles([]); // Kosongkan setelah selesai upload
};
const handleBatchDelete = async () => {
if (!window.confirm(`Yakin ingin menghapus ${selectedKeys.length} file?`)) return;
const token = localStorage.getItem('token');
for (const key of selectedKeys) {
const response = await fetch(
`https://bot.kediritechnopark.com/webhook/files/delete?key=${encodeURIComponent(key)}`,
{
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`
}
}
);
}
setSelectedKeys([]);
};
// ⬇️ Jika masih loading, tampilkan full white screen // ⬇️ Jika masih loading, tampilkan full white screen
if (loading) { if (loading) {
return <div style={{ backgroundColor: 'white', width: '100vw', height: '100vh' }} />; return <div style={{ backgroundColor: 'white', width: '100vw', height: '100vh' }} />;
@@ -354,53 +465,126 @@ const subscribeUser = async () => {
<h2 className={styles.chartTitle}>Interactions</h2> <h2 className={styles.chartTitle}>Interactions</h2>
<canvas ref={chartRef}></canvas> <canvas ref={chartRef}></canvas>
</div> </div>
<div className={styles.chartSection}> <div className={styles.chartSection}>
<h2 className={styles.chartTitle}>Update data</h2> <h2 className={styles.chartTitle}>Update data</h2>
{/* ✅ TOMBOL AKSI */}
{selectedKeys.length > 0 && (
<div className={styles.actionBar}>
<button onClick={handleBatchDownload}> Download</button>
<button onClick={handleBatchDelete}>🗑 Hapus</button>
</div>
)}
{/* ✅ AREA UPLOAD */}
<div <div
className={`${styles.uploadContainer} ${isDragging ? styles.dragActive : ""}`} className={`${styles.uploadContainer} ${isDragging ? styles.dragActive : ""}`}
onClick={() => selectedFile ? null : document.getElementById("fileInput").click()}
onDragOver={(e) => { onDragOver={(e) => {
e.preventDefault(); e.preventDefault();
setIsDragging(true); setIsDragging(true);
}} }}
onDragLeave={() => setIsDragging(false)} onDragLeave={() => setIsDragging(false)}
onDrop={(e) => { onDrop={(e) => {
e.preventDefault(); e.preventDefault();
setIsDragging(false); setIsDragging(false);
const file = e.dataTransfer.files[0]; const files = Array.from(e.dataTransfer.files);
handleFile(file); handleFiles(files);
}} }}
> >
{/* ✅ TABEL FILE */}
<table className={styles.fileTable}>
<thead>
<tr>
<th>
<input
type="checkbox"
checked={fileList.length > 0 && selectedKeys.length === fileList.length}
onChange={(e) => {
if (e.target.checked) {
setSelectedKeys(fileList.map((f) => f.json.Key));
} else {
setSelectedKeys([]);
}
}}
/>
</th>
<th>select all</th>
</tr>
</thead>
<tbody>
{fileList.map((file, index) => (
<tr key={index}>
<td>
<input
type="checkbox"
checked={selectedKeys.includes(file.json.Key)}
onChange={() => {
setSelectedKeys((prev) =>
prev.includes(file.json.Key)
? prev.filter((key) => key !== file.json.Key)
: [...prev, file.json.Key]
);
}}
/>
</td>
<td>{file.json.Key}</td>
</tr>
))}
</tbody>
</table>
<p className={styles.desktopText}> <p className={styles.desktopText}>
Drop file here, or <span className={styles.uploadLink}>Click to upload</span> Drop file here, or <span onClick={() => document.getElementById("fileInput").click()} className={styles.uploadLink}>Click to upload</span>
</p> </p>
<p className={styles.mobileText}>Click to upload</p> <p className={styles.mobileText} onClick={() => document.getElementById("fileInput").click()}>Click to upload</p>
{selectedFile && (
<>
<div className={styles.fileInfo}>
<strong>{selectedFile.name}</strong>
</div>
<div className={styles.fileInfoClose} onClick={() => setSelectedFile(null)}>
X
</div>
</>
)}
<div>
{selectedFiles.length > 0 &&
selectedFiles.map((file, index) => (
<div>
<div key={index} className={styles.fileInfo}>
<strong>{file.name}</strong>
</div>
<div
className={styles.fileInfoClose}
onClick={() =>
setSelectedFiles((prev) => prev.filter((_, i) => i !== index))
}
>
X
</div>
</div>
))}
{selectedFiles.length > 0 &&
<div>
<div onClick={()=>handleBatchUpload()} className={styles.fileUpload}>
<strong>Upload</strong>
</div>
</div>
}
</div>
<input <input
id="fileInput" id="fileInput"
type="file" type="file"
multiple
style={{ display: "none" }} style={{ display: "none" }}
onChange={(e) => { onChange={(e) => {
const file = e.target.files[0]; const files = Array.from(e.target.files);
handleFile(file); handleFiles(files);
}} }}
/> />
</div> </div>
</div> </div>
<div className={styles.footer}> <div className={styles.footer}>
&copy; 2025 Kediri Technopark &copy; 2025 Kediri Technopark
</div> </div>

View File

@@ -4,13 +4,14 @@
} }
.uploadContainer { .uploadContainer {
overflow: hidden;
margin-top: 16px; margin-top: 16px;
border: 2px dashed #ccc; border: 2px dashed #ccc;
border-radius: 12px; border-radius: 12px;
padding: 32px;
text-align: center; text-align: center;
cursor: pointer; cursor: pointer;
transition: border-color 0.3s; transition: border-color 0.3s;
padding-bottom: 30px;
} }
.uploadContainer:hover { .uploadContainer:hover {
@@ -96,6 +97,7 @@
.desktopText { .desktopText {
display: block; display: block;
/* padding: 32px; */
} }
.mobileText { .mobileText {
@@ -115,10 +117,12 @@
padding: 8px 12px; padding: 8px 12px;
border-radius: 8px 0 0 8px; border-radius: 8px 0 0 8px;
display: inline-block; display: inline-block;
margin-bottom: 10px;
} }
.fileInfoClose { .fileInfoClose {
margin-left: 2px; margin-left: 2px;
margin-right: 6px;
margin-top: 16px; margin-top: 16px;
font-size: 14px; font-size: 14px;
color: #ff0000; color: #ff0000;
@@ -130,6 +134,17 @@
} }
.fileUpload {
margin-top: 16px;
font-size: 14px;
color: white;
background: #2bb438;
padding: 8px 12px;
border-radius: 8px;
display: inline-block;
margin-bottom: 10px;
}
/* Mobile styles */ /* Mobile styles */
@media (max-width: 768px) { @media (max-width: 768px) {
.h1 { .h1 {
@@ -243,3 +258,41 @@ position: absolute;
.dropdownItem:hover { .dropdownItem:hover {
background-color: #f0f0f0; background-color: #f0f0f0;
} }
.fileTable {
width: 100%;
border-collapse: collapse;
margin-bottom: 40px;
}
.fileTable th,
.fileTable td {
border-bottom: 1px solid #ccc;
padding: 10px;
text-align: left;
}
.fileTable th {
background-color: #f2f2f2;
}
.actionBar {
margin-bottom: 12px;
display: flex;
gap: 10px;
}
.actionBar button {
padding: 6px 12px;
background-color: #00adef;
color: white;
font-weight: bold;
border: none;
border-radius: 6px;
cursor: pointer;
}
.actionBar button:hover {
background-color: #0095cc;
}

131
src/FileManager.js Normal file
View File

@@ -0,0 +1,131 @@
// import React, { useEffect, useState } from 'react';
// const FileManager = ({ token }) => {
// const [files, setFiles] = useState([]);
// const [uploadFile, setUploadFile] = useState(null);
// const API_BASE = 'https://bot.kediritechnopark.com/webhook/files';
// // Ambil daftar file
// const fetchFiles = async () => {
// try {
// const response = await fetch(`${API_BASE}/list`, {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json',
// 'Authorization': `Bearer ${token}`,
// }
// });
// const data = await response.json();
// setFiles(data);
// } catch (error) {
// console.error('Gagal mengambil daftar file:', error);
// }
// };
// // Hapus file
// const deleteFile = async (key) => {
// if (!window.confirm(`Yakin ingin hapus "${key}"?`)) return;
// try {
// await fetch(`${API_BASE}/delete`, {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json',
// 'Authorization': `Bearer ${token}`,
// },
// body: JSON.stringify({ key }),
// });
// fetchFiles();
// } catch (error) {
// console.error('Gagal menghapus file:', error);
// }
// };
// // Upload file
// const handleUpload = async () => {
// if (!uploadFile) return;
// const formData = new FormData();
// formData.append('file', uploadFile);
// try {
// await fetch(`${API_BASE}/upload`, {
// method: 'POST',
// headers: {
// 'Authorization': `Bearer ${token}`,
// },
// body: formData,
// });
// setUploadFile(null);
// fetchFiles();
// } catch (error) {
// console.error('Gagal mengunggah file:', error);
// }
// };
// // Download file
// const downloadFile = async (key) => {
// try {
// const response = await fetch(`${API_BASE}/download?key=${encodeURIComponent(key)}`, {
// method: 'GET',
// headers: {
// 'Authorization': `Bearer ${token}`,
// },
// });
// if (!response.ok) throw new Error('Download gagal');
// const blob = await response.blob();
// const url = window.URL.createObjectURL(blob);
// const a = document.createElement('a');
// a.href = url;
// a.download = key;
// document.body.appendChild(a);
// a.click();
// a.remove();
// window.URL.revokeObjectURL(url);
// } catch (error) {
// console.error('Gagal mengunduh file:', error);
// }
// };
// useEffect(() => {
// fetchFiles();
// }, []);
// return (
// <div style={{ padding: 20, fontFamily: 'Arial' }}>
// <h2>📁 Manajemen File</h2>
// <ul>
// {files.map(file => (
// <li key={file.Key} style={{ marginBottom: 8 }}>
// <strong>{file.Key}</strong>{' '}
// <button onClick={() => downloadFile(file.Key)}>⬇️ Download</button>{' '}
// <button onClick={() => deleteFile(file.Key)}>🗑️ Hapus</button>
// </li>
// ))}
// </ul>
// <hr />
// <div>
// <h3>📤 Upload File</h3>
// <input
// type="file"
// onChange={e => setUploadFile(e.target.files[0])}
// />
// <button onClick={handleUpload} disabled={!uploadFile}>
// Upload
// </button>
// </div>
// </div>
// );
// };
// export default FileManager;