ok
This commit is contained in:
22
src/App.js
22
src/App.js
@@ -13,26 +13,6 @@ const ProtectedRoute = ({ element }) => {
|
||||
return token ? element : <Navigate to="/login" />;
|
||||
};
|
||||
|
||||
// Komponen redirect berdasarkan sessionStorage
|
||||
const HomeRedirect = () => {
|
||||
const token = localStorage.getItem("token");
|
||||
const hasOpen = sessionStorage.getItem("hasOpen");
|
||||
|
||||
if (!token) {
|
||||
return <Navigate to="/login" />;
|
||||
}
|
||||
|
||||
// Jika tidak ada sessionId (anggap sebagai session baru)
|
||||
if (!hasOpen) {
|
||||
sessionStorage.setItem("hasOpen", true);
|
||||
|
||||
return <Navigate to="/scan" />;
|
||||
}
|
||||
|
||||
// Jika sudah ada sessionId
|
||||
return <Navigate to="/dashboard" />;
|
||||
};
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="App">
|
||||
@@ -43,11 +23,11 @@ function App() {
|
||||
path="/dashboard"
|
||||
element={<ProtectedRoute element={<Dashboard />} />}
|
||||
/>
|
||||
<Route path="/" element={<ProtectedRoute element={<Dashboard />} />} />
|
||||
<Route
|
||||
path="/profile"
|
||||
element={<ProtectedRoute element={<Profile />} />}
|
||||
/>
|
||||
<Route path="/" element={<HomeRedirect />} />
|
||||
<Route path="/:nik" element={<ShowImage />} />
|
||||
</Routes>
|
||||
</div>
|
||||
|
||||
@@ -70,27 +70,28 @@ const Dashboard = () => {
|
||||
verifyTokenAndFetchData();
|
||||
}, []);
|
||||
|
||||
// Memisahkan fungsi fetchOfficers agar dapat dipanggil ulang
|
||||
const fetchOfficers = async () => {
|
||||
const token = localStorage.getItem("token");
|
||||
try {
|
||||
const response = await fetch(
|
||||
"https://bot.kediritechnopark.com/webhook/list-user/psi",
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
setOfficers(data);
|
||||
} catch (error) {
|
||||
console.error("Gagal memuat daftar officer:", error.message);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchOfficers = async () => {
|
||||
const token = localStorage.getItem("token");
|
||||
try {
|
||||
const response = await fetch(
|
||||
"https://bot.kediritechnopark.com/webhook/list-user/psi",
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
setOfficers(data);
|
||||
} catch (error) {
|
||||
console.error("Gagal memuat daftar officer:", error.message);
|
||||
}
|
||||
};
|
||||
|
||||
if (user.role == "admin") {
|
||||
fetchOfficers();
|
||||
}
|
||||
@@ -133,6 +134,9 @@ const Dashboard = () => {
|
||||
setUsername("");
|
||||
setPassword("");
|
||||
setErrorMessage("");
|
||||
|
||||
// Refresh daftar officer setelah berhasil menambahkan
|
||||
await fetchOfficers();
|
||||
} catch (error) {
|
||||
setErrorMessage(error.message || "Gagal menambahkan officer");
|
||||
setSuccessMessage("");
|
||||
@@ -148,6 +152,7 @@ const Dashboard = () => {
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, []);
|
||||
|
||||
const handleDeleteOfficer = async (id) => {
|
||||
const confirmDelete = window.confirm(
|
||||
"Apakah Anda yakin ingin menghapus petugas ini?"
|
||||
@@ -268,21 +273,39 @@ const Dashboard = () => {
|
||||
{user.role === "admin" && (
|
||||
<div className={styles.formSection}>
|
||||
<h2>Daftar Petugas</h2>
|
||||
<ul className={styles.officerList}>
|
||||
{officers.map((officer) => (
|
||||
<li key={officer.id} className={styles.officerItem}>
|
||||
👤 <strong>{officer.username}</strong> —{" "}
|
||||
<em>{officer.role}</em>
|
||||
<button
|
||||
onClick={() => handleDeleteOfficer(officer.id)}
|
||||
className={styles.deleteButton}
|
||||
title="Hapus Petugas"
|
||||
>
|
||||
❌
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className={styles.officerListContainer}>
|
||||
<div className={styles.officerList}>
|
||||
{officers.length > 0 ? (
|
||||
officers.map((officer) => (
|
||||
<div key={officer.id} className={styles.officerItem}>
|
||||
<div className={styles.officerInfo}>
|
||||
<span className={styles.officerIcon}>👤</span>
|
||||
<div className={styles.officerDetails}>
|
||||
<strong className={styles.officerName}>
|
||||
{officer.username}
|
||||
</strong>
|
||||
<span className={styles.officerRole}>
|
||||
{officer.role}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleDeleteOfficer(officer.id)}
|
||||
className={styles.deleteButton}
|
||||
title="Hapus Petugas"
|
||||
>
|
||||
❌
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className={styles.emptyState}>
|
||||
<span>📋</span>
|
||||
<p>Belum ada petugas terdaftar</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr className={styles.separator} />
|
||||
<h2>Tambah Petugas Baru</h2>
|
||||
|
||||
@@ -442,3 +442,159 @@ body {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
/* CSS untuk styling daftar petugas */
|
||||
|
||||
.officerListContainer {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.officerList {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
border-radius: 6px;
|
||||
background: white;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.officerItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.officerItem:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.officerItem:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.officerInfo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.officerIcon {
|
||||
font-size: 20px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.officerDetails {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.officerName {
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.officerRole {
|
||||
font-size: 12px;
|
||||
color: #6c757d;
|
||||
font-style: italic;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.deleteButton {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s ease;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.deleteButton:hover {
|
||||
background-color: #fee;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.deleteButton:focus {
|
||||
outline: 2px solid #dc3545;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.emptyState {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.emptyState span {
|
||||
font-size: 32px;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.emptyState p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.separator {
|
||||
border: none;
|
||||
border-top: 1px solid #dee2e6;
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
/* Custom scrollbar untuk daftar petugas */
|
||||
.officerList::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.officerList::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.officerList::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.officerList::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.officerItem {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.officerInfo {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.officerName {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.officerRole {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import styless from "./Dashboard.module.css";
|
||||
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import Modal from "./Modal";
|
||||
@@ -7,6 +9,8 @@ import PaginatedFormEditable from "./PaginatedFormEditable";
|
||||
const STORAGE_KEY = "camera_canvas_gallery";
|
||||
|
||||
const CameraCanvas = () => {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const menuRef = useRef(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const videoRef = useRef(null);
|
||||
@@ -489,30 +493,57 @@ const CameraCanvas = () => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
padding: "12px 16px",
|
||||
backgroundColor: "#f5f5f5",
|
||||
fontFamily: "sans-serif",
|
||||
fontSize: "16px",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
<button
|
||||
style={{
|
||||
marginRight: "12px",
|
||||
fontSize: "18px",
|
||||
background: "none",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => navigate("/dashboard")}
|
||||
>
|
||||
<
|
||||
</button>
|
||||
<div>Scan KTP atau unggah</div>
|
||||
<div className={styless.dashboardHeader}>
|
||||
<div className={styless.logoAndTitle}>
|
||||
<img src="/PSI.png" alt="Bot Avatar" />
|
||||
<h1 className={styless.h1}>Kawal PSI Dashboard</h1>
|
||||
</div>
|
||||
|
||||
<div className={styless.dropdownContainer} ref={menuRef}>
|
||||
<button
|
||||
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
||||
className={styles.dropdownToggle}
|
||||
aria-expanded={isMenuOpen ? "true" : "false"}
|
||||
aria-haspopup="true"
|
||||
>
|
||||
<svg
|
||||
width="15"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<line x1="3" y1="6" x2="21" y2="6" />
|
||||
<line x1="3" y1="12" x2="21" y2="12" />
|
||||
<line x1="3" y1="18" x2="21" y2="18" />
|
||||
</svg>
|
||||
</button>
|
||||
{isMenuOpen && (
|
||||
<div className={styless.dropdownMenu}>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate("/profile");
|
||||
setIsMenuOpen(false);
|
||||
}}
|
||||
className={styless.dropdownItem}
|
||||
>
|
||||
Profile
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate("/dashboard");
|
||||
setIsMenuOpen(false);
|
||||
}}
|
||||
className={styless.dropdownItem}
|
||||
>
|
||||
Dashboard
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<video
|
||||
ref={videoRef}
|
||||
|
||||
@@ -50,7 +50,6 @@ body {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.dashboardHeader {
|
||||
background-color: var(--white);
|
||||
color: var(--text-primary);
|
||||
|
||||
Reference in New Issue
Block a user