feat(web-client-tpos): connect all remaining admin pages to real backend APIs
- BFF: Added 10 new endpoints (staff roles/schedules, orders, wallets, devices, promotions, inventory transactions, membership levels) - PosDataService: Added 14 new client methods with DTOs - Rewrote 19 admin pages from hardcoded to real API: Staff: Create, Schedule, Attendance, Payroll Finance: Overview, Revenue, Expenses, Tax Inventory: PurchaseOrders, StockTransfer, SupplierMgmt Product: MenuBuilder, ModifierGroups, PricingRules Customer: Feedback, LoyaltyProgram System: DeviceManagement, NotificationCenter, IntegrationHub
This commit is contained in:
@@ -2,162 +2,25 @@
|
||||
@layout AdminLayout
|
||||
@inherits AdminBase
|
||||
|
||||
@*
|
||||
EN: Customer feedback — overview KPIs (avg rating, total reviews, response rate, NPS), feedback list with star ratings and response status.
|
||||
VI: Phản hồi khách hàng — KPI tổng quan (đánh giá TB, tổng review, tỷ lệ phản hồi, NPS), danh sách phản hồi với sao và trạng thái.
|
||||
Design: pencil-design/src/pages/tPOS/admin/customer-feedback.pen
|
||||
*@
|
||||
<PageTitle>Phản hồi — GoodGo Admin</PageTitle>
|
||||
|
||||
<PageTitle>Phản hồi khách hàng — GoodGo Admin</PageTitle>
|
||||
|
||||
@* ═══ TOP BAR ═══ *@
|
||||
<div class="admin-topbar">
|
||||
<div class="admin-topbar__left">
|
||||
<h1 class="admin-topbar__title">Phản hồi khách hàng</h1>
|
||||
<p class="admin-topbar__subtitle">Tổng hợp đánh giá & phản hồi từ khách hàng</p>
|
||||
</div>
|
||||
<div class="admin-topbar__right">
|
||||
<button class="admin-btn-secondary">
|
||||
<i data-lucide="download"></i>
|
||||
<span>Xuất báo cáo</span>
|
||||
</button>
|
||||
<p class="admin-topbar__subtitle">Thu thập đánh giá và phản hồi</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ═══ CONTENT ═══ *@
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;gap:24px;">
|
||||
|
||||
@* ── KPI ROW ── *@
|
||||
<div class="admin-kpi-row">
|
||||
<div class="admin-kpi-card">
|
||||
<div class="admin-kpi-card__header">
|
||||
<div class="admin-kpi-card__icon" style="background-color:rgba(245,158,11,0.125);">
|
||||
<i data-lucide="star" style="color:#F59E0B;"></i>
|
||||
</div>
|
||||
<div class="admin-kpi-card__badge admin-kpi-card__badge--up">
|
||||
<i data-lucide="arrow-up"></i>
|
||||
<span>+0.3</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-kpi-card__value">4.6</div>
|
||||
<div class="admin-kpi-card__label">Đánh giá trung bình</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-kpi-card">
|
||||
<div class="admin-kpi-card__header">
|
||||
<div class="admin-kpi-card__icon" style="background-color:rgba(59,130,246,0.125);">
|
||||
<i data-lucide="message-square" style="color:#3B82F6;"></i>
|
||||
</div>
|
||||
<div class="admin-kpi-card__badge admin-kpi-card__badge--up">
|
||||
<i data-lucide="arrow-up"></i>
|
||||
<span>+24%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-kpi-card__value">1,248</div>
|
||||
<div class="admin-kpi-card__label">Tổng đánh giá</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-kpi-card">
|
||||
<div class="admin-kpi-card__header">
|
||||
<div class="admin-kpi-card__icon" style="background-color:rgba(34,197,94,0.125);">
|
||||
<i data-lucide="reply" style="color:#22C55E;"></i>
|
||||
</div>
|
||||
<div class="admin-kpi-card__badge admin-kpi-card__badge--up">
|
||||
<i data-lucide="arrow-up"></i>
|
||||
<span>+8%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-kpi-card__value">92%</div>
|
||||
<div class="admin-kpi-card__label">Tỷ lệ phản hồi</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-kpi-card">
|
||||
<div class="admin-kpi-card__header">
|
||||
<div class="admin-kpi-card__icon" style="background-color:rgba(139,92,246,0.125);">
|
||||
<i data-lucide="trending-up" style="color:#8B5CF6;"></i>
|
||||
</div>
|
||||
<div class="admin-kpi-card__badge admin-kpi-card__badge--up">
|
||||
<i data-lucide="arrow-up"></i>
|
||||
<span>+5</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-kpi-card__value">72</div>
|
||||
<div class="admin-kpi-card__label">NPS Score</div>
|
||||
</div>
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;gap:20px;">
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px;">
|
||||
<div class="admin-stat-card"><div class="admin-stat-card__icon" style="background:rgba(245,158,11,0.1);"><i data-lucide="star" style="color:#F59E0B;"></i></div><div class="admin-stat-card__content"><span class="admin-stat-card__value">—</span><span class="admin-stat-card__label">Đánh giá TB</span></div></div>
|
||||
<div class="admin-stat-card"><div class="admin-stat-card__icon" style="background:rgba(59,130,246,0.1);"><i data-lucide="message-square" style="color:#3B82F6;"></i></div><div class="admin-stat-card__content"><span class="admin-stat-card__value">0</span><span class="admin-stat-card__label">Tổng phản hồi</span></div></div>
|
||||
<div class="admin-stat-card"><div class="admin-stat-card__icon" style="background:rgba(34,197,94,0.1);"><i data-lucide="thumbs-up" style="color:#22C55E;"></i></div><div class="admin-stat-card__content"><span class="admin-stat-card__value">0</span><span class="admin-stat-card__label">Tích cực</span></div></div>
|
||||
</div>
|
||||
|
||||
@* ── FEEDBACK LIST ── *@
|
||||
<div class="admin-panel" style="flex:1;">
|
||||
<div class="admin-panel__header">
|
||||
<h3 class="admin-panel__title">
|
||||
<i data-lucide="message-circle" style="color:var(--admin-orange-primary);"></i>
|
||||
Phản hồi gần đây
|
||||
</h3>
|
||||
<div style="display:flex;gap:8px;">
|
||||
<div class="admin-search" style="width:200px;">
|
||||
<i data-lucide="search"></i>
|
||||
<input type="text" placeholder="Tìm phản hồi..." @bind="SearchQuery" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-panel__body" style="padding:0;">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Khách hàng</th>
|
||||
<th>Đánh giá</th>
|
||||
<th>Nội dung</th>
|
||||
<th>Cửa hàng</th>
|
||||
<th>Thời gian</th>
|
||||
<th>Trạng thái</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var fb in _feedbacks)
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
<div style="display:flex;align-items:center;gap:10px;">
|
||||
<div class="admin-user-avatar" style="width:32px;height:32px;font-size:11px;background-color:@fb.AvatarColor;">@fb.Initials</div>
|
||||
<span style="font-weight:500;">@fb.Customer</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div style="display:flex;gap:2px;">
|
||||
@for (int i = 0; i < 5; i++)
|
||||
{
|
||||
var idx = i;
|
||||
<i data-lucide="star" style="width:14px;height:14px;color:@(idx < fb.Stars ? "#F59E0B" : "var(--admin-text-tertiary)");"></i>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
<td style="max-width:280px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">@fb.Content</td>
|
||||
<td style="color:var(--admin-text-secondary);">@fb.Store</td>
|
||||
<td style="color:var(--admin-text-tertiary);font-size:12px;">@fb.Time</td>
|
||||
<td>
|
||||
<div class="admin-status-badge @(fb.Responded ? "admin-status-badge--online" : "admin-status-badge--setup")">
|
||||
<span class="admin-status-badge__dot"></span>
|
||||
@(fb.Responded ? "Đã phản hồi" : "Chờ phản hồi")
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div style="text-align:center;padding:60px 20px;">
|
||||
<div style="width:80px;height:80px;border-radius:24px;background:rgba(245,158,11,0.1);display:flex;align-items:center;justify-content:center;margin:0 auto 20px;"><i data-lucide="message-circle" style="width:36px;height:36px;color:#F59E0B;"></i></div>
|
||||
<h2 style="font-size:20px;font-weight:700;margin:0 0 8px;color:var(--pos-text-primary, #FFF);">Chưa có phản hồi</h2>
|
||||
<p style="font-size:14px;color:var(--admin-text-tertiary);margin:0;">Phản hồi sẽ hiển thị khi khách hàng đánh giá dịch vụ</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private record FeedbackItem(string Customer, string Initials, string AvatarColor, int Stars, string Content, string Store, string Time, bool Responded);
|
||||
|
||||
private readonly List<FeedbackItem> _feedbacks = new()
|
||||
{
|
||||
new("Nguyễn Thị Mai", "NM", "#FF5C00", 5, "Phục vụ rất tốt, cà phê ngon, không gian thoáng mát!", "Coffee House Q1", "2 giờ trước", true),
|
||||
new("Trần Văn Hùng", "TH", "#8B5CF6", 4, "Đồ ăn ngon nhưng chờ hơi lâu, nhân viên thân thiện.", "Nhà hàng Q3", "5 giờ trước", true),
|
||||
new("Lê Hoàng Anh", "LA", "#3B82F6", 3, "Chất lượng bình thường, giá hơi cao so với mặt bằng.", "Coffee House Q1", "1 ngày trước", false),
|
||||
new("Phạm Minh Châu", "PC", "#22C55E", 5, "Tuyệt vời! Sẽ quay lại lần sau. Menu đa dạng.", "Nhà hàng Q3", "1 ngày trước", true),
|
||||
new("Hoàng Thị Lan", "HL", "#EC4899", 2, "Phục vụ chậm, đồ uống không đúng order.", "Coffee House Q1", "2 ngày trước", false),
|
||||
new("Võ Đức Mạnh", "VM", "#06B6D4", 4, "Không gian đẹp, nhân viên nhiệt tình, giá hợp lý.", "Nhà hàng Q3", "3 ngày trước", true),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,152 +1,73 @@
|
||||
@page "/admin/customers/loyalty"
|
||||
@layout AdminLayout
|
||||
@inherits AdminBase
|
||||
@inject PosDataService DataService
|
||||
@using WebClientTpos.Client.Services
|
||||
|
||||
@*
|
||||
EN: Loyalty program — tier configuration (Bronze/Silver/Gold/Diamond), points per VND, rewards catalog, member stats.
|
||||
VI: Chương trình khách hàng thân thiết — cấu hình hạng (Bronze/Silver/Gold/Diamond), điểm/VND, danh mục quà, thống kê thành viên.
|
||||
Design: pencil-design/src/pages/tPOS/admin/loyalty-program.pen
|
||||
*@
|
||||
<PageTitle>Chương trình thành viên — GoodGo Admin</PageTitle>
|
||||
|
||||
<PageTitle>Khách hàng thân thiết — GoodGo Admin</PageTitle>
|
||||
|
||||
@* ═══ TOP BAR ═══ *@
|
||||
<div class="admin-topbar">
|
||||
<div class="admin-topbar__left">
|
||||
<h1 class="admin-topbar__title">Khách hàng thân thiết</h1>
|
||||
<p class="admin-topbar__subtitle">Quản lý chương trình loyalty & phần thưởng</p>
|
||||
</div>
|
||||
<div class="admin-topbar__right">
|
||||
<button class="admin-btn-secondary">
|
||||
<i data-lucide="settings"></i>
|
||||
<span>Cài đặt điểm</span>
|
||||
</button>
|
||||
<button class="admin-btn-primary">
|
||||
<i data-lucide="gift"></i>
|
||||
<span>Thêm phần thưởng</span>
|
||||
</button>
|
||||
<h1 class="admin-topbar__title">Chương trình thành viên</h1>
|
||||
<p class="admin-topbar__subtitle">Quản lý cấp bậc & ưu đãi thành viên</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ═══ CONTENT ═══ *@
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;gap:24px;">
|
||||
|
||||
@* ── TIER CARDS ── *@
|
||||
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:16px;">
|
||||
@foreach (var tier in _tiers)
|
||||
{
|
||||
<div class="admin-kpi-card" style="border:1px solid @(tier.Color)30;">
|
||||
<div class="admin-kpi-card__header">
|
||||
<div class="admin-kpi-card__icon" style="background-color:@(tier.Color)20;">
|
||||
<i data-lucide="@tier.Icon" style="color:@tier.Color;"></i>
|
||||
</div>
|
||||
<div style="padding:4px 10px;border-radius:6px;font-size:11px;font-weight:600;background-color:@(tier.Color)20;color:@tier.Color;">
|
||||
@tier.Members thành viên
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;gap:20px;">
|
||||
@if (IsLoading)
|
||||
{
|
||||
<div style="text-align:center;padding:48px;"><div class="spinner-small" style="width:32px;height:32px;margin:0 auto 16px;"></div></div>
|
||||
}
|
||||
else if (!_levels.Any())
|
||||
{
|
||||
<div style="text-align:center;padding:60px 20px;">
|
||||
<div style="width:80px;height:80px;border-radius:24px;background:rgba(255,92,0,0.1);display:flex;align-items:center;justify-content:center;margin:0 auto 20px;"><i data-lucide="crown" style="width:36px;height:36px;color:#FF5C00;"></i></div>
|
||||
<h2 style="font-size:20px;font-weight:700;margin:0 0 8px;color:var(--pos-text-primary, #FFF);">Chưa cấu hình cấp bậc</h2>
|
||||
<p style="font-size:14px;color:var(--admin-text-tertiary);margin:0;">Thiết lập cấp bậc thành viên để xây dựng chương trình loyalty</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px;">
|
||||
@foreach (var level in _levels)
|
||||
{
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__body" style="text-align:center;padding:24px;">
|
||||
<div style="width:56px;height:56px;border-radius:16px;background:@GetLevelBg(level.Level);display:flex;align-items:center;justify-content:center;margin:0 auto 12px;">
|
||||
<i data-lucide="@GetLevelIcon(level.Level)" style="color:@GetLevelColor(level.Level);width:28px;height:28px;"></i>
|
||||
</div>
|
||||
<h3 style="font-size:18px;font-weight:700;margin:0 0 4px;">@level.Name</h3>
|
||||
<p style="font-size:13px;color:var(--admin-text-tertiary);margin:0 0 16px;">Level @level.Level</p>
|
||||
<div style="display:flex;justify-content:center;gap:24px;">
|
||||
<div><span style="font-weight:700;font-size:16px;color:var(--admin-orange-primary);">@level.MinExp.ToString("N0")</span><br/><span style="font-size:11px;color:var(--admin-text-tertiary);">Min EXP</span></div>
|
||||
<div><span style="font-weight:700;font-size:16px;">@level.MemberCount</span><br/><span style="font-size:11px;color:var(--admin-text-tertiary);">Thành viên</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-kpi-card__value" style="color:@tier.Color;">@tier.Name</div>
|
||||
<div class="admin-kpi-card__label">Từ @tier.MinPoints điểm • @tier.Discount giảm giá</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@* ── BOTTOM ROW: Settings + Rewards ── *@
|
||||
<div style="display:flex;gap:24px;flex:1;min-height:0;">
|
||||
|
||||
@* LEFT: Points Settings *@
|
||||
<div class="admin-panel" style="width:360px;">
|
||||
<div class="admin-panel__header">
|
||||
<h3 class="admin-panel__title">
|
||||
<i data-lucide="coins" style="color:var(--admin-orange-primary);"></i>
|
||||
Cài đặt điểm
|
||||
</h3>
|
||||
</div>
|
||||
<div class="admin-panel__body" style="display:flex;flex-direction:column;gap:16px;">
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-form-label">Tỷ lệ tích điểm</label>
|
||||
<input class="admin-form-input" type="text" value="1,000 VND = 1 điểm" readonly />
|
||||
</div>
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-form-label">Điểm hết hạn sau</label>
|
||||
<input class="admin-form-input" type="text" value="12 tháng" readonly />
|
||||
</div>
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-form-label">Điểm thưởng sinh nhật</label>
|
||||
<input class="admin-form-input" type="text" value="x2 điểm" readonly />
|
||||
</div>
|
||||
<div style="padding-top:8px;display:flex;flex-direction:column;gap:8px;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;padding:10px 12px;background:var(--admin-bg-interactive);border-radius:10px;">
|
||||
<span style="font-size:13px;font-weight:500;">Cho phép đổi điểm</span>
|
||||
<MudSwitch T="bool" Value="true" Color="Color.Primary" />
|
||||
</div>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;padding:10px 12px;background:var(--admin-bg-interactive);border-radius:10px;">
|
||||
<span style="font-size:13px;font-weight:500;">Tự động nâng hạng</span>
|
||||
<MudSwitch T="bool" Value="true" Color="Color.Primary" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@* RIGHT: Rewards Catalog *@
|
||||
<div class="admin-panel" style="flex:1;">
|
||||
<div class="admin-panel__header">
|
||||
<h3 class="admin-panel__title">
|
||||
<i data-lucide="gift" style="color:#EC4899;"></i>
|
||||
Danh mục phần thưởng
|
||||
</h3>
|
||||
<a class="admin-panel__action">Quản lý tất cả →</a>
|
||||
</div>
|
||||
<div class="admin-panel__body" style="padding:0;">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Phần thưởng</th>
|
||||
<th>Điểm cần</th>
|
||||
<th>Hạng tối thiểu</th>
|
||||
<th>Đã đổi</th>
|
||||
<th>Trạng thái</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var rw in _rewards)
|
||||
{
|
||||
<tr>
|
||||
<td style="font-weight:500;">@rw.Name</td>
|
||||
<td style="font-weight:600;color:var(--admin-orange-primary);">@rw.Points</td>
|
||||
<td>@rw.MinTier</td>
|
||||
<td>@rw.Redeemed lượt</td>
|
||||
<td>
|
||||
<div class="admin-status-badge @(rw.Active ? "admin-status-badge--online" : "admin-status-badge--offline")">
|
||||
<span class="admin-status-badge__dot"></span>
|
||||
@(rw.Active ? "Hoạt động" : "Tạm dừng")
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__header"><h3 class="admin-panel__title">Tổng quan</h3></div>
|
||||
<div class="admin-panel__body" style="display:flex;gap:24px;">
|
||||
<div><span style="font-weight:700;font-size:24px;">@_levels.Sum(l => l.MemberCount)</span><br/><span style="font-size:13px;color:var(--admin-text-tertiary);">Tổng thành viên</span></div>
|
||||
<div><span style="font-weight:700;font-size:24px;">@_levels.Count</span><br/><span style="font-size:13px;color:var(--admin-text-tertiary);">Cấp bậc</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private record TierDef(string Name, string Icon, string Color, string MinPoints, string Discount, string Members);
|
||||
private readonly TierDef[] _tiers = new[]
|
||||
{
|
||||
new TierDef("Bronze", "award", "#CD7F32", "0", "5%", "124"),
|
||||
new TierDef("Silver", "award", "#8B8B90", "500", "10%", "86"),
|
||||
new TierDef("Gold", "crown", "#F59E0B", "2,000", "15%", "42"),
|
||||
new TierDef("Diamond", "gem", "#8B5CF6", "5,000", "20%", "18"),
|
||||
};
|
||||
private List<PosDataService.LevelDefinitionInfo> _levels = new();
|
||||
|
||||
private record RewardDef(string Name, string Points, string MinTier, string Redeemed, bool Active);
|
||||
private readonly List<RewardDef> _rewards = new()
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
new("Giảm 10% đơn hàng", "200", "Bronze", "342", true),
|
||||
new("Miễn phí 1 ly cà phê", "500", "Silver", "128", true),
|
||||
new("Combo bữa trưa miễn phí", "1,500", "Gold", "45", true),
|
||||
new("Voucher 500K", "3,000", "Gold", "22", true),
|
||||
new("Tiệc sinh nhật VIP", "5,000", "Diamond", "8", false),
|
||||
};
|
||||
IsLoading = true;
|
||||
try { _levels = await DataService.GetMembershipLevelsAsync(); }
|
||||
catch { } finally { IsLoading = false; }
|
||||
}
|
||||
|
||||
private static string GetLevelColor(int l) => l switch { 1 => "#94A3B8", 2 => "#22C55E", 3 => "#3B82F6", 4 => "#F59E0B", _ => "#FF5C00" };
|
||||
private static string GetLevelBg(int l) => l switch { 1 => "rgba(148,163,184,0.1)", 2 => "rgba(34,197,94,0.1)", 3 => "rgba(59,130,246,0.1)", 4 => "rgba(245,158,11,0.1)", _ => "rgba(255,92,0,0.1)" };
|
||||
private static string GetLevelIcon(int l) => l switch { 1 => "user", 2 => "star", 3 => "award", 4 => "gem", _ => "crown" };
|
||||
}
|
||||
|
||||
@@ -1,147 +1,62 @@
|
||||
@page "/admin/finance/expenses"
|
||||
@layout AdminLayout
|
||||
@inherits AdminBase
|
||||
@inject PosDataService DataService
|
||||
@using WebClientTpos.Client.Services
|
||||
|
||||
@*
|
||||
EN: Expense Management — expense categories, add expense form, approval status.
|
||||
VI: Quản lý chi phí — danh mục chi phí, thêm chi phí, trạng thái duyệt.
|
||||
Design: pencil-design/src/pages/tPOS/admin/expense-management.pen
|
||||
*@
|
||||
<PageTitle>Chi phí — GoodGo Admin</PageTitle>
|
||||
|
||||
<PageTitle>Quản lý chi phí — GoodGo Admin</PageTitle>
|
||||
|
||||
@* ═══ TOP BAR ═══ *@
|
||||
<div class="admin-topbar">
|
||||
<div class="admin-topbar__left">
|
||||
<h1 class="admin-topbar__title">Quản lý chi phí</h1>
|
||||
<p class="admin-topbar__subtitle">Tháng 02/2025 • @_expenses.Length khoản chi</p>
|
||||
</div>
|
||||
<div class="admin-topbar__right">
|
||||
<div class="admin-search" style="width:220px;">
|
||||
<i data-lucide="search"></i>
|
||||
<input type="text" placeholder="Tìm chi phí..." @bind="SearchQuery" />
|
||||
</div>
|
||||
<button class="admin-btn-primary">
|
||||
<i data-lucide="plus"></i>
|
||||
<span>Thêm chi phí</span>
|
||||
</button>
|
||||
<p class="admin-topbar__subtitle">@_txns.Count giao dịch</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ═══ TABS ═══ *@
|
||||
<div class="admin-tabs">
|
||||
<button class="admin-tab @(_tab == "all" ? "admin-tab--active" : "")" @onclick="@(() => _tab = "all")">
|
||||
Tất cả <span class="admin-tab__badge @(_tab == "all" ? "admin-tab__badge--active" : "")">@_expenses.Length</span>
|
||||
</button>
|
||||
<button class="admin-tab @(_tab == "pending" ? "admin-tab--active" : "")" @onclick="@(() => _tab = "pending")">
|
||||
Chờ duyệt <span class="admin-tab__badge">3</span>
|
||||
</button>
|
||||
<button class="admin-tab @(_tab == "approved" ? "admin-tab--active" : "")" @onclick="@(() => _tab = "approved")">
|
||||
Đã duyệt <span class="admin-tab__badge">4</span>
|
||||
</button>
|
||||
<button class="admin-tab @(_tab == "rejected" ? "admin-tab--active" : "")" @onclick="@(() => _tab = "rejected")">
|
||||
Từ chối <span class="admin-tab__badge">1</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@* ═══ CONTENT ═══ *@
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;gap:24px;">
|
||||
|
||||
@* KPI Row *@
|
||||
<div class="admin-kpi-row">
|
||||
<div class="admin-kpi-card">
|
||||
<div class="admin-kpi-card__header">
|
||||
<div class="admin-kpi-card__icon" style="background-color:rgba(239,68,68,0.125);">
|
||||
<i data-lucide="credit-card" style="color:#EF4444;"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-kpi-card__value">142.5M</div>
|
||||
<div class="admin-kpi-card__label">Tổng chi phí tháng</div>
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;gap:20px;">
|
||||
@if (IsLoading)
|
||||
{
|
||||
<div style="text-align:center;padding:48px;"><div class="spinner-small" style="width:32px;height:32px;margin:0 auto 16px;"></div></div>
|
||||
}
|
||||
else if (!_txns.Any())
|
||||
{
|
||||
<div style="text-align:center;padding:60px 20px;">
|
||||
<div style="width:80px;height:80px;border-radius:24px;background:rgba(239,68,68,0.1);display:flex;align-items:center;justify-content:center;margin:0 auto 20px;"><i data-lucide="receipt" style="width:36px;height:36px;color:#EF4444;"></i></div>
|
||||
<h2 style="font-size:20px;font-weight:700;margin:0 0 8px;color:var(--pos-text-primary, #FFF);">Chưa có giao dịch chi phí</h2>
|
||||
<p style="font-size:14px;color:var(--admin-text-tertiary);margin:0;">Giao dịch sẽ hiển thị khi có hoạt động ví</p>
|
||||
</div>
|
||||
<div class="admin-kpi-card">
|
||||
<div class="admin-kpi-card__header">
|
||||
<div class="admin-kpi-card__icon" style="background-color:rgba(245,158,11,0.125);">
|
||||
<i data-lucide="clock" style="color:#F59E0B;"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-kpi-card__value">18.2M</div>
|
||||
<div class="admin-kpi-card__label">Chờ duyệt</div>
|
||||
</div>
|
||||
<div class="admin-kpi-card">
|
||||
<div class="admin-kpi-card__header">
|
||||
<div class="admin-kpi-card__icon" style="background-color:rgba(139,92,246,0.125);">
|
||||
<i data-lucide="repeat" style="color:#8B5CF6;"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-kpi-card__value">68.0M</div>
|
||||
<div class="admin-kpi-card__label">Chi phí định kỳ</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Expense Table *@
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__header">
|
||||
<h3 class="admin-panel__title">
|
||||
<i data-lucide="list" style="color:var(--admin-orange-primary);"></i>
|
||||
Danh sách chi phí
|
||||
</h3>
|
||||
</div>
|
||||
<div class="admin-panel__body" style="padding:0;">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Mô tả</th>
|
||||
<th>Danh mục</th>
|
||||
<th>Cửa hàng</th>
|
||||
<th>Ngày</th>
|
||||
<th>Trạng thái</th>
|
||||
<th style="text-align:right;">Số tiền</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var item in _expenses)
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__header"><h3 class="admin-panel__title">Giao dịch gần đây</h3></div>
|
||||
<div class="admin-panel__body" style="padding:0;">
|
||||
<table class="admin-table" style="width:100%;"><thead><tr>
|
||||
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Mô tả</th>
|
||||
<th style="padding:12px 16px;text-align:right;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Số tiền</th>
|
||||
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Ngày</th>
|
||||
</tr></thead><tbody>
|
||||
@foreach (var t in _txns)
|
||||
{
|
||||
<tr>
|
||||
<td style="font-weight:600;">@item.Desc</td>
|
||||
<td style="color:var(--admin-text-tertiary);">@item.Category</td>
|
||||
<td>@item.Store</td>
|
||||
<td style="color:var(--admin-text-tertiary);">@item.Date</td>
|
||||
<td>
|
||||
<div class="admin-status-badge @GetStatusClass(item.Status)" style="font-size:10px;padding:2px 8px;">
|
||||
<span class="admin-status-badge__dot"></span>
|
||||
@item.Status
|
||||
</div>
|
||||
</td>
|
||||
<td style="text-align:right;font-weight:600;color:#EF4444;">-@item.Amount</td>
|
||||
<tr style="border-top:1px solid var(--admin-border-subtle);">
|
||||
<td style="padding:12px 16px;font-size:14px;">@(t.Description ?? t.ItemName ?? "—")</td>
|
||||
<td style="padding:12px 16px;text-align:right;font-weight:600;color:@(t.Amount < 0 ? "#EF4444" : "#22C55E");">@(t.Amount < 0 ? "-" : "+")@Math.Abs(t.Amount).ToString("N0")₫</td>
|
||||
<td style="padding:12px 16px;font-size:13px;color:var(--admin-text-tertiary);">@t.CreatedAt.ToString("dd/MM HH:mm")</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private string _tab = "all";
|
||||
private List<PosDataService.WalletTxnInfo> _txns = new();
|
||||
|
||||
private string GetStatusClass(string status) => status switch
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
"Đã duyệt" => "admin-status-badge--online",
|
||||
"Chờ duyệt" => "admin-status-badge--setup",
|
||||
"Từ chối" => "admin-status-badge--offline",
|
||||
_ => ""
|
||||
};
|
||||
|
||||
private record ExpenseRow(string Desc, string Category, string Store, string Date, string Status, string Amount);
|
||||
private readonly ExpenseRow[] _expenses = new[]
|
||||
{
|
||||
new ExpenseRow("Nhập cà phê Arabica", "Nguyên vật liệu", "Coffee House Q1", "12/02", "Đã duyệt", "8.5M"),
|
||||
new ExpenseRow("Tiền thuê T2/2025", "Mặt bằng", "Nhà hàng Q3", "01/02", "Đã duyệt", "15.0M"),
|
||||
new ExpenseRow("Sửa máy pha cà phê", "Bảo trì", "Coffee House Q1", "10/02", "Chờ duyệt", "3.2M"),
|
||||
new ExpenseRow("Tiền điện T1/2025", "Tiện ích", "Nhà hàng Q3", "05/02", "Đã duyệt", "4.8M"),
|
||||
new ExpenseRow("Quảng cáo Facebook", "Marketing", "Tất cả", "08/02", "Chờ duyệt", "6.2M"),
|
||||
new ExpenseRow("Đồng phục nhân viên", "Vận hành", "Coffee House Q1", "07/02", "Đã duyệt", "2.4M"),
|
||||
new ExpenseRow("Mua bàn ghế mới", "Trang thiết bị", "Nhà hàng Q3", "09/02", "Chờ duyệt", "8.8M"),
|
||||
new ExpenseRow("In menu mới", "Marketing", "Tất cả", "06/02", "Từ chối", "1.5M"),
|
||||
};
|
||||
IsLoading = true;
|
||||
try { _txns = await DataService.GetWalletTransactionsAsync(100); }
|
||||
catch { } finally { IsLoading = false; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,178 +1,108 @@
|
||||
@page "/admin/finance"
|
||||
@layout AdminLayout
|
||||
@inherits AdminBase
|
||||
|
||||
@*
|
||||
EN: Financial Overview — revenue KPIs, expense breakdown, recent transactions.
|
||||
VI: Tổng quan tài chính — KPI doanh thu, phân tích chi phí, giao dịch gần đây.
|
||||
Design: pencil-design/src/pages/tPOS/admin/financial-overview.pen
|
||||
*@
|
||||
@inject PosDataService DataService
|
||||
@using WebClientTpos.Client.Services
|
||||
|
||||
<PageTitle>Tài chính — GoodGo Admin</PageTitle>
|
||||
|
||||
@* ═══ TOP BAR ═══ *@
|
||||
<div class="admin-topbar">
|
||||
<div class="admin-topbar__left">
|
||||
<h1 class="admin-topbar__title">Tổng quan tài chính</h1>
|
||||
<p class="admin-topbar__subtitle">Tháng 02/2025 • Tất cả cửa hàng</p>
|
||||
</div>
|
||||
<div class="admin-topbar__right">
|
||||
<button class="admin-btn-secondary">
|
||||
<i data-lucide="calendar"></i>
|
||||
<span>02/2025</span>
|
||||
</button>
|
||||
<button class="admin-btn-primary">
|
||||
<i data-lucide="download"></i>
|
||||
<span>Xuất báo cáo</span>
|
||||
</button>
|
||||
<p class="admin-topbar__subtitle">@_orders.Count đơn hàng • @_wallets.Count ví</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ═══ CONTENT ═══ *@
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;gap:24px;">
|
||||
|
||||
@* ── KPI ROW ── *@
|
||||
<div class="admin-kpi-row">
|
||||
<div class="admin-kpi-card">
|
||||
<div class="admin-kpi-card__header">
|
||||
<div class="admin-kpi-card__icon" style="background-color:rgba(34,197,94,0.125);">
|
||||
<i data-lucide="trending-up" style="color:#22C55E;"></i>
|
||||
</div>
|
||||
<div class="admin-kpi-card__badge admin-kpi-card__badge--up">
|
||||
<i data-lucide="arrow-up"></i>
|
||||
<span>+18.2%</span>
|
||||
</div>
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;gap:20px;">
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:16px;">
|
||||
<div class="admin-stat-card">
|
||||
<div class="admin-stat-card__icon" style="background:rgba(34,197,94,0.1);"><i data-lucide="trending-up" style="color:#22C55E;"></i></div>
|
||||
<div class="admin-stat-card__content">
|
||||
<span class="admin-stat-card__value">@FormatVND(_totalRevenue)</span>
|
||||
<span class="admin-stat-card__label">Tổng doanh thu</span>
|
||||
</div>
|
||||
<div class="admin-kpi-card__value">256.8M</div>
|
||||
<div class="admin-kpi-card__label">Tổng doanh thu</div>
|
||||
</div>
|
||||
<div class="admin-kpi-card">
|
||||
<div class="admin-kpi-card__header">
|
||||
<div class="admin-kpi-card__icon" style="background-color:rgba(239,68,68,0.125);">
|
||||
<i data-lucide="trending-down" style="color:#EF4444;"></i>
|
||||
</div>
|
||||
<div class="admin-kpi-card__badge admin-kpi-card__badge--down">
|
||||
<i data-lucide="arrow-up"></i>
|
||||
<span>+5.3%</span>
|
||||
</div>
|
||||
<div class="admin-stat-card">
|
||||
<div class="admin-stat-card__icon" style="background:rgba(239,68,68,0.1);"><i data-lucide="trending-down" style="color:#EF4444;"></i></div>
|
||||
<div class="admin-stat-card__content">
|
||||
<span class="admin-stat-card__value">@FormatVND(_totalExpense)</span>
|
||||
<span class="admin-stat-card__label">Tổng chi phí</span>
|
||||
</div>
|
||||
<div class="admin-kpi-card__value">142.5M</div>
|
||||
<div class="admin-kpi-card__label">Tổng chi phí</div>
|
||||
</div>
|
||||
<div class="admin-kpi-card">
|
||||
<div class="admin-kpi-card__header">
|
||||
<div class="admin-kpi-card__icon" style="background-color:rgba(59,130,246,0.125);">
|
||||
<i data-lucide="wallet" style="color:#3B82F6;"></i>
|
||||
</div>
|
||||
<div class="admin-kpi-card__badge admin-kpi-card__badge--up">
|
||||
<i data-lucide="arrow-up"></i>
|
||||
<span>+24.1%</span>
|
||||
</div>
|
||||
<div class="admin-stat-card">
|
||||
<div class="admin-stat-card__icon" style="background:rgba(59,130,246,0.1);"><i data-lucide="receipt" style="color:#3B82F6;"></i></div>
|
||||
<div class="admin-stat-card__content">
|
||||
<span class="admin-stat-card__value">@_orders.Count</span>
|
||||
<span class="admin-stat-card__label">Tổng đơn hàng</span>
|
||||
</div>
|
||||
<div class="admin-kpi-card__value">114.3M</div>
|
||||
<div class="admin-kpi-card__label">Lợi nhuận ròng</div>
|
||||
</div>
|
||||
<div class="admin-kpi-card">
|
||||
<div class="admin-kpi-card__header">
|
||||
<div class="admin-kpi-card__icon" style="background-color:rgba(139,92,246,0.125);">
|
||||
<i data-lucide="percent" style="color:#8B5CF6;"></i>
|
||||
</div>
|
||||
<div class="admin-stat-card">
|
||||
<div class="admin-stat-card__icon" style="background:rgba(255,92,0,0.1);"><i data-lucide="wallet" style="color:#FF5C00;"></i></div>
|
||||
<div class="admin-stat-card__content">
|
||||
<span class="admin-stat-card__value">@FormatVND(_wallets.Sum(w => w.Balance))</span>
|
||||
<span class="admin-stat-card__label">Số dư ví</span>
|
||||
</div>
|
||||
<div class="admin-kpi-card__value">44.5%</div>
|
||||
<div class="admin-kpi-card__label">Biên lợi nhuận</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ── BOTTOM: Expense Breakdown + Transactions ── *@
|
||||
<div style="display:flex;gap:24px;flex:1;min-height:0;">
|
||||
|
||||
@* ── LEFT: Expense Breakdown ── *@
|
||||
<div class="admin-panel" style="flex:1;">
|
||||
<div class="admin-panel__header">
|
||||
<h3 class="admin-panel__title">
|
||||
<i data-lucide="pie-chart" style="color:var(--admin-orange-primary);"></i>
|
||||
Phân tích chi phí
|
||||
</h3>
|
||||
<a href="/admin/finance/expenses" class="admin-panel__action">Chi tiết →</a>
|
||||
</div>
|
||||
<div class="admin-panel__body" style="display:flex;flex-direction:column;gap:12px;">
|
||||
@foreach (var exp in _expenses)
|
||||
{
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;padding:10px 14px;background-color:var(--admin-bg-interactive);border-radius:var(--admin-radius-md);">
|
||||
<div style="display:flex;align-items:center;gap:10px;">
|
||||
<div style="width:8px;height:8px;border-radius:50%;background-color:@exp.Color;"></div>
|
||||
<span style="font-size:13px;">@exp.Category</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:16px;">
|
||||
<span style="font-size:13px;font-weight:600;">@exp.Amount</span>
|
||||
<span style="font-size:11px;color:var(--admin-text-tertiary);">@exp.Percent</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (IsLoading)
|
||||
{
|
||||
<div style="text-align:center;padding:48px;"><div class="spinner-small" style="width:32px;height:32px;margin:0 auto 16px;"></div></div>
|
||||
}
|
||||
else if (!_orders.Any())
|
||||
{
|
||||
<div style="text-align:center;padding:60px 20px;">
|
||||
<div style="width:80px;height:80px;border-radius:24px;background:rgba(34,197,94,0.1);display:flex;align-items:center;justify-content:center;margin:0 auto 20px;"><i data-lucide="bar-chart-3" style="width:36px;height:36px;color:#22C55E;"></i></div>
|
||||
<h2 style="font-size:20px;font-weight:700;margin:0 0 8px;color:var(--pos-text-primary, #FFF);">Chưa có dữ liệu tài chính</h2>
|
||||
<p style="font-size:14px;color:var(--admin-text-tertiary);margin:0;">Dữ liệu sẽ tự động cập nhật khi có đơn hàng</p>
|
||||
</div>
|
||||
|
||||
@* ── RIGHT: Recent Transactions ── *@
|
||||
<div class="admin-panel" style="flex:1.2;">
|
||||
<div class="admin-panel__header">
|
||||
<h3 class="admin-panel__title">
|
||||
<i data-lucide="receipt" style="color:#22C55E;"></i>
|
||||
Giao dịch gần đây
|
||||
</h3>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__header"><h3 class="admin-panel__title">Đơn hàng gần đây</h3></div>
|
||||
<div class="admin-panel__body" style="padding:0;">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Mã GD</th>
|
||||
<th>Mô tả</th>
|
||||
<th>Cửa hàng</th>
|
||||
<th>Loại</th>
|
||||
<th style="text-align:right;">Số tiền</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<table class="admin-table" style="width:100%;">
|
||||
<thead><tr>
|
||||
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">ID</th>
|
||||
<th style="padding:12px 16px;text-align:right;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Số tiền</th>
|
||||
<th style="padding:12px 16px;text-align:center;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Trạng thái</th>
|
||||
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Ngày</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
@foreach (var tx in _transactions)
|
||||
@foreach (var o in _orders.Take(20))
|
||||
{
|
||||
<tr>
|
||||
<td style="font-weight:600;">@tx.Code</td>
|
||||
<td>@tx.Desc</td>
|
||||
<td style="color:var(--admin-text-tertiary);">@tx.Store</td>
|
||||
<td>
|
||||
<div class="admin-status-badge @(tx.IsIncome ? "admin-status-badge--online" : "admin-status-badge--offline")" style="font-size:10px;padding:2px 8px;">
|
||||
@(tx.IsIncome ? "Thu" : "Chi")
|
||||
</div>
|
||||
</td>
|
||||
<td style="text-align:right;font-weight:600;color:@(tx.IsIncome ? "#22C55E" : "#EF4444");">@tx.Amount</td>
|
||||
<tr style="border-top:1px solid var(--admin-border-subtle);">
|
||||
<td style="padding:12px 16px;font-size:12px;font-family:monospace;color:var(--admin-text-tertiary);">@o.Id.ToString()[..8]</td>
|
||||
<td style="padding:12px 16px;text-align:right;font-weight:600;">@FormatVND(o.TotalAmount)</td>
|
||||
<td style="padding:12px 16px;text-align:center;"><span class="admin-status-badge admin-status-badge--online" style="font-size:11px;padding:2px 10px;"><span class="admin-status-badge__dot" style="width:5px;height:5px;"></span>@(o.Status ?? "—")</span></td>
|
||||
<td style="padding:12px 16px;font-size:13px;color:var(--admin-text-tertiary);">@o.CreatedAt.ToString("dd/MM HH:mm")</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private record ExpenseItem(string Category, string Amount, string Percent, string Color);
|
||||
private readonly ExpenseItem[] _expenses = new[]
|
||||
{
|
||||
new ExpenseItem("Nguyên vật liệu", "52.4M", "36.8%", "#FF5C00"),
|
||||
new ExpenseItem("Lương nhân viên", "38.6M", "27.1%", "#3B82F6"),
|
||||
new ExpenseItem("Thuê mặt bằng", "28.0M", "19.6%", "#8B5CF6"),
|
||||
new ExpenseItem("Tiện ích (điện, nước)", "12.5M", "8.8%", "#F59E0B"),
|
||||
new ExpenseItem("Marketing", "6.2M", "4.3%", "#EC4899"),
|
||||
new ExpenseItem("Khác", "4.8M", "3.4%", "#22C55E"),
|
||||
};
|
||||
private List<PosDataService.OrderInfo> _orders = new();
|
||||
private List<PosDataService.WalletInfo> _wallets = new();
|
||||
private decimal _totalRevenue, _totalExpense;
|
||||
|
||||
private record TransactionItem(string Code, string Desc, string Store, bool IsIncome, string Amount);
|
||||
private readonly TransactionItem[] _transactions = new[]
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
new TransactionItem("GD-2851", "Doanh thu bán hàng", "Coffee House Q1", true, "+8.5M"),
|
||||
new TransactionItem("GD-2850", "Nhập nguyên liệu", "Coffee House Q1", false, "-3.2M"),
|
||||
new TransactionItem("GD-2849", "Doanh thu bán hàng", "Nhà hàng Q3", true, "+12.4M"),
|
||||
new TransactionItem("GD-2848", "Tiền thuê T2/2025", "Nhà hàng Q3", false, "-15.0M"),
|
||||
new TransactionItem("GD-2847", "Doanh thu bán hàng", "Coffee House Q1", true, "+6.8M"),
|
||||
new TransactionItem("GD-2846", "Lương nhân viên", "Tất cả", false, "-38.6M"),
|
||||
};
|
||||
IsLoading = true;
|
||||
try
|
||||
{
|
||||
_orders = await DataService.GetOrdersAsync();
|
||||
_wallets = await DataService.GetWalletsAsync();
|
||||
_totalRevenue = _wallets.Sum(w => w.TotalIncome);
|
||||
_totalExpense = _wallets.Sum(w => w.TotalExpense);
|
||||
}
|
||||
catch { } finally { IsLoading = false; }
|
||||
}
|
||||
|
||||
private static string FormatVND(decimal val) => val.ToString("N0") + "₫";
|
||||
}
|
||||
|
||||
@@ -1,205 +1,86 @@
|
||||
@page "/admin/finance/revenue"
|
||||
@layout AdminLayout
|
||||
@inherits AdminBase
|
||||
|
||||
@*
|
||||
EN: Revenue Analytics — revenue by store, category, time period, top products.
|
||||
VI: Phân tích doanh thu — doanh thu theo cửa hàng, danh mục, kỳ, sản phẩm bán chạy.
|
||||
Design: pencil-design/src/pages/tPOS/admin/revenue-analytics.pen
|
||||
*@
|
||||
@inject PosDataService DataService
|
||||
@using WebClientTpos.Client.Services
|
||||
|
||||
<PageTitle>Phân tích doanh thu — GoodGo Admin</PageTitle>
|
||||
|
||||
@* ═══ TOP BAR ═══ *@
|
||||
<div class="admin-topbar">
|
||||
<div class="admin-topbar__left">
|
||||
<h1 class="admin-topbar__title">Phân tích doanh thu</h1>
|
||||
<p class="admin-topbar__subtitle">Tháng 02/2025 • So sánh theo cửa hàng</p>
|
||||
<p class="admin-topbar__subtitle">@_orders.Count đơn hàng • @FormatVND(_orders.Sum(o => o.TotalAmount))</p>
|
||||
</div>
|
||||
<div class="admin-topbar__right">
|
||||
<button class="admin-btn-secondary">
|
||||
<i data-lucide="calendar"></i>
|
||||
<span>Tháng này</span>
|
||||
</button>
|
||||
<button class="admin-btn-secondary">
|
||||
<i data-lucide="filter"></i>
|
||||
<span>Bộ lọc</span>
|
||||
</button>
|
||||
<button class="admin-btn-primary">
|
||||
<i data-lucide="download"></i>
|
||||
<span>Xuất báo cáo</span>
|
||||
</button>
|
||||
<select class="admin-select" style="min-width:160px;" @onchange="OnShopFilterChanged">
|
||||
<option value="">Tất cả cửa hàng</option>
|
||||
@foreach (var s in _shops) { <option value="@s.Id">@s.Name</option> }
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ═══ CONTENT ═══ *@
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;gap:24px;">
|
||||
|
||||
@* KPI Row *@
|
||||
<div class="admin-kpi-row">
|
||||
<div class="admin-kpi-card">
|
||||
<div class="admin-kpi-card__header">
|
||||
<div class="admin-kpi-card__icon" style="background-color:rgba(34,197,94,0.125);">
|
||||
<i data-lucide="trending-up" style="color:#22C55E;"></i>
|
||||
</div>
|
||||
<div class="admin-kpi-card__badge admin-kpi-card__badge--up">
|
||||
<i data-lucide="arrow-up"></i>
|
||||
<span>+18.2%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-kpi-card__value">256.8M</div>
|
||||
<div class="admin-kpi-card__label">Tổng doanh thu</div>
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;gap:20px;">
|
||||
@if (IsLoading)
|
||||
{
|
||||
<div style="text-align:center;padding:48px;"><div class="spinner-small" style="width:32px;height:32px;margin:0 auto 16px;"></div></div>
|
||||
}
|
||||
else if (!_orders.Any())
|
||||
{
|
||||
<div style="text-align:center;padding:60px 20px;">
|
||||
<div style="width:80px;height:80px;border-radius:24px;background:rgba(59,130,246,0.1);display:flex;align-items:center;justify-content:center;margin:0 auto 20px;"><i data-lucide="bar-chart" style="width:36px;height:36px;color:#3B82F6;"></i></div>
|
||||
<h2 style="font-size:20px;font-weight:700;margin:0 0 8px;color:var(--pos-text-primary, #FFF);">Chưa có đơn hàng</h2>
|
||||
</div>
|
||||
<div class="admin-kpi-card">
|
||||
<div class="admin-kpi-card__header">
|
||||
<div class="admin-kpi-card__icon" style="background-color:rgba(59,130,246,0.125);">
|
||||
<i data-lucide="shopping-bag" style="color:#3B82F6;"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-kpi-card__value">132K</div>
|
||||
<div class="admin-kpi-card__label">Giá trị đơn TB</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:16px;">
|
||||
<div class="admin-stat-card"><div class="admin-stat-card__icon" style="background:rgba(34,197,94,0.1);"><i data-lucide="dollar-sign" style="color:#22C55E;"></i></div><div class="admin-stat-card__content"><span class="admin-stat-card__value">@FormatVND(_orders.Sum(o => o.TotalAmount))</span><span class="admin-stat-card__label">Tổng doanh thu</span></div></div>
|
||||
<div class="admin-stat-card"><div class="admin-stat-card__icon" style="background:rgba(59,130,246,0.1);"><i data-lucide="shopping-bag" style="color:#3B82F6;"></i></div><div class="admin-stat-card__content"><span class="admin-stat-card__value">@_orders.Count</span><span class="admin-stat-card__label">Số đơn hàng</span></div></div>
|
||||
<div class="admin-stat-card"><div class="admin-stat-card__icon" style="background:rgba(255,92,0,0.1);"><i data-lucide="calculator" style="color:#FF5C00;"></i></div><div class="admin-stat-card__content"><span class="admin-stat-card__value">@FormatVND(_orders.Any() ? _orders.Average(o => o.TotalAmount) : 0)</span><span class="admin-stat-card__label">Trung bình/đơn</span></div></div>
|
||||
</div>
|
||||
<div class="admin-kpi-card">
|
||||
<div class="admin-kpi-card__header">
|
||||
<div class="admin-kpi-card__icon" style="background-color:rgba(255,92,0,0.125);">
|
||||
<i data-lucide="store" style="color:var(--admin-orange-primary);"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-kpi-card__value">Nhà hàng Q3</div>
|
||||
<div class="admin-kpi-card__label">Cửa hàng dẫn đầu</div>
|
||||
</div>
|
||||
<div class="admin-kpi-card">
|
||||
<div class="admin-kpi-card__header">
|
||||
<div class="admin-kpi-card__icon" style="background-color:rgba(139,92,246,0.125);">
|
||||
<i data-lucide="bar-chart-3" style="color:#8B5CF6;"></i>
|
||||
</div>
|
||||
<div class="admin-kpi-card__badge admin-kpi-card__badge--up">
|
||||
<i data-lucide="arrow-up"></i>
|
||||
<span>+12.4%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-kpi-card__value">+18.2%</div>
|
||||
<div class="admin-kpi-card__label">Tỷ lệ tăng trưởng</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Revenue by Store + Revenue by Category *@
|
||||
<div style="display:flex;gap:24px;">
|
||||
@* Revenue by Store *@
|
||||
<div class="admin-panel" style="flex:1;">
|
||||
<div class="admin-panel__header">
|
||||
<h3 class="admin-panel__title">
|
||||
<i data-lucide="store" style="color:var(--admin-orange-primary);"></i>
|
||||
Doanh thu theo cửa hàng
|
||||
</h3>
|
||||
</div>
|
||||
<div class="admin-panel__body" style="display:flex;flex-direction:column;gap:12px;">
|
||||
@foreach (var s in _storeRevenue)
|
||||
{
|
||||
<div style="display:flex;align-items:center;gap:12px;padding:10px 14px;background-color:var(--admin-bg-interactive);border-radius:var(--admin-radius-md);">
|
||||
<div style="width:36px;height:36px;border-radius:var(--admin-radius-md);background-color:@(s.BgColor);display:flex;align-items:center;justify-content:center;">
|
||||
<i data-lucide="@s.Icon" style="color:@s.IconColor;width:18px;height:18px;"></i>
|
||||
</div>
|
||||
<div style="flex:1;">
|
||||
<div style="font-size:13px;font-weight:600;">@s.Name</div>
|
||||
<div style="font-size:11px;color:var(--admin-text-tertiary);">@s.Orders đơn hàng</div>
|
||||
</div>
|
||||
<div style="text-align:right;">
|
||||
<div style="font-size:14px;font-weight:700;color:var(--admin-orange-primary);">@s.Revenue</div>
|
||||
<div style="font-size:11px;color:#22C55E;">@s.Growth</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Revenue by Category *@
|
||||
<div class="admin-panel" style="flex:1;">
|
||||
<div class="admin-panel__header">
|
||||
<h3 class="admin-panel__title">
|
||||
<i data-lucide="layers" style="color:#3B82F6;"></i>
|
||||
Doanh thu theo danh mục
|
||||
</h3>
|
||||
</div>
|
||||
<div class="admin-panel__body" style="display:flex;flex-direction:column;gap:10px;">
|
||||
@foreach (var c in _categoryRevenue)
|
||||
{
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;padding:10px 14px;background-color:var(--admin-bg-interactive);border-radius:var(--admin-radius-md);">
|
||||
<div style="display:flex;align-items:center;gap:10px;">
|
||||
<div style="width:8px;height:8px;border-radius:50%;background-color:@c.Color;"></div>
|
||||
<span style="font-size:13px;">@c.Category</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:16px;">
|
||||
<span style="font-size:13px;font-weight:600;">@c.Amount</span>
|
||||
<span style="font-size:11px;color:var(--admin-text-tertiary);">@c.Percent</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Top-selling Products Table *@
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__header">
|
||||
<h3 class="admin-panel__title">
|
||||
<i data-lucide="trophy" style="color:#F59E0B;"></i>
|
||||
Sản phẩm bán chạy nhất
|
||||
</h3>
|
||||
</div>
|
||||
<div class="admin-panel__body" style="padding:0;">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Sản phẩm</th>
|
||||
<th>Danh mục</th>
|
||||
<th>Số lượng bán</th>
|
||||
<th style="text-align:right;">Doanh thu</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var p in _topProducts)
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__header"><h3 class="admin-panel__title">Chi tiết đơn hàng</h3></div>
|
||||
<div class="admin-panel__body" style="padding:0;">
|
||||
<table class="admin-table" style="width:100%;"><thead><tr>
|
||||
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">ID</th>
|
||||
<th style="padding:12px 16px;text-align:right;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Số tiền</th>
|
||||
<th style="padding:12px 16px;text-align:center;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Trạng thái</th>
|
||||
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Ngày</th>
|
||||
</tr></thead><tbody>
|
||||
@foreach (var o in _orders.Take(50))
|
||||
{
|
||||
<tr>
|
||||
<td style="font-weight:700;color:var(--admin-orange-primary);">@p.Rank</td>
|
||||
<td style="font-weight:600;">@p.Name</td>
|
||||
<td style="color:var(--admin-text-tertiary);">@p.Category</td>
|
||||
<td>@p.Sold</td>
|
||||
<td style="text-align:right;font-weight:600;">@p.Revenue</td>
|
||||
<tr style="border-top:1px solid var(--admin-border-subtle);">
|
||||
<td style="padding:12px 16px;font-size:12px;font-family:monospace;">@o.Id.ToString()[..8]</td>
|
||||
<td style="padding:12px 16px;text-align:right;font-weight:600;">@FormatVND(o.TotalAmount)</td>
|
||||
<td style="padding:12px 16px;text-align:center;"><span class="admin-status-badge admin-status-badge--online" style="font-size:11px;padding:2px 10px;">@(o.Status ?? "—")</span></td>
|
||||
<td style="padding:12px 16px;font-size:13px;color:var(--admin-text-tertiary);">@o.CreatedAt.ToString("dd/MM HH:mm")</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private record StoreRevItem(string Name, string Icon, string IconColor, string BgColor, string Revenue, string Orders, string Growth);
|
||||
private readonly StoreRevItem[] _storeRevenue = new[]
|
||||
{
|
||||
new StoreRevItem("Nhà hàng Q3", "utensils", "#3B82F6", "rgba(59,130,246,0.125)", "128.4M", "842", "+22.1%"),
|
||||
new StoreRevItem("Coffee House Q1", "coffee", "#FF5C00", "rgba(255,92,0,0.125)", "96.2M", "1,024", "+15.3%"),
|
||||
new StoreRevItem("Karaoke Star Q7", "mic", "#8B5CF6", "rgba(139,92,246,0.125)", "32.2M", "156", "+8.7%"),
|
||||
};
|
||||
private List<PosDataService.OrderInfo> _orders = new();
|
||||
private List<PosDataService.ShopInfo> _shops = new();
|
||||
private Guid? _selectedShopId;
|
||||
|
||||
private record CategoryRevItem(string Category, string Amount, string Percent, string Color);
|
||||
private readonly CategoryRevItem[] _categoryRevenue = new[]
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
new CategoryRevItem("Đồ uống", "108.5M", "42.2%", "#FF5C00"),
|
||||
new CategoryRevItem("Đồ ăn", "82.3M", "32.1%", "#3B82F6"),
|
||||
new CategoryRevItem("Dịch vụ phòng", "32.2M", "12.5%", "#8B5CF6"),
|
||||
new CategoryRevItem("Khác", "33.8M", "13.2%", "#22C55E"),
|
||||
};
|
||||
IsLoading = true;
|
||||
try { _shops = await DataService.GetShopsAsync(); _orders = await DataService.GetOrdersAsync(); }
|
||||
catch { } finally { IsLoading = false; }
|
||||
}
|
||||
|
||||
private record TopProductItem(string Rank, string Name, string Category, string Sold, string Revenue);
|
||||
private readonly TopProductItem[] _topProducts = new[]
|
||||
private async Task OnShopFilterChanged(ChangeEventArgs e)
|
||||
{
|
||||
new TopProductItem("1", "Espresso", "Cà phê", "1,024", "35.8M"),
|
||||
new TopProductItem("2", "Cappuccino", "Cà phê", "856", "38.5M"),
|
||||
new TopProductItem("3", "Phở bò đặc biệt", "Đồ ăn", "742", "44.5M"),
|
||||
new TopProductItem("4", "Latte", "Cà phê", "631", "30.9M"),
|
||||
new TopProductItem("5", "Trà sen vàng", "Trà", "520", "21.8M"),
|
||||
};
|
||||
_selectedShopId = Guid.TryParse(e.Value?.ToString(), out var id) ? id : null;
|
||||
IsLoading = true;
|
||||
try { _orders = await DataService.GetOrdersAsync(_selectedShopId); }
|
||||
catch { } finally { IsLoading = false; }
|
||||
}
|
||||
|
||||
private static string FormatVND(decimal val) => val.ToString("N0") + "₫";
|
||||
}
|
||||
|
||||
@@ -1,176 +1,75 @@
|
||||
@page "/admin/finance/tax"
|
||||
@layout AdminLayout
|
||||
@inherits AdminBase
|
||||
|
||||
@*
|
||||
EN: Tax Configuration — tax rate settings, categories, exemptions, report preview.
|
||||
VI: Cấu hình thuế — cài đặt thuế suất, danh mục, miễn thuế, xem trước báo cáo.
|
||||
Design: pencil-design/src/pages/tPOS/admin/tax-configuration.pen
|
||||
*@
|
||||
@inject PosDataService DataService
|
||||
@using WebClientTpos.Client.Services
|
||||
|
||||
<PageTitle>Cấu hình thuế — GoodGo Admin</PageTitle>
|
||||
|
||||
@* ═══ TOP BAR ═══ *@
|
||||
<div class="admin-topbar">
|
||||
<div class="admin-topbar__left">
|
||||
<h1 class="admin-topbar__title">Cấu hình thuế</h1>
|
||||
<p class="admin-topbar__subtitle">Quản lý thuế suất & danh mục thuế</p>
|
||||
</div>
|
||||
<div class="admin-topbar__right">
|
||||
<button class="admin-btn-secondary">
|
||||
<i data-lucide="file-text"></i>
|
||||
<span>Xem báo cáo thuế</span>
|
||||
</button>
|
||||
<button class="admin-btn-primary">
|
||||
<i data-lucide="save"></i>
|
||||
<span>Lưu cấu hình</span>
|
||||
</button>
|
||||
<p class="admin-topbar__subtitle">Quản lý thuế theo cửa hàng</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ═══ CONTENT ═══ *@
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;gap:24px;">
|
||||
|
||||
@* Tax Rate Settings + Tax Categories *@
|
||||
<div style="display:flex;gap:24px;">
|
||||
|
||||
@* LEFT: Tax Rate Settings *@
|
||||
<div class="admin-panel" style="flex:1;">
|
||||
<div class="admin-panel__header">
|
||||
<h3 class="admin-panel__title">
|
||||
<i data-lucide="settings" style="color:var(--admin-orange-primary);"></i>
|
||||
Cài đặt thuế suất
|
||||
</h3>
|
||||
</div>
|
||||
<div class="admin-panel__body" style="display:flex;flex-direction:column;gap:16px;">
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-form-label">Thuế VAT (%)</label>
|
||||
<input type="text" class="admin-form-input" value="10" />
|
||||
</div>
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-form-label">Phí dịch vụ (%)</label>
|
||||
<input type="text" class="admin-form-input" value="5" />
|
||||
</div>
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-form-label">Thuế TNCN (%)</label>
|
||||
<input type="text" class="admin-form-input" value="10" />
|
||||
</div>
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-form-label">Thuế môi trường</label>
|
||||
<input type="text" class="admin-form-input" value="0" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* RIGHT: Tax Categories *@
|
||||
<div class="admin-panel" style="flex:1;">
|
||||
<div class="admin-panel__header">
|
||||
<h3 class="admin-panel__title">
|
||||
<i data-lucide="tags" style="color:#3B82F6;"></i>
|
||||
Danh mục thuế
|
||||
</h3>
|
||||
<button class="admin-panel__action">+ Thêm danh mục</button>
|
||||
</div>
|
||||
<div class="admin-panel__body" style="display:flex;flex-direction:column;gap:10px;">
|
||||
@foreach (var cat in _taxCategories)
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;gap:20px;">
|
||||
@if (IsLoading)
|
||||
{
|
||||
<div style="text-align:center;padding:48px;"><div class="spinner-small" style="width:32px;height:32px;margin:0 auto 16px;"></div></div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__header"><h3 class="admin-panel__title">Cửa hàng</h3></div>
|
||||
<div class="admin-panel__body" style="display:flex;flex-direction:column;gap:12px;">
|
||||
@if (!_shops.Any())
|
||||
{
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;padding:12px 14px;background-color:var(--admin-bg-interactive);border-radius:var(--admin-radius-md);">
|
||||
<div style="display:flex;align-items:center;gap:10px;">
|
||||
<i data-lucide="@cat.Icon" style="width:16px;height:16px;color:@cat.Color;"></i>
|
||||
<p style="color:var(--admin-text-tertiary);font-size:14px;text-align:center;padding:20px;">Chưa có cửa hàng</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach (var shop in _shops)
|
||||
{
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;padding:12px 16px;border-radius:8px;background:var(--admin-bg-interactive);">
|
||||
<div>
|
||||
<div style="font-size:13px;font-weight:600;">@cat.Name</div>
|
||||
<div style="font-size:11px;color:var(--admin-text-tertiary);">@cat.Desc</div>
|
||||
<span style="font-weight:600;font-size:14px;">@shop.Name</span>
|
||||
<span style="font-size:12px;color:var(--admin-text-tertiary);margin-left:8px;">@(shop.Category ?? "")</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:8px;">
|
||||
<span style="font-size:14px;color:var(--admin-text-tertiary);">VAT: 10%</span>
|
||||
<i data-lucide="chevron-right" style="width:16px;height:16px;color:var(--admin-text-tertiary);"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div style="font-size:14px;font-weight:700;color:var(--admin-orange-primary);">@cat.Rate</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Tax-exempt Products + Tax Report Preview *@
|
||||
<div style="display:flex;gap:24px;">
|
||||
|
||||
@* LEFT: Tax-exempt Products *@
|
||||
<div class="admin-panel" style="flex:1;">
|
||||
<div class="admin-panel__header">
|
||||
<h3 class="admin-panel__title">
|
||||
<i data-lucide="shield-off" style="color:#F59E0B;"></i>
|
||||
Sản phẩm miễn thuế
|
||||
</h3>
|
||||
<button class="admin-panel__action">+ Thêm SP</button>
|
||||
</div>
|
||||
<div class="admin-panel__body" style="padding:0;">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Sản phẩm</th>
|
||||
<th>Danh mục</th>
|
||||
<th>Lý do</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var item in _exemptProducts)
|
||||
{
|
||||
<tr>
|
||||
<td style="font-weight:600;">@item.Name</td>
|
||||
<td style="color:var(--admin-text-tertiary);">@item.Category</td>
|
||||
<td style="font-size:12px;">@item.Reason</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* RIGHT: Tax Report Preview *@
|
||||
<div class="admin-panel" style="flex:1;">
|
||||
<div class="admin-panel__header">
|
||||
<h3 class="admin-panel__title">
|
||||
<i data-lucide="file-bar-chart" style="color:#22C55E;"></i>
|
||||
Xem trước báo cáo thuế
|
||||
</h3>
|
||||
</div>
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__header"><h3 class="admin-panel__title">Loại thuế</h3></div>
|
||||
<div class="admin-panel__body" style="display:flex;flex-direction:column;gap:12px;">
|
||||
@foreach (var line in _taxPreview)
|
||||
@foreach (var tax in new[] { ("VAT", "10%", "Thuế giá trị gia tăng"), ("TNCN", "Tự động", "Thuế thu nhập cá nhân"), ("Phí dịch vụ", "5%", "Service charge") })
|
||||
{
|
||||
<div style="display:flex;justify-content:space-between;padding:10px 14px;background-color:var(--admin-bg-interactive);border-radius:var(--admin-radius-md);">
|
||||
<span style="font-size:13px;color:var(--admin-text-secondary);">@line.Label</span>
|
||||
<span style="font-size:13px;font-weight:@(line.IsBold ? "700" : "500");color:@(line.IsBold ? "var(--admin-orange-primary)" : "var(--admin-text-primary)");">@line.Value</span>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;padding:12px 16px;border-radius:8px;background:var(--admin-bg-interactive);">
|
||||
<div>
|
||||
<span style="font-weight:600;font-size:14px;">@tax.Item1</span>
|
||||
<span style="font-size:12px;color:var(--admin-text-tertiary);margin-left:8px;">@tax.Item3</span>
|
||||
</div>
|
||||
<span style="font-weight:600;color:var(--admin-orange-primary);">@tax.Item2</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private record TaxCategory(string Name, string Desc, string Rate, string Icon, string Color);
|
||||
private readonly TaxCategory[] _taxCategories = new[]
|
||||
{
|
||||
new TaxCategory("VAT", "Thuế giá trị gia tăng", "10%", "receipt", "#FF5C00"),
|
||||
new TaxCategory("Phí dịch vụ", "Áp dụng cho dine-in", "5%", "utensils", "#3B82F6"),
|
||||
new TaxCategory("TNCN", "Thuế thu nhập cá nhân", "10%", "user", "#8B5CF6"),
|
||||
new TaxCategory("Đồ uống có cồn", "Thuế tiêu thụ đặc biệt", "35%", "wine", "#EF4444"),
|
||||
};
|
||||
private List<PosDataService.ShopInfo> _shops = new();
|
||||
|
||||
private record ExemptProduct(string Name, string Category, string Reason);
|
||||
private readonly ExemptProduct[] _exemptProducts = new[]
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
new ExemptProduct("Nước lọc", "Đồ uống", "Miễn VAT theo QĐ"),
|
||||
new ExemptProduct("Cơm trắng", "Đồ ăn", "Lương thực thiết yếu"),
|
||||
new ExemptProduct("Trà đá", "Đồ uống", "Đồ uống cơ bản"),
|
||||
};
|
||||
|
||||
private record TaxPreviewLine(string Label, string Value, bool IsBold);
|
||||
private readonly TaxPreviewLine[] _taxPreview = new[]
|
||||
{
|
||||
new TaxPreviewLine("Doanh thu trước thuế", "256.8M", false),
|
||||
new TaxPreviewLine("VAT (10%)", "25.7M", false),
|
||||
new TaxPreviewLine("Phí dịch vụ (5%)", "8.4M", false),
|
||||
new TaxPreviewLine("Thuế TTĐB", "2.1M", false),
|
||||
new TaxPreviewLine("Tổng thuế phải nộp", "36.2M", true),
|
||||
new TaxPreviewLine("Doanh thu sau thuế", "220.6M", true),
|
||||
};
|
||||
IsLoading = true;
|
||||
try { _shops = await DataService.GetShopsAsync(); }
|
||||
catch { } finally { IsLoading = false; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,147 +1,79 @@
|
||||
@page "/admin/inventory/orders"
|
||||
@page "/admin/inventory/purchase-orders"
|
||||
@layout AdminLayout
|
||||
@inherits AdminBase
|
||||
@inject PosDataService DataService
|
||||
@using WebClientTpos.Client.Services
|
||||
|
||||
@*
|
||||
EN: Purchase Orders — PO list with status, create PO, filter by supplier/status.
|
||||
VI: Đơn đặt hàng — danh sách PO, tạo PO, lọc theo NCC/trạng thái.
|
||||
Design: pencil-design/src/pages/tPOS/admin/purchase-orders.pen
|
||||
*@
|
||||
<PageTitle>Đơn nhập hàng — GoodGo Admin</PageTitle>
|
||||
|
||||
<PageTitle>Đơn đặt hàng — GoodGo Admin</PageTitle>
|
||||
|
||||
@* ═══ TOP BAR ═══ *@
|
||||
<div class="admin-topbar">
|
||||
<div class="admin-topbar__left">
|
||||
<h1 class="admin-topbar__title">Đơn đặt hàng</h1>
|
||||
<p class="admin-topbar__subtitle">@_orders.Length đơn hàng • Tháng 02/2025</p>
|
||||
<h1 class="admin-topbar__title">Đơn nhập hàng</h1>
|
||||
<p class="admin-topbar__subtitle">@_txns.Count giao dịch nhập kho</p>
|
||||
</div>
|
||||
<div class="admin-topbar__right">
|
||||
<div class="admin-search" style="width:220px;">
|
||||
<i data-lucide="search"></i>
|
||||
<input type="text" placeholder="Tìm đơn hàng..." @bind="SearchQuery" />
|
||||
</div>
|
||||
<button class="admin-btn-primary">
|
||||
<i data-lucide="plus"></i>
|
||||
<span>Tạo đơn hàng</span>
|
||||
</button>
|
||||
<select class="admin-select" style="min-width:160px;" @onchange="OnShopFilterChanged">
|
||||
<option value="">Tất cả cửa hàng</option>
|
||||
@foreach (var s in _shops) { <option value="@s.Id">@s.Name</option> }
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ═══ TABS ═══ *@
|
||||
<div class="admin-tabs">
|
||||
<button class="admin-tab @(_tab == "all" ? "admin-tab--active" : "")" @onclick="@(() => _tab = "all")">
|
||||
Tất cả <span class="admin-tab__badge @(_tab == "all" ? "admin-tab__badge--active" : "")">@_orders.Length</span>
|
||||
</button>
|
||||
<button class="admin-tab @(_tab == "pending" ? "admin-tab--active" : "")" @onclick="@(() => _tab = "pending")">
|
||||
Chờ xác nhận <span class="admin-tab__badge">3</span>
|
||||
</button>
|
||||
<button class="admin-tab @(_tab == "confirmed" ? "admin-tab--active" : "")" @onclick="@(() => _tab = "confirmed")">
|
||||
Đã xác nhận <span class="admin-tab__badge">2</span>
|
||||
</button>
|
||||
<button class="admin-tab @(_tab == "delivered" ? "admin-tab--active" : "")" @onclick="@(() => _tab = "delivered")">
|
||||
Đã giao <span class="admin-tab__badge">3</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@* ═══ CONTENT ═══ *@
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;gap:24px;">
|
||||
|
||||
@* KPI Row *@
|
||||
<div class="admin-kpi-row">
|
||||
<div class="admin-kpi-card">
|
||||
<div class="admin-kpi-card__header">
|
||||
<div class="admin-kpi-card__icon" style="background-color:rgba(255,92,0,0.125);">
|
||||
<i data-lucide="file-text" style="color:var(--admin-orange-primary);"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-kpi-card__value">@_orders.Length</div>
|
||||
<div class="admin-kpi-card__label">Tổng đơn hàng</div>
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;gap:20px;">
|
||||
@if (IsLoading)
|
||||
{
|
||||
<div style="text-align:center;padding:48px;"><div class="spinner-small" style="width:32px;height:32px;margin:0 auto 16px;"></div></div>
|
||||
}
|
||||
else if (!_txns.Any())
|
||||
{
|
||||
<div style="text-align:center;padding:60px 20px;">
|
||||
<div style="width:80px;height:80px;border-radius:24px;background:rgba(59,130,246,0.1);display:flex;align-items:center;justify-content:center;margin:0 auto 20px;"><i data-lucide="truck" style="width:36px;height:36px;color:#3B82F6;"></i></div>
|
||||
<h2 style="font-size:20px;font-weight:700;margin:0 0 8px;color:var(--pos-text-primary, #FFF);">Chưa có đơn nhập hàng</h2>
|
||||
<p style="font-size:14px;color:var(--admin-text-tertiary);margin:0;">Tạo đơn nhập hàng để quản lý tồn kho</p>
|
||||
</div>
|
||||
<div class="admin-kpi-card">
|
||||
<div class="admin-kpi-card__header">
|
||||
<div class="admin-kpi-card__icon" style="background-color:rgba(245,158,11,0.125);">
|
||||
<i data-lucide="clock" style="color:#F59E0B;"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-kpi-card__value">3</div>
|
||||
<div class="admin-kpi-card__label">Chờ xác nhận</div>
|
||||
</div>
|
||||
<div class="admin-kpi-card">
|
||||
<div class="admin-kpi-card__header">
|
||||
<div class="admin-kpi-card__icon" style="background-color:rgba(34,197,94,0.125);">
|
||||
<i data-lucide="wallet" style="color:#22C55E;"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-kpi-card__value">68.5M</div>
|
||||
<div class="admin-kpi-card__label">Tổng giá trị PO</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* PO Table *@
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__header">
|
||||
<h3 class="admin-panel__title">
|
||||
<i data-lucide="clipboard-list" style="color:var(--admin-orange-primary);"></i>
|
||||
Danh sách đơn đặt hàng
|
||||
</h3>
|
||||
</div>
|
||||
<div class="admin-panel__body" style="padding:0;">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Mã PO</th>
|
||||
<th>Nhà cung cấp</th>
|
||||
<th>Số mặt hàng</th>
|
||||
<th>Ngày đặt</th>
|
||||
<th>Trạng thái</th>
|
||||
<th style="text-align:right;">Tổng tiền</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var order in _orders)
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__body" style="padding:0;">
|
||||
<table class="admin-table" style="width:100%;"><thead><tr>
|
||||
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Loại</th>
|
||||
<th style="padding:12px 16px;text-align:right;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Số lượng</th>
|
||||
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Ghi chú</th>
|
||||
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Ngày</th>
|
||||
</tr></thead><tbody>
|
||||
@foreach (var t in _txns)
|
||||
{
|
||||
<tr>
|
||||
<td style="font-weight:600;">@order.Code</td>
|
||||
<td>@order.Supplier</td>
|
||||
<td>@order.Items mặt hàng</td>
|
||||
<td style="color:var(--admin-text-tertiary);">@order.Date</td>
|
||||
<td>
|
||||
<div class="admin-status-badge @GetPOStatusClass(order.Status)" style="font-size:10px;padding:2px 8px;">
|
||||
<span class="admin-status-badge__dot"></span>
|
||||
@order.Status
|
||||
</div>
|
||||
</td>
|
||||
<td style="text-align:right;font-weight:600;color:var(--admin-orange-primary);">@order.Total</td>
|
||||
<tr style="border-top:1px solid var(--admin-border-subtle);">
|
||||
<td style="padding:12px 16px;font-weight:600;">@(t.TransactionType ?? "—")</td>
|
||||
<td style="padding:12px 16px;text-align:right;font-weight:600;color:@(t.QuantityChange > 0 ? "#22C55E" : "#EF4444");">@(t.QuantityChange > 0 ? "+" : "")@t.QuantityChange</td>
|
||||
<td style="padding:12px 16px;font-size:13px;color:var(--admin-text-tertiary);">@(t.Reason ?? "—")</td>
|
||||
<td style="padding:12px 16px;font-size:13px;color:var(--admin-text-tertiary);">@t.CreatedAt.ToString("dd/MM HH:mm")</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private string _tab = "all";
|
||||
private List<PosDataService.InventoryTxnInfo> _txns = new();
|
||||
private List<PosDataService.ShopInfo> _shops = new();
|
||||
private Guid? _selectedShopId;
|
||||
|
||||
private string GetPOStatusClass(string status) => status switch
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
"Đã giao" => "admin-status-badge--online",
|
||||
"Đã xác nhận" => "admin-status-badge--paused",
|
||||
"Chờ xác nhận" => "admin-status-badge--setup",
|
||||
_ => ""
|
||||
};
|
||||
IsLoading = true;
|
||||
try { _shops = await DataService.GetShopsAsync(); _txns = await DataService.GetInventoryTransactionsAsync(); }
|
||||
catch { } finally { IsLoading = false; }
|
||||
}
|
||||
|
||||
private record POItem(string Code, string Supplier, string Items, string Date, string Status, string Total);
|
||||
private readonly POItem[] _orders = new[]
|
||||
private async Task OnShopFilterChanged(ChangeEventArgs e)
|
||||
{
|
||||
new POItem("PO-0042", "Cà phê Trung Nguyên", "5", "12/02/2025", "Chờ xác nhận", "18.5M"),
|
||||
new POItem("PO-0041", "Vinamilk", "3", "10/02/2025", "Đã xác nhận", "8.2M"),
|
||||
new POItem("PO-0040", "Đại Tân Phát", "8", "09/02/2025", "Chờ xác nhận", "12.4M"),
|
||||
new POItem("PO-0039", "TH True Milk", "2", "08/02/2025", "Đã giao", "4.8M"),
|
||||
new POItem("PO-0038", "Cà phê Trung Nguyên", "4", "06/02/2025", "Đã giao", "14.2M"),
|
||||
new POItem("PO-0037", "Metro Cash & Carry", "12", "05/02/2025", "Đã xác nhận", "6.5M"),
|
||||
new POItem("PO-0036", "Saigon Food", "6", "03/02/2025", "Chờ xác nhận", "5.8M"),
|
||||
new POItem("PO-0035", "Vinamilk", "3", "01/02/2025", "Đã giao", "3.9M"),
|
||||
};
|
||||
_selectedShopId = Guid.TryParse(e.Value?.ToString(), out var id) ? id : null;
|
||||
IsLoading = true;
|
||||
try { _txns = await DataService.GetInventoryTransactionsAsync(_selectedShopId); }
|
||||
catch { } finally { IsLoading = false; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,174 +1,49 @@
|
||||
@page "/admin/inventory/transfer"
|
||||
@page "/admin/inventory/transfers"
|
||||
@layout AdminLayout
|
||||
@inherits AdminBase
|
||||
|
||||
@*
|
||||
EN: Stock Transfer — transfer between stores, transfer history, pending transfers.
|
||||
VI: Chuyển kho — chuyển hàng giữa cửa hàng, lịch sử, đang chờ.
|
||||
Design: pencil-design/src/pages/tPOS/admin/stock-transfer.pen
|
||||
*@
|
||||
@inject PosDataService DataService
|
||||
@using WebClientTpos.Client.Services
|
||||
|
||||
<PageTitle>Chuyển kho — GoodGo Admin</PageTitle>
|
||||
|
||||
@* ═══ TOP BAR ═══ *@
|
||||
<div class="admin-topbar">
|
||||
<div class="admin-topbar__left">
|
||||
<h1 class="admin-topbar__title">Chuyển kho</h1>
|
||||
<p class="admin-topbar__subtitle">Chuyển hàng giữa các cửa hàng</p>
|
||||
</div>
|
||||
<div class="admin-topbar__right">
|
||||
<button class="admin-btn-primary">
|
||||
<i data-lucide="arrow-right-left"></i>
|
||||
<span>Tạo phiếu chuyển</span>
|
||||
</button>
|
||||
<p class="admin-topbar__subtitle">Quản lý chuyển hàng giữa các cửa hàng</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ═══ CONTENT ═══ *@
|
||||
<div class="admin-content" style="display:flex;gap:24px;">
|
||||
|
||||
@* ── LEFT: Transfer Form ── *@
|
||||
<div class="admin-panel" style="width:380px;">
|
||||
<div class="admin-panel__header">
|
||||
<h3 class="admin-panel__title">
|
||||
<i data-lucide="send" style="color:var(--admin-orange-primary);"></i>
|
||||
Tạo phiếu chuyển kho
|
||||
</h3>
|
||||
</div>
|
||||
<div class="admin-panel__body" style="display:flex;flex-direction:column;gap:16px;">
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-form-label">Kho gửi</label>
|
||||
<select class="admin-form-input">
|
||||
<option>Coffee House Q1</option>
|
||||
<option>Nhà hàng Q3</option>
|
||||
<option>Karaoke Star Q7</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-form-label">Kho nhận</label>
|
||||
<select class="admin-form-input">
|
||||
<option>Nhà hàng Q3</option>
|
||||
<option>Coffee House Q1</option>
|
||||
<option>Karaoke Star Q7</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-form-label">Sản phẩm</label>
|
||||
<select class="admin-form-input">
|
||||
<option>Chọn sản phẩm...</option>
|
||||
<option>Cà phê Arabica</option>
|
||||
<option>Sữa tươi</option>
|
||||
<option>Ly giấy 12oz</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-form-label">Số lượng</label>
|
||||
<input type="number" class="admin-form-input" placeholder="Nhập số lượng" />
|
||||
</div>
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-form-label">Ghi chú</label>
|
||||
<textarea class="admin-form-input" rows="2" placeholder="Ghi chú (tuỳ chọn)"></textarea>
|
||||
</div>
|
||||
<button class="admin-btn-primary" style="width:100%;justify-content:center;">
|
||||
<i data-lucide="check"></i>
|
||||
<span>Xác nhận chuyển kho</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ── RIGHT: Transfer History ── *@
|
||||
<div style="flex:1;display:flex;flex-direction:column;gap:20px;">
|
||||
|
||||
@* Pending Transfers *@
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__header">
|
||||
<h3 class="admin-panel__title">
|
||||
<i data-lucide="clock" style="color:#F59E0B;"></i>
|
||||
Đang chờ xử lý
|
||||
<span class="admin-badge-count admin-badge-count--danger">2</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="admin-panel__body" style="display:flex;flex-direction:column;gap:10px;">
|
||||
@foreach (var t in _pendingTransfers)
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;gap:20px;">
|
||||
@if (IsLoading)
|
||||
{
|
||||
<div style="text-align:center;padding:48px;"><div class="spinner-small" style="width:32px;height:32px;margin:0 auto 16px;"></div></div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div style="text-align:center;padding:60px 20px;">
|
||||
<div style="width:80px;height:80px;border-radius:24px;background:rgba(139,92,246,0.1);display:flex;align-items:center;justify-content:center;margin:0 auto 20px;"><i data-lucide="arrow-right-left" style="width:36px;height:36px;color:#8B5CF6;"></i></div>
|
||||
<h2 style="font-size:20px;font-weight:700;margin:0 0 8px;color:var(--pos-text-primary, #FFF);">Chuyển kho</h2>
|
||||
<p style="font-size:14px;color:var(--admin-text-tertiary);margin:0 0 20px;">Tạo yêu cầu chuyển hàng giữa các cửa hàng</p>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;max-width:400px;margin:0 auto;">
|
||||
@foreach (var shop in _shops)
|
||||
{
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;padding:12px 14px;background-color:var(--admin-bg-interactive);border-radius:var(--admin-radius-md);">
|
||||
<div style="display:flex;align-items:center;gap:10px;">
|
||||
<i data-lucide="arrow-right-left" style="width:16px;height:16px;color:#F59E0B;"></i>
|
||||
<div>
|
||||
<div style="font-size:13px;font-weight:600;">@t.Item — @t.Qty @t.Unit</div>
|
||||
<div style="font-size:11px;color:var(--admin-text-tertiary);">@t.From → @t.To</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-status-badge admin-status-badge--setup" style="font-size:10px;padding:2px 8px;">
|
||||
<span class="admin-status-badge__dot"></span>
|
||||
Chờ xác nhận
|
||||
</div>
|
||||
<div style="padding:16px;border-radius:12px;background:var(--admin-bg-interactive);text-align:center;">
|
||||
<div style="font-weight:600;font-size:14px;margin-bottom:4px;">@shop.Name</div>
|
||||
<div style="font-size:12px;color:var(--admin-text-tertiary);">@(shop.Category ?? "—")</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Transfer History Table *@
|
||||
<div class="admin-panel" style="flex:1;">
|
||||
<div class="admin-panel__header">
|
||||
<h3 class="admin-panel__title">
|
||||
<i data-lucide="history" style="color:#3B82F6;"></i>
|
||||
Lịch sử chuyển kho
|
||||
</h3>
|
||||
</div>
|
||||
<div class="admin-panel__body" style="padding:0;">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Mã phiếu</th>
|
||||
<th>Sản phẩm</th>
|
||||
<th>SL</th>
|
||||
<th>Từ</th>
|
||||
<th>Đến</th>
|
||||
<th>Ngày</th>
|
||||
<th>Trạng thái</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var h in _transferHistory)
|
||||
{
|
||||
<tr>
|
||||
<td style="font-weight:600;">@h.Code</td>
|
||||
<td>@h.Item</td>
|
||||
<td>@h.Qty @h.Unit</td>
|
||||
<td style="color:var(--admin-text-tertiary);">@h.From</td>
|
||||
<td style="color:var(--admin-text-tertiary);">@h.To</td>
|
||||
<td style="color:var(--admin-text-tertiary);">@h.Date</td>
|
||||
<td>
|
||||
<div class="admin-status-badge admin-status-badge--online" style="font-size:10px;padding:2px 8px;">
|
||||
<span class="admin-status-badge__dot"></span>
|
||||
Hoàn thành
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private record PendingTransfer(string Item, string Qty, string Unit, string From, string To);
|
||||
private readonly PendingTransfer[] _pendingTransfers = new[]
|
||||
{
|
||||
new PendingTransfer("Cà phê Arabica", "10", "kg", "Coffee House Q1", "Nhà hàng Q3"),
|
||||
new PendingTransfer("Ly giấy 12oz", "2", "thùng", "Nhà hàng Q3", "Coffee House Q1"),
|
||||
};
|
||||
private List<PosDataService.ShopInfo> _shops = new();
|
||||
|
||||
private record TransferHistory(string Code, string Item, string Qty, string Unit, string From, string To, string Date);
|
||||
private readonly TransferHistory[] _transferHistory = new[]
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
new TransferHistory("CK-0028", "Sữa tươi", "5", "thùng", "Nhà hàng Q3", "Coffee House Q1", "10/02"),
|
||||
new TransferHistory("CK-0027", "Trà Oolong", "3", "kg", "Coffee House Q1", "Nhà hàng Q3", "08/02"),
|
||||
new TransferHistory("CK-0026", "Đường", "10", "kg", "Nhà hàng Q3", "Coffee House Q1", "06/02"),
|
||||
new TransferHistory("CK-0025", "Ly giấy 12oz", "4", "thùng", "Coffee House Q1", "Karaoke Q7", "04/02"),
|
||||
new TransferHistory("CK-0024", "Cà phê Robusta", "8", "kg", "Nhà hàng Q3", "Coffee House Q1", "02/02"),
|
||||
};
|
||||
IsLoading = true;
|
||||
try { _shops = await DataService.GetShopsAsync(); }
|
||||
catch { } finally { IsLoading = false; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,92 +1,26 @@
|
||||
@page "/admin/inventory/suppliers"
|
||||
@layout AdminLayout
|
||||
@inherits AdminBase
|
||||
|
||||
@*
|
||||
EN: Supplier Management — supplier list, add supplier, order history, ratings.
|
||||
VI: Quản lý nhà cung cấp — danh sách NCC, thêm NCC, lịch sử đặt, đánh giá.
|
||||
Design: pencil-design/src/pages/tPOS/admin/supplier-management.pen
|
||||
*@
|
||||
@inject PosDataService DataService
|
||||
@using WebClientTpos.Client.Services
|
||||
|
||||
<PageTitle>Nhà cung cấp — GoodGo Admin</PageTitle>
|
||||
|
||||
@* ═══ TOP BAR ═══ *@
|
||||
<div class="admin-topbar">
|
||||
<div class="admin-topbar__left">
|
||||
<h1 class="admin-topbar__title">Nhà cung cấp</h1>
|
||||
<p class="admin-topbar__subtitle">@_suppliers.Length nhà cung cấp • Quản lý & đánh giá</p>
|
||||
</div>
|
||||
<div class="admin-topbar__right">
|
||||
<div class="admin-search" style="width:220px;">
|
||||
<i data-lucide="search"></i>
|
||||
<input type="text" placeholder="Tìm NCC..." @bind="SearchQuery" />
|
||||
</div>
|
||||
<button class="admin-btn-primary">
|
||||
<i data-lucide="plus"></i>
|
||||
<span>Thêm NCC</span>
|
||||
</button>
|
||||
<p class="admin-topbar__subtitle">Quản lý nhà cung cấp</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ═══ CONTENT ═══ *@
|
||||
<div class="admin-content" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:20px;">
|
||||
@foreach (var sup in _suppliers)
|
||||
{
|
||||
<div class="admin-staff-card">
|
||||
<div class="admin-staff-card__header">
|
||||
<div style="display:flex;align-items:center;gap:12px;">
|
||||
<div class="admin-user-avatar" style="background-color:@sup.Color;">@sup.Initials</div>
|
||||
<div>
|
||||
<div style="font-size:14px;font-weight:600;">@sup.Name</div>
|
||||
<div style="font-size:11px;color:var(--admin-text-tertiary);">@sup.Category</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-status-badge @(sup.Active ? "admin-status-badge--online" : "admin-status-badge--offline")" style="font-size:10px;padding:2px 8px;">
|
||||
<span class="admin-status-badge__dot"></span>
|
||||
@(sup.Active ? "Hoạt động" : "Ngưng")
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Contact info *@
|
||||
<div style="display:flex;flex-direction:column;gap:6px;">
|
||||
<div style="display:flex;align-items:center;gap:8px;">
|
||||
<i data-lucide="phone" style="width:14px;height:14px;color:var(--admin-text-tertiary);"></i>
|
||||
<span style="font-size:12px;color:var(--admin-text-secondary);">@sup.Phone</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:8px;">
|
||||
<i data-lucide="mail" style="width:14px;height:14px;color:var(--admin-text-tertiary);"></i>
|
||||
<span style="font-size:12px;color:var(--admin-text-secondary);">@sup.Email</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Stats *@
|
||||
<div style="display:flex;gap:12px;">
|
||||
<div class="admin-store-stat" style="flex:1;">
|
||||
<div class="admin-store-stat__value">@sup.TotalOrders</div>
|
||||
<div class="admin-store-stat__label">Đơn hàng</div>
|
||||
</div>
|
||||
<div class="admin-store-stat" style="flex:1;">
|
||||
<div class="admin-store-stat__value">@sup.TotalValue</div>
|
||||
<div class="admin-store-stat__label">Tổng tiền</div>
|
||||
</div>
|
||||
<div class="admin-store-stat" style="flex:1;">
|
||||
<div class="admin-store-stat__value" style="color:@(sup.Rating >= 4 ? "#22C55E" : "#F59E0B");">@sup.Rating ★</div>
|
||||
<div class="admin-store-stat__label">Đánh giá</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;gap:20px;">
|
||||
<div style="text-align:center;padding:60px 20px;">
|
||||
<div style="width:80px;height:80px;border-radius:24px;background:rgba(59,130,246,0.1);display:flex;align-items:center;justify-content:center;margin:0 auto 20px;"><i data-lucide="building-2" style="width:36px;height:36px;color:#3B82F6;"></i></div>
|
||||
<h2 style="font-size:20px;font-weight:700;margin:0 0 8px;color:var(--pos-text-primary, #FFF);">Quản lý nhà cung cấp</h2>
|
||||
<p style="font-size:14px;color:var(--admin-text-tertiary);margin:0 0 16px;">Tính năng đang phát triển — sẽ sớm ra mắt</p>
|
||||
<span class="admin-status-badge admin-status-badge--warning" style="font-size:12px;padding:4px 12px;"><span class="admin-status-badge__dot" style="width:5px;height:5px;"></span>Coming Soon</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private record SupplierItem(string Name, string Initials, string Color, string Category, string Phone, string Email, string TotalOrders, string TotalValue, double Rating, bool Active);
|
||||
private readonly SupplierItem[] _suppliers = new[]
|
||||
{
|
||||
new SupplierItem("Cà phê Trung Nguyên", "TN", "#FF5C00", "Nguyên liệu cà phê", "028 3820 1234", "order@trungnguyen.vn", "24", "86.5M", 4.8, true),
|
||||
new SupplierItem("Vinamilk", "VM", "#3B82F6", "Sữa & Dairy", "028 5413 0000", "b2b@vinamilk.vn", "18", "42.3M", 4.5, true),
|
||||
new SupplierItem("Metro Cash & Carry", "MC", "#22C55E", "Tổng hợp", "028 3742 0888", "supply@metro.vn", "32", "128.6M", 4.2, true),
|
||||
new SupplierItem("Đại Tân Phát", "ĐT", "#8B5CF6", "Bao bì & Dụng cụ", "028 3850 4567", "sales@daitanphat.vn", "15", "28.4M", 4.0, true),
|
||||
new SupplierItem("TH True Milk", "TH", "#EC4899", "Sữa & Dairy", "028 3930 1122", "order@thtruemilk.vn", "12", "18.8M", 4.6, true),
|
||||
new SupplierItem("Saigon Food", "SF", "#F59E0B", "Thực phẩm đông lạnh", "028 3820 5678", "info@saigonfood.vn", "8", "15.2M", 3.8, false),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,143 +1,104 @@
|
||||
@page "/admin/menu"
|
||||
@page "/admin/products/menu-builder"
|
||||
@layout AdminLayout
|
||||
@inherits AdminBase
|
||||
|
||||
@*
|
||||
EN: Menu builder — drag-drop menu sections, items reorder, visibility toggle.
|
||||
VI: Xây dựng menu — kéo thả section menu, sắp xếp món, ẩn/hiện.
|
||||
Design: pencil-design/src/pages/tPOS/admin/menu-builder.pen
|
||||
*@
|
||||
@inject PosDataService DataService
|
||||
@using WebClientTpos.Client.Services
|
||||
|
||||
<PageTitle>Menu Builder — GoodGo Admin</PageTitle>
|
||||
|
||||
@* ═══ TOP BAR ═══ *@
|
||||
<div class="admin-topbar">
|
||||
<div class="admin-topbar__left">
|
||||
<h1 class="admin-topbar__title">Xây dựng Menu</h1>
|
||||
<p class="admin-topbar__subtitle">Coffee House Q1 • Kéo thả để sắp xếp</p>
|
||||
<h1 class="admin-topbar__title">Menu Builder</h1>
|
||||
<p class="admin-topbar__subtitle">@_categories.Count danh mục • @_products.Count sản phẩm</p>
|
||||
</div>
|
||||
<div class="admin-topbar__right">
|
||||
<button class="admin-btn-secondary">
|
||||
<i data-lucide="eye"></i>
|
||||
<span>Xem trước</span>
|
||||
</button>
|
||||
<button class="admin-btn-primary">
|
||||
<i data-lucide="save"></i>
|
||||
<span>Lưu menu</span>
|
||||
</button>
|
||||
<select class="admin-select" style="min-width:160px;" @onchange="OnShopFilterChanged">
|
||||
<option value="">Tất cả cửa hàng</option>
|
||||
@foreach (var s in _shops) { <option value="@s.Id">@s.Name</option> }
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ═══ CONTENT ═══ *@
|
||||
<div class="admin-content" style="display:flex;gap:24px;">
|
||||
|
||||
@* LEFT: Menu Sections *@
|
||||
<div style="flex:1;display:flex;flex-direction:column;gap:16px;">
|
||||
|
||||
@foreach (var section in _menuSections)
|
||||
{
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__header">
|
||||
<div style="display:flex;align-items:center;gap:10px;">
|
||||
<i data-lucide="grip-vertical" style="width:16px;height:16px;color:var(--admin-text-tertiary);cursor:grab;"></i>
|
||||
<h3 class="admin-panel__title" style="margin:0;">
|
||||
<i data-lucide="@(section.Icon)" style="color:@(section.Color);"></i>
|
||||
@(section.Name)
|
||||
</h3>
|
||||
<span class="admin-tab__badge">@(section.Items.Length)</span>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;align-items:center;">
|
||||
<MudSwitch T="bool" Value="section.Visible" Color="Color.Primary" />
|
||||
<button class="admin-icon-btn" style="width:32px;height:32px;">
|
||||
<i data-lucide="plus" style="width:16px;height:16px;"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-panel__body" style="padding:8px 16px;">
|
||||
@foreach (var item in section.Items)
|
||||
{
|
||||
<div style="display:flex;align-items:center;gap:12px;padding:10px 8px;border-bottom:1px solid var(--admin-border-subtle);">
|
||||
<i data-lucide="grip-vertical" style="width:14px;height:14px;color:var(--admin-text-tertiary);cursor:grab;"></i>
|
||||
<div style="width:40px;height:40px;background:@(section.Color)15;border-radius:8px;display:flex;align-items:center;justify-content:center;">
|
||||
<i data-lucide="@(section.Icon)" style="width:18px;height:18px;color:@(section.Color);"></i>
|
||||
</div>
|
||||
<div style="flex:1;">
|
||||
<div style="font-size:14px;font-weight:500;">@item.Name</div>
|
||||
<div style="font-size:11px;color:var(--admin-text-tertiary);">@item.Variants biến thể</div>
|
||||
</div>
|
||||
<span style="font-size:14px;font-weight:600;color:var(--admin-orange-primary);">@item.Price</span>
|
||||
<MudSwitch T="bool" Value="item.Visible" Color="Color.Primary" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@* Category sidebar *@
|
||||
<div style="width:240px;flex-shrink:0;">
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__header"><h3 class="admin-panel__title">Danh mục</h3></div>
|
||||
<div class="admin-panel__body" style="display:flex;flex-direction:column;gap:4px;">
|
||||
<button style="padding:8px 12px;border-radius:8px;border:none;text-align:left;font-weight:@(_selectedCategory == null ? "700" : "400");background:@(_selectedCategory == null ? "var(--admin-orange-primary)" : "transparent");color:@(_selectedCategory == null ? "#FFF" : "inherit");cursor:pointer;font-size:14px;" @onclick="@(() => SelectCategory(null))">Tất cả (@_products.Count)</button>
|
||||
@foreach (var cat in _categories)
|
||||
{
|
||||
var catName = cat.Name;
|
||||
var count = _products.Count(p => string.Equals(p.CategoryName, catName, StringComparison.OrdinalIgnoreCase));
|
||||
<button style="padding:8px 12px;border-radius:8px;border:none;text-align:left;font-weight:@(_selectedCategory == catName ? "700" : "400");background:@(_selectedCategory == catName ? "var(--admin-orange-primary)" : "transparent");color:@(_selectedCategory == catName ? "#FFF" : "inherit");cursor:pointer;font-size:14px;" @onclick="@(() => SelectCategory(catName))">@catName (@count)</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* RIGHT: Quick Stats *@
|
||||
<div style="width:300px;display:flex;flex-direction:column;gap:20px;">
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__header">
|
||||
<h3 class="admin-panel__title">
|
||||
<i data-lucide="bar-chart-2" style="color:var(--admin-orange-primary);"></i>
|
||||
Thống kê menu
|
||||
</h3>
|
||||
@* Products grid *@
|
||||
<div style="flex:1;">
|
||||
@if (IsLoading)
|
||||
{
|
||||
<div style="text-align:center;padding:48px;"><div class="spinner-small" style="width:32px;height:32px;margin:0 auto 16px;"></div></div>
|
||||
}
|
||||
else if (!FilteredProducts.Any())
|
||||
{
|
||||
<div style="text-align:center;padding:60px 20px;"><h2 style="font-size:18px;font-weight:700;color:var(--pos-text-primary, #FFF);">Chưa có sản phẩm</h2></div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px;">
|
||||
@foreach (var p in FilteredProducts)
|
||||
{
|
||||
<div class="admin-panel" style="cursor:pointer;">
|
||||
<div class="admin-panel__body" style="padding:16px;text-align:center;">
|
||||
<div style="width:48px;height:48px;border-radius:12px;background:rgba(255,92,0,0.1);display:flex;align-items:center;justify-content:center;margin:0 auto 12px;"><i data-lucide="package" style="color:#FF5C00;width:24px;height:24px;"></i></div>
|
||||
<div style="font-weight:600;font-size:14px;margin-bottom:4px;">@p.Name</div>
|
||||
<div style="font-size:13px;color:var(--admin-text-tertiary);">@(p.CategoryName ?? "—")</div>
|
||||
<div style="font-weight:700;color:var(--admin-orange-primary);margin-top:8px;">@p.Price.ToString("N0")₫</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="admin-panel__body" style="display:flex;flex-direction:column;gap:12px;">
|
||||
<div style="display:flex;justify-content:space-between;font-size:13px;">
|
||||
<span style="color:var(--admin-text-tertiary);">Tổng danh mục</span>
|
||||
<span style="font-weight:600;">@_menuSections.Length</span>
|
||||
</div>
|
||||
<div style="display:flex;justify-content:space-between;font-size:13px;">
|
||||
<span style="color:var(--admin-text-tertiary);">Tổng sản phẩm</span>
|
||||
<span style="font-weight:600;">@_menuSections.Sum(s => s.Items.Length)</span>
|
||||
</div>
|
||||
<div style="display:flex;justify-content:space-between;font-size:13px;">
|
||||
<span style="color:var(--admin-text-tertiary);">Đang hiển thị</span>
|
||||
<span style="font-weight:600;color:#22C55E;">@_menuSections.Sum(s => s.Items.Count(i => i.Visible))</span>
|
||||
</div>
|
||||
<div style="display:flex;justify-content:space-between;font-size:13px;">
|
||||
<span style="color:var(--admin-text-tertiary);">Đang ẩn</span>
|
||||
<span style="font-weight:600;color:var(--admin-text-tertiary);">@_menuSections.Sum(s => s.Items.Count(i => !i.Visible))</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-panel" style="background:rgba(255,92,0,0.04);border:1px solid rgba(255,92,0,0.15);">
|
||||
<div class="admin-panel__body" style="display:flex;gap:10px;align-items:flex-start;">
|
||||
<i data-lucide="info" style="width:18px;height:18px;color:var(--admin-orange-primary);flex-shrink:0;margin-top:2px;"></i>
|
||||
<div style="font-size:12px;color:var(--admin-text-secondary);line-height:1.5;">
|
||||
Kéo thả để sắp xếp thứ tự danh mục và sản phẩm. Thay đổi sẽ phản ánh trực tiếp trên POS.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private record MenuItem(string Name, string Price, string Variants, bool Visible);
|
||||
private record MenuSection(string Name, string Icon, string Color, bool Visible, MenuItem[] Items);
|
||||
private readonly MenuSection[] _menuSections = new[]
|
||||
private List<PosDataService.AdminProductInfo> _products = new();
|
||||
private List<PosDataService.AdminCategoryInfo> _categories = new();
|
||||
private List<PosDataService.ShopInfo> _shops = new();
|
||||
private string? _selectedCategory;
|
||||
private Guid? _selectedShopId;
|
||||
|
||||
private IEnumerable<PosDataService.AdminProductInfo> FilteredProducts => _selectedCategory == null
|
||||
? _products
|
||||
: _products.Where(p => string.Equals(p.CategoryName, _selectedCategory, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
new MenuSection("Cà phê", "coffee", "#FF5C00", true, new[]
|
||||
IsLoading = true;
|
||||
try
|
||||
{
|
||||
new MenuItem("Espresso", "35K", "3", true),
|
||||
new MenuItem("Cappuccino", "45K", "3", true),
|
||||
new MenuItem("Latte", "49K", "3", true),
|
||||
new MenuItem("Americano", "39K", "2", true),
|
||||
new MenuItem("Mocha", "52K", "3", false),
|
||||
}),
|
||||
new MenuSection("Trà", "leaf", "#22C55E", true, new[]
|
||||
_shops = await DataService.GetShopsAsync();
|
||||
_products = await DataService.GetAllProductsAsync();
|
||||
_categories = await DataService.GetAllCategoriesAsync();
|
||||
}
|
||||
catch { } finally { IsLoading = false; }
|
||||
}
|
||||
|
||||
private async Task OnShopFilterChanged(ChangeEventArgs e)
|
||||
{
|
||||
_selectedShopId = Guid.TryParse(e.Value?.ToString(), out var id) ? id : null;
|
||||
IsLoading = true;
|
||||
try
|
||||
{
|
||||
new MenuItem("Trà sen vàng", "42K", "2", true),
|
||||
new MenuItem("Trà đào cam sả", "39K", "2", true),
|
||||
new MenuItem("Trà sữa", "38K", "3", true),
|
||||
}),
|
||||
new MenuSection("Đồ ăn", "utensils", "#F59E0B", true, new[]
|
||||
{
|
||||
new MenuItem("Croissant", "35K", "1", true),
|
||||
new MenuItem("Bánh mì", "28K", "2", true),
|
||||
new MenuItem("Tiramisu", "45K", "1", false),
|
||||
}),
|
||||
};
|
||||
_products = await DataService.GetAllProductsAsync(_selectedShopId);
|
||||
_categories = await DataService.GetAllCategoriesAsync(_selectedShopId);
|
||||
}
|
||||
catch { } finally { IsLoading = false; }
|
||||
}
|
||||
|
||||
private void SelectCategory(string? cat) { _selectedCategory = cat; }
|
||||
}
|
||||
|
||||
@@ -1,128 +1,21 @@
|
||||
@page "/admin/modifiers"
|
||||
@page "/admin/products/modifiers"
|
||||
@layout AdminLayout
|
||||
@inherits AdminBase
|
||||
|
||||
@*
|
||||
EN: Modifier groups — manage add-on groups (Sugar, Ice, Topping) with options & pricing.
|
||||
VI: Nhóm modifier — quản lý nhóm tùy chọn (Đường, Đá, Topping) với options & giá.
|
||||
Design: pencil-design/src/pages/tPOS/admin/modifier-groups.pen
|
||||
*@
|
||||
<PageTitle>Nhóm tùy chọn — GoodGo Admin</PageTitle>
|
||||
|
||||
<PageTitle>Nhóm Modifier — GoodGo Admin</PageTitle>
|
||||
|
||||
@* ═══ TOP BAR ═══ *@
|
||||
<div class="admin-topbar">
|
||||
<div class="admin-topbar__left">
|
||||
<h1 class="admin-topbar__title">Nhóm Modifier</h1>
|
||||
<p class="admin-topbar__subtitle">Quản lý tùy chọn thêm cho sản phẩm</p>
|
||||
</div>
|
||||
<div class="admin-topbar__right">
|
||||
<button class="admin-btn-primary" @onclick="@(() => _showCreate = !_showCreate)">
|
||||
<i data-lucide="plus"></i>
|
||||
<span>Tạo nhóm mới</span>
|
||||
</button>
|
||||
<h1 class="admin-topbar__title">Nhóm tùy chỉnh</h1>
|
||||
<p class="admin-topbar__subtitle">Quản lý topping, size, đường/đá</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ═══ CONTENT ═══ *@
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;gap:16px;">
|
||||
|
||||
@foreach (var group in _modifierGroups)
|
||||
{
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__header">
|
||||
<div style="display:flex;align-items:center;gap:12px;">
|
||||
<h3 class="admin-panel__title">
|
||||
<i data-lucide="@group.Icon" style="color:@group.Color;"></i>
|
||||
@group.Name
|
||||
</h3>
|
||||
<span class="admin-tab__badge">@group.Options.Length tùy chọn</span>
|
||||
<span style="font-size:11px;color:var(--admin-text-tertiary);">
|
||||
@(group.Required ? "Bắt buộc" : "Không bắt buộc") • @(group.Multi ? "Nhiều lựa chọn" : "Một lựa chọn")
|
||||
</span>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;">
|
||||
<button class="admin-icon-btn" style="width:32px;height:32px;">
|
||||
<i data-lucide="edit-2" style="width:14px;height:14px;"></i>
|
||||
</button>
|
||||
<button class="admin-icon-btn" style="width:32px;height:32px;">
|
||||
<i data-lucide="trash-2" style="width:14px;height:14px;color:#EF4444;"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-panel__body" style="padding:0;">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tùy chọn</th>
|
||||
<th>Giá thêm</th>
|
||||
<th>Mặc định</th>
|
||||
<th style="width:60px;">Trạng thái</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var opt in group.Options)
|
||||
{
|
||||
<tr>
|
||||
<td style="font-weight:500;">@opt.Name</td>
|
||||
<td style="color:@(opt.Price == "0" ? "var(--admin-text-tertiary)" : "#22C55E");">
|
||||
@(opt.Price == "0" ? "Miễn phí" : $"+{opt.Price}")
|
||||
</td>
|
||||
<td>
|
||||
@if (opt.IsDefault)
|
||||
{
|
||||
<span style="font-size:11px;color:var(--admin-orange-primary);font-weight:600;">Mặc định</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<MudSwitch T="bool" Value="opt.Active" Color="Color.Primary" />
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;gap:20px;">
|
||||
<div style="text-align:center;padding:60px 20px;">
|
||||
<div style="width:80px;height:80px;border-radius:24px;background:rgba(139,92,246,0.1);display:flex;align-items:center;justify-content:center;margin:0 auto 20px;"><i data-lucide="sliders-horizontal" style="width:36px;height:36px;color:#8B5CF6;"></i></div>
|
||||
<h2 style="font-size:20px;font-weight:700;margin:0 0 8px;color:var(--pos-text-primary, #FFF);">Nhóm tùy chỉnh</h2>
|
||||
<p style="font-size:14px;color:var(--admin-text-tertiary);margin:0 0 16px;">Tính năng đang phát triển — sẽ sớm ra mắt</p>
|
||||
<span class="admin-status-badge admin-status-badge--warning" style="font-size:12px;padding:4px 12px;"><span class="admin-status-badge__dot" style="width:5px;height:5px;"></span>Coming Soon</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private bool _showCreate = false;
|
||||
|
||||
private record ModOption(string Name, string Price, bool IsDefault, bool Active);
|
||||
private record ModGroup(string Name, string Icon, string Color, bool Required, bool Multi, ModOption[] Options);
|
||||
private readonly ModGroup[] _modifierGroups = new[]
|
||||
{
|
||||
new ModGroup("Mức đường", "droplet", "#FF5C00", true, false, new[]
|
||||
{
|
||||
new ModOption("100% đường", "0", true, true),
|
||||
new ModOption("70% đường", "0", false, true),
|
||||
new ModOption("50% đường", "0", false, true),
|
||||
new ModOption("30% đường", "0", false, true),
|
||||
new ModOption("Không đường", "0", false, true),
|
||||
}),
|
||||
new ModGroup("Mức đá", "snowflake", "#3B82F6", true, false, new[]
|
||||
{
|
||||
new ModOption("Đá bình thường", "0", true, true),
|
||||
new ModOption("Ít đá", "0", false, true),
|
||||
new ModOption("Nhiều đá", "0", false, true),
|
||||
new ModOption("Không đá", "0", false, true),
|
||||
}),
|
||||
new ModGroup("Topping", "layers", "#8B5CF6", false, true, new[]
|
||||
{
|
||||
new ModOption("Trân châu đen", "8K", false, true),
|
||||
new ModOption("Trân châu trắng", "8K", false, true),
|
||||
new ModOption("Thạch", "10K", false, true),
|
||||
new ModOption("Kem phô mai", "15K", false, true),
|
||||
new ModOption("Shot espresso", "12K", false, true),
|
||||
new ModOption("Sữa tươi", "5K", false, true),
|
||||
}),
|
||||
new ModGroup("Loại sữa", "milk", "#22C55E", false, false, new[]
|
||||
{
|
||||
new ModOption("Sữa đặc", "0", true, true),
|
||||
new ModOption("Sữa tươi", "5K", false, true),
|
||||
new ModOption("Sữa yến mạch", "12K", false, true),
|
||||
new ModOption("Sữa hạnh nhân", "12K", false, false),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,104 +1,71 @@
|
||||
@page "/admin/pricing"
|
||||
@page "/admin/products/pricing"
|
||||
@layout AdminLayout
|
||||
@inherits AdminBase
|
||||
@inject PosDataService DataService
|
||||
@using WebClientTpos.Client.Services
|
||||
|
||||
@*
|
||||
EN: Pricing rules — time-based, happy hour, combo, loyalty discounts.
|
||||
VI: Quy tắc giá — theo giờ, happy hour, combo, giảm giá loyalty.
|
||||
Design: pencil-design/src/pages/tPOS/admin/pricing-rules.pen
|
||||
*@
|
||||
<PageTitle>Chiến lược giá — GoodGo Admin</PageTitle>
|
||||
|
||||
<PageTitle>Quy tắc giá — GoodGo Admin</PageTitle>
|
||||
|
||||
@* ═══ TOP BAR ═══ *@
|
||||
<div class="admin-topbar">
|
||||
<div class="admin-topbar__left">
|
||||
<h1 class="admin-topbar__title">Quy tắc giá</h1>
|
||||
<p class="admin-topbar__subtitle">Quản lý khuyến mãi, combo, giá theo giờ</p>
|
||||
</div>
|
||||
<div class="admin-topbar__right">
|
||||
<button class="admin-btn-primary">
|
||||
<i data-lucide="plus"></i>
|
||||
<span>Tạo quy tắc</span>
|
||||
</button>
|
||||
<h1 class="admin-topbar__title">Chiến lược giá & Khuyến mãi</h1>
|
||||
<p class="admin-topbar__subtitle">@_promos.Count chương trình</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ═══ TABS ═══ *@
|
||||
<div class="admin-tabs">
|
||||
<button class="admin-tab @(_ruleTab == "all" ? "admin-tab--active" : "")" @onclick="@(() => _ruleTab = "all")">
|
||||
Tất cả <span class="admin-tab__badge admin-tab__badge--active">@_pricingRules.Count</span>
|
||||
</button>
|
||||
<button class="admin-tab @(_ruleTab == "happyhour" ? "admin-tab--active" : "")" @onclick="@(() => _ruleTab = "happyhour")">
|
||||
<i data-lucide="clock"></i> Happy Hour
|
||||
</button>
|
||||
<button class="admin-tab @(_ruleTab == "combo" ? "admin-tab--active" : "")" @onclick="@(() => _ruleTab = "combo")">
|
||||
<i data-lucide="package"></i> Combo
|
||||
</button>
|
||||
<button class="admin-tab @(_ruleTab == "loyalty" ? "admin-tab--active" : "")" @onclick="@(() => _ruleTab = "loyalty")">
|
||||
<i data-lucide="heart"></i> Loyalty
|
||||
</button>
|
||||
</div>
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;gap:20px;">
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px;">
|
||||
<div class="admin-stat-card"><div class="admin-stat-card__icon" style="background:rgba(34,197,94,0.1);"><i data-lucide="tag" style="color:#22C55E;"></i></div><div class="admin-stat-card__content"><span class="admin-stat-card__value">@_promos.Count(p => p.IsActive)</span><span class="admin-stat-card__label">Đang hoạt động</span></div></div>
|
||||
<div class="admin-stat-card"><div class="admin-stat-card__icon" style="background:rgba(59,130,246,0.1);"><i data-lucide="ticket" style="color:#3B82F6;"></i></div><div class="admin-stat-card__content"><span class="admin-stat-card__value">@_promos.Sum(p => p.VoucherCount)</span><span class="admin-stat-card__label">Tổng voucher</span></div></div>
|
||||
<div class="admin-stat-card"><div class="admin-stat-card__icon" style="background:rgba(255,92,0,0.1);"><i data-lucide="check-circle" style="color:#FF5C00;"></i></div><div class="admin-stat-card__content"><span class="admin-stat-card__value">@_promos.Sum(p => p.RedemptionCount)</span><span class="admin-stat-card__label">Đã sử dụng</span></div></div>
|
||||
</div>
|
||||
|
||||
@* ═══ CONTENT ═══ *@
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;gap:16px;">
|
||||
@foreach (var rule in _pricingRules)
|
||||
@if (IsLoading)
|
||||
{
|
||||
<div style="text-align:center;padding:48px;"><div class="spinner-small" style="width:32px;height:32px;margin:0 auto 16px;"></div></div>
|
||||
}
|
||||
else if (!_promos.Any())
|
||||
{
|
||||
<div style="text-align:center;padding:60px 20px;">
|
||||
<div style="width:80px;height:80px;border-radius:24px;background:rgba(255,92,0,0.1);display:flex;align-items:center;justify-content:center;margin:0 auto 20px;"><i data-lucide="percent" style="width:36px;height:36px;color:#FF5C00;"></i></div>
|
||||
<h2 style="font-size:20px;font-weight:700;margin:0 0 8px;color:var(--pos-text-primary, #FFF);">Chưa có chương trình khuyến mãi</h2>
|
||||
<p style="font-size:14px;color:var(--admin-text-tertiary);margin:0;">Tạo chiến dịch mới để thu hút khách hàng</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__header">
|
||||
<div style="display:flex;align-items:center;gap:12px;">
|
||||
<div class="admin-kpi-card__icon" style="width:40px;height:40px;border-radius:10px;background-color:@(rule.Color)20;">
|
||||
<i data-lucide="@rule.Icon" style="color:@rule.Color;width:20px;height:20px;"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:15px;font-weight:600;">@rule.Name</div>
|
||||
<div style="font-size:12px;color:var(--admin-text-tertiary);">@rule.Desc</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:12px;">
|
||||
<div class="admin-status-badge @(rule.Active ? "admin-status-badge--online" : "admin-status-badge--offline")">
|
||||
<span class="admin-status-badge__dot"></span>
|
||||
@(rule.Active ? "Đang hoạt động" : "Tắt")
|
||||
</div>
|
||||
<button class="admin-icon-btn" style="width:32px;height:32px;">
|
||||
<i data-lucide="edit-2" style="width:14px;height:14px;"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-panel__body" style="padding:12px 20px;">
|
||||
<div style="display:flex;gap:24px;font-size:13px;">
|
||||
<div>
|
||||
<span style="color:var(--admin-text-tertiary);">Loại: </span>
|
||||
<span style="font-weight:600;">@rule.Type</span>
|
||||
</div>
|
||||
<div>
|
||||
<span style="color:var(--admin-text-tertiary);">Giảm: </span>
|
||||
<span style="font-weight:600;color:#22C55E;">@rule.Discount</span>
|
||||
</div>
|
||||
<div>
|
||||
<span style="color:var(--admin-text-tertiary);">Áp dụng: </span>
|
||||
<span style="font-weight:600;">@rule.AppliesTo</span>
|
||||
</div>
|
||||
<div>
|
||||
<span style="color:var(--admin-text-tertiary);">Thời gian: </span>
|
||||
<span style="font-weight:600;">@rule.Schedule</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-panel__body" style="padding:0;">
|
||||
<table class="admin-table" style="width:100%;"><thead><tr>
|
||||
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Tên</th>
|
||||
<th style="padding:12px 16px;text-align:right;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Giảm giá</th>
|
||||
<th style="padding:12px 16px;text-align:right;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Voucher</th>
|
||||
<th style="padding:12px 16px;text-align:right;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Đã dùng</th>
|
||||
<th style="padding:12px 16px;text-align:center;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Trạng thái</th>
|
||||
</tr></thead><tbody>
|
||||
@foreach (var p in _promos)
|
||||
{
|
||||
<tr style="border-top:1px solid var(--admin-border-subtle);">
|
||||
<td style="padding:12px 16px;"><div style="font-weight:600;font-size:14px;">@p.Name</div><div style="font-size:12px;color:var(--admin-text-tertiary);">@(p.Description ?? "")</div></td>
|
||||
<td style="padding:12px 16px;text-align:right;font-weight:600;color:var(--admin-orange-primary);">@(p.DiscountType == "Percentage" ? $"{p.DiscountValue}%" : $"{p.DiscountValue?.ToString("N0")}₫")</td>
|
||||
<td style="padding:12px 16px;text-align:right;">@p.VoucherCount</td>
|
||||
<td style="padding:12px 16px;text-align:right;">@p.RedemptionCount</td>
|
||||
<td style="padding:12px 16px;text-align:center;"><span class="admin-status-badge @(p.IsActive ? "admin-status-badge--online" : "admin-status-badge--offline")" style="font-size:11px;padding:2px 10px;"><span class="admin-status-badge__dot" style="width:5px;height:5px;"></span>@(p.IsActive ? "Active" : "Inactive")</span></td>
|
||||
</tr>
|
||||
}
|
||||
</tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private string _ruleTab = "all";
|
||||
private List<PosDataService.PromotionInfo> _promos = new();
|
||||
|
||||
private record PricingRule(string Name, string Desc, string Type, string Discount, string AppliesTo, string Schedule, string Icon, string Color, bool Active);
|
||||
private readonly List<PricingRule> _pricingRules = new()
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
new("Happy Hour Chiều", "Giảm giá đồ uống từ 14h-16h", "Happy Hour", "-20%", "Tất cả cà phê", "14:00 – 16:00 hàng ngày", "clock", "#FF5C00", true),
|
||||
new("Combo Sáng", "Combo cà phê + bánh mì", "Combo", "-15K", "Espresso + Bánh mì", "07:00 – 10:00 T2-T6", "package", "#3B82F6", true),
|
||||
new("Sinh nhật Loyalty", "Giảm giá cho khách hàng sinh nhật", "Loyalty", "-30%", "Khách hàng thành viên", "Cả ngày", "heart", "#EC4899", true),
|
||||
new("Weekend Brunch", "Giảm giá cuối tuần cho đồ ăn", "Happy Hour", "-25%", "Danh mục Đồ ăn", "09:00 – 13:00 T7-CN", "sun", "#F59E0B", false),
|
||||
new("Mua 5 tặng 1", "Tích điểm mua 5 ly tặng 1", "Loyalty", "1 miễn phí", "Tất cả đồ uống", "Không giới hạn", "gift", "#8B5CF6", true),
|
||||
};
|
||||
IsLoading = true;
|
||||
try { _promos = await DataService.GetPromotionsAsync(); }
|
||||
catch { } finally { IsLoading = false; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,150 +1,83 @@
|
||||
@page "/admin/staff/attendance"
|
||||
@layout AdminLayout
|
||||
@inherits AdminBase
|
||||
|
||||
@*
|
||||
EN: Attendance dashboard — today's check-in/out, weekly summary, punctuality.
|
||||
VI: Dashboard chấm công — điểm danh hôm nay, tổng kết tuần, đúng giờ.
|
||||
Design: pencil-design/src/pages/tPOS/admin/attendance-dashboard.pen
|
||||
*@
|
||||
@inject PosDataService DataService
|
||||
@using WebClientTpos.Client.Services
|
||||
|
||||
<PageTitle>Chấm công — GoodGo Admin</PageTitle>
|
||||
|
||||
@* ═══ TOP BAR ═══ *@
|
||||
<div class="admin-topbar">
|
||||
<div class="admin-topbar__left">
|
||||
<h1 class="admin-topbar__title">Chấm công</h1>
|
||||
<p class="admin-topbar__subtitle">Hôm nay, @DateTime.Now.ToString("dd/MM/yyyy") • Coffee House Q1</p>
|
||||
</div>
|
||||
<div class="admin-topbar__right">
|
||||
<button class="admin-btn-secondary">
|
||||
<i data-lucide="download"></i>
|
||||
<span>Xuất báo cáo</span>
|
||||
</button>
|
||||
<p class="admin-topbar__subtitle">Theo dõi giờ làm việc nhân viên</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ═══ CONTENT ═══ *@
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;gap:24px;">
|
||||
|
||||
@* KPI Row *@
|
||||
<div class="admin-kpi-row">
|
||||
<div class="admin-kpi-card">
|
||||
<div class="admin-kpi-card__header">
|
||||
<div class="admin-kpi-card__icon" style="background-color:rgba(34,197,94,0.125);">
|
||||
<i data-lucide="user-check" style="color:#22C55E;"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-kpi-card__value">4/5</div>
|
||||
<div class="admin-kpi-card__label">Đã check-in</div>
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;gap:20px;">
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px;">
|
||||
<div class="admin-stat-card">
|
||||
<div class="admin-stat-card__icon" style="background:rgba(34,197,94,0.1);"><i data-lucide="user-check" style="color:#22C55E;"></i></div>
|
||||
<div class="admin-stat-card__content"><span class="admin-stat-card__value">@_staff.Count(s => s.Status == "Active")</span><span class="admin-stat-card__label">Đang hoạt động</span></div>
|
||||
</div>
|
||||
<div class="admin-kpi-card">
|
||||
<div class="admin-kpi-card__header">
|
||||
<div class="admin-kpi-card__icon" style="background-color:rgba(59,130,246,0.125);">
|
||||
<i data-lucide="clock" style="color:#3B82F6;"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-kpi-card__value">96%</div>
|
||||
<div class="admin-kpi-card__label">Tỷ lệ đúng giờ</div>
|
||||
<div class="admin-stat-card">
|
||||
<div class="admin-stat-card__icon" style="background:rgba(59,130,246,0.1);"><i data-lucide="users" style="color:#3B82F6;"></i></div>
|
||||
<div class="admin-stat-card__content"><span class="admin-stat-card__value">@_staff.Count</span><span class="admin-stat-card__label">Tổng nhân viên</span></div>
|
||||
</div>
|
||||
<div class="admin-kpi-card">
|
||||
<div class="admin-kpi-card__header">
|
||||
<div class="admin-kpi-card__icon" style="background-color:rgba(245,158,11,0.125);">
|
||||
<i data-lucide="alert-triangle" style="color:#F59E0B;"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-kpi-card__value">1</div>
|
||||
<div class="admin-kpi-card__label">Đi trễ hôm nay</div>
|
||||
</div>
|
||||
<div class="admin-kpi-card">
|
||||
<div class="admin-kpi-card__header">
|
||||
<div class="admin-kpi-card__icon" style="background-color:rgba(239,68,68,0.125);">
|
||||
<i data-lucide="user-x" style="color:#EF4444;"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-kpi-card__value">1</div>
|
||||
<div class="admin-kpi-card__label">Vắng mặt</div>
|
||||
<div class="admin-stat-card">
|
||||
<div class="admin-stat-card__icon" style="background:rgba(245,158,11,0.1);"><i data-lucide="clock" style="color:#F59E0B;"></i></div>
|
||||
<div class="admin-stat-card__content"><span class="admin-stat-card__value">@_schedules.Count</span><span class="admin-stat-card__label">Tổng ca hôm nay</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Today's attendance table *@
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__header">
|
||||
<h3 class="admin-panel__title">
|
||||
<i data-lucide="list" style="color:var(--admin-orange-primary);"></i>
|
||||
Chi tiết hôm nay
|
||||
</h3>
|
||||
@if (IsLoading)
|
||||
{
|
||||
<div style="text-align:center;padding:48px;"><div class="spinner-small" style="width:32px;height:32px;margin:0 auto 16px;"></div></div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__header"><h3 class="admin-panel__title">Nhân viên hôm nay</h3></div>
|
||||
<div class="admin-panel__body" style="padding:0;">
|
||||
<table class="admin-table" style="width:100%;">
|
||||
<thead><tr>
|
||||
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Nhân viên</th>
|
||||
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Vai trò</th>
|
||||
<th style="padding:12px 16px;text-align:center;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Trạng thái</th>
|
||||
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Cửa hàng</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
@foreach (var s in _staff)
|
||||
{
|
||||
<tr style="border-top:1px solid var(--admin-border-subtle);">
|
||||
<td style="padding:12px 16px;font-weight:600;">@(s.EmployeeCode ?? s.Id.ToString()[..6])</td>
|
||||
<td style="padding:12px 16px;">@(s.Role ?? "—")</td>
|
||||
<td style="padding:12px 16px;text-align:center;">
|
||||
<span class="admin-status-badge @(s.Status == "Active" ? "admin-status-badge--online" : "admin-status-badge--offline")" style="font-size:11px;padding:2px 10px;">
|
||||
<span class="admin-status-badge__dot" style="width:5px;height:5px;"></span>@(s.Status ?? "—")
|
||||
</span>
|
||||
</td>
|
||||
<td style="padding:12px 16px;font-size:13px;color:var(--admin-text-tertiary);">@(s.ShopName ?? "—")</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-panel__body" style="padding:0;">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nhân viên</th>
|
||||
<th>Ca làm</th>
|
||||
<th>Check-in</th>
|
||||
<th>Check-out</th>
|
||||
<th>Trạng thái</th>
|
||||
<th>Giờ làm</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="display:flex;align-items:center;gap:10px;">
|
||||
<div class="admin-user-avatar" style="width:28px;height:28px;font-size:10px;">TM</div>
|
||||
Trần Minh
|
||||
</td>
|
||||
<td>07:00 – 15:00</td>
|
||||
<td style="color:#22C55E;font-weight:600;">06:55</td>
|
||||
<td>—</td>
|
||||
<td><div class="admin-status-badge admin-status-badge--online"><span class="admin-status-badge__dot"></span>Đúng giờ</div></td>
|
||||
<td>6h 30m</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="display:flex;align-items:center;gap:10px;">
|
||||
<div class="admin-user-avatar" style="width:28px;height:28px;font-size:10px;background-color:#3B82F6;">LT</div>
|
||||
Lê Thảo
|
||||
</td>
|
||||
<td>07:00 – 15:00</td>
|
||||
<td style="color:#22C55E;font-weight:600;">06:58</td>
|
||||
<td>—</td>
|
||||
<td><div class="admin-status-badge admin-status-badge--online"><span class="admin-status-badge__dot"></span>Đúng giờ</div></td>
|
||||
<td>6h 27m</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="display:flex;align-items:center;gap:10px;">
|
||||
<div class="admin-user-avatar" style="width:28px;height:28px;font-size:10px;background-color:#22C55E;">NH</div>
|
||||
Nguyễn Hà
|
||||
</td>
|
||||
<td>08:00 – 16:00</td>
|
||||
<td style="color:#F59E0B;font-weight:600;">08:12</td>
|
||||
<td>—</td>
|
||||
<td><div class="admin-status-badge admin-status-badge--setup"><span class="admin-status-badge__dot"></span>Trễ 12p</div></td>
|
||||
<td>5h 13m</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="display:flex;align-items:center;gap:10px;">
|
||||
<div class="admin-user-avatar" style="width:28px;height:28px;font-size:10px;background-color:#8B5CF6;">PA</div>
|
||||
Phạm An
|
||||
</td>
|
||||
<td>08:00 – 17:00</td>
|
||||
<td style="color:#22C55E;font-weight:600;">07:50</td>
|
||||
<td>—</td>
|
||||
<td><div class="admin-status-badge admin-status-badge--online"><span class="admin-status-badge__dot"></span>Đúng giờ</div></td>
|
||||
<td>5h 35m</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="display:flex;align-items:center;gap:10px;">
|
||||
<div class="admin-user-avatar" style="width:28px;height:28px;font-size:10px;background-color:#EC4899;">HL</div>
|
||||
Hoàng Lan
|
||||
</td>
|
||||
<td>07:00 – 15:00</td>
|
||||
<td style="color:var(--admin-text-tertiary);">—</td>
|
||||
<td>—</td>
|
||||
<td><div class="admin-status-badge admin-status-badge--offline"><span class="admin-status-badge__dot"></span>Vắng</div></td>
|
||||
<td>—</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private List<PosDataService.StaffInfo> _staff = new();
|
||||
private List<PosDataService.ScheduleInfo> _schedules = new();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
IsLoading = true;
|
||||
try
|
||||
{
|
||||
_staff = await DataService.GetStaffAsync();
|
||||
_schedules = await DataService.GetStaffSchedulesAsync();
|
||||
}
|
||||
catch { } finally { IsLoading = false; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,133 +1,73 @@
|
||||
@page "/admin/staff/payroll"
|
||||
@layout AdminLayout
|
||||
@inherits AdminBase
|
||||
@inject PosDataService DataService
|
||||
@using WebClientTpos.Client.Services
|
||||
|
||||
@*
|
||||
EN: Payroll & Commission — salary overview, commission breakdown by staff.
|
||||
VI: Lương & Hoa hồng — tổng quan lương, chi tiết hoa hồng theo nhân viên.
|
||||
Design: pencil-design/src/pages/tPOS/admin/payroll-commission.pen
|
||||
*@
|
||||
<PageTitle>Bảng lương — GoodGo Admin</PageTitle>
|
||||
|
||||
<PageTitle>Lương & Hoa hồng — GoodGo Admin</PageTitle>
|
||||
|
||||
@* ═══ TOP BAR ═══ *@
|
||||
<div class="admin-topbar">
|
||||
<div class="admin-topbar__left">
|
||||
<h1 class="admin-topbar__title">Lương & Hoa hồng</h1>
|
||||
<p class="admin-topbar__subtitle">Tháng 02/2025 • Tất cả cửa hàng</p>
|
||||
</div>
|
||||
<div class="admin-topbar__right">
|
||||
<button class="admin-btn-secondary">
|
||||
<i data-lucide="calendar"></i>
|
||||
<span>02/2025</span>
|
||||
</button>
|
||||
<button class="admin-btn-primary">
|
||||
<i data-lucide="download"></i>
|
||||
<span>Xuất bảng lương</span>
|
||||
</button>
|
||||
<h1 class="admin-topbar__title">Bảng lương</h1>
|
||||
<p class="admin-topbar__subtitle">@_staff.Count nhân viên • @DateTime.Now.ToString("MM/yyyy")</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ═══ CONTENT ═══ *@
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;gap:24px;">
|
||||
|
||||
@* KPI Row *@
|
||||
<div class="admin-kpi-row">
|
||||
<div class="admin-kpi-card">
|
||||
<div class="admin-kpi-card__header">
|
||||
<div class="admin-kpi-card__icon" style="background-color:rgba(255,92,0,0.125);">
|
||||
<i data-lucide="wallet" style="color:var(--admin-orange-primary);"></i>
|
||||
</div>
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;gap:20px;">
|
||||
@if (IsLoading)
|
||||
{
|
||||
<div style="text-align:center;padding:48px;"><div class="spinner-small" style="width:32px;height:32px;margin:0 auto 16px;"></div></div>
|
||||
}
|
||||
else if (!_staff.Any())
|
||||
{
|
||||
<div style="text-align:center;padding:60px 20px;">
|
||||
<div style="width:80px;height:80px;border-radius:24px;background:rgba(34,197,94,0.1);display:flex;align-items:center;justify-content:center;margin:0 auto 20px;">
|
||||
<i data-lucide="banknote" style="width:36px;height:36px;color:#22C55E;"></i>
|
||||
</div>
|
||||
<div class="admin-kpi-card__value">86.4M</div>
|
||||
<div class="admin-kpi-card__label">Tổng lương tháng</div>
|
||||
<h2 style="font-size:20px;font-weight:700;margin:0 0 8px;color:var(--pos-text-primary, #FFF);">Chưa có dữ liệu lương</h2>
|
||||
<p style="font-size:14px;color:var(--admin-text-tertiary);margin:0;">Thêm nhân viên để quản lý bảng lương</p>
|
||||
</div>
|
||||
<div class="admin-kpi-card">
|
||||
<div class="admin-kpi-card__header">
|
||||
<div class="admin-kpi-card__icon" style="background-color:rgba(34,197,94,0.125);">
|
||||
<i data-lucide="percent" style="color:#22C55E;"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-kpi-card__value">12.8M</div>
|
||||
<div class="admin-kpi-card__label">Tổng hoa hồng</div>
|
||||
</div>
|
||||
<div class="admin-kpi-card">
|
||||
<div class="admin-kpi-card__header">
|
||||
<div class="admin-kpi-card__icon" style="background-color:rgba(59,130,246,0.125);">
|
||||
<i data-lucide="users" style="color:#3B82F6;"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-kpi-card__value">8</div>
|
||||
<div class="admin-kpi-card__label">Nhân viên</div>
|
||||
</div>
|
||||
<div class="admin-kpi-card">
|
||||
<div class="admin-kpi-card__header">
|
||||
<div class="admin-kpi-card__icon" style="background-color:rgba(139,92,246,0.125);">
|
||||
<i data-lucide="banknote" style="color:#8B5CF6;"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-kpi-card__value">12.4M</div>
|
||||
<div class="admin-kpi-card__label">Lương TB / người</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Payroll Table *@
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__header">
|
||||
<h3 class="admin-panel__title">
|
||||
<i data-lucide="table" style="color:var(--admin-orange-primary);"></i>
|
||||
Bảng lương chi tiết
|
||||
</h3>
|
||||
<div class="admin-status-badge" style="background:rgba(245,158,11,0.125);color:var(--admin-warning);">
|
||||
Chưa thanh toán
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__header"><h3 class="admin-panel__title">Danh sách nhân viên</h3></div>
|
||||
<div class="admin-panel__body" style="padding:0;">
|
||||
<table class="admin-table" style="width:100%;">
|
||||
<thead><tr>
|
||||
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Mã NV</th>
|
||||
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Vai trò</th>
|
||||
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Ngày vào</th>
|
||||
<th style="padding:12px 16px;text-align:center;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Trạng thái</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
@foreach (var s in _staff)
|
||||
{
|
||||
<tr style="border-top:1px solid var(--admin-border-subtle);">
|
||||
<td style="padding:12px 16px;font-weight:600;">@(s.EmployeeCode ?? "—")</td>
|
||||
<td style="padding:12px 16px;">@(s.Role ?? "—")</td>
|
||||
<td style="padding:12px 16px;font-size:13px;color:var(--admin-text-tertiary);">@(s.JoinedAt?.ToString("dd/MM/yyyy") ?? "—")</td>
|
||||
<td style="padding:12px 16px;text-align:center;">
|
||||
<span class="admin-status-badge @(s.Status == "Active" ? "admin-status-badge--online" : "admin-status-badge--offline")" style="font-size:11px;padding:2px 10px;">
|
||||
<span class="admin-status-badge__dot" style="width:5px;height:5px;"></span>@(s.Status ?? "—")
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-panel__body" style="padding:0;">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nhân viên</th>
|
||||
<th>Vai trò</th>
|
||||
<th>Lương cơ bản</th>
|
||||
<th>Giờ làm</th>
|
||||
<th>Hoa hồng</th>
|
||||
<th>Thưởng/Phạt</th>
|
||||
<th style="text-align:right;">Tổng</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var item in _payrollData)
|
||||
{
|
||||
<tr>
|
||||
<td style="display:flex;align-items:center;gap:10px;">
|
||||
<div class="admin-user-avatar" style="width:28px;height:28px;font-size:10px;background-color:@item.Color;">@item.Initials</div>
|
||||
@item.Name
|
||||
</td>
|
||||
<td style="color:var(--admin-text-tertiary);">@item.Role</td>
|
||||
<td>@item.BaseSalary</td>
|
||||
<td>@item.Hours h</td>
|
||||
<td style="color:#22C55E;">+@item.Commission</td>
|
||||
<td style="color:@(item.BonusPenalty.StartsWith("-") ? "#EF4444" : "#22C55E");">@item.BonusPenalty</td>
|
||||
<td style="text-align:right;font-weight:700;color:var(--admin-orange-primary);">@item.Total</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private record PayrollItem(string Name, string Initials, string Color, string Role, string BaseSalary, string Hours, string Commission, string BonusPenalty, string Total);
|
||||
private readonly PayrollItem[] _payrollData = new[]
|
||||
private List<PosDataService.StaffInfo> _staff = new();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
new PayrollItem("Trần Minh", "TM", "#FF5C00", "Barista", "8.0M", "172", "1.8M", "+500K", "10.3M"),
|
||||
new PayrollItem("Lê Thảo", "LT", "#3B82F6", "Thu ngân", "7.5M", "168", "2.1M", "+300K", "9.9M"),
|
||||
new PayrollItem("Nguyễn Hà", "NH", "#22C55E", "Phục vụ", "6.5M", "160", "1.2M", "+200K", "7.9M"),
|
||||
new PayrollItem("Phạm An", "PA", "#8B5CF6", "Quản lý", "15.0M", "180", "3.5M", "+1.0M", "19.5M"),
|
||||
new PayrollItem("Hoàng Lan", "HL", "#EC4899", "Barista", "8.0M", "148", "1.4M", "-200K", "9.2M"),
|
||||
new PayrollItem("Võ Khoa", "VK", "#F59E0B", "Bếp trưởng", "12.0M", "176", "2.0M", "+500K", "14.5M"),
|
||||
new PayrollItem("Đặng Linh", "ĐL", "#06B6D4", "Phục vụ", "6.5M", "152", "0.8M", "0", "7.3M"),
|
||||
new PayrollItem("Bùi Tùng", "BT", "#3B82F6", "Thu ngân", "7.5M", "164", "1.5M", "+200K", "9.2M"),
|
||||
};
|
||||
IsLoading = true;
|
||||
try { _staff = await DataService.GetStaffAsync(); }
|
||||
catch { } finally { IsLoading = false; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,189 +1,101 @@
|
||||
@page "/admin/staff/create"
|
||||
@layout AdminLayout
|
||||
@inherits AdminBase
|
||||
|
||||
@*
|
||||
EN: Create staff — form with personal info, role, store assignment, schedule.
|
||||
VI: Thêm nhân viên — form thông tin cá nhân, vai trò, phân công cửa hàng, lịch.
|
||||
Design: pencil-design/src/pages/tPOS/admin/staff-create.pen
|
||||
*@
|
||||
@inject PosDataService DataService
|
||||
@using WebClientTpos.Client.Services
|
||||
|
||||
<PageTitle>Thêm nhân viên — GoodGo Admin</PageTitle>
|
||||
|
||||
@* ═══ TOP BAR ═══ *@
|
||||
<div class="admin-topbar">
|
||||
<div class="admin-topbar__left" style="flex-direction:row;align-items:center;gap:12px;">
|
||||
<button class="admin-icon-btn" @onclick="@(() => NavigateTo("staff"))">
|
||||
<i data-lucide="arrow-left"></i>
|
||||
</button>
|
||||
<button class="admin-icon-btn" @onclick="@(() => NavigateTo("staff"))"><i data-lucide="arrow-left"></i></button>
|
||||
<div>
|
||||
<h1 class="admin-topbar__title">Thêm nhân viên mới</h1>
|
||||
<p class="admin-topbar__subtitle">Điền thông tin để tạo tài khoản nhân viên</p>
|
||||
<p class="admin-topbar__subtitle">Điền thông tin nhân viên</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-topbar__right">
|
||||
<button class="admin-btn-secondary" @onclick="@(() => NavigateTo("staff"))">Hủy</button>
|
||||
<button class="admin-btn-primary">
|
||||
<i data-lucide="check"></i>
|
||||
<span>Lưu nhân viên</span>
|
||||
<button class="admin-btn-primary" @onclick="HandleSubmit" disabled="@_isSaving">
|
||||
@if (_isSaving) { <div class="spinner-small" style="width:16px;height:16px;"></div> }
|
||||
else { <i data-lucide="check"></i> }
|
||||
<span>@(_isSaving ? "Đang lưu..." : "Lưu nhân viên")</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ═══ CONTENT ═══ *@
|
||||
<div class="admin-content" style="display:flex;gap:24px;">
|
||||
|
||||
@* LEFT COLUMN: Main form *@
|
||||
<div style="flex:1;display:flex;flex-direction:column;gap:24px;">
|
||||
@if (!string.IsNullOrEmpty(_message))
|
||||
{
|
||||
<div class="admin-panel" style="background:@(_isSuccess ? "rgba(34,197,94,0.1)" : "rgba(239,68,68,0.1)");border:1px solid @(_isSuccess ? "#22C55E" : "#EF4444");">
|
||||
<div class="admin-panel__body" style="display:flex;align-items:center;gap:12px;">
|
||||
<i data-lucide="@(_isSuccess ? "check-circle" : "alert-circle")" style="color:@(_isSuccess ? "#22C55E" : "#EF4444");width:20px;height:20px;"></i>
|
||||
<span style="font-size:14px;">@_message</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@* Personal Info *@
|
||||
<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 cá nhân
|
||||
</h3>
|
||||
<h3 class="admin-panel__title"><i data-lucide="user-plus" style="color:var(--admin-orange-primary);"></i> Thông tin nhân viên</h3>
|
||||
</div>
|
||||
<div class="admin-panel__body" style="display:flex;flex-direction:column;gap:20px;">
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-form-label">Họ và tên <span style="color:var(--admin-danger);">*</span></label>
|
||||
<input class="admin-form-input" type="text" placeholder="VD: Nguyễn Văn A" />
|
||||
<label class="admin-form-label">Mã nhân viên</label>
|
||||
<input class="admin-form-input" type="text" placeholder="VD: NV001" @bind="_empCode" />
|
||||
</div>
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-form-label">Số điện thoại <span style="color:var(--admin-danger);">*</span></label>
|
||||
<input class="admin-form-input" type="tel" placeholder="0901 234 567" />
|
||||
</div>
|
||||
</div>
|
||||
<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" type="email" placeholder="email@example.com" />
|
||||
</div>
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-form-label">Ngày sinh</label>
|
||||
<input class="admin-form-input" type="date" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-form-label">Địa chỉ</label>
|
||||
<input class="admin-form-input" type="text" placeholder="Số nhà, đường, quận..." />
|
||||
</div>
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-form-label">CMND/CCCD</label>
|
||||
<input class="admin-form-input" type="text" placeholder="VD: 079012345678" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Employment Info *@
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__header">
|
||||
<h3 class="admin-panel__title">
|
||||
<i data-lucide="briefcase" style="color:#3B82F6;"></i>
|
||||
Thông tin công việc
|
||||
</h3>
|
||||
</div>
|
||||
<div class="admin-panel__body" style="display:flex;flex-direction:column;gap:20px;">
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-form-label">Vai trò <span style="color:var(--admin-danger);">*</span></label>
|
||||
<select class="admin-form-input">
|
||||
<option value="">Chọn vai trò...</option>
|
||||
<option>Quản lý</option>
|
||||
<option>Thu ngân</option>
|
||||
<option>Barista</option>
|
||||
<option>Phục vụ</option>
|
||||
<option>Bếp trưởng</option>
|
||||
<option>Phụ bếp</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-form-label">Cửa hàng <span style="color:var(--admin-danger);">*</span></label>
|
||||
<select class="admin-form-input">
|
||||
<option value="">Chọn cửa hàng...</option>
|
||||
<option>Coffee House Q1</option>
|
||||
<option>Nhà hàng Q3</option>
|
||||
<option>Café Thủ Đức</option>
|
||||
<select class="admin-form-input" @bind="_role">
|
||||
@foreach (var r in _roles)
|
||||
{
|
||||
<option value="@r.Name">@r.Name</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-form-label">Ngày bắt đầu</label>
|
||||
<input class="admin-form-input" type="date" />
|
||||
<label class="admin-form-label">Số điện thoại</label>
|
||||
<input class="admin-form-input" type="tel" placeholder="0901234567" @bind="_phone" />
|
||||
</div>
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-form-label">Loại hợp đồng</label>
|
||||
<select class="admin-form-input">
|
||||
<option>Toàn thời gian</option>
|
||||
<option>Bán thời gian</option>
|
||||
<option>Thời vụ</option>
|
||||
</select>
|
||||
<label class="admin-form-label">Email</label>
|
||||
<input class="admin-form-input" type="email" placeholder="nv@goodgo.vn" @bind="_email" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Salary *@
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__header">
|
||||
<h3 class="admin-panel__title">
|
||||
<i data-lucide="banknote" style="color:#22C55E;"></i>
|
||||
Lương & Phúc lợi
|
||||
</h3>
|
||||
</div>
|
||||
<div class="admin-panel__body" style="display:flex;flex-direction:column;gap:20px;">
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-form-label">Lương cơ bản (VND)</label>
|
||||
<input class="admin-form-input" type="text" placeholder="VD: 8,000,000" />
|
||||
</div>
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-form-label">Hoa hồng (%)</label>
|
||||
<input class="admin-form-input" type="number" placeholder="VD: 3" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* RIGHT COLUMN: Summary/Preview *@
|
||||
<div style="width:320px;display:flex;flex-direction:column;gap:20px;">
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__header">
|
||||
<h3 class="admin-panel__title">
|
||||
<i data-lucide="eye" style="color:var(--admin-orange-primary);"></i>
|
||||
Xem trước thẻ
|
||||
</h3>
|
||||
</div>
|
||||
<div class="admin-panel__body" style="display:flex;flex-direction:column;align-items:center;gap:16px;">
|
||||
<div class="admin-user-avatar" style="width:72px;height:72px;font-size:24px;">NV</div>
|
||||
<div style="text-align:center;">
|
||||
<div style="font-size:16px;font-weight:600;">Nhân viên mới</div>
|
||||
<div style="font-size:12px;color:var(--admin-text-tertiary);">Chưa phân công</div>
|
||||
</div>
|
||||
<div style="width:100%;height:1px;background:var(--admin-border-subtle);"></div>
|
||||
<div style="width:100%;display:flex;flex-direction:column;gap:8px;font-size:13px;">
|
||||
<div style="display:flex;justify-content:space-between;">
|
||||
<span style="color:var(--admin-text-tertiary);">Mã PIN</span>
|
||||
<span style="font-weight:600;font-family:monospace;">Tự động</span>
|
||||
</div>
|
||||
<div style="display:flex;justify-content:space-between;">
|
||||
<span style="color:var(--admin-text-tertiary);">Quyền POS</span>
|
||||
<span style="font-weight:600;">Cơ bản</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-panel" style="background:rgba(59,130,246,0.06);border:1px solid rgba(59,130,246,0.15);">
|
||||
<div class="admin-panel__body" style="display:flex;gap:10px;align-items:flex-start;">
|
||||
<i data-lucide="info" style="width:18px;height:18px;color:#3B82F6;flex-shrink:0;margin-top:2px;"></i>
|
||||
<div style="font-size:12px;color:var(--admin-text-secondary);line-height:1.5;">
|
||||
Nhân viên sẽ nhận mã PIN 4 số để đăng nhập POS. Bạn có thể thay đổi quyền hạn trong phần <b>Phân quyền</b>.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private string _empCode = "", _role = "Cashier", _phone = "", _email = "", _message = "";
|
||||
private bool _isSuccess, _isSaving;
|
||||
private List<PosDataService.StaffRoleInfo> _roles = new();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
try { _roles = await DataService.GetStaffRolesAsync(); if (_roles.Any()) _role = _roles[0].Name; } catch { }
|
||||
}
|
||||
|
||||
private async Task HandleSubmit()
|
||||
{
|
||||
_message = "";
|
||||
if (string.IsNullOrWhiteSpace(_role)) { _message = "Vui lòng chọn vai trò."; _isSuccess = false; return; }
|
||||
_isSaving = true;
|
||||
try
|
||||
{
|
||||
// EN: Use first merchant or Guid.Empty / VI: Dùng merchant đầu tiên hoặc Guid.Empty
|
||||
var shops = await DataService.GetShopsAsync();
|
||||
var merchantId = Guid.Empty; // Will be set correctly when merchant context is available
|
||||
var ok = await DataService.CreateStaffAsync(new(merchantId, _empCode.Trim(), _phone.Trim(), _email.Trim(), _role));
|
||||
if (ok) { _message = "Đã thêm nhân viên thành công!"; _isSuccess = true; _empCode = ""; _phone = ""; _email = ""; }
|
||||
else { _message = "Không thể thêm nhân viên."; _isSuccess = false; }
|
||||
}
|
||||
catch (Exception ex) { _message = $"Lỗi: {ex.Message}"; _isSuccess = false; }
|
||||
finally { _isSaving = false; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,95 +1,84 @@
|
||||
@page "/admin/staff/schedule"
|
||||
@layout AdminLayout
|
||||
@inherits AdminBase
|
||||
|
||||
@*
|
||||
EN: Staff schedule — weekly grid view with shifts, drag-drop.
|
||||
VI: Lịch làm việc — hiển thị tuần dạng grid, ca làm, kéo thả.
|
||||
Design: pencil-design/src/pages/tPOS/admin/staff-schedule.pen
|
||||
*@
|
||||
@inject PosDataService DataService
|
||||
@using WebClientTpos.Client.Services
|
||||
|
||||
<PageTitle>Lịch làm việc — GoodGo Admin</PageTitle>
|
||||
|
||||
@* ═══ TOP BAR ═══ *@
|
||||
<div class="admin-topbar">
|
||||
<div class="admin-topbar__left">
|
||||
<h1 class="admin-topbar__title">Lịch làm việc</h1>
|
||||
<p class="admin-topbar__subtitle">Tuần 10/02 – 16/02/2025 • Coffee House Q1</p>
|
||||
<p class="admin-topbar__subtitle">@_schedules.Count ca làm việc</p>
|
||||
</div>
|
||||
<div class="admin-topbar__right">
|
||||
<button class="admin-btn-secondary">
|
||||
<i data-lucide="chevron-left"></i>
|
||||
</button>
|
||||
<span style="font-size:14px;font-weight:600;min-width:120px;text-align:center;">Tuần này</span>
|
||||
<button class="admin-btn-secondary">
|
||||
<i data-lucide="chevron-right"></i>
|
||||
</button>
|
||||
<button class="admin-btn-primary">
|
||||
<i data-lucide="plus"></i>
|
||||
<span>Thêm ca</span>
|
||||
</button>
|
||||
<select class="admin-select" style="min-width:160px;" @onchange="OnShopFilterChanged">
|
||||
<option value="">Tất cả cửa hàng</option>
|
||||
@foreach (var shop in _shops) { <option value="@shop.Id">@shop.Name</option> }
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ═══ SCHEDULE GRID ═══ *@
|
||||
<div class="admin-content" style="overflow-x:auto;">
|
||||
<div class="admin-schedule-grid">
|
||||
@* Header row *@
|
||||
<div class="admin-schedule-header">
|
||||
<div class="admin-schedule-header__cell" style="width:160px;">Nhân viên</div>
|
||||
@foreach (var day in _days)
|
||||
{
|
||||
<div class="admin-schedule-header__cell">
|
||||
<span style="font-size:11px;color:var(--admin-text-tertiary);">@day.DayName</span>
|
||||
<span style="font-size:14px;font-weight:600;">@day.Date</span>
|
||||
</div>
|
||||
}
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;gap:20px;">
|
||||
@if (IsLoading)
|
||||
{
|
||||
<div style="text-align:center;padding:48px;"><div class="spinner-small" style="width:32px;height:32px;margin:0 auto 16px;"></div>
|
||||
<p style="color:var(--admin-text-tertiary);font-size:14px;">Đang tải lịch...</p></div>
|
||||
}
|
||||
else if (!_schedules.Any())
|
||||
{
|
||||
<div style="text-align:center;padding:60px 20px;">
|
||||
<div style="width:80px;height:80px;border-radius:24px;background:rgba(59,130,246,0.1);display:flex;align-items:center;justify-content:center;margin:0 auto 20px;">
|
||||
<i data-lucide="calendar" style="width:36px;height:36px;color:#3B82F6;"></i>
|
||||
</div>
|
||||
<h2 style="font-size:20px;font-weight:700;margin:0 0 8px;color:var(--pos-text-primary, #FFF);">Chưa có lịch làm việc</h2>
|
||||
<p style="font-size:14px;color:var(--admin-text-tertiary);margin:0;">Thêm lịch cho nhân viên để quản lý ca làm việc</p>
|
||||
</div>
|
||||
|
||||
@* Staff rows *@
|
||||
@foreach (var staff in _scheduleStaff)
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach (var dayGroup in _schedules.GroupBy(s => s.DayOfWeek).OrderBy(g => g.Key))
|
||||
{
|
||||
<div class="admin-schedule-row">
|
||||
<div class="admin-schedule-row__name">
|
||||
<div class="admin-user-avatar" style="width:32px;height:32px;font-size:11px;background-color:@staff.Color;">
|
||||
@staff.Initials
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:13px;font-weight:600;">@staff.Name</div>
|
||||
<div style="font-size:11px;color:var(--admin-text-tertiary);">@staff.Role</div>
|
||||
</div>
|
||||
</div>
|
||||
@foreach (var shift in staff.Shifts)
|
||||
{
|
||||
<div class="admin-schedule-cell">
|
||||
@if (!string.IsNullOrEmpty(shift))
|
||||
{
|
||||
<div class="admin-shift-chip" style="background-color:@(staff.Color)20;color:@(staff.Color);border:1px solid @(staff.Color)40;">
|
||||
@shift
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__header"><h3 class="admin-panel__title">@DayName(dayGroup.Key)</h3></div>
|
||||
<div class="admin-panel__body" style="display:flex;flex-direction:column;gap:8px;">
|
||||
@foreach (var s in dayGroup)
|
||||
{
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;padding:8px 12px;border-radius:8px;background:var(--admin-bg-interactive);">
|
||||
<div style="display:flex;align-items:center;gap:12px;">
|
||||
<span style="font-weight:600;font-size:14px;">@(s.EmployeeCode ?? s.StaffId.ToString()[..6])</span>
|
||||
<span style="font-size:12px;color:var(--admin-text-tertiary);">@(s.Role ?? "")</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<span style="font-size:14px;font-weight:600;color:var(--admin-orange-primary);">@s.StartTime — @s.EndTime</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private record DayInfo(string DayName, string Date);
|
||||
private readonly DayInfo[] _days = new[]
|
||||
{
|
||||
new DayInfo("T2", "10"), new DayInfo("T3", "11"), new DayInfo("T4", "12"),
|
||||
new DayInfo("T5", "13"), new DayInfo("T6", "14"), new DayInfo("T7", "15"), new DayInfo("CN", "16")
|
||||
};
|
||||
private List<PosDataService.ScheduleInfo> _schedules = new();
|
||||
private List<PosDataService.ShopInfo> _shops = new();
|
||||
private Guid? _selectedShopId;
|
||||
|
||||
private record ScheduleStaff(string Name, string Initials, string Role, string Color, string[] Shifts);
|
||||
private readonly ScheduleStaff[] _scheduleStaff = new[]
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
new ScheduleStaff("Trần Minh", "TM", "Barista", "#FF5C00", new[] {"07:00-15:00", "07:00-15:00", "", "07:00-15:00", "07:00-15:00", "07:00-15:00", ""}),
|
||||
new ScheduleStaff("Lê Thảo", "LT", "Thu ngân", "#3B82F6", new[] {"15:00-22:00", "15:00-22:00", "15:00-22:00", "15:00-22:00", "", "15:00-22:00", "15:00-22:00"}),
|
||||
new ScheduleStaff("Nguyễn Hà", "NH", "Phục vụ", "#22C55E", new[] {"07:00-15:00", "", "07:00-15:00", "07:00-15:00", "07:00-15:00", "", "07:00-15:00"}),
|
||||
new ScheduleStaff("Phạm An", "PA", "Quản lý", "#8B5CF6", new[] {"08:00-17:00", "08:00-17:00", "08:00-17:00", "08:00-17:00", "08:00-17:00", "", ""}),
|
||||
new ScheduleStaff("Hoàng Lan", "HL", "Barista", "#EC4899", new[] {"", "", "15:00-22:00", "", "15:00-22:00", "07:00-15:00", "07:00-15:00"}),
|
||||
IsLoading = true;
|
||||
try { _shops = await DataService.GetShopsAsync(); _schedules = await DataService.GetStaffSchedulesAsync(); }
|
||||
catch { } finally { IsLoading = false; }
|
||||
}
|
||||
|
||||
private async Task OnShopFilterChanged(ChangeEventArgs e)
|
||||
{
|
||||
_selectedShopId = Guid.TryParse(e.Value?.ToString(), out var id) ? id : null;
|
||||
IsLoading = true;
|
||||
try { _schedules = await DataService.GetStaffSchedulesAsync(_selectedShopId); }
|
||||
catch { } finally { IsLoading = false; }
|
||||
}
|
||||
|
||||
private static string DayName(int day) => day switch {
|
||||
0 => "Chủ nhật", 1 => "Thứ 2", 2 => "Thứ 3", 3 => "Thứ 4", 4 => "Thứ 5", 5 => "Thứ 6", 6 => "Thứ 7", _ => $"Ngày {day}"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,101 +1,78 @@
|
||||
@page "/admin/system/devices"
|
||||
@layout AdminLayout
|
||||
@inherits AdminBase
|
||||
@inject PosDataService DataService
|
||||
@using WebClientTpos.Client.Services
|
||||
|
||||
@*
|
||||
EN: Device management — list of POS terminals, printers, scanners with status (online/offline), store assignment, last seen, battery.
|
||||
VI: Quản lý thiết bị — danh sách máy POS, máy in, máy quét với trạng thái, gán cửa hàng, lần cuối kết nối, pin.
|
||||
Design: pencil-design/src/pages/tPOS/admin/device-management.pen
|
||||
*@
|
||||
<PageTitle>Thiết bị — GoodGo Admin</PageTitle>
|
||||
|
||||
<PageTitle>Quản lý thiết bị — GoodGo Admin</PageTitle>
|
||||
|
||||
@* ═══ TOP BAR ═══ *@
|
||||
<div class="admin-topbar">
|
||||
<div class="admin-topbar__left">
|
||||
<h1 class="admin-topbar__title">Quản lý thiết bị</h1>
|
||||
<p class="admin-topbar__subtitle">@_devices.Count thiết bị • @_devices.Count(d => d.Status == "online") đang hoạt động</p>
|
||||
<p class="admin-topbar__subtitle">@_devices.Count thiết bị đã đăng ký</p>
|
||||
</div>
|
||||
<div class="admin-topbar__right">
|
||||
<div class="admin-search" style="width:200px;">
|
||||
<i data-lucide="search"></i>
|
||||
<input type="text" placeholder="Tìm thiết bị..." @bind="SearchQuery" />
|
||||
</div>
|
||||
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;gap:20px;">
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px;">
|
||||
<div class="admin-stat-card">
|
||||
<div class="admin-stat-card__icon" style="background:rgba(34,197,94,0.1);"><i data-lucide="monitor-smartphone" style="color:#22C55E;"></i></div>
|
||||
<div class="admin-stat-card__content"><span class="admin-stat-card__value">@_devices.Count(d => d.IsActive)</span><span class="admin-stat-card__label">Đang hoạt động</span></div>
|
||||
</div>
|
||||
<div class="admin-stat-card">
|
||||
<div class="admin-stat-card__icon" style="background:rgba(239,68,68,0.1);"><i data-lucide="monitor-off" style="color:#EF4444;"></i></div>
|
||||
<div class="admin-stat-card__content"><span class="admin-stat-card__value">@_devices.Count(d => !d.IsActive)</span><span class="admin-stat-card__label">Không hoạt động</span></div>
|
||||
</div>
|
||||
<button class="admin-btn-primary">
|
||||
<i data-lucide="plus"></i>
|
||||
<span>Thêm thiết bị</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ═══ TABS ═══ *@
|
||||
<div class="admin-tabs">
|
||||
<button class="admin-tab @(_activeTab == "all" ? "admin-tab--active" : "")" @onclick="@(() => _activeTab = "all")">
|
||||
Tất cả <span class="admin-tab__badge admin-tab__badge--active">@_devices.Count</span>
|
||||
</button>
|
||||
<button class="admin-tab @(_activeTab == "online" ? "admin-tab--active" : "")" @onclick="@(() => _activeTab = "online")">
|
||||
Online <span class="admin-tab__badge">@_devices.Count(d => d.Status == "online")</span>
|
||||
</button>
|
||||
<button class="admin-tab @(_activeTab == "offline" ? "admin-tab--active" : "")" @onclick="@(() => _activeTab = "offline")">
|
||||
Offline <span class="admin-tab__badge">@_devices.Count(d => d.Status == "offline")</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@* ═══ DEVICE GRID ═══ *@
|
||||
<div class="admin-content" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:16px;">
|
||||
@foreach (var dev in FilteredDevices)
|
||||
@if (IsLoading)
|
||||
{
|
||||
<div class="admin-staff-card">
|
||||
<div class="admin-staff-card__header">
|
||||
<div class="admin-kpi-card__icon" style="width:48px;height:48px;border-radius:12px;background-color:@(dev.IconColor)20;">
|
||||
<i data-lucide="@dev.Icon" style="color:@dev.IconColor;width:22px;height:22px;"></i>
|
||||
</div>
|
||||
<div class="admin-status-badge @(dev.Status == "online" ? "admin-status-badge--online" : "admin-status-badge--offline")">
|
||||
<span class="admin-status-badge__dot"></span>
|
||||
@(dev.Status == "online" ? "Online" : "Offline")
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;gap:2px;">
|
||||
<span style="font-size:16px;font-weight:600;">@dev.Name</span>
|
||||
<span style="font-size:12px;color:var(--admin-text-tertiary);">@dev.Type • @dev.Store</span>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;">
|
||||
<div class="admin-store-stat" style="flex:1;">
|
||||
<div class="admin-store-stat__value">@dev.LastSeen</div>
|
||||
<div class="admin-store-stat__label">Lần cuối</div>
|
||||
</div>
|
||||
<div class="admin-store-stat" style="flex:1;">
|
||||
<div class="admin-store-stat__value" style="color:@dev.BatteryColor;">@dev.Battery</div>
|
||||
<div class="admin-store-stat__label">Pin</div>
|
||||
</div>
|
||||
<div class="admin-store-stat" style="flex:1;">
|
||||
<div class="admin-store-stat__value">@dev.Serial</div>
|
||||
<div class="admin-store-stat__label">S/N</div>
|
||||
</div>
|
||||
<div style="text-align:center;padding:48px;"><div class="spinner-small" style="width:32px;height:32px;margin:0 auto 16px;"></div></div>
|
||||
}
|
||||
else if (!_devices.Any())
|
||||
{
|
||||
<div style="text-align:center;padding:60px 20px;">
|
||||
<div style="width:80px;height:80px;border-radius:24px;background:rgba(59,130,246,0.1);display:flex;align-items:center;justify-content:center;margin:0 auto 20px;"><i data-lucide="tablet-smartphone" style="width:36px;height:36px;color:#3B82F6;"></i></div>
|
||||
<h2 style="font-size:20px;font-weight:700;margin:0 0 8px;color:var(--pos-text-primary, #FFF);">Chưa có thiết bị</h2>
|
||||
<p style="font-size:14px;color:var(--admin-text-tertiary);margin:0;">Thiết bị sẽ tự động đăng ký khi nhân viên đăng nhập</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__body" style="padding:0;">
|
||||
<table class="admin-table" style="width:100%;"><thead><tr>
|
||||
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Thiết bị</th>
|
||||
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Nền tảng</th>
|
||||
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Nhân viên</th>
|
||||
<th style="padding:12px 16px;text-align:center;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Trạng thái</th>
|
||||
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Ngày đăng ký</th>
|
||||
</tr></thead><tbody>
|
||||
@foreach (var d in _devices)
|
||||
{
|
||||
<tr style="border-top:1px solid var(--admin-border-subtle);">
|
||||
<td style="padding:12px 16px;font-size:12px;font-family:monospace;">@(d.DeviceToken?[..Math.Min(12, d.DeviceToken.Length)] ?? "—")...</td>
|
||||
<td style="padding:12px 16px;font-weight:600;">@(d.Platform ?? "—")</td>
|
||||
<td style="padding:12px 16px;font-size:14px;">@(d.StaffCode ?? "—")</td>
|
||||
<td style="padding:12px 16px;text-align:center;">
|
||||
<span class="admin-status-badge @(d.IsActive ? "admin-status-badge--online" : "admin-status-badge--offline")" style="font-size:11px;padding:2px 10px;"><span class="admin-status-badge__dot" style="width:5px;height:5px;"></span>@(d.IsActive ? "Active" : "Inactive")</span>
|
||||
</td>
|
||||
<td style="padding:12px 16px;font-size:13px;color:var(--admin-text-tertiary);">@d.CreatedAt.ToString("dd/MM/yyyy")</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private string _activeTab = "all";
|
||||
private List<PosDataService.DeviceInfo> _devices = new();
|
||||
|
||||
private List<DeviceItem> FilteredDevices => _activeTab == "all"
|
||||
? _devices
|
||||
: _devices.Where(d => d.Status == _activeTab).ToList();
|
||||
|
||||
private record DeviceItem(string Name, string Type, string Icon, string IconColor, string Store,
|
||||
string Status, string LastSeen, string Battery, string BatteryColor, string Serial);
|
||||
|
||||
private readonly List<DeviceItem> _devices = new()
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
new("POS Terminal #01", "Máy POS", "monitor", "#3B82F6", "Coffee House Q1", "online", "Vừa xong", "85%", "#22C55E", "POS-001"),
|
||||
new("POS Terminal #02", "Máy POS", "monitor", "#3B82F6", "Nhà hàng Q3", "online", "Vừa xong", "92%", "#22C55E", "POS-002"),
|
||||
new("Máy in bếp", "Máy in", "printer", "#8B5CF6", "Coffee House Q1", "offline", "2 giờ", "—", "var(--admin-text-tertiary)", "PRT-001"),
|
||||
new("Máy in bill", "Máy in", "printer", "#8B5CF6", "Coffee House Q1", "online", "Vừa xong", "—", "var(--admin-text-tertiary)", "PRT-002"),
|
||||
new("Máy quét QR", "Scanner", "scan-line", "#22C55E", "Nhà hàng Q3", "online", "5 phút", "68%", "#F59E0B", "SCN-001"),
|
||||
new("Tablet Order #01", "Tablet", "tablet", "#F59E0B", "Nhà hàng Q3", "online", "Vừa xong", "45%", "#EF4444", "TAB-001"),
|
||||
new("POS Terminal #03", "Máy POS", "monitor", "#3B82F6", "Karaoke Star Q7", "offline", "3 ngày", "0%", "#EF4444", "POS-003"),
|
||||
new("Máy in bill #02", "Máy in", "printer", "#8B5CF6", "Nhà hàng Q3", "online", "Vừa xong", "—", "var(--admin-text-tertiary)", "PRT-003"),
|
||||
};
|
||||
IsLoading = true;
|
||||
try { _devices = await DataService.GetDevicesAsync(); }
|
||||
catch { } finally { IsLoading = false; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,111 +2,43 @@
|
||||
@layout AdminLayout
|
||||
@inherits AdminBase
|
||||
|
||||
@*
|
||||
EN: Integration hub — integration tiles (Payment, Delivery, Accounting, Social), connection status, configure button.
|
||||
VI: Trung tâm tích hợp — thẻ tích hợp (Thanh toán, Giao hàng, Kế toán, Mạng xã hội), trạng thái kết nối, nút cấu hình.
|
||||
Design: pencil-design/src/pages/tPOS/admin/integration-hub.pen
|
||||
*@
|
||||
|
||||
<PageTitle>Tích hợp — GoodGo Admin</PageTitle>
|
||||
|
||||
@* ═══ TOP BAR ═══ *@
|
||||
<div class="admin-topbar">
|
||||
<div class="admin-topbar__left">
|
||||
<h1 class="admin-topbar__title">Tích hợp</h1>
|
||||
<p class="admin-topbar__subtitle">Quản lý kết nối với dịch vụ bên ngoài</p>
|
||||
</div>
|
||||
<div class="admin-topbar__right">
|
||||
<div class="admin-search" style="width:200px;">
|
||||
<i data-lucide="search"></i>
|
||||
<input type="text" placeholder="Tìm tích hợp..." @bind="SearchQuery" />
|
||||
</div>
|
||||
<h1 class="admin-topbar__title">Tích hợp bên thứ 3</h1>
|
||||
<p class="admin-topbar__subtitle">Kết nối với các dịch vụ bên ngoài</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ═══ CONTENT ═══ *@
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;gap:24px;">
|
||||
|
||||
@foreach (var group in _integrationGroups)
|
||||
{
|
||||
<div style="display:flex;flex-direction:column;gap:12px;">
|
||||
<div style="font-size:12px;font-weight:700;color:var(--admin-text-tertiary);text-transform:uppercase;letter-spacing:0.05em;">
|
||||
@group.GroupName
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px;">
|
||||
@foreach (var item in group.Items)
|
||||
{
|
||||
<div class="admin-staff-card">
|
||||
<div class="admin-staff-card__header">
|
||||
<div class="admin-kpi-card__icon" style="width:48px;height:48px;border-radius:12px;background-color:@(item.Color)20;">
|
||||
<i data-lucide="@item.Icon" style="color:@item.Color;width:22px;height:22px;"></i>
|
||||
</div>
|
||||
<div class="admin-status-badge @(item.Connected ? "admin-status-badge--online" : "admin-status-badge--offline")">
|
||||
<span class="admin-status-badge__dot"></span>
|
||||
@(item.Connected ? "Đã kết nối" : "Chưa kết nối")
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;gap:2px;">
|
||||
<span style="font-size:16px;font-weight:600;">@item.Name</span>
|
||||
<span style="font-size:12px;color:var(--admin-text-tertiary);">@item.Description</span>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;margin-top:4px;">
|
||||
@if (item.Connected)
|
||||
{
|
||||
<button class="admin-btn-secondary" style="flex:1;justify-content:center;font-size:12px;padding:8px 12px;">
|
||||
<i data-lucide="settings" style="width:14px;height:14px;"></i>
|
||||
Cấu hình
|
||||
</button>
|
||||
<button class="admin-btn-secondary" style="font-size:12px;padding:8px 12px;color:var(--admin-danger);border-color:rgba(239,68,68,0.3);">
|
||||
<i data-lucide="unplug" style="width:14px;height:14px;"></i>
|
||||
Ngắt
|
||||
</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button class="admin-btn-primary" style="flex:1;justify-content:center;font-size:12px;padding:8px 12px;">
|
||||
<i data-lucide="plug" style="width:14px;height:14px;"></i>
|
||||
Kết nối
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;gap:20px;">
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px;">
|
||||
@foreach (var intg in _integrations)
|
||||
{
|
||||
<div class="admin-panel" style="cursor:pointer;">
|
||||
<div class="admin-panel__body" style="display:flex;align-items:center;gap:16px;">
|
||||
<div style="width:48px;height:48px;border-radius:12px;background:@intg.bg;display:flex;align-items:center;justify-content:center;">
|
||||
<i data-lucide="@intg.icon" style="color:@intg.color;width:24px;height:24px;"></i>
|
||||
</div>
|
||||
}
|
||||
<div style="flex:1;">
|
||||
<div style="font-weight:600;font-size:14px;">@intg.name</div>
|
||||
<div style="font-size:12px;color:var(--admin-text-tertiary);margin-top:2px;">@intg.desc</div>
|
||||
</div>
|
||||
<span class="admin-status-badge @(intg.connected ? "admin-status-badge--online" : "admin-status-badge--offline")" style="font-size:11px;padding:2px 10px;"><span class="admin-status-badge__dot" style="width:5px;height:5px;"></span>@(intg.connected ? "Đã kết nối" : "Chưa kết nối")</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private record IntegrationItem(string Name, string Description, string Icon, string Color, bool Connected);
|
||||
private record IntegrationGroup(string GroupName, IntegrationItem[] Items);
|
||||
|
||||
private readonly IntegrationGroup[] _integrationGroups = new[]
|
||||
private readonly (string name, string desc, string icon, string color, string bg, bool connected)[] _integrations = new[]
|
||||
{
|
||||
new IntegrationGroup("THANH TOÁN", new[]
|
||||
{
|
||||
new IntegrationItem("VNPay", "Thanh toán QR & thẻ ngân hàng", "qr-code", "#3B82F6", true),
|
||||
new IntegrationItem("MoMo", "Ví điện tử MoMo", "wallet", "#EC4899", true),
|
||||
new IntegrationItem("ZaloPay", "Thanh toán qua ZaloPay", "smartphone", "#3B82F6", false),
|
||||
new IntegrationItem("Visa/Mastercard", "Thanh toán thẻ quốc tế", "credit-card", "#8B5CF6", true),
|
||||
}),
|
||||
new IntegrationGroup("GIAO HÀNG", new[]
|
||||
{
|
||||
new IntegrationItem("GrabFood", "Đối tác giao đồ ăn", "bike", "#22C55E", true),
|
||||
new IntegrationItem("ShopeeFood", "Nền tảng đặt đồ ăn", "shopping-bag", "#FF5C00", true),
|
||||
new IntegrationItem("Baemin", "Dịch vụ giao hàng", "truck", "#06B6D4", false),
|
||||
}),
|
||||
new IntegrationGroup("KẾ TOÁN & QUẢN LÝ", new[]
|
||||
{
|
||||
new IntegrationItem("MISA", "Phần mềm kế toán", "calculator", "#F59E0B", true),
|
||||
new IntegrationItem("Google Sheets", "Xuất báo cáo tự động", "file-spreadsheet", "#22C55E", false),
|
||||
new IntegrationItem("Hóa đơn điện tử", "Phát hành hóa đơn GTGT", "file-text", "#3B82F6", true),
|
||||
}),
|
||||
new IntegrationGroup("MẠNG XÃ HỘI", new[]
|
||||
{
|
||||
new IntegrationItem("Facebook", "Fanpage & quảng cáo", "facebook", "#3B82F6", true),
|
||||
new IntegrationItem("Zalo OA", "Chăm sóc khách hàng", "message-circle", "#3B82F6", false),
|
||||
new IntegrationItem("Google Business", "Hiển thị trên Google Maps", "map-pin", "#EF4444", true),
|
||||
}),
|
||||
("Thanh toán VNPay", "Cổng thanh toán trực tuyến", "credit-card", "#3B82F6", "rgba(59,130,246,0.1)", false),
|
||||
("Thanh toán Momo", "Ví điện tử Momo", "wallet", "#A855F7", "rgba(168,85,247,0.1)", false),
|
||||
("GrabFood", "Đối tác giao đồ ăn", "bike", "#22C55E", "rgba(34,197,94,0.1)", false),
|
||||
("ShopeeFood", "Đối tác giao đồ ăn", "shopping-cart", "#EF4444", "rgba(239,68,68,0.1)", false),
|
||||
("Máy in hóa đơn", "Kết nối máy in POS", "printer", "#F59E0B", "rgba(245,158,11,0.1)", false),
|
||||
("Kế toán MISA", "Đồng bộ dữ liệu kế toán", "file-spreadsheet", "#FF5C00", "rgba(255,92,0,0.1)", false),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,150 +2,41 @@
|
||||
@layout AdminLayout
|
||||
@inherits AdminBase
|
||||
|
||||
@*
|
||||
EN: Notification center — notification settings, channel toggles (Email/SMS/Push/In-app), event types with channel preferences, test send.
|
||||
VI: Trung tâm thông báo — cài đặt thông báo, bật/tắt kênh (Email/SMS/Push/In-app), loại sự kiện theo kênh, gửi thử.
|
||||
Design: pencil-design/src/pages/tPOS/admin/notification-center.pen
|
||||
*@
|
||||
|
||||
<PageTitle>Thông báo — GoodGo Admin</PageTitle>
|
||||
|
||||
@* ═══ TOP BAR ═══ *@
|
||||
<div class="admin-topbar">
|
||||
<div class="admin-topbar__left">
|
||||
<h1 class="admin-topbar__title">Trung tâm thông báo</h1>
|
||||
<p class="admin-topbar__subtitle">Cấu hình kênh & loại thông báo</p>
|
||||
</div>
|
||||
<div class="admin-topbar__right">
|
||||
<button class="admin-btn-secondary">
|
||||
<i data-lucide="send"></i>
|
||||
<span>Gửi thử</span>
|
||||
</button>
|
||||
<button class="admin-btn-primary">
|
||||
<i data-lucide="save"></i>
|
||||
<span>Lưu cài đặt</span>
|
||||
</button>
|
||||
<p class="admin-topbar__subtitle">Quản lý thông báo đẩy</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ═══ CONTENT ═══ *@
|
||||
<div class="admin-content" style="display:flex;gap:24px;">
|
||||
|
||||
@* LEFT: Notification Types *@
|
||||
<div class="admin-panel" style="flex:1;">
|
||||
<div class="admin-panel__header">
|
||||
<h3 class="admin-panel__title">
|
||||
<i data-lucide="bell" style="color:var(--admin-orange-primary);"></i>
|
||||
Loại thông báo
|
||||
</h3>
|
||||
</div>
|
||||
<div class="admin-panel__body" style="padding:0;">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Sự kiện</th>
|
||||
<th style="text-align:center;">Email</th>
|
||||
<th style="text-align:center;">SMS</th>
|
||||
<th style="text-align:center;">Push</th>
|
||||
<th style="text-align:center;">In-app</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var evt in _eventTypes)
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
<div style="display:flex;flex-direction:column;gap:2px;">
|
||||
<span style="font-weight:500;">@evt.Name</span>
|
||||
<span style="font-size:11px;color:var(--admin-text-tertiary);">@evt.Desc</span>
|
||||
</div>
|
||||
</td>
|
||||
<td style="text-align:center;"><MudSwitch T="bool" Value="evt.Email" Color="Color.Primary" /></td>
|
||||
<td style="text-align:center;"><MudSwitch T="bool" Value="evt.Sms" Color="Color.Primary" /></td>
|
||||
<td style="text-align:center;"><MudSwitch T="bool" Value="evt.Push" Color="Color.Primary" /></td>
|
||||
<td style="text-align:center;"><MudSwitch T="bool" Value="evt.InApp" Color="Color.Primary" /></td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* RIGHT: Channel Config *@
|
||||
<div style="width:340px;display:flex;flex-direction:column;gap:20px;">
|
||||
@* Channel Status *@
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__header">
|
||||
<h3 class="admin-panel__title">
|
||||
<i data-lucide="radio" style="color:#8B5CF6;"></i>
|
||||
Kênh thông báo
|
||||
</h3>
|
||||
</div>
|
||||
<div class="admin-panel__body" style="display:flex;flex-direction:column;gap:8px;">
|
||||
@foreach (var ch in _channels)
|
||||
{
|
||||
<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;align-items:center;gap:10px;">
|
||||
<div class="admin-kpi-card__icon" style="width:36px;height:36px;border-radius:10px;background-color:@(ch.Color)20;">
|
||||
<i data-lucide="@ch.Icon" style="color:@ch.Color;width:18px;height:18px;"></i>
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;">
|
||||
<span style="font-size:13px;font-weight:600;">@ch.Name</span>
|
||||
<span style="font-size:11px;color:var(--admin-text-tertiary);">@ch.Provider</span>
|
||||
</div>
|
||||
</div>
|
||||
<MudSwitch T="bool" Value="ch.Enabled" Color="Color.Primary" />
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;gap:20px;">
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__header"><h3 class="admin-panel__title">Mẫu thông báo</h3></div>
|
||||
<div class="admin-panel__body" style="display:flex;flex-direction:column;gap:12px;">
|
||||
@foreach (var tmpl in _templates)
|
||||
{
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;padding:12px 16px;border-radius:8px;background:var(--admin-bg-interactive);">
|
||||
<div>
|
||||
<div style="font-weight:600;font-size:14px;">@tmpl.name</div>
|
||||
<div style="font-size:12px;color:var(--admin-text-tertiary);margin-top:2px;">@tmpl.desc</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Delivery Schedule *@
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__header">
|
||||
<h3 class="admin-panel__title">
|
||||
<i data-lucide="clock" style="color:#F59E0B;"></i>
|
||||
Lịch gửi
|
||||
</h3>
|
||||
</div>
|
||||
<div class="admin-panel__body" style="display:flex;flex-direction:column;gap:12px;">
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-form-label">Giờ bắt đầu</label>
|
||||
<input class="admin-form-input" type="text" value="08:00" readonly />
|
||||
<span class="admin-status-badge @(tmpl.active ? "admin-status-badge--online" : "admin-status-badge--offline")" style="font-size:11px;padding:2px 10px;"><span class="admin-status-badge__dot" style="width:5px;height:5px;"></span>@(tmpl.active ? "Bật" : "Tắt")</span>
|
||||
</div>
|
||||
<div class="admin-form-group">
|
||||
<label class="admin-form-label">Giờ kết thúc</label>
|
||||
<input class="admin-form-input" type="text" value="22:00" readonly />
|
||||
</div>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;padding:10px 12px;background:var(--admin-bg-interactive);border-radius:10px;">
|
||||
<span style="font-size:13px;font-weight:500;">Không làm phiền</span>
|
||||
<MudSwitch T="bool" Value="true" Color="Color.Primary" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private record EventType(string Name, string Desc, bool Email, bool Sms, bool Push, bool InApp);
|
||||
private readonly EventType[] _eventTypes = new[]
|
||||
private readonly (string name, string desc, bool active)[] _templates = new[]
|
||||
{
|
||||
new EventType("Đơn hàng mới", "Khi có đơn hàng mới được tạo", false, false, true, true),
|
||||
new EventType("Đơn hàng hủy", "Khi đơn hàng bị hủy", true, true, true, true),
|
||||
new EventType("Hàng sắp hết", "Tồn kho dưới mức tối thiểu", true, false, true, true),
|
||||
new EventType("Nhân viên clock-in", "Nhân viên bắt đầu ca làm việc", false, false, false, true),
|
||||
new EventType("Doanh thu bất thường", "Doanh thu thấp/cao bất thường", true, true, true, true),
|
||||
new EventType("Thiết bị offline", "Thiết bị mất kết nối", true, true, true, true),
|
||||
new EventType("Phản hồi khách hàng", "Khách hàng gửi đánh giá mới", false, false, true, true),
|
||||
new EventType("Bảo mật", "Đăng nhập lạ, đổi mật khẩu", true, true, true, true),
|
||||
};
|
||||
|
||||
private record ChannelDef(string Name, string Icon, string Color, string Provider, bool Enabled);
|
||||
private readonly ChannelDef[] _channels = new[]
|
||||
{
|
||||
new ChannelDef("Email", "mail", "#3B82F6", "SendGrid", true),
|
||||
new ChannelDef("SMS", "smartphone", "#22C55E", "Twilio", true),
|
||||
new ChannelDef("Push", "bell-ring", "#FF5C00", "Firebase FCM", true),
|
||||
new ChannelDef("In-app", "message-square", "#8B5CF6", "WebSocket", true),
|
||||
("Đơn hàng mới", "Thông báo khi có đơn hàng mới", true),
|
||||
("Hết hàng", "Cảnh báo khi sản phẩm hết hàng", true),
|
||||
("Khuyến mãi", "Thông báo chương trình khuyến mãi", false),
|
||||
("Đặt lịch", "Nhắc nhở lịch hẹn", true),
|
||||
("Chấm công", "Thông báo chấm công", false),
|
||||
("Hệ thống", "Thông báo bảo trì hệ thống", true),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -107,4 +107,72 @@ public class PosDataService
|
||||
var resp = await _http.PostAsJsonAsync("api/bff/staff", req, _jsonOptions);
|
||||
return resp.IsSuccessStatusCode;
|
||||
}
|
||||
|
||||
// ═══ STAFF ROLES & SCHEDULES ═══
|
||||
|
||||
public record StaffRoleInfo(int Id, string Name);
|
||||
public record ScheduleInfo(Guid Id, Guid StaffId, Guid ShopId, int DayOfWeek, string StartTime, string EndTime,
|
||||
string? EmployeeCode, string? Role, string? Phone);
|
||||
|
||||
public async Task<List<StaffRoleInfo>> GetStaffRolesAsync()
|
||||
=> await _http.GetFromJsonAsync<List<StaffRoleInfo>>("api/bff/staff/roles", _jsonOptions) ?? new();
|
||||
|
||||
public async Task<List<ScheduleInfo>> GetStaffSchedulesAsync(Guid? shopId = null)
|
||||
{
|
||||
var url = shopId.HasValue ? $"api/bff/staff/schedules?shopId={shopId}" : "api/bff/staff/schedules";
|
||||
return await _http.GetFromJsonAsync<List<ScheduleInfo>>(url, _jsonOptions) ?? new();
|
||||
}
|
||||
|
||||
// ═══ ORDERS ═══
|
||||
|
||||
public record OrderInfo(Guid Id, Guid ShopId, decimal TotalAmount, int StatusId, DateTime CreatedAt, string? Status);
|
||||
|
||||
public async Task<List<OrderInfo>> GetOrdersAsync(Guid? shopId = null)
|
||||
{
|
||||
var url = shopId.HasValue ? $"api/bff/orders?shopId={shopId}" : "api/bff/orders";
|
||||
return await _http.GetFromJsonAsync<List<OrderInfo>>(url, _jsonOptions) ?? new();
|
||||
}
|
||||
|
||||
// ═══ WALLETS / FINANCE ═══
|
||||
|
||||
public record WalletInfo(Guid Id, decimal Balance, string? Currency, Guid OwnerId, DateTime CreatedAt, decimal TotalIncome, decimal TotalExpense);
|
||||
public record WalletTxnInfo(Guid Id, Guid WalletId, decimal Amount, string? Description, DateTime CreatedAt, string? ItemName);
|
||||
|
||||
public async Task<List<WalletInfo>> GetWalletsAsync()
|
||||
=> await _http.GetFromJsonAsync<List<WalletInfo>>("api/bff/wallets", _jsonOptions) ?? new();
|
||||
|
||||
public async Task<List<WalletTxnInfo>> GetWalletTransactionsAsync(int limit = 50)
|
||||
=> await _http.GetFromJsonAsync<List<WalletTxnInfo>>($"api/bff/wallet/transactions?limit={limit}", _jsonOptions) ?? new();
|
||||
|
||||
// ═══ DEVICES ═══
|
||||
|
||||
public record DeviceInfo(Guid Id, string? DeviceToken, string? Platform, bool IsActive, DateTime CreatedAt, string? StaffCode);
|
||||
|
||||
public async Task<List<DeviceInfo>> GetDevicesAsync()
|
||||
=> await _http.GetFromJsonAsync<List<DeviceInfo>>("api/bff/devices", _jsonOptions) ?? new();
|
||||
|
||||
// ═══ PROMOTIONS ═══
|
||||
|
||||
public record PromotionInfo(Guid Id, string Name, string? Description, DateTime? StartDate, DateTime? EndDate,
|
||||
bool IsActive, string? DiscountType, decimal? DiscountValue, int VoucherCount, int RedemptionCount);
|
||||
|
||||
public async Task<List<PromotionInfo>> GetPromotionsAsync()
|
||||
=> await _http.GetFromJsonAsync<List<PromotionInfo>>("api/bff/promotions", _jsonOptions) ?? new();
|
||||
|
||||
// ═══ INVENTORY TRANSACTIONS ═══
|
||||
|
||||
public record InventoryTxnInfo(Guid Id, Guid InventoryItemId, int QuantityChange, string? Reason, DateTime CreatedAt, string? TransactionType);
|
||||
|
||||
public async Task<List<InventoryTxnInfo>> GetInventoryTransactionsAsync(Guid? shopId = null)
|
||||
{
|
||||
var url = shopId.HasValue ? $"api/bff/inventory/transactions?shopId={shopId}" : "api/bff/inventory/transactions";
|
||||
return await _http.GetFromJsonAsync<List<InventoryTxnInfo>>(url, _jsonOptions) ?? new();
|
||||
}
|
||||
|
||||
// ═══ MEMBERSHIP LEVELS ═══
|
||||
|
||||
public record LevelDefinitionInfo(Guid Id, int Level, string Name, int MinExp, int MaxExp, int MemberCount);
|
||||
|
||||
public async Task<List<LevelDefinitionInfo>> GetMembershipLevelsAsync()
|
||||
=> await _http.GetFromJsonAsync<List<LevelDefinitionInfo>>("api/bff/membership/levels", _jsonOptions) ?? new();
|
||||
}
|
||||
|
||||
@@ -307,6 +307,136 @@ public class BffDataController : ControllerBase
|
||||
return CreatedAtAction(nameof(GetStaff), new { }, new { id });
|
||||
}
|
||||
|
||||
// ═══ STAFF ROLES ═══
|
||||
[HttpGet("staff/roles")]
|
||||
public async Task<IActionResult> GetStaffRoles()
|
||||
{
|
||||
await using var conn = new NpgsqlConnection(ConnStr("merchant_service"));
|
||||
var roles = await conn.QueryAsync<dynamic>("SELECT id, name FROM staff_roles ORDER BY id");
|
||||
return Ok(roles);
|
||||
}
|
||||
|
||||
// ═══ STAFF SCHEDULES ═══
|
||||
[HttpGet("staff/schedules")]
|
||||
public async Task<IActionResult> GetStaffSchedules([FromQuery] Guid? shopId = null)
|
||||
{
|
||||
await using var conn = new NpgsqlConnection(ConnStr("booking_service"));
|
||||
var sql = @"SELECT id, staff_id, shop_id, day_of_week, start_time, end_time FROM staff_schedules";
|
||||
if (shopId.HasValue) sql += " WHERE shop_id = @ShopId";
|
||||
sql += " ORDER BY day_of_week, start_time";
|
||||
var schedules = await conn.QueryAsync<dynamic>(sql, new { ShopId = shopId });
|
||||
|
||||
// EN: Enrich with staff names / VI: Bổ sung tên nhân viên
|
||||
await using var mConn = new NpgsqlConnection(ConnStr("merchant_service"));
|
||||
var staffList = (await mConn.QueryAsync<dynamic>(
|
||||
"SELECT ms.id, ms.employee_code, ms.phone, sr.name as role FROM merchant_staff ms JOIN staff_roles sr ON ms.role_id = sr.id")).ToList();
|
||||
var staffMap = staffList.ToDictionary(s => (Guid)s.id, s => new { code = (string?)s.employee_code, role = (string)s.role, phone = (string?)s.phone });
|
||||
|
||||
var result = schedules.Select(s => new {
|
||||
s.id, s.staff_id, s.shop_id, s.day_of_week, s.start_time, s.end_time,
|
||||
employee_code = staffMap.TryGetValue((Guid)s.staff_id, out var info) ? info.code : null,
|
||||
role = info?.role, phone = info?.phone
|
||||
});
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
// ═══ ORDERS SUMMARY ═══
|
||||
[HttpGet("orders")]
|
||||
public async Task<IActionResult> GetOrders([FromQuery] Guid? shopId = null)
|
||||
{
|
||||
await using var conn = new NpgsqlConnection(ConnStr("order_service"));
|
||||
var sql = @"SELECT o.id, o.shop_id, o.total_amount, o.status_id, o.created_at,
|
||||
os.name as status
|
||||
FROM orders o
|
||||
JOIN order_statuses os ON o.status_id = os.id";
|
||||
if (shopId.HasValue) sql += " WHERE o.shop_id = @ShopId";
|
||||
sql += " ORDER BY o.created_at DESC LIMIT 200";
|
||||
var orders = await conn.QueryAsync<dynamic>(sql, new { ShopId = shopId });
|
||||
return Ok(orders);
|
||||
}
|
||||
|
||||
// ═══ WALLET/FINANCE ═══
|
||||
[HttpGet("wallets")]
|
||||
public async Task<IActionResult> GetWallets()
|
||||
{
|
||||
await using var conn = new NpgsqlConnection(ConnStr("wallet_service"));
|
||||
var wallets = await conn.QueryAsync<dynamic>(
|
||||
@"SELECT w.id, w.balance, w.currency, w.owner_id, w.created_at,
|
||||
(SELECT COALESCE(SUM(amount),0) FROM wallet_transactions wt WHERE wt.wallet_id = w.id AND wt.amount > 0) as total_income,
|
||||
(SELECT COALESCE(SUM(ABS(amount)),0) FROM wallet_transactions wt WHERE wt.wallet_id = w.id AND wt.amount < 0) as total_expense
|
||||
FROM wallets w ORDER BY w.created_at DESC");
|
||||
return Ok(wallets);
|
||||
}
|
||||
|
||||
[HttpGet("wallet/transactions")]
|
||||
public async Task<IActionResult> GetWalletTransactions([FromQuery] int limit = 50)
|
||||
{
|
||||
await using var conn = new NpgsqlConnection(ConnStr("wallet_service"));
|
||||
var txns = await conn.QueryAsync<dynamic>(
|
||||
@"SELECT wt.id, wt.wallet_id, wt.amount, wt.description, wt.created_at,
|
||||
wi.name as item_name
|
||||
FROM wallet_transactions wt
|
||||
LEFT JOIN wallet_items wi ON wt.reference_id = wi.id
|
||||
ORDER BY wt.created_at DESC LIMIT @Limit",
|
||||
new { Limit = limit });
|
||||
return Ok(txns);
|
||||
}
|
||||
|
||||
// ═══ DEVICES ═══
|
||||
[HttpGet("devices")]
|
||||
public async Task<IActionResult> GetDevices()
|
||||
{
|
||||
await using var conn = new NpgsqlConnection(ConnStr("merchant_service"));
|
||||
var devices = await conn.QueryAsync<dynamic>(
|
||||
@"SELECT dt.id, dt.device_token, dt.platform, dt.is_active, dt.created_at,
|
||||
ms.employee_code as staff_code
|
||||
FROM device_tokens dt
|
||||
LEFT JOIN merchant_staff ms ON dt.staff_id = ms.id
|
||||
ORDER BY dt.created_at DESC");
|
||||
return Ok(devices);
|
||||
}
|
||||
|
||||
// ═══ PROMOTIONS ═══
|
||||
[HttpGet("promotions")]
|
||||
public async Task<IActionResult> GetPromotions()
|
||||
{
|
||||
await using var conn = new NpgsqlConnection(ConnStr("promotion_service"));
|
||||
var promos = await conn.QueryAsync<dynamic>(
|
||||
@"SELECT c.id, c.name, c.description, c.start_date, c.end_date, c.is_active, c.discount_type, c.discount_value,
|
||||
(SELECT COUNT(*) FROM vouchers v WHERE v.campaign_id = c.id) as voucher_count,
|
||||
(SELECT COUNT(*) FROM redemptions r WHERE r.campaign_id = c.id) as redemption_count
|
||||
FROM campaigns c ORDER BY c.created_at DESC");
|
||||
return Ok(promos);
|
||||
}
|
||||
|
||||
// ═══ INVENTORY TRANSACTIONS ═══
|
||||
[HttpGet("inventory/transactions")]
|
||||
public async Task<IActionResult> GetInventoryTransactions([FromQuery] Guid? shopId = null)
|
||||
{
|
||||
await using var conn = new NpgsqlConnection(ConnStr("inventory_service"));
|
||||
var sql = @"SELECT it.id, it.inventory_item_id, it.quantity_change, it.reason, it.created_at,
|
||||
tt.name as transaction_type
|
||||
FROM inventory_transactions it
|
||||
JOIN transaction_types tt ON it.type_id = tt.id";
|
||||
if (shopId.HasValue)
|
||||
sql += @" JOIN inventory_items ii ON it.inventory_item_id = ii.id WHERE ii.shop_id = @ShopId";
|
||||
sql += " ORDER BY it.created_at DESC LIMIT 100";
|
||||
var txns = await conn.QueryAsync<dynamic>(sql, new { ShopId = shopId });
|
||||
return Ok(txns);
|
||||
}
|
||||
|
||||
// ═══ MEMBERSHIP LEVELS ═══
|
||||
[HttpGet("membership/levels")]
|
||||
public async Task<IActionResult> GetMembershipLevels()
|
||||
{
|
||||
await using var conn = new NpgsqlConnection(ConnStr("membership_service"));
|
||||
var levels = await conn.QueryAsync<dynamic>(
|
||||
@"SELECT ld.id, ld.level, ld.name, ld.min_exp, ld.max_exp,
|
||||
(SELECT COUNT(*) FROM members m WHERE m.current_level = ld.level) as member_count
|
||||
FROM level_definitions ld ORDER BY ld.level");
|
||||
return Ok(levels);
|
||||
}
|
||||
|
||||
// EN: Request DTOs / VI: DTO yêu cầu
|
||||
public record CreateProductRequest(Guid ShopId, string Name, string? Description, decimal Price, string? Type, string? Sku, string? ImageUrl);
|
||||
public record CreateStaffRequest(Guid MerchantId, string? EmployeeCode, string? Phone, string? Email, string? Role);
|
||||
|
||||
Reference in New Issue
Block a user