feat(web-client-tpos): rewrite staff, roles, audit, settings pages with real API data
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
@page "/admin/settings"
|
||||
@layout AdminLayout
|
||||
@inherits AdminBase
|
||||
@inject WebClientTpos.Client.Services.AuthStateService AuthState
|
||||
@inject WebClientTpos.Client.Services.PosDataService DataService
|
||||
|
||||
@*
|
||||
EN: Admin settings page — account info, shop overview, general settings.
|
||||
VI: Trang cài đặt admin — thông tin tài khoản, tổng quan cửa hàng, cài đặt chung.
|
||||
*@
|
||||
|
||||
<PageTitle>Cài đặt — GoodGo Admin</PageTitle>
|
||||
|
||||
@* ═══ TOP BAR ═══ *@
|
||||
<div class="admin-topbar">
|
||||
<div class="admin-topbar__left">
|
||||
<h1 class="admin-topbar__title">Cài đặt</h1>
|
||||
<p class="admin-topbar__subtitle">Quản lý tài khoản và cấu hình hệ thống</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ═══ TABS ═══ *@
|
||||
<div class="admin-tabs">
|
||||
<button class="admin-tab @(_tab == "general" ? "admin-tab--active" : "")" @onclick="@(() => _tab = "general")">
|
||||
<i data-lucide="settings" style="width:14px;height:14px;"></i> Tổng quan
|
||||
</button>
|
||||
<button class="admin-tab @(_tab == "account" ? "admin-tab--active" : "")" @onclick="@(() => _tab = "account")">
|
||||
<i data-lucide="user" style="width:14px;height:14px;"></i> Tài khoản
|
||||
</button>
|
||||
<button class="admin-tab @(_tab == "notifications" ? "admin-tab--active" : "")" @onclick="@(() => _tab = "notifications")">
|
||||
<i data-lucide="bell" style="width:14px;height:14px;"></i> Thông báo
|
||||
</button>
|
||||
<button class="admin-tab @(_tab == "security" ? "admin-tab--active" : "")" @onclick="@(() => _tab = "security")">
|
||||
<i data-lucide="lock" style="width:14px;height:14px;"></i> Bảo mật
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@* ═══ CONTENT ═══ *@
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;gap:24px;">
|
||||
|
||||
@if (_tab == "general")
|
||||
{
|
||||
@* ── System Info ── *@
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__header">
|
||||
<h3 class="admin-panel__title">
|
||||
<i data-lucide="info" style="color:var(--admin-orange-primary);"></i>
|
||||
Thông tin hệ thống
|
||||
</h3>
|
||||
</div>
|
||||
<div class="admin-panel__body" style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-form-label">Phiên bản</label>
|
||||
<input class="admin-form-input" value="GoodGo Admin v1.0.0" readonly />
|
||||
</div>
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-form-label">Môi trường</label>
|
||||
<input class="admin-form-input" value="Development" readonly />
|
||||
</div>
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-form-label">Số cửa hàng</label>
|
||||
<input class="admin-form-input" value="@_shopCount cửa hàng" readonly />
|
||||
</div>
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-form-label">Múi giờ</label>
|
||||
<input class="admin-form-input" value="Asia/Ho_Chi_Minh (UTC+7)" readonly />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ── Service Health ── *@
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__header">
|
||||
<h3 class="admin-panel__title">
|
||||
<i data-lucide="activity" style="color:#22C55E;"></i>
|
||||
Trạng thái dịch vụ
|
||||
</h3>
|
||||
</div>
|
||||
<div class="admin-panel__body" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:12px;">
|
||||
@foreach (var svc in _services)
|
||||
{
|
||||
<div style="display:flex;align-items:center;gap:10px;padding:12px 14px;background:var(--admin-bg-interactive);border-radius:10px;">
|
||||
<div class="admin-status-badge admin-status-badge--online">
|
||||
<span class="admin-status-badge__dot"></span>
|
||||
Online
|
||||
</div>
|
||||
<span style="font-size:13px;font-weight:500;">@svc</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else if (_tab == "account")
|
||||
{
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__header">
|
||||
<h3 class="admin-panel__title">
|
||||
<i data-lucide="user" style="color:var(--admin-orange-primary);"></i>
|
||||
Thông tin tài khoản
|
||||
</h3>
|
||||
</div>
|
||||
<div class="admin-panel__body" style="display:flex;flex-direction:column;gap:16px;">
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-form-label">Email</label>
|
||||
<input class="admin-form-input" value="@(AuthState.UserEmail ?? "—")" readonly />
|
||||
</div>
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-form-label">Vai trò</label>
|
||||
<input class="admin-form-input" value="Owner" readonly />
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-form-label">Trạng thái</label>
|
||||
<div class="admin-status-badge admin-status-badge--online" style="width:fit-content;">
|
||||
<span class="admin-status-badge__dot"></span>
|
||||
Đã xác thực
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else if (_tab == "notifications")
|
||||
{
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__header">
|
||||
<h3 class="admin-panel__title">
|
||||
<i data-lucide="bell" style="color:#F59E0B;"></i>
|
||||
Cài đặt thông báo
|
||||
</h3>
|
||||
</div>
|
||||
<div class="admin-panel__body" style="display:flex;flex-direction:column;gap:12px;">
|
||||
@foreach (var notif in _notifSettings)
|
||||
{
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;padding:12px 14px;background:var(--admin-bg-interactive);border-radius:10px;">
|
||||
<div style="display:flex;flex-direction:column;">
|
||||
<span style="font-size:13px;font-weight:600;">@notif.Label</span>
|
||||
<span style="font-size:11px;color:var(--admin-text-tertiary);">@notif.Desc</span>
|
||||
</div>
|
||||
<MudSwitch T="bool" Value="notif.Enabled" Color="Color.Primary" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else if (_tab == "security")
|
||||
{
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__header">
|
||||
<h3 class="admin-panel__title">
|
||||
<i data-lucide="lock" style="color:#EF4444;"></i>
|
||||
Bảo mật
|
||||
</h3>
|
||||
</div>
|
||||
<div class="admin-panel__body" style="display:flex;flex-direction:column;gap:16px;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;padding:12px 14px;background:var(--admin-bg-interactive);border-radius:10px;">
|
||||
<div>
|
||||
<span style="font-size:13px;font-weight:600;">Đổi mật khẩu</span>
|
||||
<p style="font-size:11px;color:var(--admin-text-tertiary);margin:0;">Thay đổi mật khẩu đăng nhập</p>
|
||||
</div>
|
||||
<button class="admin-btn-secondary" style="font-size:12px;padding:6px 12px;">Đổi mật khẩu</button>
|
||||
</div>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;padding:12px 14px;background:var(--admin-bg-interactive);border-radius:10px;">
|
||||
<div>
|
||||
<span style="font-size:13px;font-weight:600;">Xác thực 2 bước (2FA)</span>
|
||||
<p style="font-size:11px;color:var(--admin-text-tertiary);margin:0;">Bảo mật tài khoản bằng mã OTP</p>
|
||||
</div>
|
||||
<MudSwitch T="bool" Value="false" Color="Color.Primary" />
|
||||
</div>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;padding:12px 14px;background:var(--admin-bg-interactive);border-radius:10px;">
|
||||
<div>
|
||||
<span style="font-size:13px;font-weight:600;">Phiên đăng nhập</span>
|
||||
<p style="font-size:11px;color:var(--admin-text-tertiary);margin:0;">Quản lý các phiên đang hoạt động</p>
|
||||
</div>
|
||||
<button class="admin-btn-secondary" style="font-size:12px;padding:6px 12px;">Xem phiên</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ─── DANGER ZONE ─── *@
|
||||
<div class="admin-panel" style="border:1px solid rgba(239,68,68,0.3);">
|
||||
<div class="admin-panel__header">
|
||||
<h3 class="admin-panel__title" style="color:#EF4444;">
|
||||
<i data-lucide="alert-triangle" style="color:#EF4444;"></i>
|
||||
Vùng nguy hiểm
|
||||
</h3>
|
||||
</div>
|
||||
<div class="admin-panel__body">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;">
|
||||
<div>
|
||||
<span style="font-size:13px;font-weight:600;">Xóa tài khoản</span>
|
||||
<p style="font-size:11px;color:var(--admin-text-tertiary);margin:0;">Xóa vĩnh viễn tài khoản và toàn bộ dữ liệu</p>
|
||||
</div>
|
||||
<button class="admin-btn-secondary" style="font-size:12px;padding:6px 12px;color:#EF4444;border-color:rgba(239,68,68,0.3);">Xóa tài khoản</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private string _tab = "general";
|
||||
private int _shopCount = 0;
|
||||
|
||||
private readonly string[] _services = { "API Gateway", "IAM Service", "Merchant Service", "Catalog Service", "Order Service" };
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var shops = await DataService.GetShopsAsync();
|
||||
_shopCount = shops.Count;
|
||||
}
|
||||
catch { _shopCount = 0; }
|
||||
}
|
||||
|
||||
private record NotifSetting(string Label, string Desc, bool Enabled);
|
||||
private readonly NotifSetting[] _notifSettings = new[]
|
||||
{
|
||||
new NotifSetting("Đơn hàng mới", "Thông báo khi có đơn hàng mới", true),
|
||||
new NotifSetting("Đơn hàng hủy", "Thông báo khi đơn hàng bị hủy", true),
|
||||
new NotifSetting("Hàng sắp hết", "Tồn kho dưới mức tối thiểu", true),
|
||||
new NotifSetting("Nhân viên check-in", "Nhân viên bắt đầu ca", false),
|
||||
new NotifSetting("Doanh thu bất thường", "Cảnh báo doanh thu", true),
|
||||
new NotifSetting("Email hàng tuần", "Báo cáo doanh thu hàng tuần", true),
|
||||
};
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
@page "/admin/roles"
|
||||
@layout AdminLayout
|
||||
@inherits AdminBase
|
||||
@inject WebClientTpos.Client.Services.IamApiService IamService
|
||||
@using WebClientTpos.Client.Services
|
||||
|
||||
@*
|
||||
EN: Role permissions — manage roles and their POS/admin permissions.
|
||||
VI: Phân quyền — quản lý vai trò và quyền hạn POS/admin.
|
||||
Design: pencil-design/src/pages/tPOS/admin/role-permissions.pen
|
||||
EN: Role permissions — real roles from IAM Service API.
|
||||
VI: Phân quyền — dữ liệu roles thật từ IAM Service API.
|
||||
*@
|
||||
|
||||
<PageTitle>Phân quyền — GoodGo Admin</PageTitle>
|
||||
@@ -25,49 +26,91 @@
|
||||
</div>
|
||||
|
||||
@* ═══ CONTENT ═══ *@
|
||||
<div class="admin-content" style="display:flex;gap:24px;">
|
||||
|
||||
@* LEFT: Roles list *@
|
||||
<div style="width:280px;display:flex;flex-direction:column;gap:8px;">
|
||||
@foreach (var role in _roles)
|
||||
{
|
||||
var r = role;
|
||||
<button class="admin-role-card @(_selectedRole == r.Key ? "admin-role-card--active" : "")" @onclick="@(() => _selectedRole = r.Key)">
|
||||
<div style="display:flex;align-items:center;gap:10px;">
|
||||
<div class="admin-kpi-card__icon" style="width:36px;height:36px;border-radius:10px;background-color:@(r.Color)20;">
|
||||
<i data-lucide="@r.Icon" style="color:@r.Color;width:18px;height:18px;"></i>
|
||||
</div>
|
||||
<div style="text-align:left;">
|
||||
<div style="font-size:14px;font-weight:600;">@r.Name</div>
|
||||
<div style="font-size:11px;color:var(--admin-text-tertiary);">@r.Count người</div>
|
||||
</div>
|
||||
</div>
|
||||
<i data-lucide="chevron-right" style="width:16px;height:16px;color:var(--admin-text-tertiary);"></i>
|
||||
</button>
|
||||
}
|
||||
@if (_loading)
|
||||
{
|
||||
<div class="admin-content" style="display:flex;align-items:center;justify-content:center;min-height:300px;">
|
||||
<MudProgressCircular Color="Color.Primary" Indeterminate="true" />
|
||||
</div>
|
||||
|
||||
@* RIGHT: Permissions grid *@
|
||||
<div style="flex:1;display:flex;flex-direction:column;gap:20px;">
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__header">
|
||||
<h3 class="admin-panel__title">
|
||||
<i data-lucide="shield" style="color:var(--admin-orange-primary);"></i>
|
||||
Quyền hạn — @(_roles.FirstOrDefault(r => r.Key == _selectedRole)?.Name ?? "")
|
||||
</h3>
|
||||
<button class="admin-btn-secondary" style="font-size:12px;padding:6px 12px;">
|
||||
<i data-lucide="edit-2" style="width:14px;height:14px;"></i>
|
||||
Chỉnh sửa
|
||||
</button>
|
||||
</div>
|
||||
<div class="admin-panel__body" style="display:flex;flex-direction:column;gap:16px;">
|
||||
@foreach (var group in _permissionGroups)
|
||||
{
|
||||
<div style="display:flex;flex-direction:column;gap:8px;">
|
||||
<div style="font-size:12px;font-weight:700;color:var(--admin-text-tertiary);text-transform:uppercase;letter-spacing:0.05em;">
|
||||
@group.GroupName
|
||||
}
|
||||
else if (!_roles.Any())
|
||||
{
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:300px;gap:12px;">
|
||||
<i data-lucide="shield" style="width:48px;height:48px;color:var(--admin-text-tertiary);"></i>
|
||||
<span style="color:var(--admin-text-tertiary);font-size:15px;">Chưa có vai trò nào</span>
|
||||
<span style="color:var(--admin-text-tertiary);font-size:13px;">Vai trò sẽ được tạo khi hệ thống IAM hoạt động</span>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="admin-content" style="display:flex;gap:24px;">
|
||||
@* LEFT: Roles list *@
|
||||
<div style="width:280px;display:flex;flex-direction:column;gap:8px;">
|
||||
@foreach (var role in _roles)
|
||||
{
|
||||
<button class="admin-role-card @(_selectedId == role.Id ? "admin-role-card--active" : "")"
|
||||
@onclick="@(() => _selectedId = role.Id)">
|
||||
<div style="display:flex;align-items:center;gap:10px;">
|
||||
<div class="admin-kpi-card__icon" style="width:36px;height:36px;border-radius:10px;background-color:@(GetRoleColor(role.Name))20;">
|
||||
<i data-lucide="@GetRoleIcon(role.Name)" style="color:@GetRoleColor(role.Name);width:18px;height:18px;"></i>
|
||||
</div>
|
||||
@foreach (var perm in group.Permissions)
|
||||
<div style="text-align:left;">
|
||||
<div style="font-size:14px;font-weight:600;">@role.Name</div>
|
||||
<div style="font-size:11px;color:var(--admin-text-tertiary);">
|
||||
@(role.UserCount ?? 0) người
|
||||
@if (role.IsSystem) { <span>• Hệ thống</span> }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<i data-lucide="chevron-right" style="width:16px;height:16px;color:var(--admin-text-tertiary);"></i>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
@* RIGHT: Role Details *@
|
||||
<div style="flex:1;display:flex;flex-direction:column;gap:20px;">
|
||||
@{ var selected = _roles.FirstOrDefault(r => r.Id == _selectedId); }
|
||||
@if (selected != null)
|
||||
{
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__header">
|
||||
<h3 class="admin-panel__title">
|
||||
<i data-lucide="shield" style="color:var(--admin-orange-primary);"></i>
|
||||
Chi tiết — @selected.Name
|
||||
</h3>
|
||||
</div>
|
||||
<div class="admin-panel__body" style="display:flex;flex-direction:column;gap:16px;">
|
||||
<div style="display:flex;gap:24px;">
|
||||
<div class="admin-store-stat" style="flex:1;">
|
||||
<div class="admin-store-stat__value">@selected.Name</div>
|
||||
<div class="admin-store-stat__label">Tên vai trò</div>
|
||||
</div>
|
||||
<div class="admin-store-stat" style="flex:1;">
|
||||
<div class="admin-store-stat__value">@(selected.UserCount ?? 0)</div>
|
||||
<div class="admin-store-stat__label">Số người dùng</div>
|
||||
</div>
|
||||
<div class="admin-store-stat" style="flex:1;">
|
||||
<div class="admin-store-stat__value">@(selected.IsSystem ? "Có" : "Không")</div>
|
||||
<div class="admin-store-stat__label">Vai trò hệ thống</div>
|
||||
</div>
|
||||
<div class="admin-store-stat" style="flex:1;">
|
||||
<div class="admin-store-stat__value">@selected.CreatedAt.ToString("dd/MM/yy")</div>
|
||||
<div class="admin-store-stat__label">Ngày tạo</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrEmpty(selected.Description))
|
||||
{
|
||||
<div style="padding:12px 14px;background:var(--admin-bg-interactive);border-radius:10px;">
|
||||
<span style="font-size:12px;font-weight:600;color:var(--admin-text-tertiary);text-transform:uppercase;letter-spacing:0.05em;">Mô tả</span>
|
||||
<p style="font-size:14px;margin-top:4px;">@selected.Description</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@* Permission Groups — placeholder since IAM doesn't expose permissions yet *@
|
||||
<div style="font-size:12px;font-weight:700;color:var(--admin-text-tertiary);text-transform:uppercase;letter-spacing:0.05em;">
|
||||
Quyền hạn
|
||||
</div>
|
||||
@foreach (var perm in _defaultPermissions)
|
||||
{
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;padding:10px 12px;background:var(--admin-bg-interactive);border-radius:10px;">
|
||||
<div style="display:flex;flex-direction:column;">
|
||||
@@ -78,49 +121,55 @@
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
private string _selectedRole = "manager";
|
||||
private bool _loading = true;
|
||||
private Guid? _selectedId;
|
||||
private List<IamApiService.RoleDto> _roles = new();
|
||||
|
||||
private record RoleDef(string Key, string Name, string Icon, string Color, string Count);
|
||||
private readonly RoleDef[] _roles = new[]
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
new RoleDef("owner", "Chủ sở hữu", "crown", "#FF5C00", "1"),
|
||||
new RoleDef("manager", "Quản lý", "user-check", "#8B5CF6", "2"),
|
||||
new RoleDef("cashier", "Thu ngân", "calculator", "#3B82F6", "3"),
|
||||
new RoleDef("barista", "Barista", "coffee", "#22C55E", "2"),
|
||||
new RoleDef("server", "Phục vụ", "hand", "#EC4899", "2"),
|
||||
try { _roles = await IamService.GetRolesAsync(); }
|
||||
catch { _roles = new(); }
|
||||
finally
|
||||
{
|
||||
_selectedId = _roles.FirstOrDefault()?.Id;
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetRoleColor(string name) => name.ToLower() switch
|
||||
{
|
||||
"admin" or "administrator" => "#FF5C00",
|
||||
"merchant" or "owner" => "#8B5CF6",
|
||||
"cashier" => "#3B82F6",
|
||||
"manager" => "#22C55E",
|
||||
_ => "#6B7280"
|
||||
};
|
||||
|
||||
private record Permission(string Label, string Desc, bool Allowed);
|
||||
private record PermissionGroup(string GroupName, Permission[] Permissions);
|
||||
private readonly PermissionGroup[] _permissionGroups = new[]
|
||||
private static string GetRoleIcon(string name) => name.ToLower() switch
|
||||
{
|
||||
new PermissionGroup("POS", new[]
|
||||
{
|
||||
new Permission("Tạo đơn hàng", "Tạo và xử lý đơn hàng", true),
|
||||
new Permission("Áp dụng giảm giá", "Giảm giá cho đơn hàng", true),
|
||||
new Permission("Hủy đơn hàng", "Hủy đơn đã tạo", true),
|
||||
new Permission("Hoàn tiền", "Hoàn tiền cho khách", false),
|
||||
new Permission("Đóng ca", "Kết thúc ca bán hàng", true),
|
||||
}),
|
||||
new PermissionGroup("QUẢN LÝ", new[]
|
||||
{
|
||||
new Permission("Xem báo cáo", "Truy cập báo cáo doanh thu", true),
|
||||
new Permission("Quản lý nhân sự", "Thêm/sửa/xóa nhân viên", false),
|
||||
new Permission("Quản lý sản phẩm", "Thêm/sửa/xóa sản phẩm", true),
|
||||
new Permission("Quản lý kho", "Nhập/xuất kho hàng", true),
|
||||
}),
|
||||
new PermissionGroup("HỆ THỐNG", new[]
|
||||
{
|
||||
new Permission("Cài đặt cửa hàng", "Thay đổi thông tin cửa hàng", false),
|
||||
new Permission("Quản lý thiết bị", "Thêm/xóa thiết bị POS", false),
|
||||
new Permission("Xem audit log", "Xem lịch sử thao tác", true),
|
||||
}),
|
||||
"admin" or "administrator" => "crown",
|
||||
"merchant" or "owner" => "user-check",
|
||||
"cashier" => "calculator",
|
||||
"manager" => "briefcase",
|
||||
_ => "shield"
|
||||
};
|
||||
|
||||
// EN: Default permission display (placeholder until IAM exposes permissions API)
|
||||
// VI: Hiển thị quyền mặc định (placeholder cho đến khi IAM cung cấp API permissions)
|
||||
private record PermDef(string Label, string Desc, bool Allowed);
|
||||
private readonly PermDef[] _defaultPermissions = new[]
|
||||
{
|
||||
new PermDef("Tạo đơn hàng", "Tạo và xử lý đơn hàng", true),
|
||||
new PermDef("Áp dụng giảm giá", "Giảm giá cho đơn hàng", true),
|
||||
new PermDef("Xem báo cáo", "Truy cập báo cáo doanh thu", false),
|
||||
new PermDef("Quản lý nhân sự", "Thêm/sửa/xóa nhân viên", false),
|
||||
new PermDef("Cài đặt cửa hàng", "Thay đổi thông tin cửa hàng", false),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
@page "/admin/staff"
|
||||
@layout AdminLayout
|
||||
@inherits AdminBase
|
||||
@inject WebClientTpos.Client.Services.PosDataService DataService
|
||||
@using WebClientTpos.Client.Services
|
||||
|
||||
@*
|
||||
EN: Staff directory — grid of staff cards with search, filter by store/role.
|
||||
VI: Danh bạ nhân sự — grid thẻ nhân viên, tìm kiếm, lọc theo cửa hàng/vai trò.
|
||||
Design: pencil-design/src/pages/tPOS/admin/staff-directory.pen
|
||||
EN: Staff directory — real data from BFF, grid of staff cards with search & filter.
|
||||
VI: Danh bạ nhân sự — dữ liệu thật từ BFF, grid thẻ nhân viên, tìm kiếm & lọc.
|
||||
*@
|
||||
|
||||
<PageTitle>Quản lý nhân sự — GoodGo Admin</PageTitle>
|
||||
@@ -19,12 +20,8 @@
|
||||
<div class="admin-topbar__right">
|
||||
<div class="admin-search" style="width:220px;">
|
||||
<i data-lucide="search"></i>
|
||||
<input type="text" placeholder="Tìm nhân viên..." @bind="SearchQuery" />
|
||||
<input type="text" placeholder="Tìm nhân viên..." @bind="SearchQuery" @bind:event="oninput" />
|
||||
</div>
|
||||
<button class="admin-btn-secondary">
|
||||
<i data-lucide="filter"></i>
|
||||
<span>Bộ lọc</span>
|
||||
</button>
|
||||
<button class="admin-btn-primary" @onclick="@(() => NavigateTo("staff/create"))">
|
||||
<i data-lucide="user-plus"></i>
|
||||
<span>Thêm nhân viên</span>
|
||||
@@ -38,62 +35,134 @@
|
||||
Tất cả <span class="admin-tab__badge admin-tab__badge--active">@_staffList.Count</span>
|
||||
</button>
|
||||
<button class="admin-tab @(_activeTab == "active" ? "admin-tab--active" : "")" @onclick="@(() => _activeTab = "active")">
|
||||
Đang làm việc <span class="admin-tab__badge">@_staffList.Count(s => s.Status == "active")</span>
|
||||
Đang làm <span class="admin-tab__badge">@_staffList.Count(s => s.Status == "Active")</span>
|
||||
</button>
|
||||
<button class="admin-tab @(_activeTab == "off" ? "admin-tab--active" : "")" @onclick="@(() => _activeTab = "off")">
|
||||
Nghỉ phép <span class="admin-tab__badge">@_staffList.Count(s => s.Status == "off")</span>
|
||||
<button class="admin-tab @(_activeTab == "invited" ? "admin-tab--active" : "")" @onclick="@(() => _activeTab = "invited")">
|
||||
Chờ xác nhận <span class="admin-tab__badge">@_staffList.Count(s => s.Status == "Invited")</span>
|
||||
</button>
|
||||
<button class="admin-tab @(_activeTab == "inactive" ? "admin-tab--active" : "")" @onclick="@(() => _activeTab = "inactive")">
|
||||
Ngưng hoạt động <span class="admin-tab__badge">@_staffList.Count(s => s.Status == "Inactive" || s.Status == "Terminated")</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@* ═══ STAFF GRID ═══ *@
|
||||
<div class="admin-content" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px;">
|
||||
@foreach (var staff in _staffList)
|
||||
{
|
||||
<div class="admin-staff-card" @onclick="@(() => NavigateTo($"staff/{staff.Id}"))">
|
||||
<div class="admin-staff-card__header">
|
||||
<div class="admin-user-avatar" style="width:48px;height:48px;font-size:16px;background-color:@staff.AvatarColor;">
|
||||
@staff.Initials
|
||||
@* ═══ CONTENT ═══ *@
|
||||
@if (_loading)
|
||||
{
|
||||
<div class="admin-content" style="display:flex;align-items:center;justify-content:center;min-height:300px;">
|
||||
<MudProgressCircular Color="Color.Primary" Indeterminate="true" />
|
||||
</div>
|
||||
}
|
||||
else if (!FilteredStaff.Any())
|
||||
{
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:300px;gap:12px;">
|
||||
<i data-lucide="users" style="width:48px;height:48px;color:var(--admin-text-tertiary);"></i>
|
||||
<span style="color:var(--admin-text-tertiary);font-size:15px;">Chưa có nhân viên nào</span>
|
||||
<button class="admin-btn-primary" @onclick="@(() => NavigateTo("staff/create"))">
|
||||
<i data-lucide="user-plus"></i>
|
||||
<span>Thêm nhân viên đầu tiên</span>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="admin-content" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px;">
|
||||
@foreach (var staff in FilteredStaff)
|
||||
{
|
||||
<div class="admin-staff-card">
|
||||
<div class="admin-staff-card__header">
|
||||
<div class="admin-user-avatar" style="width:48px;height:48px;font-size:16px;background-color:@GetAvatarColor(staff.Role ?? "");">
|
||||
@GetInitials(staff.Email ?? staff.EmployeeCode ?? "?")
|
||||
</div>
|
||||
<div class="admin-status-badge @GetStatusCss(staff.Status)">
|
||||
<span class="admin-status-badge__dot"></span>
|
||||
@GetStatusLabel(staff.Status)
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-status-badge @(staff.Status == "active" ? "admin-status-badge--online" : "admin-status-badge--setup")">
|
||||
<span class="admin-status-badge__dot"></span>
|
||||
@(staff.Status == "active" ? "Đang ca" : "Nghỉ")
|
||||
<div style="display:flex;flex-direction:column;gap:2px;">
|
||||
<span style="font-size:16px;font-weight:600;">@(staff.Email ?? staff.EmployeeCode ?? "Nhân viên")</span>
|
||||
<span style="font-size:12px;color:var(--admin-text-tertiary);">@(staff.Role ?? "—") • @(staff.ShopName ?? "Chưa gán")</span>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;">
|
||||
<div class="admin-store-stat" style="flex:1;">
|
||||
<div class="admin-store-stat__value">@(staff.EmployeeCode ?? "—")</div>
|
||||
<div class="admin-store-stat__label">Mã NV</div>
|
||||
</div>
|
||||
<div class="admin-store-stat" style="flex:1;">
|
||||
<div class="admin-store-stat__value">@(staff.Phone ?? "—")</div>
|
||||
<div class="admin-store-stat__label">Điện thoại</div>
|
||||
</div>
|
||||
<div class="admin-store-stat" style="flex:1;">
|
||||
<div class="admin-store-stat__value">@FormatDate(staff.JoinedAt)</div>
|
||||
<div class="admin-store-stat__label">Ngày vào</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;gap:2px;">
|
||||
<span style="font-size:16px;font-weight:600;">@staff.Name</span>
|
||||
<span style="font-size:12px;color:var(--admin-text-tertiary);">@staff.Role • @staff.Store</span>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;">
|
||||
<div class="admin-store-stat" style="flex:1;">
|
||||
<div class="admin-store-stat__value">@staff.Hours</div>
|
||||
<div class="admin-store-stat__label">Giờ/tuần</div>
|
||||
</div>
|
||||
<div class="admin-store-stat" style="flex:1;">
|
||||
<div class="admin-store-stat__value">@staff.Orders</div>
|
||||
<div class="admin-store-stat__label">Đơn hôm nay</div>
|
||||
</div>
|
||||
<div class="admin-store-stat" style="flex:1;">
|
||||
<div class="admin-store-stat__value">@staff.Rating</div>
|
||||
<div class="admin-store-stat__label">Đánh giá</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
private bool _loading = true;
|
||||
private string _activeTab = "all";
|
||||
private List<PosDataService.StaffInfo> _staffList = new();
|
||||
|
||||
private record StaffMember(string Id, string Name, string Initials, string Role, string Store, string Status, string AvatarColor, string Hours, string Orders, string Rating);
|
||||
private readonly List<StaffMember> _staffList = new()
|
||||
private IEnumerable<PosDataService.StaffInfo> FilteredStaff
|
||||
{
|
||||
new("1", "Trần Minh Đức", "TM", "Barista", "Coffee House Q1", "active", "#FF5C00", "42", "28", "4.8"),
|
||||
new("2", "Lê Thị Thảo", "LT", "Thu ngân", "Coffee House Q1", "active", "#3B82F6", "38", "45", "4.9"),
|
||||
new("3", "Nguyễn Hà My", "NH", "Phục vụ", "Nhà hàng Q3", "active", "#22C55E", "40", "32", "4.7"),
|
||||
new("4", "Phạm Văn An", "PA", "Quản lý", "Nhà hàng Q3", "active", "#8B5CF6", "45", "0", "4.6"),
|
||||
new("5", "Hoàng Thị Lan", "HL", "Barista", "Coffee House Q1", "off", "#EC4899", "0", "0", "4.5"),
|
||||
new("6", "Võ Đình Khoa", "VK", "Bếp trưởng", "Nhà hàng Q3", "active", "#F59E0B", "44", "18", "4.9"),
|
||||
new("7", "Đặng Phương Linh", "ĐL", "Phục vụ", "Nhà hàng Q3", "active", "#06B6D4", "36", "22", "4.4"),
|
||||
new("8", "Bùi Thanh Tùng", "BT", "Thu ngân", "Café Thủ Đức", "active", "#3B82F6", "38", "30", "4.7"),
|
||||
get
|
||||
{
|
||||
var list = _activeTab switch
|
||||
{
|
||||
"active" => _staffList.Where(s => s.Status == "Active"),
|
||||
"invited" => _staffList.Where(s => s.Status == "Invited"),
|
||||
"inactive" => _staffList.Where(s => s.Status == "Inactive" || s.Status == "Terminated"),
|
||||
_ => _staffList
|
||||
};
|
||||
if (!string.IsNullOrEmpty(SearchQuery))
|
||||
list = list.Where(s => (s.Email ?? "").Contains(SearchQuery, StringComparison.OrdinalIgnoreCase)
|
||||
|| (s.EmployeeCode ?? "").Contains(SearchQuery, StringComparison.OrdinalIgnoreCase)
|
||||
|| (s.Phone ?? "").Contains(SearchQuery, StringComparison.OrdinalIgnoreCase)
|
||||
|| (s.ShopName ?? "").Contains(SearchQuery, StringComparison.OrdinalIgnoreCase));
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
try { _staffList = await DataService.GetStaffAsync(); }
|
||||
catch { _staffList = new(); }
|
||||
finally { _loading = false; }
|
||||
}
|
||||
|
||||
private static string GetInitials(string value)
|
||||
{
|
||||
if (value.Contains('@')) value = value.Split('@')[0];
|
||||
return value.Length >= 2 ? value[..2].ToUpper() : value.ToUpper();
|
||||
}
|
||||
|
||||
private static string GetAvatarColor(string role) => role switch
|
||||
{
|
||||
"Cashier" => "#3B82F6",
|
||||
"Waiter" => "#22C55E",
|
||||
"Manager" => "#8B5CF6",
|
||||
"Admin" => "#FF5C00",
|
||||
_ => "#6B7280"
|
||||
};
|
||||
|
||||
private static string GetStatusCss(string? status) => status switch
|
||||
{
|
||||
"Active" => "admin-status-badge--online",
|
||||
"Invited" => "admin-status-badge--setup",
|
||||
_ => "admin-status-badge--offline"
|
||||
};
|
||||
|
||||
private static string GetStatusLabel(string? status) => status switch
|
||||
{
|
||||
"Active" => "Đang làm",
|
||||
"Invited" => "Chờ xác nhận",
|
||||
"Inactive" => "Tạm nghỉ",
|
||||
"Terminated" => "Đã nghỉ",
|
||||
_ => status ?? "—"
|
||||
};
|
||||
|
||||
private static string FormatDate(DateTime? dt) => dt.HasValue ? dt.Value.ToString("dd/MM/yy") : "—";
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
@page "/admin/system/audit"
|
||||
@layout AdminLayout
|
||||
@inherits AdminBase
|
||||
@inject WebClientTpos.Client.Services.IamApiService IamService
|
||||
@using WebClientTpos.Client.Services
|
||||
|
||||
@*
|
||||
EN: Audit log — action log table with timestamp, user, action, resource, status. Search + filter by action type, date range, user.
|
||||
VI: Nhật ký hệ thống — bảng log thao tác với thời gian, người dùng, hành động, tài nguyên, trạng thái. Tìm kiếm + lọc.
|
||||
Design: pencil-design/src/pages/tPOS/admin/audit-log.pen
|
||||
EN: Audit log — real audit log data from IAM Service.
|
||||
VI: Nhật ký hệ thống — dữ liệu audit log thật từ IAM Service.
|
||||
*@
|
||||
|
||||
<PageTitle>Nhật ký hệ thống — GoodGo Admin</PageTitle>
|
||||
@@ -19,15 +20,11 @@
|
||||
<div class="admin-topbar__right">
|
||||
<div class="admin-search" style="width:220px;">
|
||||
<i data-lucide="search"></i>
|
||||
<input type="text" placeholder="Tìm trong log..." @bind="SearchQuery" />
|
||||
<input type="text" placeholder="Tìm trong log..." @bind="SearchQuery" @bind:event="oninput" />
|
||||
</div>
|
||||
<button class="admin-btn-secondary">
|
||||
<i data-lucide="filter"></i>
|
||||
<span>Bộ lọc</span>
|
||||
</button>
|
||||
<button class="admin-btn-secondary">
|
||||
<i data-lucide="download"></i>
|
||||
<span>Xuất log</span>
|
||||
<i data-lucide="refresh-cw"></i>
|
||||
<span>Làm mới</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -38,85 +35,144 @@
|
||||
Tất cả <span class="admin-tab__badge admin-tab__badge--active">@_logs.Count</span>
|
||||
</button>
|
||||
<button class="admin-tab @(_activeTab == "auth" ? "admin-tab--active" : "")" @onclick="@(() => _activeTab = "auth")">
|
||||
Xác thực <span class="admin-tab__badge">@_logs.Count(l => l.Category == "auth")</span>
|
||||
Xác thực <span class="admin-tab__badge">@_logs.Count(l => IsCategory(l, "auth"))</span>
|
||||
</button>
|
||||
<button class="admin-tab @(_activeTab == "config" ? "admin-tab--active" : "")" @onclick="@(() => _activeTab = "config")">
|
||||
Cấu hình <span class="admin-tab__badge">@_logs.Count(l => l.Category == "config")</span>
|
||||
Cấu hình <span class="admin-tab__badge">@_logs.Count(l => IsCategory(l, "config"))</span>
|
||||
</button>
|
||||
<button class="admin-tab @(_activeTab == "data" ? "admin-tab--active" : "")" @onclick="@(() => _activeTab = "data")">
|
||||
Dữ liệu <span class="admin-tab__badge">@_logs.Count(l => l.Category == "data")</span>
|
||||
Dữ liệu <span class="admin-tab__badge">@_logs.Count(l => IsCategory(l, "data"))</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@* ═══ LOG TABLE ═══ *@
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;gap:0;">
|
||||
<div class="admin-panel" style="flex:1;">
|
||||
<div class="admin-panel__body" style="padding:0;">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Thời gian</th>
|
||||
<th>Người dùng</th>
|
||||
<th>Hành động</th>
|
||||
<th>Tài nguyên</th>
|
||||
<th>IP</th>
|
||||
<th>Trạng thái</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var log in FilteredLogs)
|
||||
{
|
||||
@* ═══ CONTENT ═══ *@
|
||||
@if (_loading)
|
||||
{
|
||||
<div class="admin-content" style="display:flex;align-items:center;justify-content:center;min-height:300px;">
|
||||
<MudProgressCircular Color="Color.Primary" Indeterminate="true" />
|
||||
</div>
|
||||
}
|
||||
else if (!FilteredLogs.Any())
|
||||
{
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:300px;gap:12px;">
|
||||
<i data-lucide="file-text" style="width:48px;height:48px;color:var(--admin-text-tertiary);"></i>
|
||||
<span style="color:var(--admin-text-tertiary);font-size:15px;">Chưa có log nào</span>
|
||||
<span style="color:var(--admin-text-tertiary);font-size:13px;">Log sẽ xuất hiện khi có hoạt động trong hệ thống</span>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;gap:0;">
|
||||
<div class="admin-panel" style="flex:1;">
|
||||
<div class="admin-panel__body" style="padding:0;">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<td style="font-size:12px;color:var(--admin-text-tertiary);white-space:nowrap;">@log.Timestamp</td>
|
||||
<td>
|
||||
<div style="display:flex;align-items:center;gap:8px;">
|
||||
<div class="admin-user-avatar" style="width:28px;height:28px;font-size:10px;background-color:@log.AvatarColor;">@log.Initials</div>
|
||||
<span style="font-weight:500;">@log.User</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div style="display:flex;align-items:center;gap:6px;">
|
||||
<i data-lucide="@log.ActionIcon" style="width:14px;height:14px;color:@log.ActionColor;"></i>
|
||||
<span>@log.Action</span>
|
||||
</div>
|
||||
</td>
|
||||
<td style="color:var(--admin-text-secondary);">@log.Resource</td>
|
||||
<td style="font-size:12px;color:var(--admin-text-tertiary);font-family:monospace;">@log.IP</td>
|
||||
<td>
|
||||
<div class="admin-status-badge @(log.Status == "success" ? "admin-status-badge--online" : "admin-status-badge--offline")">
|
||||
<span class="admin-status-badge__dot"></span>
|
||||
@(log.Status == "success" ? "Thành công" : "Thất bại")
|
||||
</div>
|
||||
</td>
|
||||
<th>Thời gian</th>
|
||||
<th>Người dùng</th>
|
||||
<th>Hành động</th>
|
||||
<th>Tài nguyên</th>
|
||||
<th>IP</th>
|
||||
<th>Trạng thái</th>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var log in FilteredLogs)
|
||||
{
|
||||
<tr>
|
||||
<td style="font-size:12px;color:var(--admin-text-tertiary);white-space:nowrap;">@FormatTime(log.Timestamp)</td>
|
||||
<td>
|
||||
<div style="display:flex;align-items:center;gap:8px;">
|
||||
<div class="admin-user-avatar" style="width:28px;height:28px;font-size:10px;background-color:#8B5CF6;">
|
||||
@GetInitials(log.ActorName ?? log.ActorId ?? "?")
|
||||
</div>
|
||||
<span style="font-weight:500;">@(log.ActorName ?? log.ActorId ?? "System")</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div style="display:flex;align-items:center;gap:6px;">
|
||||
<i data-lucide="@GetEventIcon(log.EventType)" style="width:14px;height:14px;color:@GetEventColor(log.EventType);"></i>
|
||||
<span>@(log.EventType ?? "—")</span>
|
||||
</div>
|
||||
</td>
|
||||
<td style="color:var(--admin-text-secondary);">@(log.ResourceType ?? "—") @(log.ResourceId != null ? $"#{log.ResourceId[..Math.Min(8, log.ResourceId.Length)]}" : "")</td>
|
||||
<td style="font-size:12px;color:var(--admin-text-tertiary);font-family:monospace;">@(log.IpAddress ?? "—")</td>
|
||||
<td>
|
||||
<div class="admin-status-badge @(log.Status == "Success" ? "admin-status-badge--online" : "admin-status-badge--offline")">
|
||||
<span class="admin-status-badge__dot"></span>
|
||||
@(log.Status == "Success" ? "Thành công" : log.Status ?? "—")
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
private bool _loading = true;
|
||||
private string _activeTab = "all";
|
||||
private List<IamApiService.AuditLogDto> _logs = new();
|
||||
|
||||
private List<LogEntry> FilteredLogs => _activeTab == "all"
|
||||
? _logs
|
||||
: _logs.Where(l => l.Category == _activeTab).ToList();
|
||||
|
||||
private record LogEntry(string Timestamp, string User, string Initials, string AvatarColor, string Action,
|
||||
string ActionIcon, string ActionColor, string Resource, string IP, string Status, string Category);
|
||||
|
||||
private readonly List<LogEntry> _logs = new()
|
||||
private IEnumerable<IamApiService.AuditLogDto> FilteredLogs
|
||||
{
|
||||
new("12/02/2026 14:32:05", "Trần Minh Đức", "TM", "#FF5C00", "Đăng nhập", "log-in", "#22C55E", "Phiên làm việc", "192.168.1.45", "success", "auth"),
|
||||
new("12/02/2026 14:28:11", "Admin System", "AS", "#8B5CF6", "Cập nhật giá", "edit", "#3B82F6", "Sản phẩm #142", "10.0.0.1", "success", "data"),
|
||||
new("12/02/2026 14:15:30", "Lê Thị Thảo", "LT", "#3B82F6", "Tạo đơn hàng", "plus-circle", "#22C55E", "Đơn #2848", "192.168.1.50", "success", "data"),
|
||||
new("12/02/2026 13:55:22", "Phạm Văn An", "PA", "#22C55E", "Thay đổi quyền", "shield", "#F59E0B", "Role: Barista", "192.168.1.12", "success", "config"),
|
||||
new("12/02/2026 13:42:08", "Nguyễn Hà My", "NH", "#EC4899", "Đăng nhập thất bại", "log-in", "#EF4444", "Phiên làm việc", "192.168.1.88", "failed", "auth"),
|
||||
new("12/02/2026 13:30:00", "Admin System", "AS", "#8B5CF6", "Backup dữ liệu", "database", "#3B82F6", "DB chính", "10.0.0.1", "success", "config"),
|
||||
new("12/02/2026 12:18:45", "Võ Đình Khoa", "VK", "#F59E0B", "Nhập kho", "package", "#22C55E", "Kho Coffee Q1", "192.168.1.33", "success", "data"),
|
||||
new("12/02/2026 11:55:12", "Trần Minh Đức", "TM", "#FF5C00", "Cập nhật cài đặt", "settings", "#F59E0B", "Cửa hàng Q1", "192.168.1.45", "success", "config"),
|
||||
new("12/02/2026 10:30:00", "Lê Thị Thảo", "LT", "#3B82F6", "Hủy đơn hàng", "x-circle", "#EF4444", "Đơn #2845", "192.168.1.50", "success", "data"),
|
||||
new("12/02/2026 09:15:33", "Phạm Văn An", "PA", "#22C55E", "Đăng nhập", "log-in", "#22C55E", "Phiên làm việc", "192.168.1.12", "success", "auth"),
|
||||
get
|
||||
{
|
||||
var list = _activeTab == "all" ? _logs.AsEnumerable() : _logs.Where(l => IsCategory(l, _activeTab));
|
||||
if (!string.IsNullOrEmpty(SearchQuery))
|
||||
list = list.Where(l => (l.EventType ?? "").Contains(SearchQuery, StringComparison.OrdinalIgnoreCase)
|
||||
|| (l.ActorName ?? "").Contains(SearchQuery, StringComparison.OrdinalIgnoreCase)
|
||||
|| (l.ResourceType ?? "").Contains(SearchQuery, StringComparison.OrdinalIgnoreCase));
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
try { _logs = await IamService.GetAuditLogsAsync(); }
|
||||
catch { _logs = new(); }
|
||||
finally { _loading = false; }
|
||||
}
|
||||
|
||||
private static bool IsCategory(IamApiService.AuditLogDto log, string cat)
|
||||
{
|
||||
var evt = (log.EventType ?? "").ToLower();
|
||||
return cat switch
|
||||
{
|
||||
"auth" => evt.Contains("login") || evt.Contains("auth") || evt.Contains("token") || evt.Contains("password"),
|
||||
"config" => evt.Contains("config") || evt.Contains("setting") || evt.Contains("role") || evt.Contains("permission"),
|
||||
"data" => evt.Contains("create") || evt.Contains("update") || evt.Contains("delete") || evt.Contains("order"),
|
||||
_ => true
|
||||
};
|
||||
}
|
||||
|
||||
private static string FormatTime(DateTime? dt) => dt?.ToString("dd/MM/yyyy HH:mm:ss") ?? "—";
|
||||
|
||||
private static string GetInitials(string v)
|
||||
{
|
||||
if (v.Contains('@')) v = v.Split('@')[0];
|
||||
return v.Length >= 2 ? v[..2].ToUpper() : v.ToUpper();
|
||||
}
|
||||
|
||||
private static string GetEventIcon(string? eventType) => (eventType ?? "").ToLower() switch
|
||||
{
|
||||
var e when e.Contains("login") => "log-in",
|
||||
var e when e.Contains("create") => "plus-circle",
|
||||
var e when e.Contains("update") => "edit",
|
||||
var e when e.Contains("delete") => "trash-2",
|
||||
var e when e.Contains("role") => "shield",
|
||||
_ => "activity"
|
||||
};
|
||||
|
||||
private static string GetEventColor(string? eventType) => (eventType ?? "").ToLower() switch
|
||||
{
|
||||
var e when e.Contains("login") => "#22C55E",
|
||||
var e when e.Contains("create") => "#3B82F6",
|
||||
var e when e.Contains("update") => "#F59E0B",
|
||||
var e when e.Contains("delete") => "#EF4444",
|
||||
_ => "#8B5CF6"
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user