feat(admin): P4 — Notifications, Customer detail, Photo upload

- G9: Notification bell with dropdown panel (4 demo alerts)
- G6+: Customer detail expandable rows + action buttons
- B4: Before/After photo upload with dual dropzones + history grid
This commit is contained in:
Ho Ngoc Hai
2026-03-01 04:43:18 +07:00
parent 644751be7b
commit 81e357d226

View File

@@ -17,6 +17,42 @@
<h1 class="admin-topbar__title">@_sectionTitle</h1>
<p class="admin-topbar__subtitle">@(_shopName ?? "Cửa hàng") • @_verticalLabel</p>
</div>
<div style="display:flex;align-items:center;gap:12px;">
@* G9: Notification Bell *@
<div style="position:relative;">
<button @onclick='() => { _showNotifications = !_showNotifications; StateHasChanged(); }'
style="width:40px;height:40px;border-radius:12px;border:1px solid var(--admin-border-subtle);background:var(--admin-bg-elevated);display:flex;align-items:center;justify-content:center;cursor:pointer;position:relative;">
<i data-lucide="bell" style="width:18px;height:18px;color:var(--admin-text-secondary);"></i>
<span style="position:absolute;top:6px;right:6px;width:8px;height:8px;border-radius:50%;background:#EF4444;border:2px solid var(--admin-bg-elevated);"></span>
</button>
@if (_showNotifications)
{
<div style="position:absolute;right:0;top:48px;width:320px;background:var(--admin-bg-elevated);border:1px solid var(--admin-border-subtle);border-radius:12px;box-shadow:0 8px 32px rgba(0,0,0,0.3);z-index:100;overflow:hidden;">
<div style="padding:14px 16px;border-bottom:1px solid var(--admin-border-subtle);display:flex;justify-content:space-between;align-items:center;">
<span style="font-weight:700;font-size:14px;">🔔 Thông báo</span>
<span style="font-size:11px;color:var(--admin-orange-primary);cursor:pointer;">Xem tất cả</span>
</div>
@foreach (var notif in new[] {
new { Icon = "⚠️", Title = "Sắp hết hàng", Desc = "Espresso chỉ còn 3 phần", Time = "5 phút trước", Color = "#F59E0B" },
new { Icon = "📦", Title = "Đơn hàng mới", Desc = "Bàn 3 — Phở bò x2, Gỏi cuốn x1", Time = "12 phút trước", Color = "#3B82F6" },
new { Icon = "✅", Title = "Ca làm hoàn thành", Desc = "Nguyễn Văn A đã kết thúc ca chiều", Time = "1 giờ trước", Color = "#22C55E" },
new { Icon = "💰", Title = "Giao dịch ví", Desc = "Nạp thêm 500,000₫ vào ví store", Time = "3 giờ trước", Color = "#8B5CF6" } })
{
<div style="padding:12px 16px;border-bottom:1px solid var(--admin-border-subtle);cursor:pointer;transition:background 0.15s;" onmouseover="this.style.background='rgba(255,92,0,0.05)'" onmouseout="this.style.background='transparent'">
<div style="display:flex;gap:10px;align-items:flex-start;">
<span style="font-size:18px;">@notif.Icon</span>
<div style="flex:1;">
<div style="font-weight:600;font-size:13px;">@notif.Title</div>
<div style="font-size:12px;color:var(--admin-text-tertiary);margin-top:2px;">@notif.Desc</div>
<div style="font-size:11px;color:var(--admin-text-quaternary, #555);margin-top:4px;">@notif.Time</div>
</div>
</div>
</div>
}
</div>
}
</div>
</div>
</div>
@* ═══ CONTENT ═══ *@
@@ -431,15 +467,50 @@
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Cấp bậc</th>
<th style="padding:12px 16px;text-align:right;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">EXP</th>
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Ngày tham gia</th>
<th style="padding:12px 16px;text-align:center;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);"></th>
</tr></thead><tbody>
@foreach (var m in filteredMembers)
{
<tr style="border-top:1px solid var(--admin-border-subtle);">
var isExpanded = _selectedCustomerId == m.Id;
<tr style="border-top:1px solid var(--admin-border-subtle);cursor:pointer;transition:background 0.15s;@(isExpanded ? "background:rgba(255,92,0,0.05);" : "")" @onclick='() => { _selectedCustomerId = isExpanded ? null : m.Id; StateHasChanged(); }'>
<td style="padding:12px 16px;font-weight:600;font-family:monospace;font-size:12px;">@m.Id.ToString()[..8]</td>
<td style="padding:12px 16px;"><span class="admin-status-badge admin-status-badge--online" style="font-size:11px;padding:2px 10px;">@(m.LevelName ?? "—")</span></td>
<td style="padding:12px 16px;text-align:right;font-weight:600;color:var(--admin-orange-primary);">@m.TotalExpEarned.ToString("N0")</td>
<td style="padding:12px 16px;font-size:13px;color:var(--admin-text-tertiary);">@m.CreatedAt.ToString("dd/MM/yyyy")</td>
<td style="padding:12px 16px;text-align:center;"><i data-lucide="@(isExpanded ? "chevron-up" : "chevron-down")" style="width:14px;height:14px;color:var(--admin-text-tertiary);"></i></td>
</tr>
@if (isExpanded)
{
<tr><td colspan="5" style="padding:0;">
<div style="padding:16px 20px;background:rgba(255,92,0,0.03);border-top:2px solid var(--admin-orange-primary);">
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px;">
<div>
<label style="font-size:11px;font-weight:600;color:var(--admin-text-tertiary);display:block;margin-bottom:4px;">🆔 Mã KH</label>
<div style="font-family:monospace;font-size:12px;">@m.Id.ToString()</div>
</div>
<div>
<label style="font-size:11px;font-weight:600;color:var(--admin-text-tertiary);display:block;margin-bottom:4px;">⭐ Cấp bậc hiện tại</label>
<div style="font-weight:600;">@(m.LevelName ?? "Chưa xếp hạng") • @m.TotalExpEarned.ToString("N0") EXP</div>
</div>
<div>
<label style="font-size:11px;font-weight:600;color:var(--admin-text-tertiary);display:block;margin-bottom:4px;">📅 Tham gia từ</label>
<div style="font-size:13px;">@m.CreatedAt.ToString("dd/MM/yyyy HH:mm")</div>
</div>
</div>
<div style="display:flex;gap:8px;margin-top:12px;">
<button style="padding:6px 14px;border-radius:8px;border:1px solid var(--admin-border-subtle);background:var(--admin-bg-elevated);color:var(--admin-text-primary);font-size:12px;cursor:pointer;display:flex;align-items:center;gap:6px;">
<i data-lucide="gift" style="width:12px;height:12px;"></i> Tặng ưu đãi
</button>
<button style="padding:6px 14px;border-radius:8px;border:1px solid var(--admin-border-subtle);background:var(--admin-bg-elevated);color:var(--admin-text-primary);font-size:12px;cursor:pointer;display:flex;align-items:center;gap:6px;">
<i data-lucide="message-square" style="width:12px;height:12px;"></i> Gửi tin
</button>
<button style="padding:6px 14px;border-radius:8px;border:1px solid var(--admin-border-subtle);background:var(--admin-bg-elevated);color:var(--admin-text-primary);font-size:12px;cursor:pointer;display:flex;align-items:center;gap:6px;">
<i data-lucide="history" style="width:12px;height:12px;"></i> Lịch sử đơn
</button>
</div>
</div>
</td></tr>
}
}
</tbody></table>
</div>
@@ -842,15 +913,37 @@
{
<div class="admin-panel">
<div class="admin-panel__header"><h3 class="admin-panel__title">📷 Ảnh Before / After</h3></div>
<div class="admin-panel__body" style="text-align:center;padding:40px 20px;">
<div style="width:64px;height:64px;border-radius:20px;background:rgba(236,72,153,0.1);display:flex;align-items:center;justify-content:center;margin:0 auto 16px;">
<i data-lucide="camera" style="width:28px;height:28px;color:#EC4899;"></i>
<div class="admin-panel__body">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:20px;">
<div><label style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;">Khách hàng</label><input type="text" placeholder="Tìm khách hàng..." style="width:100%;padding:10px 14px;border-radius:8px;background:var(--admin-bg-elevated);border:1px solid var(--admin-border-subtle);font-size:14px;color:var(--admin-text-primary);" /></div>
<div><label style="font-size:12px;font-weight:600;display:block;margin-bottom:4px;">Liệu trình</label><select style="width:100%;padding:10px 14px;border-radius:8px;background:var(--admin-bg-elevated);border:1px solid var(--admin-border-subtle);font-size:14px;color:var(--admin-text-primary);"><option>Chọn liệu trình...</option><option>Trị nám 5 buổi</option><option>Trẻ hóa da 3 buổi</option></select></div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:20px;">
@foreach (var (label, icon) in new[] { ("Before", "image"), ("After", "image-plus") })
{
<div style="border:2px dashed var(--admin-border-subtle);border-radius:12px;padding:32px 20px;text-align:center;cursor:pointer;transition:all 0.2s;" onmouseover="this.style.borderColor='var(--admin-orange-primary)'" onmouseout="this.style.borderColor='var(--admin-border-subtle)'">
<div style="width:48px;height:48px;border-radius:14px;background:rgba(236,72,153,0.1);display:flex;align-items:center;justify-content:center;margin:0 auto 12px;"><i data-lucide="@icon" style="width:24px;height:24px;color:#EC4899;"></i></div>
<div style="font-weight:600;font-size:14px;margin-bottom:4px;">Upload ảnh @label</div>
<div style="font-size:12px;color:var(--admin-text-tertiary);">Kéo thả hoặc click để chọn ảnh</div>
<div style="font-size:11px;color:var(--admin-text-tertiary);margin-top:4px;">JPG, PNG, HEIC • Max 10MB</div>
</div>
}
</div>
<div style="padding-top:16px;border-top:1px solid var(--admin-border-subtle);">
<h4 style="font-size:14px;font-weight:600;margin:0 0 12px;">📅 Lịch sử ảnh</h4>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:12px;">
@foreach (var (date, desc, color) in new[] { ("15/02", "Buổi 1 — Before", "#3B82F6"), ("22/02", "Buổi 2 — After", "#22C55E"), ("01/03", "Buổi 3 — After", "#8B5CF6") })
{
<div style="border-radius:10px;border:1px solid var(--admin-border-subtle);overflow:hidden;">
<div style="height:100px;background:linear-gradient(135deg, @($"{color}22"), @($"{color}11"));display:flex;align-items:center;justify-content:center;"><i data-lucide="image" style="width:28px;height:28px;color:@color;opacity:0.5;"></i></div>
<div style="padding:8px 10px;">
<div style="font-size:11px;font-weight:600;">@desc</div>
<div style="font-size:10px;color:var(--admin-text-tertiary);">@date/2026</div>
</div>
</div>
}
</div>
</div>
<h3 style="font-size:18px;font-weight:700;margin:0 0 8px;">So sánh kết quả điều trị</h3>
<p style="font-size:14px;color:var(--admin-text-tertiary);margin:0 0 20px;max-width:400px;margin-left:auto;margin-right:auto;">Upload ảnh trước/sau điều trị để theo dõi tiến triển và tư vấn khách hàng.</p>
<button style="padding:10px 20px;border-radius:8px;border:2px dashed var(--admin-border-subtle);background:transparent;color:var(--admin-text-tertiary);font-size:13px;cursor:pointer;">
<i data-lucide="upload" style="width:16px;height:16px;vertical-align:middle;margin-right:6px;"></i>Chọn ảnh để upload
</button>
</div>
</div>
}
@@ -1056,6 +1149,9 @@
private int _calendarWeekOffset;
private string _kdsStation = "all";
private string _treatmentTab = "treatment";
// P4 state: notifications, customer detail
private bool _showNotifications;
private Guid? _selectedCustomerId;
private List<PosDataService.ResourceInfo> _resources = new();
// Customer filter state
private string _customerSearch = "";