mirror of
https://github.com/mivodev/mivo.git
synced 2026-01-26 05:25:42 +07:00
Initial Release v1.0.0: Full feature set with Docker automation, Nginx/Alpine stack
This commit is contained in:
236
app/Views/hotspot/profiles/add.php
Normal file
236
app/Views/hotspot/profiles/add.php
Normal file
@@ -0,0 +1,236 @@
|
||||
<?php
|
||||
$title = "Add User Profile";
|
||||
require_once ROOT . '/app/Views/layouts/header_main.php';
|
||||
?>
|
||||
|
||||
<div class="max-w-5xl mx-auto">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between mb-8 gap-4">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight" data-i18n="hotspot_profiles.form.add_title">Add Profile</h1>
|
||||
<p class="text-accents-5" data-i18n="hotspot_profiles.form.subtitle" data-i18n-params='{"name": "<?= htmlspecialchars($session) ?>"}'>Create a new hotspot user profile for: <span class="text-foreground font-medium"><?= htmlspecialchars($session) ?></span></p>
|
||||
</div>
|
||||
<a href="/<?= htmlspecialchars($session) ?>/hotspot/profiles" class="btn btn-secondary w-full sm:w-auto justify-center" data-i18n="common.back">
|
||||
<i data-lucide="arrow-left" class="w-4 h-4 mr-2"></i> Back
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<!-- Main Form Column -->
|
||||
<div class="lg:col-span-2">
|
||||
<div class="card p-6 border-accents-2 shadow-sm">
|
||||
<h3 class="text-lg font-semibold mb-6 flex items-center gap-2">
|
||||
<div class="p-2 bg-primary/10 rounded-lg text-primary">
|
||||
<i data-lucide="settings" class="w-5 h-5"></i>
|
||||
</div>
|
||||
<span data-i18n="hotspot_profiles.form.settings">New Profile Settings</span>
|
||||
</h3>
|
||||
|
||||
<form action="/<?= htmlspecialchars($session) ?>/hotspot/profile/store" method="POST" class="space-y-6">
|
||||
<input type="hidden" name="session" value="<?= htmlspecialchars($session) ?>">
|
||||
|
||||
<!-- General Settings Section -->
|
||||
<div class="space-y-4">
|
||||
<h4 class="text-xs font-bold text-accents-5 uppercase tracking-wider border-b border-accents-2 pb-2" data-i18n="hotspot_profiles.form.general">General</h4>
|
||||
|
||||
<!-- Name -->
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm font-medium text-accents-6"><span data-i18n="common.name">Name</span> <span class="text-red-500">*</span></label>
|
||||
<input type="text" name="name" required class="form-input w-full" data-i18n-placeholder="hotspot_profiles.form.name_placeholder" placeholder="e.g. 1Hour-Package">
|
||||
</div>
|
||||
|
||||
<!-- Pools & Shared Users -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm font-medium text-accents-6" data-i18n="hotspot_profiles.form.address_pool">Address Pool</label>
|
||||
<select name="address-pool" class="custom-select w-full" data-search="true">
|
||||
<option value="none" data-i18n="common.forms.none">none</option>
|
||||
<?php foreach ($pools as $pool): ?>
|
||||
<?php if(isset($pool['name'])): ?>
|
||||
<option value="<?= htmlspecialchars($pool['name']) ?>"><?= htmlspecialchars($pool['name']) ?></option>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm font-medium text-accents-6" data-i18n="hotspot_profiles.form.shared_users">Shared Users</label>
|
||||
<div class="relative group">
|
||||
<span class="absolute left-3 top-2.5 text-accents-6 z-10 group-focus-within:text-primary transition-colors pointer-events-none">
|
||||
<i data-lucide="users" class="w-4 h-4"></i>
|
||||
</span>
|
||||
<input type="number" name="shared-users" value="1" min="1" class="form-input pl-10 w-full" placeholder="1">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Limits Section -->
|
||||
<div class="space-y-4">
|
||||
<h4 class="text-xs font-bold text-accents-5 uppercase tracking-wider border-b border-accents-2 pb-2" data-i18n="hotspot_profiles.form.limits_queues">Limits & Queues</h4>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- Rate Limit -->
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm font-medium text-accents-6" data-i18n="hotspot_profiles.form.rate_limit">Rate Limit (Rx/Tx)</label>
|
||||
<div class="relative group">
|
||||
<span class="absolute left-3 top-2.5 text-accents-6 z-10 group-focus-within:text-primary transition-colors pointer-events-none">
|
||||
<i data-lucide="activity" class="w-4 h-4"></i>
|
||||
</span>
|
||||
<input type="text" name="rate-limit" class="form-input pl-10 w-full" data-i18n-placeholder="hotspot_profiles.form.rate_limit_help" placeholder="e.g. 512k/1M">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Parent Queue -->
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm font-medium text-accents-6" data-i18n="hotspot_profiles.form.parent_queue">Parent Queue</label>
|
||||
<select name="parent-queue" class="custom-select w-full" data-search="true">
|
||||
<option value="none" data-i18n="common.forms.none">none</option>
|
||||
<?php foreach ($queues as $q): ?>
|
||||
<option value="<?= htmlspecialchars($q) ?>"><?= htmlspecialchars($q) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pricing & Validity -->
|
||||
<div class="space-y-4">
|
||||
<h4 class="text-xs font-bold text-accents-5 uppercase tracking-wider border-b border-accents-2 pb-2" data-i18n="hotspot_profiles.form.pricing_validity">Pricing & Validity</h4>
|
||||
|
||||
<!-- Expired Mode -->
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm font-medium text-accents-6" data-i18n="hotspot_profiles.form.expired_mode">Expired Mode</label>
|
||||
<select name="expired_mode" id="expired-mode" class="custom-select w-full">
|
||||
<option value="none" data-i18n="common.forms.none" selected>none</option>
|
||||
<option value="rem">Remove</option>
|
||||
<option value="ntf">Notice</option>
|
||||
<option value="remc">Remove & Record</option>
|
||||
<option value="ntfc">Notice & Record</option>
|
||||
</select>
|
||||
<p class="text-xs text-accents-5" data-i18n="hotspot_profiles.form.expired_mode_help">Action when validity expires.</p>
|
||||
</div>
|
||||
|
||||
<!-- Validity (Hidden by default unless mode selected) -->
|
||||
<div id="validity-group" class="hidden space-y-1 transition-all duration-300">
|
||||
<label class="block text-sm font-medium text-accents-6" data-i18n="hotspot_profiles.form.validity">Validity</label>
|
||||
<div class="flex w-full">
|
||||
<div class="relative flex-1 group">
|
||||
<span class="absolute right-3 top-2.5 text-accents-6 z-10 text-xs font-bold pointer-events-none">D</span>
|
||||
<input type="number" name="validity_d" min="0" class="form-input w-full pr-8 rounded-r-none border-r-0 focus:ring-2 focus:ring-primary/20 focus:z-10 transition-all font-mono text-center" placeholder="0">
|
||||
</div>
|
||||
<div class="relative flex-1 group">
|
||||
<span class="absolute right-3 top-2.5 text-accents-6 z-10 text-xs font-bold pointer-events-none">H</span>
|
||||
<input type="number" name="validity_h" min="0" class="form-input w-full pr-8 rounded-none border-r-0 focus:ring-2 focus:ring-primary/20 focus:z-10 transition-all font-mono text-center" placeholder="0">
|
||||
</div>
|
||||
<div class="relative flex-1 group">
|
||||
<span class="absolute right-3 top-2.5 text-accents-6 z-10 text-xs font-bold pointer-events-none">M</span>
|
||||
<input type="number" name="validity_m" min="0" class="form-input w-full pr-8 rounded-l-none focus:ring-2 focus:ring-primary/20 focus:z-10 transition-all font-mono text-center" placeholder="0">
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-accents-5" data-i18n="hotspot_profiles.form.validity_help">Days / Hours / Minutes</p>
|
||||
</div>
|
||||
|
||||
<!-- Prices -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm font-medium text-accents-6" data-i18n="hotspot_profiles.form.price">Price (Rp)</label>
|
||||
<div class="relative group">
|
||||
<span class="absolute left-3 top-2.5 text-accents-6 z-10 group-focus-within:text-primary transition-colors pointer-events-none">
|
||||
<i data-lucide="tag" class="w-4 h-4"></i>
|
||||
</span>
|
||||
<input type="number" name="price" class="form-input pl-10 w-full" placeholder="e.g. 5000">
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm font-medium text-accents-6" data-i18n="hotspot_profiles.form.selling_price">Selling Price (Rp)</label>
|
||||
<div class="relative group">
|
||||
<span class="absolute left-3 top-2.5 text-accents-6 z-10 group-focus-within:text-primary transition-colors pointer-events-none">
|
||||
<i data-lucide="shopping-bag" class="w-4 h-4"></i>
|
||||
</span>
|
||||
<input type="number" name="selling_price" class="form-input pl-10 w-full" placeholder="e.g. 7000">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lock User -->
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm font-medium text-accents-6" data-i18n="hotspot_profiles.form.lock_user">Lock User</label>
|
||||
<select name="lock_user" class="custom-select w-full">
|
||||
<option value="Disable" data-i18n="common.forms.disabled">Disable</option>
|
||||
<option value="Enable" data-i18n="common.forms.enabled">Enable</option>
|
||||
</select>
|
||||
<p class="text-xs text-accents-5" data-i18n="hotspot_profiles.form.lock_user_help">Lock user to one specific MAC address.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-6 border-t border-accents-2 flex justify-end gap-3">
|
||||
<a href="/<?= htmlspecialchars($session) ?>/hotspot/profiles" class="btn btn-secondary" data-i18n="common.cancel">Cancel</a>
|
||||
<button type="submit" class="btn btn-primary px-8 shadow-lg shadow-primary/20 hover:shadow-primary/40 transition-shadow">
|
||||
<i data-lucide="save" class="w-4 h-4 mr-2"></i>
|
||||
<span data-i18n="hotspot_profiles.form.save">Save Profile</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sticky Quick Tips Column -->
|
||||
<div class="lg:col-span-1">
|
||||
<div class="sticky top-6 space-y-6">
|
||||
<div class="card p-6 bg-accents-1/50 border-accents-2 border-dashed">
|
||||
<h3 class="font-semibold mb-4 flex items-center gap-2 text-foreground" data-i18n="hotspot_profiles.form.quick_tips">
|
||||
<i data-lucide="lightbulb" class="w-4 h-4 text-yellow-500"></i>
|
||||
Quick Tips
|
||||
</h3>
|
||||
<ul class="text-sm text-accents-5 space-y-3">
|
||||
<li class="flex gap-2">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-primary mt-1.5 flex-shrink-0"></span>
|
||||
<span data-i18n="hotspot_profiles.form.tip_rate_limit"><strong>Rate Limit</strong>: Rx/Tx (Upload/Download). Example: <code>512k/1M</code></span>
|
||||
</li>
|
||||
<li class="flex gap-2">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-primary mt-1.5 flex-shrink-0"></span>
|
||||
<span data-i18n="hotspot_profiles.form.tip_expired_mode"><strong>Expired Mode</strong>: Select 'Remove' or 'Notice' to enable Validity.</span>
|
||||
</li>
|
||||
<li class="flex gap-2">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-primary mt-1.5 flex-shrink-0"></span>
|
||||
<span data-i18n="hotspot_profiles.form.tip_parent_queue"><strong>Parent Queue</strong>: Assigns users to a specific parent queue for bandwidth management.</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once ROOT . '/app/Views/layouts/footer_main.php'; ?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Custom Select Init
|
||||
if (typeof CustomSelect !== 'undefined') {
|
||||
document.querySelectorAll('.custom-select').forEach(select => {
|
||||
new CustomSelect(select);
|
||||
});
|
||||
}
|
||||
|
||||
// Validity Toggle Logic
|
||||
const modeSelect = document.getElementById('expired-mode');
|
||||
const validityGroup = document.getElementById('validity-group');
|
||||
|
||||
function toggleValidity() {
|
||||
if (!modeSelect || !validityGroup) return;
|
||||
|
||||
// Show validity ONLY if mode != none
|
||||
if (modeSelect.value === 'none') {
|
||||
validityGroup.classList.add('hidden');
|
||||
} else {
|
||||
validityGroup.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
if (modeSelect) {
|
||||
// Initial check
|
||||
toggleValidity();
|
||||
// Listen for changes
|
||||
modeSelect.addEventListener('change', toggleValidity);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
241
app/Views/hotspot/profiles/edit.php
Normal file
241
app/Views/hotspot/profiles/edit.php
Normal file
@@ -0,0 +1,241 @@
|
||||
<?php
|
||||
$title = "Edit User Profile";
|
||||
require_once ROOT . '/app/Views/layouts/header_main.php';
|
||||
?>
|
||||
|
||||
<div class="max-w-5xl mx-auto">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between mb-8 gap-4">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight" data-i18n="hotspot_profiles.form.edit_title">Edit Profile</h1>
|
||||
<p class="text-accents-5" data-i18n="hotspot_profiles.form.edit_subtitle" data-i18n-params='{"name": "<?= htmlspecialchars($profile['name'] ?? '') ?>"}'>Edit hotspot user profile: <span class="text-foreground font-medium"><?= htmlspecialchars($profile['name'] ?? '') ?></span></p>
|
||||
</div>
|
||||
<a href="/<?= htmlspecialchars($session) ?>/hotspot/profiles" class="btn btn-secondary w-full sm:w-auto justify-center" data-i18n="common.back">
|
||||
<i data-lucide="arrow-left" class="w-4 h-4 mr-2"></i> Back
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<!-- Main Form Column -->
|
||||
<div class="lg:col-span-2">
|
||||
<div class="card p-6 border-accents-2 shadow-sm">
|
||||
<h3 class="text-lg font-semibold mb-6 flex items-center gap-2">
|
||||
<div class="p-2 bg-primary/10 rounded-lg text-primary">
|
||||
<i data-lucide="edit" class="w-5 h-5"></i>
|
||||
</div>
|
||||
<span data-i18n="hotspot_profiles.form.edit_title">Edit Profile</span>
|
||||
</h3>
|
||||
|
||||
<form action="/<?= htmlspecialchars($session) ?>/hotspot/profile/update" method="POST" class="space-y-6">
|
||||
<input type="hidden" name="session" value="<?= htmlspecialchars($session) ?>">
|
||||
<input type="hidden" name="id" value="<?= htmlspecialchars($profile['.id']) ?>">
|
||||
|
||||
<!-- General Settings Section -->
|
||||
<div class="space-y-4">
|
||||
<h4 class="text-xs font-bold text-accents-5 uppercase tracking-wider border-b border-accents-2 pb-2" data-i18n="hotspot_profiles.form.general">General</h4>
|
||||
|
||||
<!-- Name -->
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm font-medium text-accents-6"><span data-i18n="common.name">Name</span> <span class="text-red-500">*</span></label>
|
||||
<input type="text" name="name" value="<?= htmlspecialchars($profile['name'] ?? '') ?>" required class="form-input w-full" data-i18n-placeholder="hotspot_profiles.form.name_placeholder" placeholder="e.g. 1Hour-Package">
|
||||
</div>
|
||||
|
||||
<!-- Pools & Shared Users -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm font-medium text-accents-6" data-i18n="hotspot_profiles.form.address_pool">Address Pool</label>
|
||||
<select name="address-pool" class="custom-select w-full" data-search="true">
|
||||
<option value="none" data-i18n="common.forms.none" <?= ($profile['address-pool'] ?? 'none') === 'none' ? 'selected' : '' ?>>none</option>
|
||||
<?php foreach ($pools as $pool): ?>
|
||||
<?php if(isset($pool['name'])): ?>
|
||||
<option value="<?= htmlspecialchars($pool['name']) ?>" <?= ($profile['address-pool'] ?? '') === $pool['name'] ? 'selected' : '' ?>>
|
||||
<?= htmlspecialchars($pool['name']) ?>
|
||||
</option>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm font-medium text-accents-6" data-i18n="hotspot_profiles.form.shared_users">Shared Users</label>
|
||||
<div class="relative group">
|
||||
<span class="absolute left-3 top-2.5 text-accents-6 z-10 group-focus-within:text-primary transition-colors pointer-events-none">
|
||||
<i data-lucide="users" class="w-4 h-4"></i>
|
||||
</span>
|
||||
<input type="number" name="shared-users" value="<?= htmlspecialchars($profile['shared-users'] ?? '1') ?>" min="1" class="form-input pl-10 w-full" placeholder="1">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Limits Section -->
|
||||
<div class="space-y-4">
|
||||
<h4 class="text-xs font-bold text-accents-5 uppercase tracking-wider border-b border-accents-2 pb-2" data-i18n="hotspot_profiles.form.limits_queues">Limits & Queues</h4>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- Rate Limit -->
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm font-medium text-accents-6" data-i18n="hotspot_profiles.form.rate_limit">Rate Limit (Rx/Tx)</label>
|
||||
<div class="relative group">
|
||||
<span class="absolute left-3 top-2.5 text-accents-6 z-10 group-focus-within:text-primary transition-colors pointer-events-none">
|
||||
<i data-lucide="activity" class="w-4 h-4"></i>
|
||||
</span>
|
||||
<input type="text" name="rate-limit" value="<?= htmlspecialchars($profile['rate-limit'] ?? '') ?>" class="form-input pl-10 w-full" data-i18n-placeholder="hotspot_profiles.form.rate_limit_help" placeholder="e.g. 512k/1M">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Parent Queue -->
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm font-medium text-accents-6" data-i18n="hotspot_profiles.form.parent_queue">Parent Queue</label>
|
||||
<select name="parent-queue" class="custom-select w-full" data-search="true">
|
||||
<option value="none" data-i18n="common.forms.none" <?= ($profile['parent-queue'] ?? 'none') === 'none' ? 'selected' : '' ?>>none</option>
|
||||
<?php foreach ($queues as $q): ?>
|
||||
<option value="<?= htmlspecialchars($q) ?>" <?= ($profile['parent-queue'] ?? '') === $q ? 'selected' : '' ?>>
|
||||
<?= htmlspecialchars($q) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pricing & Validity -->
|
||||
<div class="space-y-4">
|
||||
<h4 class="text-xs font-bold text-accents-5 uppercase tracking-wider border-b border-accents-2 pb-2" data-i18n="hotspot_profiles.form.pricing_validity">Pricing & Validity</h4>
|
||||
|
||||
<!-- Expired Mode -->
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm font-medium text-accents-6" data-i18n="hotspot_profiles.form.expired_mode">Expired Mode</label>
|
||||
<?php $exMode = $profile['meta']['expired_mode'] ?? 'none'; ?>
|
||||
<select name="expired_mode" id="expired-mode" class="custom-select w-full">
|
||||
<option value="none" data-i18n="common.forms.none" <?= ($exMode === 'none' || $exMode === '') ? 'selected' : '' ?>>none</option>
|
||||
<option value="rem" <?= $exMode === 'rem' ? 'selected' : '' ?>>Remove</option>
|
||||
<option value="ntf" <?= $exMode === 'ntf' ? 'selected' : '' ?>>Notice</option>
|
||||
<option value="remc" <?= $exMode === 'remc' ? 'selected' : '' ?>>Remove & Record</option>
|
||||
<option value="ntfc" <?= $exMode === 'ntfc' ? 'selected' : '' ?>>Notice & Record</option>
|
||||
</select>
|
||||
<p class="text-xs text-accents-5" data-i18n="hotspot_profiles.form.expired_mode_help">Action when validity expires.</p>
|
||||
</div>
|
||||
|
||||
<!-- Validity (Hidden by default unless mode selected) -->
|
||||
<div id="validity-group" class="hidden space-y-1 transition-all duration-300">
|
||||
<label class="block text-sm font-medium text-accents-6" data-i18n="hotspot_profiles.form.validity">Validity</label>
|
||||
<div class="flex w-full">
|
||||
<div class="relative flex-1 group">
|
||||
<span class="absolute right-3 top-2.5 text-accents-6 z-10 text-xs font-bold pointer-events-none">D</span>
|
||||
<input type="number" name="validity_d" value="<?= htmlspecialchars($profile['val_d'] ?? '') ?>" min="0" class="form-input w-full pr-8 rounded-r-none border-r-0 focus:ring-2 focus:ring-primary/20 focus:z-10 transition-all font-mono text-center" placeholder="0">
|
||||
</div>
|
||||
<div class="relative flex-1 group">
|
||||
<span class="absolute right-3 top-2.5 text-accents-6 z-10 text-xs font-bold pointer-events-none">H</span>
|
||||
<input type="number" name="validity_h" value="<?= htmlspecialchars($profile['val_h'] ?? '') ?>" min="0" class="form-input w-full pr-8 rounded-none border-r-0 focus:ring-2 focus:ring-primary/20 focus:z-10 transition-all font-mono text-center" placeholder="0">
|
||||
</div>
|
||||
<div class="relative flex-1 group">
|
||||
<span class="absolute right-3 top-2.5 text-accents-6 z-10 text-xs font-bold pointer-events-none">M</span>
|
||||
<input type="number" name="validity_m" value="<?= htmlspecialchars($profile['val_m'] ?? '') ?>" min="0" class="form-input w-full pr-8 rounded-l-none focus:ring-2 focus:ring-primary/20 focus:z-10 transition-all font-mono text-center" placeholder="0">
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-accents-5" data-i18n="hotspot_profiles.form.validity_help">Days / Hours / Minutes</p>
|
||||
</div>
|
||||
|
||||
<!-- Prices -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm font-medium text-accents-6" data-i18n="hotspot_profiles.form.price">Price (Rp)</label>
|
||||
<div class="relative group">
|
||||
<span class="absolute left-3 top-2.5 text-accents-6 z-10 group-focus-within:text-primary transition-colors pointer-events-none">
|
||||
<i data-lucide="tag" class="w-4 h-4"></i>
|
||||
</span>
|
||||
<input type="number" name="price" value="<?= htmlspecialchars($profile['meta']['price'] ?? '') ?>" class="form-input pl-10 w-full" placeholder="e.g. 5000">
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm font-medium text-accents-6" data-i18n="hotspot_profiles.form.selling_price">Selling Price (Rp)</label>
|
||||
<div class="relative group">
|
||||
<span class="absolute left-3 top-2.5 text-accents-6 z-10 group-focus-within:text-primary transition-colors pointer-events-none">
|
||||
<i data-lucide="shopping-bag" class="w-4 h-4"></i>
|
||||
</span>
|
||||
<input type="number" name="selling_price" value="<?= htmlspecialchars($profile['meta']['selling_price'] ?? '') ?>" class="form-input pl-10 w-full" placeholder="e.g. 7000">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lock User -->
|
||||
<div class="space-y-1">
|
||||
<label class="block text-sm font-medium text-accents-6" data-i18n="hotspot_profiles.form.lock_user">Lock User</label>
|
||||
<?php $lock = $profile['meta']['lock_user'] ?? 'Disable'; ?>
|
||||
<select name="lock_user" class="custom-select w-full">
|
||||
<option value="Disable" data-i18n="common.forms.disabled" <?= $lock === 'Disable' ? 'selected' : '' ?>>Disable</option>
|
||||
<option value="Enable" data-i18n="common.forms.enabled" <?= $lock === 'Enable' ? 'selected' : '' ?>>Enable</option>
|
||||
</select>
|
||||
<p class="text-xs text-accents-5" data-i18n="hotspot_profiles.form.lock_user_help">Lock user to one specific MAC address.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-6 border-t border-accents-2 flex justify-end gap-3">
|
||||
<a href="/<?= htmlspecialchars($session) ?>/hotspot/profiles" class="btn btn-secondary" data-i18n="common.cancel">Cancel</a>
|
||||
<button type="submit" class="btn btn-primary px-8 shadow-lg shadow-primary/20 hover:shadow-primary/40 transition-shadow">
|
||||
<i data-lucide="save" class="w-4 h-4 mr-2"></i>
|
||||
<span data-i18n="common.forms.save_changes">Save Changes</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sticky Quick Tips Column -->
|
||||
<div class="lg:col-span-1">
|
||||
<div class="sticky top-6 space-y-6">
|
||||
<div class="card p-6 bg-accents-1/50 border-accents-2 border-dashed">
|
||||
<h3 class="font-semibold mb-4 flex items-center gap-2 text-foreground" data-i18n="hotspot_profiles.form.quick_tips">
|
||||
<i data-lucide="lightbulb" class="w-4 h-4 text-yellow-500"></i>
|
||||
Quick Tips
|
||||
</h3>
|
||||
<ul class="text-sm text-accents-5 space-y-3">
|
||||
<li class="flex gap-2">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-primary mt-1.5 flex-shrink-0"></span>
|
||||
<span data-i18n="hotspot_profiles.form.tip_rate_limit"><strong>Rate Limit</strong>: Rx/Tx (Upload/Download). Example: <code>512k/1M</code></span>
|
||||
</li>
|
||||
<li class="flex gap-2">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-primary mt-1.5 flex-shrink-0"></span>
|
||||
<span data-i18n="hotspot_profiles.form.tip_expired_mode"><strong>Expired Mode</strong>: Select 'Remove' or 'Notice' to enable Validity.</span>
|
||||
</li>
|
||||
<li class="flex gap-2">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-primary mt-1.5 flex-shrink-0"></span>
|
||||
<span data-i18n="hotspot_profiles.form.tip_parent_queue"><strong>Parent Queue</strong>: Assigns users to a specific parent queue for bandwidth management.</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once ROOT . '/app/Views/layouts/footer_main.php'; ?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Custom Select Init
|
||||
if (typeof CustomSelect !== 'undefined') {
|
||||
document.querySelectorAll('.custom-select').forEach(select => {
|
||||
new CustomSelect(select);
|
||||
});
|
||||
}
|
||||
|
||||
// Validity Toggle Logic
|
||||
const modeSelect = document.getElementById('expired-mode');
|
||||
const validityGroup = document.getElementById('validity-group');
|
||||
|
||||
function toggleValidity() {
|
||||
if (!modeSelect || !validityGroup) return;
|
||||
|
||||
// Show validity ONLY if mode != none
|
||||
if (modeSelect.value === 'none') {
|
||||
validityGroup.classList.add('hidden');
|
||||
} else {
|
||||
validityGroup.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// Initial check
|
||||
toggleValidity();
|
||||
// Listen for changes
|
||||
modeSelect.addEventListener('change', toggleValidity);
|
||||
});
|
||||
</script>
|
||||
311
app/Views/hotspot/profiles/index.php
Normal file
311
app/Views/hotspot/profiles/index.php
Normal file
@@ -0,0 +1,311 @@
|
||||
<?php
|
||||
$title = "User Profiles";
|
||||
require_once ROOT . '/app/Views/layouts/header_main.php';
|
||||
|
||||
// Prepare Filters Data
|
||||
$uniqueModes = [];
|
||||
if (!empty($profiles)) {
|
||||
foreach ($profiles as $p) {
|
||||
$m = $p['meta']['expired_mode_formatted'] ?? '';
|
||||
if(!empty($m)) $uniqueModes[$m] = $m;
|
||||
}
|
||||
}
|
||||
sort($uniqueModes);
|
||||
?>
|
||||
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between mb-8 gap-4">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight" data-i18n="hotspot_profiles.title">User Profiles</h1>
|
||||
<p class="text-accents-5"><span data-i18n="hotspot_profiles.subtitle">Manage hotspot rate limits and pricing for session</span> <span class="text-foreground font-medium"><?= htmlspecialchars($session) ?></span></p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<a href="/<?= htmlspecialchars($session) ?>/dashboard" class="btn btn-secondary">
|
||||
<i data-lucide="arrow-left" class="w-4 h-4 mr-2"></i> <span data-i18n="common.dashboard">Dashboard</span>
|
||||
</a>
|
||||
<a href="/<?= htmlspecialchars($session) ?>/hotspot/profile/add" class="btn btn-primary">
|
||||
<i data-lucide="plus" class="w-4 h-4 mr-2"></i> <span data-i18n="hotspot_profiles.add_profile">Add Profile</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (isset($error)): ?>
|
||||
<div class="bg-red-50 text-red-600 p-4 rounded-lg mb-6 flex items-center">
|
||||
<i data-lucide="alert-circle" class="w-5 h-5 mr-3"></i>
|
||||
<?= htmlspecialchars($error) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Filters & Table -->
|
||||
<div class="space-y-4">
|
||||
<!-- Filter Bar -->
|
||||
<div class="flex flex-col md:flex-row gap-4 justify-between items-center">
|
||||
<!-- Search -->
|
||||
<div class="input-group md:w-64 z-10">
|
||||
<div class="input-icon">
|
||||
<i data-lucide="search" class="h-4 w-4"></i>
|
||||
</div>
|
||||
<input type="text" id="global-search" class="form-input-search w-full" placeholder="Search profile...">
|
||||
</div>
|
||||
|
||||
<!-- Dropdowns -->
|
||||
<div class="flex gap-2 w-full md:w-auto">
|
||||
<div class="w-48">
|
||||
<select id="filter-mode" class="custom-select form-filter" data-search="true">
|
||||
<option value="" data-i18n="hotspot_profiles.all_modes">All Expired Modes</option>
|
||||
<?php foreach($uniqueModes as $m): ?>
|
||||
<option value="<?= htmlspecialchars($m) ?>"><?= htmlspecialchars($m) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<table class="table-glass" id="profiles-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-sort="name" class="sortable cursor-pointer hover:text-foreground select-none" data-i18n="hotspot_profiles.name">Name</th>
|
||||
<th data-i18n="hotspot_profiles.shared_users">Shared Users</th>
|
||||
<th data-i18n="hotspot_profiles.rate_limit">Rate Limit</th>
|
||||
<th data-i18n="hotspot_profiles.parent_queue">Parent Queue</th>
|
||||
<th data-sort="mode" class="sortable cursor-pointer hover:text-foreground select-none" data-i18n="hotspot_profiles.expired_mode">Expired Mode</th>
|
||||
<th data-i18n="hotspot_profiles.validity">Validity</th>
|
||||
<th data-i18n="hotspot_profiles.price">Price</th>
|
||||
<th data-i18n="hotspot_profiles.selling_price">Selling Price</th>
|
||||
<th data-i18n="hotspot_profiles.lock_user">Lock User</th>
|
||||
<th class="text-right" data-i18n="common.actions">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="table-body">
|
||||
<?php if (!empty($profiles)): ?>
|
||||
<?php foreach ($profiles as $profile): ?>
|
||||
<tr class="table-row-item"
|
||||
data-name="<?= strtolower($profile['name'] ?? '') ?>"
|
||||
data-mode="<?= htmlspecialchars($profile['meta']['expired_mode_formatted'] ?? '') ?>">
|
||||
|
||||
<td>
|
||||
<div class="flex items-center">
|
||||
<div class="h-8 w-8 rounded bg-purple-100 dark:bg-purple-900/30 text-purple-600 dark:text-purple-400 flex items-center justify-center text-xs font-bold mr-3">
|
||||
<i data-lucide="ticket" class="w-4 h-4"></i>
|
||||
</div>
|
||||
<div class="text-sm font-medium text-foreground">
|
||||
<a href="/<?= htmlspecialchars($session) ?>/hotspot/profile/edit/<?= $profile['.id'] ?>" class="hover:underline hover:text-purple-600 dark:hover:text-purple-400">
|
||||
<?= htmlspecialchars($profile['name'] ?? '-') ?>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="text-sm font-semibold"><?= htmlspecialchars($profile['shared-users'] ?? '1') ?></span>
|
||||
<span class="text-xs text-accents-5">dev</span>
|
||||
</td>
|
||||
<td>
|
||||
<?php if(!empty($profile['rate-limit'])): ?>
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400">
|
||||
<?= htmlspecialchars($profile['rate-limit']) ?>
|
||||
</span>
|
||||
<?php else: ?>
|
||||
<span class="text-xs text-accents-4">-</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="text-sm text-accents-6">
|
||||
<?= htmlspecialchars($profile['parent-queue'] ?? '-') ?>
|
||||
</td>
|
||||
<td class="text-sm text-accents-6">
|
||||
<?= htmlspecialchars($profile['meta']['expired_mode_formatted'] ?? '') ?>
|
||||
</td>
|
||||
<td class="text-sm text-accents-6">
|
||||
<?= htmlspecialchars($profile['meta']['validity'] ?? '') ?>
|
||||
</td>
|
||||
<td class="text-sm text-accents-6">
|
||||
<?= htmlspecialchars($profile['meta']['price'] ?? '') ?>
|
||||
</td>
|
||||
<td class="text-sm text-accents-6">
|
||||
<?= htmlspecialchars($profile['meta']['selling_price'] ?? '') ?>
|
||||
</td>
|
||||
<td class="text-sm text-accents-6">
|
||||
<?= htmlspecialchars($profile['meta']['lock_user'] ?? '') ?>
|
||||
</td>
|
||||
|
||||
<td class="text-right text-sm font-medium">
|
||||
<div class="flex items-center justify-end gap-2 table-actions-reveal">
|
||||
<a href="/<?= htmlspecialchars($session) ?>/hotspot/profile/edit/<?= $profile['.id'] ?>" class="btn bg-blue-50 hover:bg-blue-100 text-blue-600 dark:bg-blue-900/20 dark:hover:bg-blue-900/40 border-transparent h-8 px-2 rounded transition-colors" title="Edit">
|
||||
<i data-lucide="edit-2" class="w-4 h-4"></i>
|
||||
</a>
|
||||
<form action="/<?= htmlspecialchars($session) ?>/hotspot/profile/delete" method="POST" onsubmit="event.preventDefault(); Mivo.confirm(window.i18n ? window.i18n.t('hotspot_profiles.title') : 'Delete Profile?', window.i18n ? window.i18n.t('common.confirm_delete') : 'Are you sure you want to delete profile <?= $profile['name'] ?>?', window.i18n ? window.i18n.t('common.delete') : 'Delete', window.i18n ? window.i18n.t('common.cancel') : 'Cancel').then(res => { if(res) this.submit(); });" class="inline">
|
||||
<input type="hidden" name="session" value="<?= htmlspecialchars($session) ?>">
|
||||
<input type="hidden" name="id" value="<?= $profile['.id'] ?>">
|
||||
<button type="submit" class="btn bg-red-50 hover:bg-red-100 text-red-600 dark:bg-red-900/20 dark:hover:bg-red-900/40 border-transparent h-8 px-2 rounded transition-colors" title="Delete">
|
||||
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="px-6 py-4 border-t border-white/10 flex items-center justify-between" id="pagination-controls">
|
||||
<div class="text-sm text-accents-5">
|
||||
Showing <span id="start-idx" class="font-medium text-foreground">0</span> to <span id="end-idx" class="font-medium text-foreground">0</span> of <span id="total-count" class="font-medium text-foreground">0</span> profiles
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button id="prev-btn" class="btn btn-sm btn-secondary" disabled data-i18n="common.previous">Previous</button>
|
||||
<div id="page-numbers" class="flex gap-1"></div>
|
||||
<button id="next-btn" class="btn btn-sm btn-secondary" disabled data-i18n="common.next">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once ROOT . '/app/Views/layouts/footer_main.php'; ?>
|
||||
<script>
|
||||
class TableManager {
|
||||
constructor(rows, itemsPerPage = 10) {
|
||||
this.allRows = Array.from(rows);
|
||||
this.filteredRows = this.allRows;
|
||||
this.itemsPerPage = itemsPerPage;
|
||||
this.currentPage = 1;
|
||||
|
||||
this.elements = {
|
||||
body: document.getElementById('table-body'),
|
||||
startIdx: document.getElementById('start-idx'),
|
||||
endIdx: document.getElementById('end-idx'),
|
||||
totalCount: document.getElementById('total-count'),
|
||||
prevBtn: document.getElementById('prev-btn'),
|
||||
nextBtn: document.getElementById('next-btn'),
|
||||
pageNumbers: document.getElementById('page-numbers')
|
||||
};
|
||||
|
||||
this.filters = {
|
||||
search: '',
|
||||
mode: ''
|
||||
};
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Translate placeholder
|
||||
const searchInput = document.getElementById('global-search');
|
||||
if (searchInput && window.i18n) {
|
||||
searchInput.placeholder = window.i18n.t('common.table.search_placeholder');
|
||||
}
|
||||
this.setupListeners();
|
||||
this.update();
|
||||
|
||||
// Listen for language change
|
||||
window.addEventListener('languageChanged', () => {
|
||||
const searchInput = document.getElementById('global-search');
|
||||
if (searchInput && window.i18n) {
|
||||
searchInput.placeholder = window.i18n.t('common.table.search_placeholder');
|
||||
}
|
||||
this.render();
|
||||
});
|
||||
}
|
||||
|
||||
setupListeners() {
|
||||
document.getElementById('global-search').addEventListener('input', (e) => {
|
||||
this.filters.search = e.target.value.toLowerCase();
|
||||
this.currentPage = 1;
|
||||
this.update();
|
||||
});
|
||||
|
||||
this.elements.prevBtn.addEventListener('click', () => {
|
||||
if (this.currentPage > 1) {
|
||||
this.currentPage--;
|
||||
this.render();
|
||||
}
|
||||
});
|
||||
|
||||
this.elements.nextBtn.addEventListener('click', () => {
|
||||
const maxPage = Math.ceil(this.filteredRows.length / this.itemsPerPage);
|
||||
if (this.currentPage < maxPage) {
|
||||
this.currentPage++;
|
||||
this.render();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('filter-mode').addEventListener('change', (e) => {
|
||||
this.filters.mode = e.target.value;
|
||||
this.currentPage = 1;
|
||||
this.update();
|
||||
});
|
||||
}
|
||||
|
||||
update() {
|
||||
this.filteredRows = this.allRows.filter(row => {
|
||||
const name = row.dataset.name || '';
|
||||
const mode = row.dataset.mode || '';
|
||||
|
||||
if (this.filters.search && !name.includes(this.filters.search)) return false;
|
||||
if (this.filters.mode && mode !== this.filters.mode) return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
this.render();
|
||||
}
|
||||
|
||||
render() {
|
||||
const total = this.filteredRows.length;
|
||||
const maxPage = Math.ceil(total / this.itemsPerPage) || 1;
|
||||
|
||||
if (this.currentPage > maxPage) this.currentPage = maxPage;
|
||||
|
||||
const start = (this.currentPage - 1) * this.itemsPerPage;
|
||||
const end = Math.min(start + this.itemsPerPage, total);
|
||||
|
||||
this.elements.startIdx.textContent = total === 0 ? 0 : start + 1;
|
||||
this.elements.endIdx.textContent = end;
|
||||
this.elements.totalCount.textContent = total;
|
||||
|
||||
// Update Text (Use Translation)
|
||||
if (window.i18n && document.getElementById('pagination-controls')) {
|
||||
const text = window.i18n.t('common.table.showing', {
|
||||
start: total === 0 ? 0 : start + 1,
|
||||
end: end,
|
||||
total: total
|
||||
});
|
||||
// Find and update the text node if possible
|
||||
const container = document.getElementById('pagination-controls').querySelector('.text-accents-5');
|
||||
if(container) {
|
||||
container.innerHTML = text.replace('{start}', `<span class="font-medium text-foreground">${total === 0 ? 0 : start + 1}</span>`)
|
||||
.replace('{end}', `<span class="font-medium text-foreground">${end}</span>`)
|
||||
.replace('{total}', `<span class="font-medium text-foreground">${total}</span>`);
|
||||
}
|
||||
}
|
||||
|
||||
this.elements.body.innerHTML = '';
|
||||
|
||||
const pageRows = this.filteredRows.slice(start, end);
|
||||
pageRows.forEach(row => this.elements.body.appendChild(row));
|
||||
|
||||
this.elements.prevBtn.disabled = this.currentPage === 1;
|
||||
this.elements.nextBtn.disabled = this.currentPage === maxPage || total === 0;
|
||||
|
||||
if (this.elements.pageNumbers) {
|
||||
const pageText = window.i18n ? window.i18n.t('common.page_of', {current: this.currentPage, total: maxPage}) : `Page ${this.currentPage} of ${maxPage}`;
|
||||
this.elements.pageNumbers.innerHTML = `<span class="px-3 py-1 text-sm font-medium bg-accents-2 rounded text-accents-6">${pageText}</span>`;
|
||||
}
|
||||
|
||||
if (typeof lucide !== 'undefined') {
|
||||
lucide.createIcons();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (typeof CustomSelect !== 'undefined') {
|
||||
document.querySelectorAll('.custom-select').forEach(select => {
|
||||
new CustomSelect(select);
|
||||
});
|
||||
}
|
||||
|
||||
const rows = document.querySelectorAll('.table-row-item');
|
||||
new TableManager(rows, 10);
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user