feat(ui): enhance user list responsiveness on mobile and tablet

Improves usability of the user list for smaller screens with responsive
detail views and filter controls.

- Added collapsible detail rows to show key user info (Status, Traffic, Expiry, etc.) via toggle button.
- Adjusted table column visibility using Bootstrap (`d-none`, `d-md-table-cell`).
- Updated JS event handlers and column indices for responsive layout support.
- Replaced filter button group with a hamburger dropdown on mobile/tablet.
- Used Bootstrap responsive display classes to toggle between dropdown and full button set.
- Optimized card header space for smaller devices.
This commit is contained in:
ReturnFI
2025-09-20 12:58:38 +00:00
parent 6eccaa58c1
commit 51f375d1e8

View File

@ -2,6 +2,28 @@
{% block title %}Users{% endblock %} {% block title %}Users{% endblock %}
{% block styles %}
<style>
.user-details-content p {
margin-bottom: 0.5rem;
display: flex;
justify-content: space-between;
border-bottom: 1px solid #dee2e6;
padding-bottom: 0.5rem;
}
.user-details-content p:last-child {
border-bottom: none;
}
.user-details-content strong {
margin-right: 10px;
}
.user-details-actions .btn {
margin-right: 5px;
margin-bottom: 5px;
}
</style>
{% endblock %}
{% block content %} {% block content %}
<div class="content-header"> <div class="content-header">
<div class="container-fluid"> <div class="container-fluid">
@ -20,27 +42,42 @@
<h3 class="card-title">User List {% if users %}({{ users|length }}){% endif %}</h3> <h3 class="card-title">User List {% if users %}({{ users|length }}){% endif %}</h3>
<div class="card-tools d-flex align-items-center flex-wrap"> <div class="card-tools d-flex align-items-center flex-wrap">
<div class="mr-2 mb-2"> <!-- Mobile Filter Dropdown -->
<div class="dropdown d-md-none mr-2 mb-2">
<button class="btn btn-sm btn-default dropdown-toggle" type="button" id="filterDropdown" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fas fa-filter"></i>
</button>
<div class="dropdown-menu" aria-labelledby="filterDropdown">
<a class="dropdown-item filter-button" href="#" data-filter="all"><i class="fas fa-list fa-fw mr-2"></i> All</a>
<a class="dropdown-item filter-button" href="#" data-filter="on-hold"><i class="fas fa-pause-circle fa-fw mr-2 text-warning"></i> Hold</a>
<a class="dropdown-item filter-button" href="#" data-filter="online"><i class="fas fa-wifi fa-fw mr-2 text-info"></i> Online</a>
<a class="dropdown-item filter-button" href="#" data-filter="enable"><i class="fas fa-check fa-fw mr-2 text-success"></i> Enable</a>
<a class="dropdown-item filter-button" href="#" data-filter="disable"><i class="fas fa-ban fa-fw mr-2 text-danger"></i> Disable</a>
</div>
</div>
<!-- Desktop Filter Buttons -->
<div class="mr-2 mb-2 d-none d-md-block">
<button type="button" class="btn btn-sm btn-default filter-button" data-filter="all"> <button type="button" class="btn btn-sm btn-default filter-button" data-filter="all">
<i class="fas fa-list"></i> All <i class="fas fa-list"></i> All
</button> </button>
</div> </div>
<div class="mr-2 mb-2"> <div class="mr-2 mb-2 d-none d-md-block">
<button type="button" class="btn btn-sm btn-warning filter-button" data-filter="on-hold"> <button type="button" class="btn btn-sm btn-warning filter-button" data-filter="on-hold">
<i class="fas fa-pause-circle"></i> Hold <i class="fas fa-pause-circle"></i> Hold
</button> </button>
</div> </div>
<div class="mr-2 mb-2"> <div class="mr-2 mb-2 d-none d-md-block">
<button type="button" class="btn btn-sm btn-info filter-button" data-filter="online"> <button type="button" class="btn btn-sm btn-info filter-button" data-filter="online">
<i class="fas fa-wifi"></i> Online <i class="fas fa-wifi"></i> Online
</button> </button>
</div> </div>
<div class="mr-2 mb-2"> <div class="mr-2 mb-2 d-none d-md-block">
<button type="button" class="btn btn-sm btn-success filter-button" data-filter="enable"> <button type="button" class="btn btn-sm btn-success filter-button" data-filter="enable">
<i class="fas fa-check"></i> Enable <i class="fas fa-check"></i> Enable
</button> </button>
</div> </div>
<div class="mr-2 mb-2"> <div class="mr-2 mb-2 d-none d-md-block">
<button type="button" class="btn btn-sm btn-danger filter-button" data-filter="disable"> <button type="button" class="btn btn-sm btn-danger filter-button" data-filter="disable">
<i class="fas fa-ban"></i> Disable <i class="fas fa-ban"></i> Disable
</button> </button>
@ -78,26 +115,28 @@
<input type="checkbox" id="selectAll"> <input type="checkbox" id="selectAll">
</th> </th>
<th>#</th> <th>#</th>
<th>Status</th>
<th>Username</th> <th>Username</th>
<th>Traffic Usage</th> <th class="d-none d-md-table-cell">Status</th>
<th class="text-nowrap">Expiry Date</th> <th class="d-none d-md-table-cell">Traffic Usage</th>
<th class="text-nowrap">Expiry Days</th> <th class="d-none d-md-table-cell text-nowrap">Expiry Date</th>
<th class="text-nowrap">Day Usage</th> <th class="d-none d-md-table-cell text-nowrap">Expiry Days</th>
<th>Enable</th> <th class="d-none d-md-table-cell text-nowrap">Day Usage</th>
<th class="text-nowrap requires-iplimit-service" style="display: none;">Unlimited IP</th> <th class="d-none d-md-table-cell">Enable</th>
<th class="text-nowrap">Configs</th> <th class="d-none d-md-table-cell text-nowrap requires-iplimit-service" style="display: none;">Unlimited IP</th>
<th class="text-nowrap">Actions</th> <th class="d-none d-md-table-cell text-nowrap">Configs</th>
<th class="d-none d-md-table-cell text-nowrap">Actions</th>
<th class="d-md-none text-center">Details</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for user in users|sort(attribute='username', case_sensitive=false) %} {% for user in users|sort(attribute='username', case_sensitive=false) %}
<tr> <tr class="user-main-row">
<td> <td>
<input type="checkbox" class="user-checkbox" value="{{ user.username }}"> <input type="checkbox" class="user-checkbox" value="{{ user.username }}">
</td> </td>
<td>{{ loop.index }}</td> <td>{{ loop.index }}</td>
<td> <td data-username="{{ user.username }}">{{ user.username }}</td>
<td class="d-none d-md-table-cell">
{% if user.status == "Online" %} {% if user.status == "Online" %}
<i class="fas fa-circle text-success"></i> Online <i class="fas fa-circle text-success"></i> Online
{% if user.online_count and user.online_count > 0 %} {% if user.online_count and user.online_count > 0 %}
@ -113,32 +152,31 @@
<i class="fas fa-circle text-danger"></i> {{ user.status }} <i class="fas fa-circle text-danger"></i> {{ user.status }}
{% endif %} {% endif %}
</td> </td>
<td data-username="{{ user.username }}">{{ user.username }}</td> <td class="d-none d-md-table-cell">{{ user.traffic_used }}</td>
<td>{{ user.traffic_used }}</td> <td class="d-none d-md-table-cell">{{ user.expiry_date }}</td>
<td>{{ user.expiry_date }}</td> <td class="d-none d-md-table-cell">{{ user.expiry_days }}</td>
<td>{{ user.expiry_days }}</td> <td class="d-none d-md-table-cell">{{ user.day_usage }}</td>
<td>{{ user.day_usage }}</td> <td class="d-none d-md-table-cell">
<td>
{% if user.enable %} {% if user.enable %}
<i class="fas fa-check-circle text-success"></i> <i class="fas fa-check-circle text-success"></i>
{% else %} {% else %}
<i class="fas fa-times-circle text-danger"></i> <i class="fas fa-times-circle text-danger"></i>
{% endif %} {% endif %}
</td> </td>
<td class="unlimited-ip-cell requires-iplimit-service" style="display: none;"> <td class="d-none d-md-table-cell unlimited-ip-cell requires-iplimit-service" style="display: none;">
{% if user.unlimited_ip %} {% if user.unlimited_ip %}
<i class="fas fa-shield-alt text-primary"></i> <i class="fas fa-shield-alt text-primary"></i>
{% else %} {% else %}
<i class="fas fa-times-circle text-muted"></i> <i class="fas fa-times-circle text-muted"></i>
{% endif %} {% endif %}
</td> </td>
<td class="text-nowrap"> <td class="d-none d-md-table-cell text-nowrap">
<a href="#" class="config-link" data-toggle="modal" data-target="#qrcodeModal" <a href="#" class="config-link" data-toggle="modal" data-target="#qrcodeModal"
data-username="{{ user.username }}"> data-username="{{ user.username }}">
<i class="fas fa-qrcode"></i> <i class="fas fa-qrcode"></i>
</a> </a>
</td> </td>
<td class="text-nowrap"> <td class="d-none d-md-table-cell text-nowrap">
<button type="button" class="btn btn-sm btn-info edit-user" <button type="button" class="btn btn-sm btn-info edit-user"
data-user="{{ user.username }}" data-toggle="modal" data-user="{{ user.username }}" data-toggle="modal"
data-target="#editUserModal"> data-target="#editUserModal">
@ -153,6 +191,75 @@
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</button> </button>
</td> </td>
<td class="d-md-none text-center">
<button type="button" class="btn btn-sm btn-secondary toggle-details-btn">
<i class="fas fa-plus"></i>
</button>
</td>
</tr>
<tr class="user-details-row d-md-none" style="display: none;">
<td colspan="4">
<div class="user-details-content p-2">
<p><strong>Status:</strong>
<span>
{% if user.status == "Online" %}
<i class="fas fa-circle text-success"></i> Online
{% if user.online_count and user.online_count > 0 %}
({{ user.online_count }})
{% endif %}
{% elif user.status == "Offline" %}
<i class="fas fa-circle text-secondary"></i> Offline
{% elif user.status == "On-hold" %}
<i class="fas fa-circle text-warning"></i> On-hold
{% elif user.status == "Conflict" %}
<i class="fas fa-circle text-danger"></i> Conflict
{% else %}
<i class="fas fa-circle text-danger"></i> {{ user.status }}
{% endif %}
</span>
</p>
<p><strong>Traffic Usage:</strong> <span>{{ user.traffic_used }}</span></p>
<p><strong>Expiry Date:</strong> <span>{{ user.expiry_date }}</span></p>
<p><strong>Expiry Days:</strong> <span>{{ user.expiry_days }}</span></p>
<p><strong>Day Usage:</strong> <span>{{ user.day_usage }}</span></p>
<p><strong>Enable:</strong>
<span>
{% if user.enable %}
<i class="fas fa-check-circle text-success"></i> Enabled
{% else %}
<i class="fas fa-times-circle text-danger"></i> Disabled
{% endif %}
</span>
</p>
<p class="requires-iplimit-service" style="display: none;"><strong>Unlimited IP:</strong>
<span>
{% if user.unlimited_ip %}
<i class="fas fa-shield-alt text-primary"></i> Yes
{% else %}
<i class="fas fa-times-circle text-muted"></i> No
{% endif %}
</span>
</p>
<div class="mt-2">
<strong>Configs:</strong>
<a href="#" class="btn btn-sm btn-outline-secondary config-link" data-toggle="modal" data-target="#qrcodeModal" data-username="{{ user.username }}">
<i class="fas fa-qrcode"></i> Show
</a>
</div>
<div class="mt-2 user-details-actions">
<strong>Actions:</strong>
<button type="button" class="btn btn-sm btn-info edit-user" data-user="{{ user.username }}" data-toggle="modal" data-target="#editUserModal">
<i class="fas fa-edit"></i> Edit
</button>
<button type="button" class="btn btn-sm btn-warning reset-user" data-user="{{ user.username }}">
<i class="fas fa-undo"></i> Reset
</button>
<button type="button" class="btn btn-sm btn-danger delete-user" data-user="{{ user.username }}">
<i class="fas fa-trash"></i> Delete
</button>
</div>
</div>
</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@ -375,24 +482,28 @@
validateUsername(this, `#${this.id}Error`); validateUsername(this, `#${this.id}Error`);
}); });
$(".filter-button").on("click", function () { $(".filter-button").on("click", function (e) {
e.preventDefault();
const filter = $(this).data("filter"); const filter = $(this).data("filter");
$("#selectAll").prop("checked", false); $("#selectAll").prop("checked", false);
$("#userTable tbody tr").each(function () { $("#userTable tbody tr.user-main-row").each(function () {
let showRow; let showRow;
switch (filter) { switch (filter) {
case "on-hold": showRow = $(this).find("td:eq(2) i").hasClass("text-warning"); break; case "on-hold": showRow = $(this).find("td:eq(3) i").hasClass("text-warning"); break;
case "online": showRow = $(this).find("td:eq(2) i").hasClass("text-success"); break; case "online": showRow = $(this).find("td:eq(3) i").hasClass("text-success"); break;
case "enable": showRow = $(this).find("td:eq(8) i").hasClass("text-success"); break; case "enable": showRow = $(this).find("td:eq(8) i").hasClass("text-success"); break;
case "disable": showRow = $(this).find("td:eq(8) i").hasClass("text-danger"); break; case "disable": showRow = $(this).find("td:eq(8) i").hasClass("text-danger"); break;
default: showRow = true; default: showRow = true;
} }
$(this).toggle(showRow).find(".user-checkbox").prop("checked", false); $(this).toggle(showRow).find(".user-checkbox").prop("checked", false);
if (!showRow) {
$(this).next('tr.user-details-row').hide();
}
}); });
}); });
$("#selectAll").on("change", function () { $("#selectAll").on("change", function () {
$("#userTable tbody tr:visible .user-checkbox").prop("checked", this.checked); $("#userTable tbody tr.user-main-row:visible .user-checkbox").prop("checked", this.checked);
}); });
$("#deleteSelected").on("click", function () { $("#deleteSelected").on("click", function () {
@ -457,16 +568,18 @@
$("#editUserModal").on("show.bs.modal", function (event) { $("#editUserModal").on("show.bs.modal", function (event) {
const user = $(event.relatedTarget).data("user"); const user = $(event.relatedTarget).data("user");
const row = $(event.relatedTarget).closest("tr"); const clickedRow = $(event.relatedTarget).closest("tr");
const trafficText = row.find("td:eq(4)").text(); const dataRow = clickedRow.hasClass('user-main-row') ? clickedRow : clickedRow.prev('.user-main-row');
const expiryText = row.find("td:eq(6)").text();
const trafficText = dataRow.find("td:eq(4)").text();
const expiryText = dataRow.find("td:eq(6)").text();
$("#originalUsername").val(user); $("#originalUsername").val(user);
$("#editUsername").val(user); $("#editUsername").val(user);
$("#editTrafficLimit").val(parseFloat(trafficText.split('/')[1]) || 0); $("#editTrafficLimit").val(parseFloat(trafficText.split('/')[1]) || 0);
$("#editExpirationDays").val(parseInt(expiryText) || 0); $("#editExpirationDays").val(parseInt(expiryText) || 0);
$("#editBlocked").prop("checked", !row.find("td:eq(8) i").hasClass("text-success")); $("#editBlocked").prop("checked", !dataRow.find("td:eq(8) i").hasClass("text-success"));
$("#editUnlimitedIp").prop("checked", row.find(".unlimited-ip-cell i").hasClass("text-primary")); $("#editUnlimitedIp").prop("checked", dataRow.find(".unlimited-ip-cell i").hasClass("text-primary"));
validateUsername('#editUsername', '#editUsernameError'); validateUsername('#editUsername', '#editUsernameError');
}); });
@ -625,12 +738,30 @@
function filterUsers() { function filterUsers() {
const searchText = $("#searchInput").val().toLowerCase(); const searchText = $("#searchInput").val().toLowerCase();
$("#userTable tbody tr").each(function () { $("#userTable tbody tr.user-main-row").each(function () {
const username = $(this).find("td:eq(3)").text().toLowerCase(); const username = $(this).find("td:eq(2)").text().toLowerCase();
$(this).toggle(username.includes(searchText)); const isVisible = username.includes(searchText);
$(this).toggle(isVisible);
if (!isVisible) {
$(this).next('tr.user-details-row').hide();
}
}); });
} }
$('#userTable').on('click', '.toggle-details-btn', function() {
const $this = $(this);
const icon = $this.find('i');
const detailsRow = $this.closest('tr.user-main-row').next('tr.user-details-row');
detailsRow.toggle();
if (detailsRow.is(':visible')) {
icon.removeClass('fa-plus').addClass('fa-minus');
} else {
icon.removeClass('fa-minus').addClass('fa-plus');
}
});
$('#addUserModal').on('show.bs.modal', function () { $('#addUserModal').on('show.bs.modal', function () {
$('#addUserForm, #addBulkUsersForm').trigger('reset'); $('#addUserForm, #addBulkUsersForm').trigger('reset');
$('#addUsernameError, #addBulkPrefixError').text(''); $('#addUsernameError, #addBulkPrefixError').text('');
@ -642,7 +773,8 @@
$('#addUserModal a[data-toggle="tab"]').first().tab('show'); $('#addUserModal a[data-toggle="tab"]').first().tab('show');
}); });
$("#searchButton, #searchInput").on("click keyup", filterUsers); $("#searchButton").on("click", filterUsers);
$("#searchInput").on("keyup", filterUsers);
checkIpLimitServiceStatus(); checkIpLimitServiceStatus();
}); });