diff --git a/package-lock.json b/package-lock.json index 33602d0..b68dcac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "chart.js": "^4.4.9", "react": "^19.1.0", "react-dom": "^19.1.0", + "react-icons": "^5.5.0", "react-router-dom": "^7.6.1", "react-scripts": "5.0.1", "web-vitals": "^2.1.4" @@ -13955,6 +13956,15 @@ "integrity": "sha512-SN/U6Ytxf1QGkw/9ve5Y+NxBbZM6Ht95tuXNMKs8EJyFa/Vy/+Co3stop3KBHARfn/giv+Lj1uUnTfOJ3moFEQ==", "license": "MIT" }, + "node_modules/react-icons": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", diff --git a/package.json b/package.json index 38d2d30..3660990 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "chart.js": "^4.4.9", "react": "^19.1.0", "react-dom": "^19.1.0", + "react-icons": "^5.5.0", "react-router-dom": "^7.6.1", "react-scripts": "5.0.1", "web-vitals": "^2.1.4" diff --git a/src/App.js b/src/App.js index faa5b2a..0288c70 100644 --- a/src/App.js +++ b/src/App.js @@ -9,6 +9,7 @@ import ChatBot from './ChatBot'; import Login from './Login'; import './App.css'; +import ProfileTab from './ProfileTab'; function ChatBotWrapper() { const { agentId } = useParams(); @@ -56,6 +57,10 @@ function App() { path="/dashboard" element={} />} /> + } />} + /> diff --git a/src/Dashboard.js b/src/Dashboard.js index 3520c33..2b544b7 100644 --- a/src/Dashboard.js +++ b/src/Dashboard.js @@ -6,6 +6,8 @@ import Modal from './Modal'; import Conversations from './Conversations'; import DiscussedTopics from './DiscussedTopics'; +import FollowUps from './FollowUps'; + import Chart from 'chart.js/auto'; import NotificationPrompt from './NotificationPrompt'; @@ -15,6 +17,7 @@ const Dashboard = () => { const chartRef = useRef(null); const chartInstanceRef = useRef(null); const [conversations, setConversations] = useState([]); + const [followUps, setFollowUps] = useState([]); const [discussedTopics, setDiscussedTopics] = useState([]); const [modalContent, setModalContent] = useState(null); const [rawData, setRawData] = useState([]); @@ -28,7 +31,6 @@ const Dashboard = () => { const [isDragging, setIsDragging] = useState(false); const [selectedFile, setSelectedFile] = useState(null); - const [isLoggedIn, setIsLoggedIn] = useState(false); const navigate = useNavigate(); @@ -38,10 +40,6 @@ const Dashboard = () => { } }; - useEffect(() => { - const token = localStorage.getItem('token'); - setIsLoggedIn(!!token); - }, []); const handleLogout = () => { localStorage.removeItem('token'); @@ -89,7 +87,7 @@ const Dashboard = () => { const token = localStorage.getItem('token'); try { - const response = await fetch('https://bot.kediritechnopark.com/webhook/profile', { + const response = await fetch('https://bot.kediritechnopark.com/webhook/dashboard', { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -109,6 +107,7 @@ const Dashboard = () => { const data = await response.json(); console.log(data); setDiscussedTopics(data?.result?.topics) + setFollowUps(data?.result?.interested_users) const graphObj = data.result.graph; const rawDataArray = Object.entries(graphObj).map(([hour, sesi]) => ({ @@ -218,12 +217,14 @@ const Dashboard = () => { WEB: 'Web App', TGG: 'Telegram', WGG: 'Whatsapp', + IGG: 'Instagram', }; const prefixColors = { WEB: { border: '#e2b834', background: 'rgba(226,184,52,0.6)' }, TGG: { border: '#24A1DE', background: 'rgba(36,161,222,0.6)' }, WGG: { border: '#25d366', background: 'rgba(37,211,102,0.6)' }, + IGG: { border: '#d62976', background: 'rgba(214,41,118,0.6)' }, }; const prefixes = Object.keys(prefixLabelMap); @@ -273,6 +274,9 @@ const hours = parsedHours.map((date, index) => { legend: { display: true, position: 'bottom', + labels: { + boxWidth: 15 + } }, }, scales: { @@ -299,9 +303,7 @@ const hours = parsedHours.map((date, index) => { return (
- {isLoggedIn ? ( - -
{/* ✅ Pindahkan ref ke sini */} +
)}
- - ) : ( - Login - )} Bot Avatar

