From b9b4e4c859bd8face05c8d89d7f0c914d9e84a04 Mon Sep 17 00:00:00 2001 From: "MOCH. PASHA ARDYAN PUTRA" Date: Sun, 29 Jun 2025 07:43:53 +0000 Subject: [PATCH] ok --- package-lock.json | 50 +++++ package.json | 1 + src/App.js | 26 ++- src/Dashboard.js | 271 +++++++++++++++++++++++ src/Dashboard.module.css | 459 +++++++++++++++++++++++++++++++++++++++ src/Login.js | 84 +++++++ src/Login.module.css | 73 +++++++ 7 files changed, 962 insertions(+), 2 deletions(-) create mode 100644 src/Dashboard.js create mode 100644 src/Dashboard.module.css create mode 100644 src/Login.js create mode 100644 src/Login.module.css diff --git a/package-lock.json b/package-lock.json index d078af7..2f7fa4e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@testing-library/user-event": "^13.5.0", "react": "^19.1.0", "react-dom": "^19.1.0", + "react-router-dom": "^7.6.2", "react-scripts": "5.0.1", "tesseract.js": "^6.0.1", "web-vitals": "^2.1.4" @@ -12944,6 +12945,50 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.2.tgz", + "integrity": "sha512-U7Nv3y+bMimgWjhlT5CRdzHPu2/KVmqPwKUCChW8en5P3znxUqwlYFlbmyj8Rgp1SF6zs5X4+77kBVknkg6a0w==", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.6.2.tgz", + "integrity": "sha512-Q8zb6VlTbdYKK5JJBLQEN06oTUa/RAbG/oQS1auK1I0TbJOXktqm+QENEVJU6QvWynlXPRBXI3fiOQcSEA78rA==", + "dependencies": { + "react-router": "7.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-router/node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "engines": { + "node": ">=18" + } + }, "node_modules/react-scripts": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", @@ -13781,6 +13826,11 @@ "node": ">= 0.8.0" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", diff --git a/package.json b/package.json index 67be00c..4ab9a48 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "@testing-library/user-event": "^13.5.0", "react": "^19.1.0", "react-dom": "^19.1.0", + "react-router-dom": "^7.6.2", "react-scripts": "5.0.1", "tesseract.js": "^6.0.1", "web-vitals": "^2.1.4" diff --git a/src/App.js b/src/App.js index 11ef573..cee21b1 100644 --- a/src/App.js +++ b/src/App.js @@ -1,11 +1,33 @@ -import logo from "./logo.svg"; import "./App.css"; + +import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; + +import Dashboard from "./Dashboard"; + +import Login from "./Login"; import CameraKtp from "./KTPScanner"; +import "./App.css"; + +// ✅ Komponen proteksi route +const ProtectedRoute = ({ element }) => { + const token = localStorage.getItem("token"); + return token ? element : ; +}; + function App() { return (
- + + + } /> + } /> + } />} + /> + +
); } diff --git a/src/Dashboard.js b/src/Dashboard.js new file mode 100644 index 0000000..ae2ffda --- /dev/null +++ b/src/Dashboard.js @@ -0,0 +1,271 @@ +import React, { useState, useRef, useEffect } from "react"; +import styles from "./Dashboard.module.css"; +import { useNavigate } from "react-router-dom"; +// Pastikan Anda sudah menginstal Recharts: npm install recharts +// import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts'; // Contoh Recharts + +const Dashboard = () => { + const navigate = useNavigate(); + const [isMenuOpen, setIsMenuOpen] = useState(false); + const menuRef = useRef(null); + + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [selectedRole, setSelectedRole] = useState("officer"); + const [successMessage, setSuccessMessage] = useState(""); + const [errorMessage, setErrorMessage] = useState(""); + const [user, setUser] = useState({}); + const [totalFilesSentToday, setTotalFilesSentToday] = useState(0); + const [totalFilesSentMonth, setTotalFilesSentMonth] = useState(0); + const [totalFilesSentOverall, setTotalFilesSentOverall] = useState(0); + const [officerPerformanceData, setOfficerPerformanceData] = useState([]); + + useEffect(() => { + const token = localStorage.getItem("token"); + if (!token) { + window.location.href = "/login"; + } + }, []); + + useEffect(() => { + const verifyTokenAndFetchData = async () => { + const token = localStorage.getItem("token"); + if (!token) { + window.location.href = "/login"; + return; + } + + try { + const response = await fetch( + "https://bot.kediritechnopark.com/webhook/dashboard/psi", + { + method: "GET", + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); + + const data = await response.json(); + + if (!response.ok || !data[0].payload.username) { + throw new Error("Unauthorized"); + } + + setUser(data[0].payload); + + // Pastikan API Anda mengembalikan data ini, contoh: + // data[0].payload.stats = { today: 120, month: 2500, overall: 15000 }; + // data[0].payload.officerPerformance = [{ name: "Budi", filesSent: 50 }, { name: "Ani", filesSent: 70 }]; + + if (data[0].payload.stats) { + setTotalFilesSentToday(data[0].payload.stats.today); + setTotalFilesSentMonth(data[0].payload.stats.month); + setTotalFilesSentOverall(data[0].payload.stats.overall); + } + + if (data[0].payload.officerPerformance) { + setOfficerPerformanceData(data[0].payload.officerPerformance); + } + + } catch (error) { + console.error("Token tidak valid:", error.message); + localStorage.removeItem("token"); + window.location.href = "/login"; + } + }; + + verifyTokenAndFetchData(); + }, []); + + const handleLogout = () => { + localStorage.removeItem("token"); + localStorage.removeItem("user"); + localStorage.removeItem("role"); + window.location.reload(); + }; + + const handleAddOfficer = async (e) => { + e.preventDefault(); + + const token = localStorage.getItem("token"); + + try { + const response = await fetch( + "https://bot.kediritechnopark.com/webhook/add-officer", + { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + username, + password, + role: selectedRole, + }), + } + ); + + const data = await response.json(); + + if (!response.ok || data.success === false) { + throw new Error(data.message || "Gagal menambahkan officer"); + } + + setSuccessMessage("Officer berhasil ditambahkan"); + setUsername(""); + setPassword(""); + setSelectedRole("officer"); + setErrorMessage(""); + // Pertimbangkan untuk memuat ulang data performa jika penambahan officer baru mempengaruhi grafik + } catch (error) { + setErrorMessage(error.message || "Gagal menambahkan officer"); + setSuccessMessage(""); + } + }; + + useEffect(() => { + const handleClickOutside = (event) => { + if (menuRef.current && !menuRef.current.contains(event.target)) { + setIsMenuOpen(false); + } + }; + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + return ( +
+
+
+ Bot Avatar +

