1263 lines
44 KiB
HTML
1263 lines
44 KiB
HTML
|
|
<!DOCTYPE html>
|
||
|
|
<html lang="id">
|
||
|
|
<head>
|
||
|
|
<meta charset="UTF-8" />
|
||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
|
|
<title>Pengaturan - Btekno Retribusi Admin</title>
|
||
|
|
<link rel="stylesheet" href="css/app.css" />
|
||
|
|
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
|
||
|
|
<style>
|
||
|
|
.settings-layout {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
gap: 1.5rem;
|
||
|
|
}
|
||
|
|
.list-section {
|
||
|
|
background: #fff;
|
||
|
|
border-radius: 0.5rem;
|
||
|
|
border: 1px solid #e5e7eb;
|
||
|
|
padding: 1.5rem;
|
||
|
|
}
|
||
|
|
.section-header {
|
||
|
|
display: flex;
|
||
|
|
justify-content: space-between;
|
||
|
|
align-items: center;
|
||
|
|
margin-bottom: 1rem;
|
||
|
|
padding-bottom: 0.75rem;
|
||
|
|
border-bottom: 2px solid #e5e7eb;
|
||
|
|
}
|
||
|
|
.section-title {
|
||
|
|
font-size: 1.1rem;
|
||
|
|
font-weight: 600;
|
||
|
|
color: #111827;
|
||
|
|
}
|
||
|
|
.btn-add {
|
||
|
|
padding: 0.5rem 1rem;
|
||
|
|
border-radius: 0.5rem;
|
||
|
|
font-size: 0.85rem;
|
||
|
|
font-weight: 500;
|
||
|
|
cursor: pointer;
|
||
|
|
border: none;
|
||
|
|
background-color: #111827;
|
||
|
|
color: white;
|
||
|
|
transition: all 0.15s ease;
|
||
|
|
}
|
||
|
|
.btn-add:hover {
|
||
|
|
background-color: #000000;
|
||
|
|
}
|
||
|
|
.list-item {
|
||
|
|
padding: 0.75rem;
|
||
|
|
border-bottom: 1px solid #e5e7eb;
|
||
|
|
display: flex;
|
||
|
|
justify-content: space-between;
|
||
|
|
align-items: center;
|
||
|
|
}
|
||
|
|
.list-item:last-child {
|
||
|
|
border-bottom: none;
|
||
|
|
}
|
||
|
|
.list-item:hover {
|
||
|
|
background: #f9fafb;
|
||
|
|
}
|
||
|
|
.list-item-info {
|
||
|
|
flex: 1;
|
||
|
|
}
|
||
|
|
.list-item-title {
|
||
|
|
font-weight: 600;
|
||
|
|
color: #111827;
|
||
|
|
margin-bottom: 0.25rem;
|
||
|
|
}
|
||
|
|
.list-item-subtitle {
|
||
|
|
font-size: 0.85rem;
|
||
|
|
color: #6b7280;
|
||
|
|
}
|
||
|
|
.list-item-actions {
|
||
|
|
display: flex;
|
||
|
|
gap: 0.5rem;
|
||
|
|
}
|
||
|
|
.badge {
|
||
|
|
display: inline-block;
|
||
|
|
padding: 0.25rem 0.5rem;
|
||
|
|
border-radius: 0.25rem;
|
||
|
|
font-size: 0.75rem;
|
||
|
|
font-weight: 500;
|
||
|
|
}
|
||
|
|
.badge-active {
|
||
|
|
background: #d1fae5;
|
||
|
|
color: #065f46;
|
||
|
|
}
|
||
|
|
.badge-inactive {
|
||
|
|
background: #fee2e2;
|
||
|
|
color: #991b1b;
|
||
|
|
}
|
||
|
|
.btn {
|
||
|
|
padding: 0.35rem 0.75rem;
|
||
|
|
border-radius: 0.5rem;
|
||
|
|
font-size: 0.8rem;
|
||
|
|
font-weight: 500;
|
||
|
|
cursor: pointer;
|
||
|
|
border: none;
|
||
|
|
transition: all 0.15s ease;
|
||
|
|
}
|
||
|
|
.btn-secondary {
|
||
|
|
background-color: #f3f4f6;
|
||
|
|
color: #374151;
|
||
|
|
border: 1px solid #d1d5db;
|
||
|
|
}
|
||
|
|
.btn-secondary:hover {
|
||
|
|
background-color: #e5e7eb;
|
||
|
|
}
|
||
|
|
.btn-danger {
|
||
|
|
background-color: #ef4444;
|
||
|
|
color: white;
|
||
|
|
}
|
||
|
|
.btn-danger:hover {
|
||
|
|
background-color: #dc2626;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Modal Styles */
|
||
|
|
.modal-overlay {
|
||
|
|
display: none;
|
||
|
|
position: fixed;
|
||
|
|
inset: 0;
|
||
|
|
background-color: rgba(0, 0, 0, 0.5);
|
||
|
|
z-index: 1000;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
padding: 1rem;
|
||
|
|
}
|
||
|
|
.modal-overlay.active {
|
||
|
|
display: flex;
|
||
|
|
}
|
||
|
|
.modal {
|
||
|
|
background: #fff;
|
||
|
|
border-radius: 0.75rem;
|
||
|
|
border: 1px solid #e5e7eb;
|
||
|
|
width: 100%;
|
||
|
|
max-width: 500px;
|
||
|
|
max-height: 90vh;
|
||
|
|
overflow-y: auto;
|
||
|
|
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||
|
|
}
|
||
|
|
.modal-header {
|
||
|
|
padding: 1.25rem 1.5rem;
|
||
|
|
border-bottom: 1px solid #e5e7eb;
|
||
|
|
display: flex;
|
||
|
|
justify-content: space-between;
|
||
|
|
align-items: center;
|
||
|
|
}
|
||
|
|
.modal-title {
|
||
|
|
font-size: 1.1rem;
|
||
|
|
font-weight: 600;
|
||
|
|
color: #111827;
|
||
|
|
}
|
||
|
|
.modal-close {
|
||
|
|
background: none;
|
||
|
|
border: none;
|
||
|
|
font-size: 1.5rem;
|
||
|
|
color: #6b7280;
|
||
|
|
cursor: pointer;
|
||
|
|
padding: 0;
|
||
|
|
width: 2rem;
|
||
|
|
height: 2rem;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
border-radius: 0.375rem;
|
||
|
|
transition: all 0.15s ease;
|
||
|
|
}
|
||
|
|
.modal-close:hover {
|
||
|
|
background: #f3f4f6;
|
||
|
|
color: #111827;
|
||
|
|
}
|
||
|
|
.modal-body {
|
||
|
|
padding: 1.5rem;
|
||
|
|
}
|
||
|
|
.form-group {
|
||
|
|
margin-bottom: 1rem;
|
||
|
|
}
|
||
|
|
.form-label {
|
||
|
|
display: block;
|
||
|
|
font-size: 0.85rem;
|
||
|
|
font-weight: 500;
|
||
|
|
margin-bottom: 0.35rem;
|
||
|
|
color: #374151;
|
||
|
|
}
|
||
|
|
.form-input,
|
||
|
|
.form-select {
|
||
|
|
width: 100%;
|
||
|
|
border-radius: 0.5rem;
|
||
|
|
border: 1px solid #d1d5db;
|
||
|
|
padding: 0.5rem 0.75rem;
|
||
|
|
font-size: 0.9rem;
|
||
|
|
background-color: #ffffff;
|
||
|
|
color: #111827;
|
||
|
|
}
|
||
|
|
.form-input:focus,
|
||
|
|
.form-select:focus {
|
||
|
|
outline: none;
|
||
|
|
border-color: #111827;
|
||
|
|
box-shadow: 0 0 0 1px #1118271a;
|
||
|
|
}
|
||
|
|
.form-checkbox {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 0.5rem;
|
||
|
|
}
|
||
|
|
.form-checkbox input {
|
||
|
|
width: 1.25rem;
|
||
|
|
height: 1.25rem;
|
||
|
|
cursor: pointer;
|
||
|
|
}
|
||
|
|
.form-actions {
|
||
|
|
display: flex;
|
||
|
|
gap: 0.5rem;
|
||
|
|
margin-top: 1.5rem;
|
||
|
|
padding-top: 1rem;
|
||
|
|
border-top: 1px solid #e5e7eb;
|
||
|
|
}
|
||
|
|
.btn-primary {
|
||
|
|
padding: 0.5rem 1rem;
|
||
|
|
border-radius: 0.5rem;
|
||
|
|
font-size: 0.9rem;
|
||
|
|
font-weight: 500;
|
||
|
|
cursor: pointer;
|
||
|
|
border: none;
|
||
|
|
background-color: #111827;
|
||
|
|
color: white;
|
||
|
|
transition: all 0.15s ease;
|
||
|
|
flex: 1;
|
||
|
|
}
|
||
|
|
.btn-primary:hover {
|
||
|
|
background-color: #000000;
|
||
|
|
}
|
||
|
|
.btn-cancel {
|
||
|
|
padding: 0.5rem 1rem;
|
||
|
|
border-radius: 0.5rem;
|
||
|
|
font-size: 0.9rem;
|
||
|
|
font-weight: 500;
|
||
|
|
cursor: pointer;
|
||
|
|
border: 1px solid #d1d5db;
|
||
|
|
background-color: #f3f4f6;
|
||
|
|
color: #374151;
|
||
|
|
transition: all 0.15s ease;
|
||
|
|
}
|
||
|
|
.btn-cancel:hover {
|
||
|
|
background-color: #e5e7eb;
|
||
|
|
}
|
||
|
|
.error-text {
|
||
|
|
color: #b91c1c;
|
||
|
|
font-size: 0.85rem;
|
||
|
|
margin-top: 0.5rem;
|
||
|
|
}
|
||
|
|
.success-text {
|
||
|
|
color: #059669;
|
||
|
|
font-size: 0.85rem;
|
||
|
|
margin-top: 0.5rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Gate Card Styles */
|
||
|
|
.gate-card {
|
||
|
|
background: #fff;
|
||
|
|
border: 1px solid #e5e7eb;
|
||
|
|
border-radius: 0.5rem;
|
||
|
|
padding: 1.5rem;
|
||
|
|
margin-bottom: 1rem;
|
||
|
|
display: grid;
|
||
|
|
grid-template-columns: 1fr 400px;
|
||
|
|
gap: 1.5rem;
|
||
|
|
align-items: start;
|
||
|
|
}
|
||
|
|
@media (max-width: 1024px) {
|
||
|
|
.gate-card {
|
||
|
|
grid-template-columns: 1fr;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
.gate-info {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
gap: 0.5rem;
|
||
|
|
}
|
||
|
|
.gate-info-row {
|
||
|
|
display: flex;
|
||
|
|
align-items: flex-start;
|
||
|
|
gap: 0.5rem;
|
||
|
|
}
|
||
|
|
.gate-info-label {
|
||
|
|
font-size: 0.9rem;
|
||
|
|
font-weight: 500;
|
||
|
|
color: #374151;
|
||
|
|
min-width: 110px;
|
||
|
|
}
|
||
|
|
.gate-info-value {
|
||
|
|
font-size: 0.9rem;
|
||
|
|
color: #111827;
|
||
|
|
flex: 1;
|
||
|
|
}
|
||
|
|
.gate-info-value.url {
|
||
|
|
color: #059669;
|
||
|
|
word-break: break-all;
|
||
|
|
}
|
||
|
|
.gate-preview {
|
||
|
|
background: #f3f4f6;
|
||
|
|
border: 1px solid #e5e7eb;
|
||
|
|
border-radius: 0.5rem;
|
||
|
|
overflow: hidden;
|
||
|
|
position: relative;
|
||
|
|
aspect-ratio: 16/9;
|
||
|
|
min-height: 225px;
|
||
|
|
}
|
||
|
|
.gate-preview-placeholder {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
height: 100%;
|
||
|
|
color: #9ca3af;
|
||
|
|
font-size: 0.9rem;
|
||
|
|
background: #f3f4f6;
|
||
|
|
text-transform: lowercase;
|
||
|
|
}
|
||
|
|
.gate-preview video {
|
||
|
|
width: 100%;
|
||
|
|
height: 100%;
|
||
|
|
object-fit: contain;
|
||
|
|
background: #000;
|
||
|
|
}
|
||
|
|
.gate-preview-toggle {
|
||
|
|
position: absolute;
|
||
|
|
top: 0.5rem;
|
||
|
|
right: 0.5rem;
|
||
|
|
z-index: 10;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 0.5rem;
|
||
|
|
background: rgba(0, 0, 0, 0.75);
|
||
|
|
padding: 0.5rem 0.75rem;
|
||
|
|
border-radius: 0.375rem;
|
||
|
|
color: white;
|
||
|
|
font-size: 0.8rem;
|
||
|
|
backdrop-filter: blur(4px);
|
||
|
|
}
|
||
|
|
.gate-preview-toggle label {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 0.5rem;
|
||
|
|
cursor: pointer;
|
||
|
|
margin: 0;
|
||
|
|
color: white;
|
||
|
|
font-weight: 500;
|
||
|
|
}
|
||
|
|
.gate-preview-toggle .toggle-switch {
|
||
|
|
position: relative;
|
||
|
|
display: inline-block;
|
||
|
|
width: 36px;
|
||
|
|
height: 20px;
|
||
|
|
}
|
||
|
|
.gate-preview-toggle .toggle-switch input {
|
||
|
|
opacity: 0;
|
||
|
|
width: 0;
|
||
|
|
height: 0;
|
||
|
|
}
|
||
|
|
.gate-preview-toggle .toggle-slider {
|
||
|
|
position: absolute;
|
||
|
|
cursor: pointer;
|
||
|
|
top: 0;
|
||
|
|
left: 0;
|
||
|
|
right: 0;
|
||
|
|
bottom: 0;
|
||
|
|
background-color: rgba(255, 255, 255, 0.3);
|
||
|
|
transition: 0.2s;
|
||
|
|
border-radius: 20px;
|
||
|
|
}
|
||
|
|
.gate-preview-toggle .toggle-slider:before {
|
||
|
|
position: absolute;
|
||
|
|
content: "";
|
||
|
|
height: 14px;
|
||
|
|
width: 14px;
|
||
|
|
left: 3px;
|
||
|
|
bottom: 3px;
|
||
|
|
background-color: white;
|
||
|
|
transition: 0.2s;
|
||
|
|
border-radius: 50%;
|
||
|
|
}
|
||
|
|
.gate-preview-toggle input:checked + .toggle-slider {
|
||
|
|
background-color: #10b981;
|
||
|
|
}
|
||
|
|
.gate-preview-toggle input:checked + .toggle-slider:before {
|
||
|
|
transform: translateX(16px);
|
||
|
|
}
|
||
|
|
.gate-preview-toggle input:disabled + .toggle-slider {
|
||
|
|
opacity: 0.5;
|
||
|
|
cursor: not-allowed;
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
</head>
|
||
|
|
<body>
|
||
|
|
<div class="page">
|
||
|
|
<header class="topbar">
|
||
|
|
<div class="topbar-title">Pengaturan</div>
|
||
|
|
<div class="topbar-actions">
|
||
|
|
<a href="dashboard.html" class="topbar-link">Dashboard</a>
|
||
|
|
<a href="event.html" class="topbar-link">Events</a>
|
||
|
|
<button id="logout-button" class="topbar-link">Logout</button>
|
||
|
|
</div>
|
||
|
|
</header>
|
||
|
|
|
||
|
|
<main class="container">
|
||
|
|
<div class="settings-layout">
|
||
|
|
<!-- Lokasi Section -->
|
||
|
|
<div class="list-section">
|
||
|
|
<div class="section-header">
|
||
|
|
<h2 class="section-title">Lokasi</h2>
|
||
|
|
<button class="btn-add" onclick="openLocationModal()">+ Tambah Lokasi</button>
|
||
|
|
</div>
|
||
|
|
<div id="locations-list">
|
||
|
|
<div style="text-align: center; padding: 2rem; color: #6b7280;">Memuat data...</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Gate Section -->
|
||
|
|
<div class="list-section">
|
||
|
|
<div class="section-header">
|
||
|
|
<h2 class="section-title">Gate</h2>
|
||
|
|
<button class="btn-add" onclick="openGateModal()">+ Tambah Gate</button>
|
||
|
|
</div>
|
||
|
|
<div id="gates-list">
|
||
|
|
<div style="text-align: center; padding: 2rem; color: #6b7280;">Memuat data...</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</main>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Modal Lokasi -->
|
||
|
|
<div id="location-modal" class="modal-overlay" onclick="closeModalOnOverlay(event, 'location-modal')">
|
||
|
|
<div class="modal" onclick="event.stopPropagation()">
|
||
|
|
<div class="modal-header">
|
||
|
|
<h3 class="modal-title" id="location-modal-title">Tambah Lokasi</h3>
|
||
|
|
<button class="modal-close" onclick="closeLocationModal()">×</button>
|
||
|
|
</div>
|
||
|
|
<div class="modal-body">
|
||
|
|
<form id="location-form">
|
||
|
|
<div class="form-group">
|
||
|
|
<label for="location-code" class="form-label">Kode Lokasi *</label>
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
id="location-code"
|
||
|
|
class="form-input"
|
||
|
|
required
|
||
|
|
pattern="[a-z0-9_\-]{1,64}"
|
||
|
|
placeholder="contoh: kerkof_01"
|
||
|
|
/>
|
||
|
|
<small style="color: #6b7280; font-size: 0.75rem;">Format: huruf kecil, angka, underscore, dash (max 64 karakter)</small>
|
||
|
|
</div>
|
||
|
|
<div class="form-group">
|
||
|
|
<label for="location-name" class="form-label">Nama Lokasi *</label>
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
id="location-name"
|
||
|
|
class="form-input"
|
||
|
|
required
|
||
|
|
placeholder="contoh: Kerkof"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
<div class="form-group">
|
||
|
|
<label for="location-type" class="form-label">Tipe *</label>
|
||
|
|
<select id="location-type" class="form-select" required>
|
||
|
|
<option value="">Pilih Tipe</option>
|
||
|
|
<option value="kerkof">Kerkof</option>
|
||
|
|
<option value="pasar">Pasar</option>
|
||
|
|
<option value="parkir">Parkir</option>
|
||
|
|
<option value="wisata">Wisata</option>
|
||
|
|
<option value="lainnya">Lainnya</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
<div class="form-group">
|
||
|
|
<div class="form-checkbox">
|
||
|
|
<input type="checkbox" id="location-active" checked />
|
||
|
|
<label for="location-active" class="form-label" style="margin: 0;">Aktif</label>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div id="location-error" class="error-text" style="display: none;"></div>
|
||
|
|
<div id="location-success" class="success-text" style="display: none;"></div>
|
||
|
|
<div class="form-actions">
|
||
|
|
<button type="button" class="btn-cancel" onclick="closeLocationModal()">Batal</button>
|
||
|
|
<button type="submit" class="btn-primary">Simpan</button>
|
||
|
|
</div>
|
||
|
|
</form>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Modal Gate -->
|
||
|
|
<div id="gate-modal" class="modal-overlay" onclick="closeModalOnOverlay(event, 'gate-modal')">
|
||
|
|
<div class="modal" onclick="event.stopPropagation()">
|
||
|
|
<div class="modal-header">
|
||
|
|
<h3 class="modal-title" id="gate-modal-title">Tambah Gate</h3>
|
||
|
|
<button class="modal-close" onclick="closeGateModal()">×</button>
|
||
|
|
</div>
|
||
|
|
<div class="modal-body">
|
||
|
|
<form id="gate-form">
|
||
|
|
<div class="form-group">
|
||
|
|
<label for="gate-location" class="form-label">Lokasi *</label>
|
||
|
|
<select id="gate-location" class="form-select" required>
|
||
|
|
<option value="">Pilih Lokasi</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
<div class="form-group">
|
||
|
|
<label for="gate-code" class="form-label">Kode Gate *</label>
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
id="gate-code"
|
||
|
|
class="form-input"
|
||
|
|
required
|
||
|
|
pattern="[a-z0-9_\-]{1,64}"
|
||
|
|
placeholder="contoh: gate01"
|
||
|
|
/>
|
||
|
|
<small style="color: #6b7280; font-size: 0.75rem;">Format: huruf kecil, angka, underscore, dash (max 64 karakter)</small>
|
||
|
|
</div>
|
||
|
|
<div class="form-group">
|
||
|
|
<label for="gate-name" class="form-label">Nama Gate *</label>
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
id="gate-name"
|
||
|
|
class="form-input"
|
||
|
|
required
|
||
|
|
placeholder="contoh: Gate Utama"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
<div class="form-group">
|
||
|
|
<label for="gate-direction" class="form-label">Arah *</label>
|
||
|
|
<select id="gate-direction" class="form-select" required>
|
||
|
|
<option value="">Pilih Arah</option>
|
||
|
|
<option value="in">Masuk</option>
|
||
|
|
<option value="out">Keluar</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
<div class="form-group">
|
||
|
|
<label for="gate-camera" class="form-label">URL Kamera (HLS)</label>
|
||
|
|
<input
|
||
|
|
type="url"
|
||
|
|
id="gate-camera"
|
||
|
|
class="form-input"
|
||
|
|
placeholder="contoh: https://example.com/cam1/index.m3u8"
|
||
|
|
/>
|
||
|
|
<small style="color: #6b7280; font-size: 0.75rem;">URL stream HLS untuk live camera</small>
|
||
|
|
</div>
|
||
|
|
<div class="form-group">
|
||
|
|
<div class="form-checkbox">
|
||
|
|
<input type="checkbox" id="gate-active" checked />
|
||
|
|
<label for="gate-active" class="form-label" style="margin: 0;">Aktif</label>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div id="gate-error" class="error-text" style="display: none;"></div>
|
||
|
|
<div id="gate-success" class="success-text" style="display: none;"></div>
|
||
|
|
<div class="form-actions">
|
||
|
|
<button type="button" class="btn-cancel" onclick="closeGateModal()">Batal</button>
|
||
|
|
<button type="submit" class="btn-primary">Simpan</button>
|
||
|
|
</div>
|
||
|
|
</form>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<script type="module">
|
||
|
|
import { Auth } from './js/auth.js';
|
||
|
|
import {
|
||
|
|
apiGetLocations,
|
||
|
|
apiGetGates,
|
||
|
|
API_CONFIG
|
||
|
|
} from './js/api.js';
|
||
|
|
|
||
|
|
// Check auth
|
||
|
|
if (!Auth.isAuthenticated()) {
|
||
|
|
window.location.href = '../index.php';
|
||
|
|
}
|
||
|
|
|
||
|
|
// Logout handler
|
||
|
|
document.getElementById('logout-button')?.addEventListener('click', () => {
|
||
|
|
Auth.logout();
|
||
|
|
});
|
||
|
|
|
||
|
|
// Helper untuk build query string
|
||
|
|
function buildQuery(params = {}) {
|
||
|
|
const search = new URLSearchParams();
|
||
|
|
Object.entries(params).forEach(([key, value]) => {
|
||
|
|
if (value !== undefined && value !== null && value !== '') {
|
||
|
|
search.append(key, value);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
const qs = search.toString();
|
||
|
|
return qs ? `?${qs}` : '';
|
||
|
|
}
|
||
|
|
|
||
|
|
// API functions untuk CRUD
|
||
|
|
async function apiCreateLocation(data) {
|
||
|
|
const url = `${API_CONFIG.BASE_URL}/retribusi/v1/frontend/locations`;
|
||
|
|
const token = localStorage.getItem('token') || '';
|
||
|
|
const res = await fetch(url, {
|
||
|
|
method: 'POST',
|
||
|
|
headers: {
|
||
|
|
'Content-Type': 'application/json',
|
||
|
|
'X-API-KEY': API_CONFIG.API_KEY,
|
||
|
|
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
||
|
|
},
|
||
|
|
body: JSON.stringify(data)
|
||
|
|
});
|
||
|
|
if (!res.ok) {
|
||
|
|
const error = await res.json();
|
||
|
|
if (error.fields) {
|
||
|
|
const fieldErrors = Object.entries(error.fields).map(([field, msg]) => `${field}: ${msg}`).join(', ');
|
||
|
|
throw new Error(fieldErrors || error.message || `HTTP ${res.status}`);
|
||
|
|
}
|
||
|
|
throw new Error(error.message || `HTTP ${res.status}`);
|
||
|
|
}
|
||
|
|
return await res.json();
|
||
|
|
}
|
||
|
|
|
||
|
|
async function apiUpdateLocation(code, data) {
|
||
|
|
const url = `${API_CONFIG.BASE_URL}/retribusi/v1/frontend/locations/${code}`;
|
||
|
|
const token = localStorage.getItem('token') || '';
|
||
|
|
const res = await fetch(url, {
|
||
|
|
method: 'PUT',
|
||
|
|
headers: {
|
||
|
|
'Content-Type': 'application/json',
|
||
|
|
'X-API-KEY': API_CONFIG.API_KEY,
|
||
|
|
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
||
|
|
},
|
||
|
|
body: JSON.stringify(data)
|
||
|
|
});
|
||
|
|
if (!res.ok) {
|
||
|
|
const error = await res.json();
|
||
|
|
if (error.fields) {
|
||
|
|
const fieldErrors = Object.entries(error.fields).map(([field, msg]) => `${field}: ${msg}`).join(', ');
|
||
|
|
throw new Error(fieldErrors || error.message || `HTTP ${res.status}`);
|
||
|
|
}
|
||
|
|
throw new Error(error.message || `HTTP ${res.status}`);
|
||
|
|
}
|
||
|
|
return await res.json();
|
||
|
|
}
|
||
|
|
|
||
|
|
async function apiDeleteLocation(code) {
|
||
|
|
const url = `${API_CONFIG.BASE_URL}/retribusi/v1/frontend/locations/${code}`;
|
||
|
|
const token = localStorage.getItem('token') || '';
|
||
|
|
const res = await fetch(url, {
|
||
|
|
method: 'DELETE',
|
||
|
|
headers: {
|
||
|
|
'Content-Type': 'application/json',
|
||
|
|
'X-API-KEY': API_CONFIG.API_KEY,
|
||
|
|
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
||
|
|
}
|
||
|
|
});
|
||
|
|
if (!res.ok) {
|
||
|
|
const error = await res.json();
|
||
|
|
throw new Error(error.message || `HTTP ${res.status}`);
|
||
|
|
}
|
||
|
|
return await res.json();
|
||
|
|
}
|
||
|
|
|
||
|
|
async function apiCreateGate(data) {
|
||
|
|
const url = `${API_CONFIG.BASE_URL}/retribusi/v1/frontend/gates`;
|
||
|
|
const token = localStorage.getItem('token') || '';
|
||
|
|
const res = await fetch(url, {
|
||
|
|
method: 'POST',
|
||
|
|
headers: {
|
||
|
|
'Content-Type': 'application/json',
|
||
|
|
'X-API-KEY': API_CONFIG.API_KEY,
|
||
|
|
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
||
|
|
},
|
||
|
|
body: JSON.stringify(data)
|
||
|
|
});
|
||
|
|
if (!res.ok) {
|
||
|
|
const error = await res.json();
|
||
|
|
if (error.fields) {
|
||
|
|
const fieldErrors = Object.entries(error.fields).map(([field, msg]) => `${field}: ${msg}`).join(', ');
|
||
|
|
throw new Error(fieldErrors || error.message || `HTTP ${res.status}`);
|
||
|
|
}
|
||
|
|
throw new Error(error.message || `HTTP ${res.status}`);
|
||
|
|
}
|
||
|
|
return await res.json();
|
||
|
|
}
|
||
|
|
|
||
|
|
async function apiUpdateGate(locationCode, gateCode, data) {
|
||
|
|
const url = `${API_CONFIG.BASE_URL}/retribusi/v1/frontend/gates/${locationCode}/${gateCode}`;
|
||
|
|
const token = localStorage.getItem('token') || '';
|
||
|
|
const res = await fetch(url, {
|
||
|
|
method: 'PUT',
|
||
|
|
headers: {
|
||
|
|
'Content-Type': 'application/json',
|
||
|
|
'X-API-KEY': API_CONFIG.API_KEY,
|
||
|
|
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
||
|
|
},
|
||
|
|
body: JSON.stringify(data)
|
||
|
|
});
|
||
|
|
if (!res.ok) {
|
||
|
|
const error = await res.json();
|
||
|
|
if (error.fields) {
|
||
|
|
const fieldErrors = Object.entries(error.fields).map(([field, msg]) => `${field}: ${msg}`).join(', ');
|
||
|
|
throw new Error(fieldErrors || error.message || `HTTP ${res.status}`);
|
||
|
|
}
|
||
|
|
throw new Error(error.message || `HTTP ${res.status}`);
|
||
|
|
}
|
||
|
|
return await res.json();
|
||
|
|
}
|
||
|
|
|
||
|
|
async function apiDeleteGate(locationCode, gateCode) {
|
||
|
|
const url = `${API_CONFIG.BASE_URL}/retribusi/v1/frontend/gates/${locationCode}/${gateCode}`;
|
||
|
|
const token = localStorage.getItem('token') || '';
|
||
|
|
const res = await fetch(url, {
|
||
|
|
method: 'DELETE',
|
||
|
|
headers: {
|
||
|
|
'Content-Type': 'application/json',
|
||
|
|
'X-API-KEY': API_CONFIG.API_KEY,
|
||
|
|
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
||
|
|
}
|
||
|
|
});
|
||
|
|
if (!res.ok) {
|
||
|
|
const error = await res.json();
|
||
|
|
throw new Error(error.message || `HTTP ${res.status}`);
|
||
|
|
}
|
||
|
|
return await res.json();
|
||
|
|
}
|
||
|
|
|
||
|
|
// State
|
||
|
|
let editingLocation = null;
|
||
|
|
let editingGate = null;
|
||
|
|
let locations = [];
|
||
|
|
|
||
|
|
// Load locations
|
||
|
|
async function loadLocations() {
|
||
|
|
try {
|
||
|
|
const response = await apiGetLocations({ limit: 100 });
|
||
|
|
let items = [];
|
||
|
|
if (Array.isArray(response)) {
|
||
|
|
items = response;
|
||
|
|
} else if (response && Array.isArray(response.data)) {
|
||
|
|
items = response.data;
|
||
|
|
}
|
||
|
|
locations = items;
|
||
|
|
renderLocationsList(items);
|
||
|
|
updateLocationSelect(items);
|
||
|
|
} catch (err) {
|
||
|
|
console.error('loadLocations error', err);
|
||
|
|
document.getElementById('locations-list').innerHTML =
|
||
|
|
'<div style="text-align: center; padding: 2rem; color: #b91c1c;">Gagal memuat data lokasi</div>';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Load gates
|
||
|
|
async function loadGates(locationCode = null) {
|
||
|
|
try {
|
||
|
|
const response = await apiGetGates(locationCode, { limit: 100 });
|
||
|
|
let items = [];
|
||
|
|
if (Array.isArray(response)) {
|
||
|
|
items = response;
|
||
|
|
} else if (response && Array.isArray(response.data)) {
|
||
|
|
items = response.data;
|
||
|
|
}
|
||
|
|
renderGatesList(items, locationCode);
|
||
|
|
} catch (err) {
|
||
|
|
console.error('loadGates error', err);
|
||
|
|
const container = document.getElementById('gates-list');
|
||
|
|
if (container) {
|
||
|
|
container.innerHTML = '<div style="text-align: center; padding: 2rem; color: #b91c1c;">Gagal memuat data gate</div>';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Update location select dropdown
|
||
|
|
function updateLocationSelect(locations) {
|
||
|
|
const select = document.getElementById('gate-location');
|
||
|
|
if (!select) return;
|
||
|
|
select.innerHTML = '<option value="">Pilih Lokasi</option>';
|
||
|
|
locations.forEach(loc => {
|
||
|
|
const opt = document.createElement('option');
|
||
|
|
opt.value = loc.code || loc.location_code || '';
|
||
|
|
opt.textContent = loc.name || loc.label || opt.value;
|
||
|
|
select.appendChild(opt);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Render locations list
|
||
|
|
function renderLocationsList(locations) {
|
||
|
|
const container = document.getElementById('locations-list');
|
||
|
|
if (!container) return;
|
||
|
|
|
||
|
|
if (!locations || locations.length === 0) {
|
||
|
|
container.innerHTML = '<div style="text-align: center; padding: 2rem; color: #6b7280;">Tidak ada lokasi</div>';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
container.innerHTML = locations.map(loc => {
|
||
|
|
const code = loc.code || loc.location_code || '';
|
||
|
|
const name = loc.name || loc.label || code;
|
||
|
|
const type = loc.type || '-';
|
||
|
|
const isActive = loc.is_active !== undefined ? loc.is_active : true;
|
||
|
|
|
||
|
|
return `
|
||
|
|
<div class="list-item">
|
||
|
|
<div class="list-item-info">
|
||
|
|
<div class="list-item-title">${name} <span class="badge ${isActive ? 'badge-active' : 'badge-inactive'}">${isActive ? 'Aktif' : 'Nonaktif'}</span></div>
|
||
|
|
<div class="list-item-subtitle">Kode: ${code} | Tipe: ${type}</div>
|
||
|
|
</div>
|
||
|
|
<div class="list-item-actions">
|
||
|
|
<button class="btn btn-secondary" onclick="editLocation('${code}')">Edit</button>
|
||
|
|
<button class="btn btn-danger" onclick="deleteLocation('${code}')">Hapus</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
}).join('');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Render gates list
|
||
|
|
function renderGatesList(gates, locationCode = null) {
|
||
|
|
const container = document.getElementById('gates-list');
|
||
|
|
if (!container) return;
|
||
|
|
|
||
|
|
if (!gates || gates.length === 0) {
|
||
|
|
container.innerHTML = '<div style="text-align: center; padding: 2rem; color: #6b7280;">Tidak ada gate</div>';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
container.innerHTML = gates.map((gate, index) => {
|
||
|
|
const location = gate.location_code || '';
|
||
|
|
const code = gate.gate_code || gate.code || '';
|
||
|
|
const name = gate.name || gate.label || code;
|
||
|
|
const direction = gate.direction || '-';
|
||
|
|
const camera = gate.camera || null;
|
||
|
|
const isActive = gate.is_active !== undefined ? gate.is_active : true;
|
||
|
|
const cameraEnabled = camera !== null && camera !== '';
|
||
|
|
const directionText = direction === 'in' ? 'Masuk' : direction === 'out' ? 'Keluar' : direction;
|
||
|
|
const locationName = locations.find(l => (l.code || l.location_code) === location)?.name || location;
|
||
|
|
const gateId = `gate-${location}-${code}-${index}`;
|
||
|
|
|
||
|
|
return `
|
||
|
|
<div class="gate-card">
|
||
|
|
<div class="gate-info">
|
||
|
|
<div class="gate-info-row">
|
||
|
|
<span class="gate-info-label">Lokasi:</span>
|
||
|
|
<span class="gate-info-value">${locationName}</span>
|
||
|
|
</div>
|
||
|
|
<div class="gate-info-row">
|
||
|
|
<span class="gate-info-label">Gate:</span>
|
||
|
|
<span class="gate-info-value">${name} | ${code}</span>
|
||
|
|
</div>
|
||
|
|
<div class="gate-info-row">
|
||
|
|
<span class="gate-info-label">Arah:</span>
|
||
|
|
<span class="gate-info-value">${directionText}</span>
|
||
|
|
</div>
|
||
|
|
<div class="gate-info-row">
|
||
|
|
<span class="gate-info-label">URL Kamera:</span>
|
||
|
|
<span class="gate-info-value ${cameraEnabled ? 'url' : ''}">${camera || '-'}</span>
|
||
|
|
</div>
|
||
|
|
<div class="gate-info-row">
|
||
|
|
<span class="gate-info-label">Status:</span>
|
||
|
|
<span class="gate-info-value">
|
||
|
|
<span class="badge ${isActive ? 'badge-active' : 'badge-inactive'}">${isActive ? 'Aktif' : 'Nonaktif'}</span>
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
<div style="margin-top: 0.75rem; display: flex; gap: 0.5rem;">
|
||
|
|
<button class="btn btn-secondary" onclick="editGate('${location}', '${code}')">Edit</button>
|
||
|
|
<button class="btn btn-danger" onclick="deleteGate('${location}', '${code}')">Hapus</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="gate-preview">
|
||
|
|
<div class="gate-preview-toggle">
|
||
|
|
<label>
|
||
|
|
<span>Preview</span>
|
||
|
|
<span class="toggle-switch">
|
||
|
|
<input type="checkbox" id="preview-${gateId}" onchange="togglePreview('${gateId}', '${camera || ''}', this.checked)" ${cameraEnabled ? '' : 'disabled'}>
|
||
|
|
<span class="toggle-slider"></span>
|
||
|
|
</span>
|
||
|
|
</label>
|
||
|
|
</div>
|
||
|
|
<div id="preview-placeholder-${gateId}" class="gate-preview-placeholder">
|
||
|
|
${cameraEnabled ? 'preview' : 'preview'}
|
||
|
|
</div>
|
||
|
|
<video id="preview-video-${gateId}" style="display: none;" muted playsinline></video>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
}).join('');
|
||
|
|
}
|
||
|
|
|
||
|
|
// HLS video players cache
|
||
|
|
const hlsPlayers = {};
|
||
|
|
|
||
|
|
// Toggle preview
|
||
|
|
window.togglePreview = function(gateId, cameraUrl, enabled) {
|
||
|
|
const videoEl = document.getElementById(`preview-video-${gateId}`);
|
||
|
|
const placeholderEl = document.getElementById(`preview-placeholder-${gateId}`);
|
||
|
|
|
||
|
|
if (!videoEl || !placeholderEl) return;
|
||
|
|
|
||
|
|
if (enabled && cameraUrl) {
|
||
|
|
// Show video, hide placeholder
|
||
|
|
placeholderEl.style.display = 'none';
|
||
|
|
videoEl.style.display = 'block';
|
||
|
|
|
||
|
|
// Initialize HLS player
|
||
|
|
if (Hls.isSupported()) {
|
||
|
|
// Destroy existing player if any
|
||
|
|
if (hlsPlayers[gateId]) {
|
||
|
|
hlsPlayers[gateId].destroy();
|
||
|
|
}
|
||
|
|
|
||
|
|
const hls = new Hls({
|
||
|
|
enableWorker: true,
|
||
|
|
lowLatencyMode: true
|
||
|
|
});
|
||
|
|
|
||
|
|
hls.loadSource(cameraUrl);
|
||
|
|
hls.attachMedia(videoEl);
|
||
|
|
|
||
|
|
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
||
|
|
videoEl.play().catch(err => {
|
||
|
|
console.error('Video play error:', err);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
hls.on(Hls.Events.ERROR, (event, data) => {
|
||
|
|
if (data.fatal) {
|
||
|
|
switch (data.type) {
|
||
|
|
case Hls.ErrorTypes.NETWORK_ERROR:
|
||
|
|
console.error('HLS network error, trying to recover...');
|
||
|
|
hls.startLoad();
|
||
|
|
break;
|
||
|
|
case Hls.ErrorTypes.MEDIA_ERROR:
|
||
|
|
console.error('HLS media error, trying to recover...');
|
||
|
|
hls.recoverMediaError();
|
||
|
|
break;
|
||
|
|
default:
|
||
|
|
console.error('HLS fatal error, destroying player');
|
||
|
|
hls.destroy();
|
||
|
|
delete hlsPlayers[gateId];
|
||
|
|
placeholderEl.style.display = 'flex';
|
||
|
|
videoEl.style.display = 'none';
|
||
|
|
placeholderEl.textContent = 'Error loading stream';
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
hlsPlayers[gateId] = hls;
|
||
|
|
} else if (videoEl.canPlayType('application/vnd.apple.mpegurl')) {
|
||
|
|
// Native HLS support (Safari)
|
||
|
|
videoEl.src = cameraUrl;
|
||
|
|
videoEl.play().catch(err => {
|
||
|
|
console.error('Video play error:', err);
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
console.error('HLS tidak didukung di browser ini');
|
||
|
|
placeholderEl.style.display = 'flex';
|
||
|
|
videoEl.style.display = 'none';
|
||
|
|
placeholderEl.textContent = 'HLS tidak didukung';
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
// Hide video, show placeholder
|
||
|
|
videoEl.style.display = 'none';
|
||
|
|
placeholderEl.style.display = 'flex';
|
||
|
|
|
||
|
|
// Destroy HLS player if exists
|
||
|
|
if (hlsPlayers[gateId]) {
|
||
|
|
hlsPlayers[gateId].destroy();
|
||
|
|
delete hlsPlayers[gateId];
|
||
|
|
}
|
||
|
|
|
||
|
|
// Stop video
|
||
|
|
videoEl.pause();
|
||
|
|
videoEl.src = '';
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Modal functions
|
||
|
|
function openLocationModal() {
|
||
|
|
editingLocation = null;
|
||
|
|
document.getElementById('location-form').reset();
|
||
|
|
document.getElementById('location-code').disabled = false;
|
||
|
|
document.getElementById('location-modal-title').textContent = 'Tambah Lokasi';
|
||
|
|
document.getElementById('location-error').style.display = 'none';
|
||
|
|
document.getElementById('location-success').style.display = 'none';
|
||
|
|
document.getElementById('location-modal').classList.add('active');
|
||
|
|
}
|
||
|
|
|
||
|
|
function closeLocationModal() {
|
||
|
|
document.getElementById('location-modal').classList.remove('active');
|
||
|
|
document.getElementById('location-form').reset();
|
||
|
|
editingLocation = null;
|
||
|
|
document.getElementById('location-code').disabled = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
function openGateModal() {
|
||
|
|
editingGate = null;
|
||
|
|
document.getElementById('gate-form').reset();
|
||
|
|
document.getElementById('gate-location').disabled = false;
|
||
|
|
document.getElementById('gate-code').disabled = false;
|
||
|
|
document.getElementById('gate-modal-title').textContent = 'Tambah Gate';
|
||
|
|
document.getElementById('gate-error').style.display = 'none';
|
||
|
|
document.getElementById('gate-success').style.display = 'none';
|
||
|
|
document.getElementById('gate-modal').classList.add('active');
|
||
|
|
}
|
||
|
|
|
||
|
|
function closeGateModal() {
|
||
|
|
document.getElementById('gate-modal').classList.remove('active');
|
||
|
|
document.getElementById('gate-form').reset();
|
||
|
|
editingGate = null;
|
||
|
|
document.getElementById('gate-location').disabled = false;
|
||
|
|
document.getElementById('gate-code').disabled = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
function closeModalOnOverlay(event, modalId) {
|
||
|
|
if (event.target.id === modalId) {
|
||
|
|
if (modalId === 'location-modal') {
|
||
|
|
closeLocationModal();
|
||
|
|
} else if (modalId === 'gate-modal') {
|
||
|
|
closeGateModal();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Make functions global
|
||
|
|
window.openLocationModal = openLocationModal;
|
||
|
|
window.openGateModal = openGateModal;
|
||
|
|
window.closeLocationModal = closeLocationModal;
|
||
|
|
window.closeGateModal = closeGateModal;
|
||
|
|
window.closeModalOnOverlay = closeModalOnOverlay;
|
||
|
|
|
||
|
|
// Location form handler
|
||
|
|
document.getElementById('location-form').addEventListener('submit', async (e) => {
|
||
|
|
e.preventDefault();
|
||
|
|
const errorEl = document.getElementById('location-error');
|
||
|
|
const successEl = document.getElementById('location-success');
|
||
|
|
errorEl.style.display = 'none';
|
||
|
|
successEl.style.display = 'none';
|
||
|
|
|
||
|
|
const type = document.getElementById('location-type').value;
|
||
|
|
if (!type) {
|
||
|
|
errorEl.textContent = 'Pilih tipe lokasi terlebih dahulu';
|
||
|
|
errorEl.style.display = 'block';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const data = {
|
||
|
|
code: document.getElementById('location-code').value.trim(),
|
||
|
|
name: document.getElementById('location-name').value.trim(),
|
||
|
|
type: type,
|
||
|
|
is_active: document.getElementById('location-active').checked ? 1 : 0
|
||
|
|
};
|
||
|
|
|
||
|
|
try {
|
||
|
|
if (editingLocation) {
|
||
|
|
await apiUpdateLocation(editingLocation, data);
|
||
|
|
successEl.textContent = 'Lokasi berhasil diupdate!';
|
||
|
|
} else {
|
||
|
|
await apiCreateLocation(data);
|
||
|
|
successEl.textContent = 'Lokasi berhasil ditambahkan!';
|
||
|
|
}
|
||
|
|
successEl.style.display = 'block';
|
||
|
|
await loadLocations();
|
||
|
|
setTimeout(() => {
|
||
|
|
closeLocationModal();
|
||
|
|
successEl.style.display = 'none';
|
||
|
|
}, 1500);
|
||
|
|
} catch (err) {
|
||
|
|
errorEl.textContent = err.message || 'Gagal menyimpan lokasi';
|
||
|
|
errorEl.style.display = 'block';
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// Gate form handler
|
||
|
|
document.getElementById('gate-form').addEventListener('submit', async (e) => {
|
||
|
|
e.preventDefault();
|
||
|
|
const errorEl = document.getElementById('gate-error');
|
||
|
|
const successEl = document.getElementById('gate-success');
|
||
|
|
errorEl.style.display = 'none';
|
||
|
|
successEl.style.display = 'none';
|
||
|
|
|
||
|
|
const locationCode = document.getElementById('gate-location').value;
|
||
|
|
if (!locationCode) {
|
||
|
|
errorEl.textContent = 'Pilih lokasi terlebih dahulu';
|
||
|
|
errorEl.style.display = 'block';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const direction = document.getElementById('gate-direction').value;
|
||
|
|
if (!direction) {
|
||
|
|
errorEl.textContent = 'Pilih arah gate terlebih dahulu';
|
||
|
|
errorEl.style.display = 'block';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const cameraValue = document.getElementById('gate-camera').value.trim();
|
||
|
|
const data = {
|
||
|
|
location_code: locationCode,
|
||
|
|
gate_code: document.getElementById('gate-code').value.trim(),
|
||
|
|
name: document.getElementById('gate-name').value.trim(),
|
||
|
|
direction: direction,
|
||
|
|
camera: cameraValue || null,
|
||
|
|
is_active: document.getElementById('gate-active').checked ? 1 : 0
|
||
|
|
};
|
||
|
|
|
||
|
|
try {
|
||
|
|
if (editingGate) {
|
||
|
|
await apiUpdateGate(editingGate.locationCode, editingGate.gateCode, data);
|
||
|
|
successEl.textContent = 'Gate berhasil diupdate!';
|
||
|
|
} else {
|
||
|
|
await apiCreateGate(data);
|
||
|
|
successEl.textContent = 'Gate berhasil ditambahkan!';
|
||
|
|
}
|
||
|
|
successEl.style.display = 'block';
|
||
|
|
await loadGates();
|
||
|
|
setTimeout(() => {
|
||
|
|
closeGateModal();
|
||
|
|
successEl.style.display = 'none';
|
||
|
|
}, 1500);
|
||
|
|
} catch (err) {
|
||
|
|
errorEl.textContent = err.message || 'Gagal menyimpan gate';
|
||
|
|
errorEl.style.display = 'block';
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// Location select change - load gates
|
||
|
|
document.getElementById('gate-location').addEventListener('change', (e) => {
|
||
|
|
const locationCode = e.target.value;
|
||
|
|
if (locationCode) {
|
||
|
|
loadGates(locationCode);
|
||
|
|
} else {
|
||
|
|
document.getElementById('gates-list').innerHTML =
|
||
|
|
'<div style="text-align: center; padding: 2rem; color: #6b7280;">Pilih lokasi untuk melihat gates</div>';
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// Edit location
|
||
|
|
window.editLocation = async function(code) {
|
||
|
|
const location = locations.find(l => (l.code || l.location_code) === code);
|
||
|
|
if (!location) return;
|
||
|
|
|
||
|
|
editingLocation = code;
|
||
|
|
document.getElementById('location-code').value = location.code || location.location_code || '';
|
||
|
|
document.getElementById('location-code').disabled = true;
|
||
|
|
document.getElementById('location-name').value = location.name || location.label || '';
|
||
|
|
document.getElementById('location-type').value = location.type || '';
|
||
|
|
document.getElementById('location-active').checked = location.is_active !== undefined ? location.is_active : true;
|
||
|
|
|
||
|
|
document.getElementById('location-modal-title').textContent = 'Edit Lokasi';
|
||
|
|
document.getElementById('location-error').style.display = 'none';
|
||
|
|
document.getElementById('location-success').style.display = 'none';
|
||
|
|
document.getElementById('location-modal').classList.add('active');
|
||
|
|
};
|
||
|
|
|
||
|
|
// Delete location
|
||
|
|
window.deleteLocation = async function(code) {
|
||
|
|
if (!confirm(`Yakin ingin menghapus lokasi ${code}?`)) return;
|
||
|
|
try {
|
||
|
|
await apiDeleteLocation(code);
|
||
|
|
await loadLocations();
|
||
|
|
alert('Lokasi berhasil dihapus!');
|
||
|
|
} catch (err) {
|
||
|
|
alert('Gagal menghapus lokasi: ' + err.message);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Edit gate
|
||
|
|
window.editGate = async function(locationCode, gateCode) {
|
||
|
|
try {
|
||
|
|
const url = `${API_CONFIG.BASE_URL}/retribusi/v1/frontend/gates/${locationCode}/${gateCode}`;
|
||
|
|
const token = localStorage.getItem('token') || '';
|
||
|
|
const res = await fetch(url, {
|
||
|
|
headers: {
|
||
|
|
'Content-Type': 'application/json',
|
||
|
|
'X-API-KEY': API_CONFIG.API_KEY,
|
||
|
|
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
||
|
|
}
|
||
|
|
});
|
||
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||
|
|
const response = await res.json();
|
||
|
|
const gate = response.data || response;
|
||
|
|
|
||
|
|
editingGate = { locationCode, gateCode };
|
||
|
|
document.getElementById('gate-location').value = gate.location_code || locationCode;
|
||
|
|
document.getElementById('gate-location').disabled = true;
|
||
|
|
document.getElementById('gate-code').value = gate.gate_code || gateCode;
|
||
|
|
document.getElementById('gate-code').disabled = true;
|
||
|
|
document.getElementById('gate-name').value = gate.name || '';
|
||
|
|
document.getElementById('gate-direction').value = gate.direction || '';
|
||
|
|
document.getElementById('gate-camera').value = gate.camera || '';
|
||
|
|
document.getElementById('gate-active').checked = gate.is_active !== undefined ? gate.is_active : true;
|
||
|
|
|
||
|
|
document.getElementById('gate-modal-title').textContent = 'Edit Gate';
|
||
|
|
document.getElementById('gate-error').style.display = 'none';
|
||
|
|
document.getElementById('gate-success').style.display = 'none';
|
||
|
|
document.getElementById('gate-modal').classList.add('active');
|
||
|
|
|
||
|
|
await loadGates();
|
||
|
|
} catch (err) {
|
||
|
|
alert('Gagal memuat data gate: ' + err.message);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Delete gate
|
||
|
|
window.deleteGate = async function(locationCode, gateCode) {
|
||
|
|
if (!confirm(`Yakin ingin menghapus gate ${gateCode}?`)) return;
|
||
|
|
try {
|
||
|
|
await apiDeleteGate(locationCode, gateCode);
|
||
|
|
await loadGates();
|
||
|
|
alert('Gate berhasil dihapus!');
|
||
|
|
} catch (err) {
|
||
|
|
alert('Gagal menghapus gate: ' + err.message);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Toggle camera on/off
|
||
|
|
window.toggleCamera = async function(locationCode, gateCode, enabled) {
|
||
|
|
try {
|
||
|
|
// Get current gate data
|
||
|
|
const url = `${API_CONFIG.BASE_URL}/retribusi/v1/frontend/gates/${locationCode}/${gateCode}`;
|
||
|
|
const token = localStorage.getItem('token') || '';
|
||
|
|
const res = await fetch(url, {
|
||
|
|
headers: {
|
||
|
|
'Content-Type': 'application/json',
|
||
|
|
'X-API-KEY': API_CONFIG.API_KEY,
|
||
|
|
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
||
|
|
}
|
||
|
|
});
|
||
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||
|
|
const response = await res.json();
|
||
|
|
const gate = response.data || response;
|
||
|
|
|
||
|
|
// If enabling camera but no URL exists, prompt user to add URL
|
||
|
|
if (enabled && (!gate.camera || gate.camera.trim() === '')) {
|
||
|
|
const cameraUrl = prompt('Masukkan URL kamera (HLS):');
|
||
|
|
if (!cameraUrl || cameraUrl.trim() === '') {
|
||
|
|
// Revert toggle
|
||
|
|
const checkbox = document.querySelector(`input[onchange*="${locationCode}"][onchange*="${gateCode}"]`);
|
||
|
|
if (checkbox) checkbox.checked = false;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Update with camera URL
|
||
|
|
await apiUpdateGate(locationCode, gateCode, { camera: cameraUrl.trim() });
|
||
|
|
} else if (!enabled) {
|
||
|
|
// Disable camera (set to null)
|
||
|
|
await apiUpdateGate(locationCode, gateCode, { camera: null });
|
||
|
|
}
|
||
|
|
|
||
|
|
// Reload gates
|
||
|
|
await loadGates();
|
||
|
|
} catch (err) {
|
||
|
|
console.error('toggleCamera error', err);
|
||
|
|
alert('Gagal mengubah status kamera: ' + err.message);
|
||
|
|
// Revert toggle on error
|
||
|
|
const checkbox = document.querySelector(`input[onchange*="${locationCode}"][onchange*="${gateCode}"]`);
|
||
|
|
if (checkbox) checkbox.checked = !enabled;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Init
|
||
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
||
|
|
await loadLocations();
|
||
|
|
await loadGates(); // Load all gates on init
|
||
|
|
});
|
||
|
|
</script>
|
||
|
|
</body>
|
||
|
|
</html>
|