Dermalounge AI Admin Dashboard

@@ -339,8 +337,8 @@ const hours = parsedHours.map((date, index) => {

{stats.botMessages}

Respons Bot

-
-

{8}

+
setModalContent()}> +

{followUps.length}

Follow up

diff --git a/src/Dashboard.module.css b/src/Dashboard.module.css index 52dcf65..860f527 100644 --- a/src/Dashboard.module.css +++ b/src/Dashboard.module.css @@ -126,7 +126,10 @@ /* Mobile styles */ @media (max-width: 768px) { - +.h1 { + color: white; + font-size: 23px; +} .dashboardContainer { max-width: 900px; margin: 30px auto; @@ -192,11 +195,11 @@ position: absolute; .logoutButton:hover { background-color: #cb0f0f; } + .dropdownContainer { position: relative; display: inline-block; - -position: absolute; + position: absolute; top: 10px; right: 10px; } diff --git a/src/FollowUps.js b/src/FollowUps.js new file mode 100644 index 0000000..4fe0020 --- /dev/null +++ b/src/FollowUps.js @@ -0,0 +1,40 @@ +import React from 'react'; +import styles from './FollowUps.module.css'; + +const FollowUps = ({ data }) => { + return ( +
+

User yang tertarik

+
+ {data.map(user => ( +
+
+

{user.name}

+ + {new Date(user.created_at).toLocaleString('id-ID', { + dateStyle: 'medium', + timeStyle: 'short', + timeZone: 'Asia/Jakarta' + })} + +
+

{user.notes}

+
+ {user.contact_info} + + Chat WhatsApp + +
+
+ ))} +
+
+ ); +}; + +export default FollowUps; diff --git a/src/FollowUps.module.css b/src/FollowUps.module.css new file mode 100644 index 0000000..348a108 --- /dev/null +++ b/src/FollowUps.module.css @@ -0,0 +1,111 @@ +.container { + padding: 24px; + background-color: #f7f9fa; + font-family: 'Amazon Ember', sans-serif; +} + +.title { + font-size: 22px; + font-weight: 600; + color: #16191f; + margin-bottom: 20px; + text-align: center; +} + +.grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 16px; +} + +.card { + background-color: #ffffff; + border: 1px solid #d1d5db; + border-radius: 8px; + padding: 16px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + display: flex; + flex-direction: column; + justify-content: space-between; + transition: box-shadow 0.2s ease; +} + +.card:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); +} + +.header { + display: flex; + justify-content: space-between; + align-items: flex-start; + flex-wrap: wrap; + gap: 4px; +} + +.header h3 { + font-size: 18px; + margin: 0; + color: #232f3e; +} + +.date { + font-size: 12px; + color: #6b7280; + white-space: nowrap; +} + +.notes { + margin: 12px 0; + color: #374151; + font-size: 14px; +} + +.footer { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 8px; + margin-top: 8px; +} + +.contact { + font-size: 13px; + color: #374151; + word-break: break-all; +} + +.chatBtn { + background-color: #25d366; + color: #ffffff; + text-decoration: none; + padding: 6px 12px; + border-radius: 4px; + font-size: 13px; + font-weight: 500; + transition: background-color 0.2s ease; + white-space: nowrap; +} + +.chatBtn:hover { + background-color: #1da851; +} + +@media (max-width: 480px) { + .title { + font-size: 18px; + } + + .card { + padding: 12px; + } + + .notes { + font-size: 13px; + } + + .chatBtn { + padding: 6px 10px; + font-size: 12px; + } +} diff --git a/src/Login.js b/src/Login.js index 047c20e..d366719 100644 --- a/src/Login.js +++ b/src/Login.js @@ -30,24 +30,7 @@ const Login = () => { if (loginData?.success && loginData?.token) { localStorage.setItem('token', loginData.token); - - const profileResponse = await fetch('https://bot.kediritechnopark.com/webhook/profile', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${loginData.token}` - } - }); - - const profileDataRaw = await profileResponse.json(); - const profileData = Array.isArray(profileDataRaw) ? profileDataRaw[0] : profileDataRaw; - - if (profileData?.success) { - localStorage.setItem('user', JSON.stringify(profileData.user)); - window.location.href = '/dashboard'; - } else { - setError('Token tidak valid'); - } + window.location.href = '/dashboard'; } else { setError(loginData?.message || 'Username atau password salah'); } diff --git a/src/ProfileTab.js b/src/ProfileTab.js new file mode 100644 index 0000000..2bd8c7a --- /dev/null +++ b/src/ProfileTab.js @@ -0,0 +1,207 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { FaPen } from 'react-icons/fa'; +import styles from './ProfileTab.module.css'; +import { useNavigate } from 'react-router-dom'; + +const ProfileTab = () => { + const menuRef = useRef(null); + const navigate = useNavigate(); + const [isEditing, setIsEditing] = useState(false); + const [isMenuOpen, setIsMenuOpen] = useState(false); + + // 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); + }; + }, []); + + 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(); + }; + + useEffect(() => { + const fetchData = async () => { + const token = localStorage.getItem('token'); + + try { + const response = await fetch('https://bot.kediritechnopark.com/webhook/dashboard?profileOnly=true', { + 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); + + setProfile(data.profile_data); + } catch (error) { + console.error('Error:', error); + navigate('/login'); + } + }; + + fetchData(); // Jalankan langsung saat komponen di-mount + + }, [navigate]); + + const [profile, setProfile] = useState({ + name: "Rikolo", + company: "Dermalounge", + address: "Jl. Pahlawan No.123, Kediri", + email: "admin@dermalounge.com", + phone: "08123456789", + image: "/dermalounge.jpg" + }); + + const licenses = [ + { id: 1, type: "AI Bot License", number: "DL-2025-AI001", validUntil: "2026-12-31" }, + { id: 2, type: "Clinic Data Access", number: "DL-2025-CL002", validUntil: "2026-06-30" } + ]; + + const handleChange = (e) => { + const { name, value } = e.target; + setProfile((prev) => ({ ...prev, [name]: value })); + }; + const handleSave = async () => { + try { + const token = localStorage.getItem('token'); + + const response = await fetch('https://bot.kediritechnopark.com/webhook/profile', { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}`, + }, + body: JSON.stringify(profile), + }); + + if (!response.ok) throw new Error('Gagal menyimpan profil'); + + const result = await response.json(); + console.log('Profil berhasil diperbarui:', result); + + setIsEditing(false); + alert('Profil berhasil disimpan!'); + } catch (error) { + console.error('Error saat menyimpan profil:', error); + alert('Terjadi kesalahan saat menyimpan profil.'); + } + }; + + return ( +
+
+

Profil Perusahaan

+
+ + + {isMenuOpen && ( +
+ + + + + + + +
+ )} +
+
+ +
+ Company Logo +
+ {["name", "company", "address", "email", "phone"].map((field) => ( +
+ + + +
+ ))} + {isEditing && +
+ Simpan +
+ } +
+
+ +
+

License

+
+ {licenses.map((item) => ( +
+

{item.type}

+

No: {item.number}

+

Berlaku sampai: {item.validUntil}

+
+ ))} +
+
+
+ ); +}; + +export default ProfileTab; diff --git a/src/ProfileTab.module.css b/src/ProfileTab.module.css new file mode 100644 index 0000000..b14a068 --- /dev/null +++ b/src/ProfileTab.module.css @@ -0,0 +1,218 @@ +/* Container */ +.dashboardContainer { + width: 100%; + padding: 20px; + max-width: 1200px; + margin: 0 auto; + box-sizing: border-box; +} + +/* Header */ +.profileHeader { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + margin-bottom: 20px; + gap: 12px; +} + +.editButton { + background-color: #075e54; + color: white; + padding: 6px 12px; + font-size: 12px; + border: none; + border-radius: 8px; + display: flex; + align-items: center; + gap: 6px; + cursor: pointer; + transition: background-color 0.3s ease; +} + +.editButton:hover { + background-color: #0f9b8a; +} + +/* Profile Section */ +.profileSection { + display: flex; + flex-direction: row; + align-items: flex-start; + gap: 20px; + flex-wrap: wrap; + margin-bottom: 30px; +} + +.companyImage { + width: 100px; + height: 100px; + border-radius: 12px; + object-fit: cover; + flex-shrink: 0; +} + +.profileDetails { + flex: 1; + min-width: 260px; +} + +.profileDetails p { + margin: 4px 0; + font-size: 14px; +} + +/* License Section */ +.licenseSection { + margin-top: 20px; +} + +.licenseCards { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 16px; + margin-top: 10px; +} + +.licenseCard { + background: linear-gradient(135deg, #075e54, #128c7e); + color: white; + padding: 16px; + border-radius: 12px; + box-shadow: 0 4px 8px rgba(0,0,0,0.1); + font-size: 14px; +} + +/* ==================== + Responsive Breakpoints +==================== */ + +/* Tablet (768px – 1023px) */ +@media screen and (max-width: 1023px) { + .profileSection { + flex-direction: row; + justify-content: flex-start; + } + + .profileDetails { + min-width: 100%; + } + + .licenseCards { + grid-template-columns: repeat(2, 1fr); + } +} + +/* Mobile (≤ 767px) */ +@media screen and (max-width: 767px) { + .profileHeader { + flex-direction: column; + align-items: flex-start; + } + + .editButton { + align-self: flex-end; + } + + .profileSection { + flex-direction: column; + align-items: center; + text-align: center; + } + + .companyImage { + margin-bottom: 12px; + } + + .profileDetails { + text-align: left; + width: 100%; + } + + .licenseCards { + grid-template-columns: 1fr; + } + + .licenseCard { + font-size: 13px; + } +} +.profileInputGroup { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + margin-bottom: 10px; +} + +.profileInputGroup label { + min-width: 100px; /* atau sesuai label terpanjang */ + font-size: 14px; +} + +.editableInput { + font-size: 14px; + padding: 4px 8px; + border: 1px solid #ccc; + border-radius: 6px; + background-color: #f0f8ff; + transition: all 0.3s ease; + color: #333; + + width: auto; + max-width: 100%; + flex: 1; /* biar input bisa melar jika ruang tersedia */ +} + +/* Saat tidak dalam mode editing */ +.readOnly { + border-color: transparent; + background-color: transparent; + pointer-events: none; + color: #000; +} + + +.dropdownContainer { + position: relative; + display: inline-block; + position: absolute; + top: 37px; + right: 10px; +} + +.dropdownToggle { + color: #ffff; + background-color: #255e54; + padding: 8px 12px; + border: none; + border-radius: 4px; + cursor: pointer; +} + +.dropdownMenu { + position: absolute; + right: 0; + top: 100%; + background-color: white; + border: 1px solid #ccc; + min-width: 160px; + box-shadow: 0 8px 16px rgba(0,0,0,0.1); + z-index: 1; + border-radius: 4px; +} + +.dropdownItem { + padding: 10px 16px; + text-align: left; + width: 100%; + background: none; + border: none; + cursor: pointer; +} + +.dropdownItem:hover { + background-color: #f0f0f0; +} +