PSI Dashboard

+
+ +
+ {user.username || "Guest"} + + {isMenuOpen && ( +
+ + +
+ )} +
+
+ +
+ {/* Summary Cards */} +
+
+

Total Hari Ini

+

{totalFilesSentToday}

+
+
+

Total Bulan Ini

+

{totalFilesSentMonth}

+
+
+

Total Keseluruhan

+

{totalFilesSentOverall}

+
+
+ + {/* Grid for Form (Admin) and Chart (Admin & Officer) */} +
+ {user.role === "admin" && ( /* Render form hanya jika admin */ +
+

Tambah Officer Baru

+
+ + + + +
+ + {successMessage &&

{successMessage}

} + {errorMessage &&

{errorMessage}

} +
+ )} + + {/* Chart Section - Visible to both Admin and Officer */} +
+

Performa Pengiriman File Petugas

+ {officerPerformanceData.length > 0 ? ( + // Contoh implementasi Recharts: + /* + + + + + + + + + */ +
+ Grafik performa petugas akan ditampilkan di sini. + (Integrasikan library grafik seperti Recharts/Chart.js) +
+ ) : ( +

Tidak ada data performa petugas untuk ditampilkan.

+ )} +
+
+
+ +
© 2025 Kediri Technopark
+
+ ); +}; + +export default Dashboard; \ No newline at end of file diff --git a/src/Dashboard.module.css b/src/Dashboard.module.css new file mode 100644 index 0000000..90b2f5f --- /dev/null +++ b/src/Dashboard.module.css @@ -0,0 +1,459 @@ +/* Variabel Warna */ +:root { + --primary-red: #E53935; + --secondary-red: #EF5350; + --dark-red: #C62828; + --light-gray: #EEEEEE; + --dark-gray: #424242; + --white: #FFFFFF; + --success-green: #4CAF50; + --warning-yellow: #FFC107; + --text-color-light: #F5F5F5; /* Teks terang di latar belakang gelap */ + --text-color-dark: #212121; /* Teks gelap di latar belakang terang */ +} + +/* Base Styles & Reset */ +* { + box-sizing: border-box; /* Pastikan padding dan border tidak menambah lebar elemen */ +} + +body { + margin: 0; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; /* Font lebih modern */ + line-height: 1.6; + color: var(--text-color-dark); +} + +.dashboardContainer { + background-color: var(--light-gray); + min-height: 100vh; + display: flex; + flex-direction: column; +} + +/* --- Header --- */ +.dashboardHeader { + background-color: var(--primary-red); + color: var(--text-color-light); + padding: 1rem; /* Menggunakan rem */ + display: flex; + justify-content: space-between; + align-items: center; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); +} + +.logoAndTitle { + display: flex; + align-items: center; + flex-shrink: 0; /* Mencegah logo dan judul mengecil terlalu banyak */ +} + +.logoAndTitle img { + width: 2.5rem; /* Ukuran rem */ + height: 2.5rem; + border-radius: 50%; + margin-right: 0.8rem; +} + +.dashboardHeader .h1 { + margin: 0; + font-size: 1.5rem; /* Ukuran rem untuk mobile */ + font-weight: bold; + white-space: nowrap; /* Mencegah judul patah baris */ +} + +/* Dropdown Menu */ +.dropdownContainer { + position: relative; + display: flex; + align-items: center; + gap: 0.8rem; + flex-shrink: 0; /* Mencegah mengecil */ +} + +.userDisplayName { + color: var(--text-color-light); + font-weight: bold; + font-size: 0.9rem; + white-space: nowrap; /* Pastikan nama pengguna tidak patah baris */ +} + +.dropdownToggle { + background-color: var(--dark-red); + color: var(--text-color-light); + border: none; + padding: 0.5rem 0.75rem; /* Padding rem */ + border-radius: 0.3rem; + cursor: pointer; + font-size: 1rem; + transition: background-color 0.3s ease; + min-width: 3rem; /* Pastikan tombol tidak terlalu kecil */ +} + +.dropdownToggle:hover { + background-color: var(--secondary-red); +} + +.dropdownMenu { + position: absolute; + top: calc(100% + 0.5rem); /* Posisikan di bawah tombol dengan jarak */ + right: 0; + background-color: var(--white); + border-radius: 0.3rem; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); + z-index: 10; + display: flex; + flex-direction: column; + min-width: 8rem; /* Lebar minimum */ + overflow: hidden; /* Pastikan item tidak keluar */ +} + +.dropdownItem { + background: none; + border: none; + padding: 0.6rem 0.8rem; + text-align: left; + cursor: pointer; + color: var(--text-color-dark); + transition: background-color 0.3s ease; + font-size: 0.9rem; + white-space: nowrap; /* Mencegah item menu patah baris */ +} + +.dropdownItem:hover { + background-color: var(--light-gray); +} + +/* --- Main Content --- */ +.mainContent { + flex-grow: 1; + padding: 1.25rem; /* Padding rem */ + display: flex; + flex-direction: column; + gap: 1.5rem; /* Jarak antar bagian utama */ +} + +/* Summary Cards Container */ +.summaryCardsContainer { + display: flex; + flex-wrap: wrap; /* Mengizinkan kartu melipat */ + gap: 1rem; /* Jarak antar kartu */ + justify-content: center; /* Tengahkan kartu di mobile */ + padding: 1rem; + background-color: var(--white); + border-radius: 0.6rem; + box-shadow: 0 2px 5px rgba(0,0,0,0.05); +} + +.summaryCard { + background-color: var(--light-gray); + color: var(--text-color-dark); + padding: 1rem; + border-radius: 0.5rem; + border: 1px solid var(--secondary-red); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + text-align: center; + flex: 1 1 calc(50% - 0.75rem); /* Di layar mobile, 2 kartu per baris */ + min-width: 120px; /* Batasan agar tidak terlalu kecil */ + max-width: 250px; /* Batasan agar tidak terlalu lebar di mobile */ +} + +.summaryCard h3 { + margin-top: 0; + font-size: 0.9rem; + color: var(--dark-red); +} + +.summaryCard p { + font-size: 1.6rem; + font-weight: bold; + color: var(--dark-gray); + margin-bottom: 0; +} + +/* Dashboard Grid for Form and Chart */ +.dashboardGrid { + display: grid; + grid-template-columns: 1fr; /* Default: satu kolom untuk mobile */ + gap: 1.5rem; /* Jarak antar bagian */ + flex-grow: 1; /* Agar grid bisa membesar mengisi sisa ruang */ +} + +.formSection, +.chartSection { + background-color: var(--white); + padding: 1.5rem; /* Padding rem */ + border-radius: 0.6rem; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); +} + +.formSection h2, +.chartSection h2 { + color: var(--dark-red); + margin-top: 0; + margin-bottom: 1.25rem; + text-align: center; + font-size: 1.4rem; +} + +.form label { + display: block; + margin-bottom: 0.8rem; + color: var(--text-color-dark); + font-weight: bold; + font-size: 0.95rem; +} + +.form input[type="text"], +.form input[type="password"], +.form select { + width: 100%; + padding: 0.75rem; + margin-top: 0.3rem; + border: 1px solid var(--light-gray); + border-radius: 0.3rem; + font-size: 1rem; + transition: border-color 0.3s ease, box-shadow 0.3s ease; +} + +.form input[type="text"]:focus, +.form input[type="password"]:focus, +.form select:focus { + border-color: var(--primary-red); + box-shadow: 0 0 0 3px rgba(229, 57, 53, 0.2); /* Ring focus */ + outline: none; +} + +.submitButton { + background-color: var(--primary-red); + color: var(--white); + border: none; + padding: 0.8rem 1.5rem; + border-radius: 0.5rem; + cursor: pointer; + font-size: 1rem; + font-weight: bold; + margin-top: 1.25rem; + width: 100%; + transition: background-color 0.3s ease, transform 0.2s ease; +} + +.submitButton:hover { + background-color: var(--secondary-red); + transform: translateY(-2px); /* Efek sedikit naik */ +} + +/* Messages */ +.success { + background-color: #e6ffe6; + color: var(--success-green); + border: 1px solid var(--success-green); + padding: 0.8rem; + border-radius: 0.3rem; + margin-top: 1rem; + text-align: center; + font-size: 0.9rem; +} + +.error { + background-color: #ffe6e6; + color: var(--primary-red); + border: 1px solid var(--primary-red); + padding: 0.8rem; + border-radius: 0.3rem; + margin-top: 1rem; + text-align: center; + font-size: 0.9rem; +} + +.warning { + background-color: #fffbe6; + color: var(--warning-yellow); + border: 1px solid var(--warning-yellow); + padding: 1rem; + border-radius: 0.3rem; + margin-top: 1.25rem; + text-align: center; + font-weight: bold; + font-size: 1rem; +} + +/* Footer */ +.footer { + background-color: var(--dark-red); + color: var(--text-color-light); + text-align: center; + padding: 0.75rem; + margin-top: auto; + font-size: 0.85rem; +} + +.chartPlaceholder { + background-color: var(--light-gray); + height: 20rem; /* Menggunakan rem */ + display: flex; + justify-content: center; + align-items: center; + color: var(--dark-gray); + font-style: italic; + border-radius: 0.6rem; + border: 1px dashed var(--secondary-red); + font-size: 1rem; +} + +/* --- Media Queries for Tablets and Desktops --- */ + +/* Tablet-sized screens and up */ +@media (min-width: 768px) { + .dashboardHeader { + padding: 1rem 2rem; + } + + .logoAndTitle img { + width: 3rem; + height: 3rem; + } + + .dashboardHeader .h1 { + font-size: 1.8rem; + } + + .userDisplayName { + font-size: 1rem; + } + + .dropdownToggle { + padding: 0.6rem 1rem; + font-size: 1rem; + } + + .dropdownMenu { + min-width: 10rem; + } + + .dropdownItem { + font-size: 1rem; + } + + .mainContent { + padding: 1.5rem 2.5rem; /* Padding lebih besar di tablet/desktop */ + gap: 2rem; + } + + .summaryCardsContainer { + justify-content: flex-start; /* Sejajarkan ke kiri di tablet/desktop */ + padding: 1.5rem; + } + + .summaryCard { + flex: 1 1 calc(33.33% - 1rem); /* 3 kartu per baris di tablet */ + max-width: 200px; /* Batasi lebar kartu agar tetap rapi */ + } + + .summaryCard h3 { + font-size: 1rem; + } + + .summaryCard p { + font-size: 2rem; + } + + .dashboardGrid { + grid-template-columns: 1fr 2fr; /* Dua kolom: form (1/3), chart (2/3) */ + gap: 2rem; + } + + .formSection, + .chartSection { + padding: 2rem; + } + + .formSection h2, + .chartSection h2 { + font-size: 1.6rem; + } + + .form label { + font-size: 1rem; + } + + .form input[type="text"], + .form input[type="password"], + .form select { + padding: 0.8rem; + font-size: 1rem; + } + + .submitButton { + padding: 1rem 1.8rem; + font-size: 1.1rem; + } + + .warning { + font-size: 1rem; + } + + .chartPlaceholder { + height: 25rem; /* Tinggi placeholder lebih besar di tablet/desktop */ + } +} + +/* Desktop-sized screens and up */ +@media (min-width: 1024px) { + .dashboardHeader { + padding: 1.5rem 3rem; + } + + .logoAndTitle img { + width: 3.5rem; + height: 3.5rem; + } + + .dashboardHeader .h1 { + font-size: 2.2rem; + } + + .mainContent { + padding: 2rem 4rem; + gap: 2.5rem; + } + + .summaryCardsContainer { + padding: 2rem; + } + + .summaryCard { + flex: 0 0 auto; /* Ukuran tetap di desktop */ + min-width: 180px; + } + + .dashboardGrid { + gap: 2.5rem; + } + + .formSection, + .chartSection { + padding: 2.5rem; + } + + .formSection h2, + .chartSection h2 { + font-size: 1.8rem; + } + + .submitButton { + padding: 1.1rem 2rem; + } + + .chartPlaceholder { + height: 30rem; + } +} + +/* Jika hanya officer, pastikan chart mengambil seluruh lebar kolom */ +/* Ini sebenarnya sudah ditangani oleh grid secara default, tapi bisa diperjelas */ +@media (min-width: 768px) { + .dashboardGrid > *:only-child { /* Jika hanya ada satu anak elemen di dalam dashboardGrid */ + grid-column: 1 / -1; /* Ambil seluruh lebar dari kolom pertama hingga terakhir */ + } +} \ No newline at end of file diff --git a/src/Login.js b/src/Login.js new file mode 100644 index 0000000..fd879d3 --- /dev/null +++ b/src/Login.js @@ -0,0 +1,84 @@ +import React, { useState } from "react"; +import styles from "./Login.module.css"; + +const Login = () => { + const [formData, setFormData] = useState({ + username: "", + password: "", + }); + + const [error, setError] = useState(""); + + const handleChange = (e) => { + setFormData({ ...formData, [e.target.name]: e.target.value }); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + + try { + const loginResponse = await fetch( + "https://bot.kediritechnopark.com/webhook/login/psi", + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(formData), + } + ); + + const loginDataRaw = await loginResponse.json(); + const loginData = Array.isArray(loginDataRaw) + ? loginDataRaw[0] + : loginDataRaw; + + if (loginData?.success && loginData?.token) { + localStorage.setItem("token", loginData.token); + window.location.href = "/dashboard"; + } else { + setError(loginData?.message || "Username atau password salah"); + } + } catch (err) { + console.error("Login Error:", err); + setError("Gagal terhubung ke server"); + } + }; + + return ( +
+
+ Logo +

Dermalounge AI Admin Login

+

+ Silakan masuk untuk melanjutkan ke dashboard +

+
+ + + {error &&

{error}

} + +
+
© 2025 Kediri Technopark
+
+
+ ); +}; + +export default Login; diff --git a/src/Login.module.css b/src/Login.module.css new file mode 100644 index 0000000..3ee3f1f --- /dev/null +++ b/src/Login.module.css @@ -0,0 +1,73 @@ +.loginContainer { + display: flex; + align-items: center; + justify-content: center; + height: 100vh; + background: #f0f2f5; + width: 100vw; +} + +.loginBox { + background: #fff; + padding: 40px; + border-radius: 10px; + box-shadow: 0 4px 25px rgba(0, 0, 0, 0.1); + text-align: center; + width: 100%; + max-width: 400px; +} + +.logo { + width: 80px; + margin-bottom: 20px; +} + +.h1 { + margin-bottom: 10px; + font-size: 24px; + color: #333; +} + +.subtitle { + font-size: 14px; + color: #777; + margin-bottom: 30px; +} + +.form { + display: flex; + flex-direction: column; +} + +.input { + padding: 10px 15px; + margin-bottom: 15px; + border: 1px solid #ccc; + border-radius: 6px; + font-size: 16px; +} + +.button { + background-color: #337f83; + color: white; + border: none; + padding: 12px; + border-radius: 6px; + font-size: 16px; + cursor: pointer; +} + +.button:hover { + background-color: #3c9a9f; +} + +.error { + color: red; + margin-bottom: 10px; +} + +.footer { + margin-top: 20px; + font-size: 12px; + color: #aaa; +}