feat: Introduce new marketing module with dedicated layout, styling, and pages for AI content, live chat, and analytics.

This commit is contained in:
Ho Ngoc Hai
2026-02-12 22:28:39 +07:00
parent d1f8dc0782
commit ba57eb7e03
11 changed files with 2053 additions and 0 deletions

View File

@@ -0,0 +1,117 @@
@*
EN: Marketing layout — Industrial sidebar + TopBar + Content area.
VI: Layout Marketing — Sidebar công nghiệp + TopBar + Vùng nội dung.
Design: pencil-design/src/pages/tMarketing/desktop/
*@
@inherits LayoutComponentBase
@inject NavigationManager NavigationManager
@inject IJSRuntime JS
<MudThemeProvider IsDarkMode="true" Theme="_theme" />
<MudPopoverProvider />
<MudDialogProvider />
<MudSnackbarProvider />
<div class="mkt-layout">
@* ═══ SIDEBAR / THANH ĐIỀU HƯỚNG ═══ *@
<aside class="mkt-sidebar @(_sidebarOpen ? "mkt-sidebar--open" : "")">
@* Logo *@
<div class="mkt-sidebar__logo">
<div class="mkt-sidebar__logo-icon">M</div>
<span class="mkt-sidebar__logo-text">tMARKETING</span>
</div>
@* Navigation / Điều hướng *@
<nav class="mkt-sidebar__nav">
<NavLink href="/marketing" class="mkt-nav-item" Match="NavLinkMatch.All" ActiveClass="mkt-nav-item--active">
<span class="mkt-nav-num">01</span>
<span>HUB</span>
</NavLink>
<NavLink href="/marketing/livechat" class="mkt-nav-item" ActiveClass="mkt-nav-item--active">
<span class="mkt-nav-num">02</span>
<span>CHAT</span>
</NavLink>
<NavLink href="/marketing/customers" class="mkt-nav-item" ActiveClass="mkt-nav-item--active">
<span class="mkt-nav-num">03</span>
<span>CRM</span>
</NavLink>
<NavLink href="/marketing/content" class="mkt-nav-item" ActiveClass="mkt-nav-item--active">
<span class="mkt-nav-num">04</span>
<span>AI STUDIO</span>
</NavLink>
<NavLink href="/marketing/analytics" class="mkt-nav-item" ActiveClass="mkt-nav-item--active">
<span class="mkt-nav-num">05</span>
<span>ANALYTICS</span>
</NavLink>
<NavLink href="/marketing/chatbot" class="mkt-nav-item" ActiveClass="mkt-nav-item--active">
<span class="mkt-nav-num">06</span>
<span>CHATBOT</span>
</NavLink>
<NavLink href="/marketing/ai-chatbot" class="mkt-nav-item" ActiveClass="mkt-nav-item--active">
<span class="mkt-nav-num">07</span>
<span>AI BOT</span>
</NavLink>
</nav>
@* Stats card / Thẻ thống kê *@
<div class="mkt-sidebar__stats">
<div class="mkt-sidebar__stats-header">
<i data-lucide="bar-chart-2"></i>
<span>REACH TODAY</span>
</div>
<div class="mkt-sidebar__stats-value">45.2K</div>
</div>
@* User profile / Hồ sơ người dùng *@
<div class="mkt-sidebar__user">
<div class="mkt-user-avatar">AD</div>
<span class="mkt-user-name">ADMIN</span>
<button @onclick="Logout" title="Đăng xuất">
<i data-lucide="chevron-right"></i>
</button>
</div>
</aside>
@* Mobile overlay *@
@if (_sidebarOpen)
{
<div class="mkt-sidebar-overlay" @onclick="CloseSidebar"></div>
}
@* ═══ MAIN AREA / VÙNG NỘI DUNG ═══ *@
<main class="mkt-main">
@Body
</main>
</div>
@code {
private bool _sidebarOpen = false;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
// EN: Re-init Lucide icons after every render (Blazor navigation replaces DOM)
// VI: Khởi tạo lại Lucide icons sau mỗi lần render
try { await JS.InvokeVoidAsync("lucide.createIcons"); } catch { }
}
private void ToggleSidebar() => _sidebarOpen = !_sidebarOpen;
private void CloseSidebar() => _sidebarOpen = false;
private void Logout() => NavigationManager.NavigateTo("/login");
private MudTheme _theme = new()
{
PaletteDark = new PaletteDark()
{
Primary = "#FACC15",
PrimaryContrastText = "#18181B",
AppbarBackground = "#0F0F10",
AppbarText = "#FAFAFA",
Background = "#18181B",
Surface = "#0F0F10",
TextPrimary = "#FAFAFA",
TextSecondary = "#71717A",
ActionDefault = "#FAFAFA",
LinesDefault = "#27272A"
}
};
}

View File

@@ -0,0 +1,210 @@
@page "/marketing/ai-chatbot"
@layout MarketingLayout
@inherits MarketingBase
@*
EN: AI Chatbot — Configure and manage AI-powered chatbot scenarios.
VI: AI Chatbot — Cấu hình và quản lý kịch bản chatbot AI.
Design: pencil-design/src/pages/tMarketing/desktop/ai-chatbot.pen
*@
<PageTitle>AI Chatbot — tMarketing</PageTitle>
<div style="display:flex;flex:1;min-height:0;">
@* ═══ LEFT: Scenario List ═══ *@
<div style="width:260px;background:var(--mkt-bg-card);border-right:1px solid var(--mkt-border);display:flex;flex-direction:column;">
<div style="padding:16px;border-bottom:1px solid var(--mkt-border);display:flex;align-items:center;justify-content:space-between;">
<div style="display:flex;align-items:center;gap:8px;">
<i data-lucide="brain" style="width:16px;height:16px;color:var(--mkt-yellow-primary);"></i>
<span style="font-size:13px;font-weight:700;color:var(--mkt-text-primary);letter-spacing:0.5px;">SCENARIOS</span>
</div>
<div style="width:28px;height:28px;background:var(--mkt-yellow-primary);border-radius:6px;display:flex;align-items:center;justify-content:center;cursor:pointer;">
<i data-lucide="plus" style="width:14px;height:14px;color:var(--mkt-bg-card);"></i>
</div>
</div>
<div style="flex:1;overflow-y:auto;display:flex;flex-direction:column;gap:4px;padding:8px;">
@foreach (var scenario in _scenarios)
{
<div style="padding:12px;border-radius:8px;background:@(scenario.IsSelected ? "rgba(250,204,21,0.06)" : "transparent");cursor:pointer;display:flex;flex-direction:column;gap:6px;">
<div style="display:flex;justify-content:space-between;align-items:center;">
<span style="font-size:13px;font-weight:600;color:var(--mkt-text-primary);">@scenario.Name</span>
<span class="mkt-badge @scenario.BadgeClass">@scenario.Status</span>
</div>
<span style="font-size:11px;color:var(--mkt-text-secondary);">@scenario.Description</span>
<div style="display:flex;gap:8px;">
@if (!string.IsNullOrEmpty(scenario.Stat1))
{
<span style="font-size:10px;color:var(--mkt-text-tertiary);">@scenario.Stat1</span>
}
@if (!string.IsNullOrEmpty(scenario.Stat2))
{
<span style="font-size:10px;color:var(--mkt-text-tertiary);">@scenario.Stat2</span>
}
</div>
</div>
}
</div>
</div>
@* ═══ CENTER: Scenario Detail ═══ *@
<div style="flex:1;display:flex;flex-direction:column;background:var(--mkt-bg-page);">
@* Header *@
<div style="padding:16px 20px;background:var(--mkt-bg-card);border-bottom:1px solid var(--mkt-border);display:flex;align-items:center;justify-content:space-between;">
<div style="display:flex;flex-direction:column;gap:2px;">
<span style="font-size:15px;font-weight:700;color:var(--mkt-text-primary);">Product Consultant</span>
<span style="font-size:11px;color:var(--mkt-text-secondary);">Tư vấn sản phẩm tự động với AI</span>
</div>
<div style="display:flex;gap:8px;">
<button class="mkt-btn-ghost">
<i data-lucide="settings"></i>
<span>SETTINGS</span>
</button>
<button class="mkt-btn-primary">
<i data-lucide="play"></i>
<span>ACTIVATE</span>
</button>
</div>
</div>
@* Scenario content *@
<div style="flex:1;display:flex;gap:20px;padding:20px;overflow-y:auto;">
@* Training Data Panel *@
<div class="mkt-panel" style="flex:1;">
<div class="mkt-panel__header">
<h3 class="mkt-panel__title">
<i data-lucide="database"></i>
TRAINING DATA
</h3>
<button class="mkt-btn-ghost" style="padding:4px 8px;">
<i data-lucide="upload"></i>
<span style="font-size:10px;">UPLOAD</span>
</button>
</div>
<div class="mkt-panel__body" style="display:flex;flex-direction:column;gap:10px;">
@foreach (var ds in _dataSources)
{
<div style="display:flex;align-items:center;gap:10px;padding:10px;background:var(--mkt-bg-page);border-radius:6px;">
<i data-lucide="@ds.Icon" style="width:14px;height:14px;color:@ds.Color;"></i>
<div style="flex:1;display:flex;flex-direction:column;gap:1px;">
<span style="font-size:12px;font-weight:600;color:var(--mkt-text-primary);">@ds.Name</span>
<span style="font-size:10px;color:var(--mkt-text-tertiary);">@ds.Size • @ds.Updated</span>
</div>
<span class="mkt-badge mkt-badge--active">SYNCED</span>
</div>
}
</div>
</div>
@* Performance Metrics *@
<div class="mkt-panel" style="flex:1;">
<div class="mkt-panel__header">
<h3 class="mkt-panel__title">
<i data-lucide="bar-chart-2"></i>
PERFORMANCE
</h3>
</div>
<div class="mkt-panel__body" style="display:flex;flex-direction:column;gap:14px;">
@foreach (var metric in _metrics)
{
<div style="display:flex;flex-direction:column;gap:4px;">
<div style="display:flex;justify-content:space-between;">
<span style="font-size:11px;color:var(--mkt-text-secondary);">@metric.Label</span>
<span style="font-size:12px;font-weight:700;color:var(--mkt-text-primary);">@metric.Value</span>
</div>
<div style="width:100%;height:4px;background:var(--mkt-border);border-radius:2px;overflow:hidden;">
<div style="width:@metric.FillPct;height:100%;background:@metric.Color;border-radius:2px;"></div>
</div>
</div>
}
</div>
</div>
</div>
</div>
@* ═══ RIGHT: Test Chat ═══ *@
<div style="width:280px;background:var(--mkt-bg-card);border-left:1px solid var(--mkt-border);display:flex;flex-direction:column;">
<div style="padding:16px;border-bottom:1px solid var(--mkt-border);display:flex;align-items:center;gap:8px;">
<i data-lucide="message-square" style="width:16px;height:16px;color:var(--mkt-yellow-primary);"></i>
<span style="font-size:13px;font-weight:700;color:var(--mkt-text-primary);letter-spacing:0.5px;">TEST CHAT</span>
</div>
@* Chat messages *@
<div style="flex:1;padding:12px;display:flex;flex-direction:column;gap:10px;overflow-y:auto;">
@* User message *@
<div style="display:flex;justify-content:flex-end;">
<div style="padding:8px 12px;background:var(--mkt-yellow-dim);border-radius:8px;border-top-right-radius:2px;max-width:85%;">
<p style="margin:0;font-size:12px;color:var(--mkt-text-primary);">Cho hỏi về sản phẩm coffee đặc biệt</p>
</div>
</div>
@* AI response *@
<div style="display:flex;gap:6px;">
<div style="width:24px;height:24px;border-radius:50%;background:var(--mkt-yellow-dim);display:flex;align-items:center;justify-content:center;flex-shrink:0;">
<i data-lucide="bot" style="width:12px;height:12px;color:var(--mkt-yellow-primary);"></i>
</div>
<div style="padding:8px 12px;background:var(--mkt-bg-page);border-radius:8px;border-top-left-radius:2px;max-width:85%;">
<p style="margin:0;font-size:12px;color:var(--mkt-text-primary);">Xin chào! 🤖 Chúng tôi có các dòng coffee đặc biệt:</p>
<p style="margin:4px 0 0;font-size:11px;color:var(--mkt-text-secondary);">1. Specialty Blend — 89K<br>2. Single Origin — 120K<br>3. Cold Brew — 75K</p>
</div>
</div>
@* User message *@
<div style="display:flex;justify-content:flex-end;">
<div style="padding:8px 12px;background:var(--mkt-yellow-dim);border-radius:8px;border-top-right-radius:2px;max-width:85%;">
<p style="margin:0;font-size:12px;color:var(--mkt-text-primary);">Specialty Blend có gì đặc biệt?</p>
</div>
</div>
@* AI response *@
<div style="display:flex;gap:6px;">
<div style="width:24px;height:24px;border-radius:50%;background:var(--mkt-yellow-dim);display:flex;align-items:center;justify-content:center;flex-shrink:0;">
<i data-lucide="bot" style="width:12px;height:12px;color:var(--mkt-yellow-primary);"></i>
</div>
<div style="padding:8px 12px;background:var(--mkt-bg-page);border-radius:8px;border-top-left-radius:2px;max-width:85%;">
<p style="margin:0;font-size:12px;color:var(--mkt-text-primary);">Specialty Blend là hỗn hợp từ 3 nguồn Arabica chọn lọc, rang medium để giữ hương vị phong phú. Notes: chocolate, caramel, citrus 🍊</p>
</div>
</div>
</div>
@* Input *@
<div style="padding:12px;border-top:1px solid var(--mkt-border);display:flex;gap:8px;">
<div style="flex:1;padding:8px 12px;background:var(--mkt-bg-page);border-radius:6px;">
<input type="text" placeholder="Type to test..." style="width:100%;background:none;border:none;outline:none;color:var(--mkt-text-primary);font-size:12px;" />
</div>
<div style="width:36px;height:36px;background:var(--mkt-yellow-primary);border-radius:6px;display:flex;align-items:center;justify-content:center;cursor:pointer;">
<i data-lucide="send" style="width:14px;height:14px;color:var(--mkt-bg-card);"></i>
</div>
</div>
</div>
</div>
@code {
// EN: Demo scenario data / VI: Dữ liệu kịch bản demo
private record ScenarioInfo(string Name, string Status, string BadgeClass, string Description, string Stat1, string Stat2, bool IsSelected);
private record DataSource(string Name, string Icon, string Color, string Size, string Updated);
private record MetricInfo(string Label, string Value, string FillPct, string Color);
private readonly ScenarioInfo[] _scenarios = new[]
{
new ScenarioInfo("Product Consultant", "ACTIVE", "mkt-badge--active", "Tư vấn sản phẩm tự động với AI", "2.4K convos", "92% success", true),
new ScenarioInfo("FAQ Handler", "ACTIVE", "mkt-badge--active", "Trả lời câu hỏi thường gặp", "5.1K convos", "88% success", false),
new ScenarioInfo("Lead Generator", "TRAINING", "mkt-badge--training", "Thu thập thông tin khách hàng", "342 test runs", "", false),
new ScenarioInfo("Promo Campaign", "DRAFT", "mkt-badge--draft", "Giới thiệu khuyến mãi mới", "", "", false),
};
private readonly DataSource[] _dataSources = new[]
{
new DataSource("Product Catalog", "package", "#FACC15", "2.4 MB", "Updated 2h ago"),
new DataSource("FAQ Database", "help-circle", "#3B82F6", "892 KB", "Updated 1d ago"),
new DataSource("Price List", "tag", "#22C55E", "156 KB", "Updated 6h ago"),
};
private readonly MetricInfo[] _metrics = new[]
{
new MetricInfo("Success Rate", "92%", "92%", "#22C55E"),
new MetricInfo("Avg Response Time", "1.2s", "75%", "#3B82F6"),
new MetricInfo("Customer Satisfaction", "4.5/5", "90%", "#FACC15"),
new MetricInfo("Handoff Rate", "8%", "8%", "#EF4444"),
};
}

View File

@@ -0,0 +1,176 @@
@page "/marketing/content"
@layout MarketingLayout
@inherits MarketingBase
@*
EN: AI Content Studio — Generate and schedule content with Google Gemini AI.
VI: AI Content Studio — Tạo và lên lịch nội dung với Google Gemini AI.
Design: pencil-design/src/pages/tMarketing/desktop/ai-content-studio.pen
*@
<PageTitle>AI Content Studio — tMarketing</PageTitle>
@* ═══ TOP BAR ═══ *@
<div class="mkt-topbar">
<div class="mkt-topbar__left">
<h1 class="mkt-topbar__title">AI CONTENT STUDIO</h1>
<p class="mkt-topbar__subtitle">Generate content with Google Gemini AI</p>
</div>
<div class="mkt-topbar__right">
<button class="mkt-btn-ghost">
<i data-lucide="file-text"></i>
<span>DRAFTS</span>
</button>
<button class="mkt-btn-primary">
<i data-lucide="calendar"></i>
<span>SCHEDULE</span>
</button>
</div>
</div>
@* ═══ CONTENT ═══ *@
<div style="display:flex;gap:0;flex:1;min-height:0;padding:0 28px 28px;">
@* ── LEFT: Calendar + Upcoming ── *@
<div style="flex:1;display:flex;flex-direction:column;gap:20px;padding-right:20px;">
@* Content Calendar *@
<div class="mkt-panel">
<div class="mkt-panel__header">
<h3 class="mkt-panel__title">
<i data-lucide="calendar"></i>
CONTENT CALENDAR
</h3>
<span style="font-size:12px;font-weight:600;color:var(--mkt-text-primary);letter-spacing:0.5px;">FEBRUARY 2026</span>
</div>
<div class="mkt-panel__body">
@* Calendar grid *@
<div style="display:grid;grid-template-columns:repeat(7,1fr);gap:1px;text-align:center;">
@* Day headers *@
@foreach (var day in new[] { "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT" })
{
<div style="padding:8px 4px;font-size:10px;font-weight:600;color:var(--mkt-text-tertiary);letter-spacing:0.5px;">@day</div>
}
@* Calendar days *@
@for (int i = 0; i < 35; i++)
{
var dayNum = i - 0 + 1; // Starting from day 1
if (dayNum < 1 || dayNum > 28)
{
<div style="padding:8px 4px;"></div>
}
else
{
var hasEvent = _calendarEvents.ContainsKey(dayNum);
<div style="padding:6px 4px;border-radius:4px;cursor:pointer;@(hasEvent ? "background:var(--mkt-yellow-dim);" : "")">
<span style="font-size:12px;color:var(--mkt-text-primary);">@dayNum</span>
@if (hasEvent)
{
<div style="font-size:8px;color:var(--mkt-yellow-primary);margin-top:2px;">@_calendarEvents[dayNum]</div>
}
</div>
}
}
</div>
</div>
</div>
@* Upcoming Posts *@
<div class="mkt-panel" style="flex:1;">
<div class="mkt-panel__header">
<h3 class="mkt-panel__title">
<i data-lucide="clock"></i>
UPCOMING POSTS
</h3>
</div>
<div class="mkt-panel__body" style="display:flex;flex-direction:column;gap:12px;">
@foreach (var post in _upcomingPosts)
{
<div style="display:flex;gap:12px;align-items:center;padding:10px;border-radius:6px;background:var(--mkt-bg-page);">
<div style="display:flex;flex-direction:column;align-items:center;padding:6px 10px;border-radius:6px;background:var(--mkt-yellow-dim);">
<span style="font-size:18px;font-weight:700;color:var(--mkt-yellow-primary);">@post.Day</span>
<span style="font-size:10px;color:var(--mkt-text-tertiary);">@post.Month</span>
</div>
<div style="flex:1;display:flex;flex-direction:column;gap:2px;">
<span style="font-size:13px;font-weight:600;color:var(--mkt-text-primary);">@post.Title</span>
<span style="font-size:11px;color:var(--mkt-text-secondary);">@post.Channels</span>
</div>
<span style="font-size:11px;color:var(--mkt-text-tertiary);">@post.Time</span>
</div>
}
</div>
</div>
</div>
@* ── RIGHT: AI Generation Panel ── *@
<div style="width:380px;background:var(--mkt-bg-card);border-radius:10px;display:flex;flex-direction:column;">
@* Header *@
<div style="padding:16px 20px;border-bottom:1px solid var(--mkt-border);display:flex;align-items:center;gap:8px;">
<i data-lucide="sparkles" style="width:16px;height:16px;color:var(--mkt-yellow-primary);"></i>
<span style="font-size:13px;font-weight:700;color:var(--mkt-text-primary);letter-spacing:0.5px;">AI GENERATION</span>
</div>
@* Form *@
<div style="padding:16px 20px;display:flex;flex-direction:column;gap:16px;flex:1;">
<div>
<label style="font-size:11px;font-weight:600;color:var(--mkt-text-tertiary);letter-spacing:0.5px;display:block;margin-bottom:6px;">PROMPT</label>
<div style="padding:10px;background:var(--mkt-bg-page);border-radius:6px;min-height:80px;">
<textarea placeholder="Describe the content you want to create..." style="width:100%;background:none;border:none;outline:none;color:var(--mkt-text-primary);font-size:12px;font-family:var(--mkt-font);resize:none;min-height:60px;"></textarea>
</div>
</div>
<div>
<label style="font-size:11px;font-weight:600;color:var(--mkt-text-tertiary);letter-spacing:0.5px;display:block;margin-bottom:6px;">STYLE</label>
<div style="display:flex;gap:6px;">
<button class="mkt-btn-ghost" style="flex:1;justify-content:center;">Professional</button>
<button class="mkt-btn-ghost" style="flex:1;justify-content:center;background:var(--mkt-yellow-dim);color:var(--mkt-yellow-primary);border-color:var(--mkt-yellow-primary);">Casual</button>
<button class="mkt-btn-ghost" style="flex:1;justify-content:center;">Fun</button>
</div>
</div>
<div>
<label style="font-size:11px;font-weight:600;color:var(--mkt-text-tertiary);letter-spacing:0.5px;display:block;margin-bottom:6px;">OUTPUT</label>
<div style="display:flex;gap:6px;">
<button class="mkt-btn-ghost" style="flex:1;justify-content:center;background:var(--mkt-yellow-dim);color:var(--mkt-yellow-primary);border-color:var(--mkt-yellow-primary);">Post</button>
<button class="mkt-btn-ghost" style="flex:1;justify-content:center;">Story</button>
<button class="mkt-btn-ghost" style="flex:1;justify-content:center;">Ad</button>
</div>
</div>
@* Preview *@
<div style="flex:1;">
<label style="font-size:11px;font-weight:600;color:var(--mkt-text-tertiary);letter-spacing:0.5px;display:block;margin-bottom:6px;">PREVIEW</label>
<div style="padding:12px;background:var(--mkt-bg-page);border-radius:6px;min-height:100px;display:flex;flex-direction:column;gap:6px;">
<span style="font-size:12px;color:var(--mkt-text-secondary);font-style:italic;">AI-generated content will appear here...</span>
<span style="font-size:10px;color:var(--mkt-text-tertiary);">Powered by Google Gemini</span>
</div>
</div>
</div>
@* Generate button *@
<div style="padding:16px 20px;">
<button style="width:100%;display:flex;align-items:center;justify-content:center;gap:8px;padding:12px;background:#A855F7;border:none;border-radius:8px;font-size:13px;font-weight:700;color:white;cursor:pointer;letter-spacing:0.5px;">
<i data-lucide="wand-2"></i>
GENERATE WITH GEMINI
</button>
</div>
</div>
</div>
@code {
// EN: Demo calendar data / VI: Dữ liệu lịch demo
private record UpcomingPost(string Day, string Month, string Title, string Channels, string Time);
private readonly Dictionary<int, string> _calendarEvents = new()
{
{ 6, "FB Post" },
{ 8, "IG Reel" },
{ 10, "AI Banner" },
{ 14, "Valentine" },
};
private readonly UpcomingPost[] _upcomingPosts = new[]
{
new UpcomingPost("06", "FEB", "Spring Collection Announcement", "FB • IG", "10:00 AM"),
new UpcomingPost("10", "FEB", "AI Generated Banner", "GEMINI", "2:00 PM"),
new UpcomingPost("14", "FEB", "Valentine's Day Special Campaign", "FB • IG • ZALO", "9:00 AM"),
};
}

View File

@@ -0,0 +1,180 @@
@page "/marketing/analytics"
@layout MarketingLayout
@inherits MarketingBase
@*
EN: Analytics Dashboard — AI-powered insights and performance metrics.
VI: Analytics Dashboard — Phân tích thông minh và chỉ số hiệu suất.
Design: pencil-design/src/pages/tMarketing/desktop/analytics-dashboard.pen
*@
<PageTitle>Analytics — tMarketing</PageTitle>
@* ═══ TOP BAR ═══ *@
<div class="mkt-topbar">
<div class="mkt-topbar__left">
<h1 class="mkt-topbar__title">ANALYTICS</h1>
<p class="mkt-topbar__subtitle">AI-powered insights and performance metrics</p>
</div>
<div class="mkt-topbar__right">
<button class="mkt-btn-ghost">
<i data-lucide="calendar"></i>
<span>LAST 7 DAYS</span>
<i data-lucide="chevron-down" style="width:12px;height:12px;"></i>
</button>
<button class="mkt-btn-primary">
<i data-lucide="download"></i>
<span>EXPORT</span>
</button>
</div>
</div>
@* ═══ CONTENT ═══ *@
<div class="mkt-content">
@* ── KPI Row ── *@
<div class="mkt-kpi-row">
@foreach (var kpi in _kpis)
{
<div class="mkt-kpi-card">
<div class="mkt-kpi-card__header">
<div class="mkt-kpi-card__icon" style="background:@kpi.IconBg;">
<i data-lucide="@kpi.Icon" style="color:@kpi.IconColor;"></i>
</div>
<span class="mkt-kpi-card__label">@kpi.Label</span>
</div>
<span class="mkt-kpi-card__value">@kpi.Value</span>
<div class="mkt-kpi-card__badge mkt-kpi-card__badge--up">
<i data-lucide="arrow-up"></i>
@kpi.Change
</div>
</div>
}
</div>
@* ── ROW 2: Engagement Chart + Top Content ── *@
<div style="display:flex;gap:20px;">
@* Engagement Chart (placeholder) *@
<div class="mkt-panel" style="flex:1;">
<div class="mkt-panel__header">
<h3 class="mkt-panel__title">
<i data-lucide="trending-up"></i>
ENGAGEMENT OVERVIEW
</h3>
<div style="display:flex;gap:8px;">
<span style="font-size:11px;font-weight:600;color:var(--mkt-yellow-primary);cursor:pointer;">WEEK</span>
<span style="font-size:11px;color:var(--mkt-text-tertiary);cursor:pointer;">MONTH</span>
<span style="font-size:11px;color:var(--mkt-text-tertiary);cursor:pointer;">YEAR</span>
</div>
</div>
<div class="mkt-panel__body" style="height:200px;display:flex;align-items:flex-end;gap:12px;padding-top:20px;">
@* Mini chart bars *@
@foreach (var bar in _chartBars)
{
<div style="flex:1;display:flex;flex-direction:column;align-items:center;gap:4px;">
<div style="width:100%;height:@(bar.Height)px;background:linear-gradient(to top, var(--mkt-yellow-primary), rgba(250,204,21,0.3));border-radius:4px 4px 0 0;"></div>
<span style="font-size:10px;color:var(--mkt-text-tertiary);">@bar.Day</span>
</div>
}
</div>
</div>
@* Top Content *@
<div class="mkt-panel" style="width:320px;">
<div class="mkt-panel__header">
<h3 class="mkt-panel__title">
<i data-lucide="star"></i>
TOP CONTENT
</h3>
</div>
<div class="mkt-panel__body" style="display:flex;flex-direction:column;gap:12px;">
@foreach (var content in _topContent)
{
<div style="display:flex;justify-content:space-between;align-items:center;padding:8px 0;border-bottom:1px solid var(--mkt-border);">
<div style="display:flex;flex-direction:column;gap:2px;">
<span style="font-size:12px;font-weight:600;color:var(--mkt-text-primary);">@content.Title</span>
<span style="font-size:10px;color:var(--mkt-text-tertiary);">@content.Channel • @content.Time</span>
</div>
<div style="display:flex;flex-direction:column;align-items:flex-end;gap:2px;">
<span style="font-size:13px;font-weight:700;color:var(--mkt-yellow-primary);">@content.Reach</span>
<span style="font-size:10px;color:var(--mkt-success);">@content.Engagement</span>
</div>
</div>
}
</div>
</div>
</div>
@* ── ROW 3: Channel Performance ── *@
<div class="mkt-panel">
<div class="mkt-panel__header">
<h3 class="mkt-panel__title">
<i data-lucide="bar-chart-2"></i>
CHANNEL PERFORMANCE
</h3>
</div>
<div class="mkt-panel__body">
<div style="display:flex;gap:16px;">
@foreach (var channel in _channelPerf)
{
<div style="flex:1;display:flex;flex-direction:column;gap:8px;padding:16px;background:var(--mkt-bg-page);border-radius:8px;">
<div style="display:flex;align-items:center;gap:8px;">
<div style="width:10px;height:10px;border-radius:50%;background:@channel.Color;"></div>
<span style="font-size:12px;font-weight:600;color:var(--mkt-text-primary);">@channel.Name</span>
</div>
<div style="display:flex;justify-content:space-between;">
<div>
<div style="font-size:10px;color:var(--mkt-text-tertiary);">Reach</div>
<div style="font-size:16px;font-weight:700;color:var(--mkt-text-primary);">@channel.Reach</div>
</div>
<div>
<div style="font-size:10px;color:var(--mkt-text-tertiary);">Engagement</div>
<div style="font-size:16px;font-weight:700;color:var(--mkt-text-primary);">@channel.Engagement</div>
</div>
</div>
@* Progress bar *@
<div style="width:100%;height:4px;background:var(--mkt-border);border-radius:2px;overflow:hidden;">
<div style="width:@channel.FillPct;height:100%;background:@channel.Color;border-radius:2px;"></div>
</div>
</div>
}
</div>
</div>
</div>
</div>
@code {
// EN: Demo analytics data / VI: Dữ liệu phân tích demo
private record KpiData(string Label, string Value, string Change, string Icon, string IconColor, string IconBg);
private record ChartBar(string Day, int Height);
private record TopContentItem(string Title, string Channel, string Time, string Reach, string Engagement);
private record ChannelPerf(string Name, string Color, string Reach, string Engagement, string FillPct);
private readonly KpiData[] _kpis = new[]
{
new KpiData("TOTAL REACH", "245.8K", "+18.5%", "eye", "#22C55E", "rgba(34,197,94,0.12)"),
new KpiData("ENGAGEMENT", "14.2%", "+5.3%", "heart", "#EC4899", "rgba(236,72,153,0.12)"),
new KpiData("CONVERSIONS", "1,842", "+22.1%", "target", "#3B82F6", "rgba(59,130,246,0.12)"),
new KpiData("ROI", "324%", "+45.2%", "trending-up", "#FACC15", "rgba(250,204,21,0.12)"),
};
private readonly ChartBar[] _chartBars = new[]
{
new ChartBar("Mon", 80), new ChartBar("Tue", 120), new ChartBar("Wed", 95),
new ChartBar("Thu", 150), new ChartBar("Fri", 130), new ChartBar("Sat", 170), new ChartBar("Sun", 110),
};
private readonly TopContentItem[] _topContent = new[]
{
new TopContentItem("Spring Collection Ad", "FB", "2 days ago", "45.2K", "+24%"),
new TopContentItem("Behind the Scenes", "IG", "3 days ago", "32.1K", "+18%"),
new TopContentItem("Promo Announcement", "ZALO", "5 days ago", "28.4K", "+15%"),
};
private readonly ChannelPerf[] _channelPerf = new[]
{
new ChannelPerf("Facebook", "#3B82F6", "125K", "12.4%", "75%"),
new ChannelPerf("Instagram", "#E1306C", "85K", "18.2%", "65%"),
new ChannelPerf("Zalo OA", "#0068FF", "45K", "22.1%", "55%"),
new ChannelPerf("WhatsApp", "#25D366", "15K", "8.5%", "35%"),
};
}

View File

@@ -0,0 +1,145 @@
@page "/marketing/chatbot"
@layout MarketingLayout
@inherits MarketingBase
@*
EN: Chatbot Automation — Build automation flows for customer engagement.
VI: Chatbot Automation — Xây dựng luồng tự động hóa tương tác khách hàng.
Design: pencil-design/src/pages/tMarketing/desktop/chatbot-automation.pen
*@
<PageTitle>Chatbot Automation — tMarketing</PageTitle>
<div style="display:flex;flex:1;min-height:0;">
@* ═══ LEFT: Flow List ═══ *@
<div style="width:280px;background:var(--mkt-bg-card);border-right:1px solid var(--mkt-border);display:flex;flex-direction:column;">
<div style="padding:16px;border-bottom:1px solid var(--mkt-border);display:flex;align-items:center;justify-content:space-between;">
<div style="display:flex;align-items:center;gap:8px;">
<i data-lucide="git-branch" style="width:16px;height:16px;color:var(--mkt-yellow-primary);"></i>
<span style="font-size:13px;font-weight:700;color:var(--mkt-text-primary);letter-spacing:0.5px;">FLOWS</span>
</div>
<div style="width:28px;height:28px;background:var(--mkt-yellow-primary);border-radius:6px;display:flex;align-items:center;justify-content:center;cursor:pointer;">
<i data-lucide="plus" style="width:14px;height:14px;color:var(--mkt-bg-card);"></i>
</div>
</div>
<div style="flex:1;overflow-y:auto;display:flex;flex-direction:column;gap:4px;padding:8px;">
@foreach (var flow in _flows)
{
<div style="padding:12px;border-radius:8px;background:@(flow.IsActive ? "rgba(250,204,21,0.06)" : "transparent");cursor:pointer;display:flex;flex-direction:column;gap:6px;">
<div style="display:flex;justify-content:space-between;align-items:center;">
<span style="font-size:13px;font-weight:600;color:var(--mkt-text-primary);">@flow.Name</span>
</div>
<span style="font-size:11px;color:var(--mkt-text-secondary);">@flow.Description</span>
<div style="display:flex;gap:8px;">
<span style="font-size:10px;color:var(--mkt-text-tertiary);">@flow.Stat1</span>
<span style="font-size:10px;color:var(--mkt-text-tertiary);">@flow.Stat2</span>
</div>
</div>
}
</div>
</div>
@* ═══ CENTER: Flow Builder Canvas ═══ *@
<div style="flex:1;display:flex;flex-direction:column;background:var(--mkt-bg-page);">
@* Header *@
<div style="padding:16px 20px;background:var(--mkt-bg-card);border-bottom:1px solid var(--mkt-border);display:flex;align-items:center;justify-content:space-between;">
<div style="display:flex;flex-direction:column;gap:2px;">
<span style="font-size:15px;font-weight:700;color:var(--mkt-text-primary);">Welcome Message</span>
<span style="font-size:11px;color:var(--mkt-text-secondary);">Greet new customers automatically</span>
</div>
<div style="display:flex;gap:8px;">
<button class="mkt-btn-ghost">
<i data-lucide="play"></i>
<span>TEST</span>
</button>
<button class="mkt-btn-primary">
<i data-lucide="check"></i>
<span>PUBLISH</span>
</button>
</div>
</div>
@* Flow canvas *@
<div style="flex:1;display:flex;align-items:center;justify-content:center;gap:0;padding:40px;">
@* Trigger Node *@
<div style="width:280px;padding:16px;background:rgba(34,197,94,0.08);border:1px solid rgba(34,197,94,0.2);border-radius:10px;display:flex;flex-direction:column;gap:8px;">
<div style="display:flex;align-items:center;gap:8px;">
<i data-lucide="zap" style="width:14px;height:14px;color:var(--mkt-success);"></i>
<span style="font-size:12px;font-weight:700;color:var(--mkt-success);letter-spacing:0.5px;">TRIGGER</span>
</div>
<span style="font-size:13px;font-weight:600;color:var(--mkt-text-primary);">New Customer Message</span>
<span style="font-size:11px;color:var(--mkt-text-secondary);">When customer sends first message</span>
</div>
@* Connector *@
<div style="width:40px;height:2px;background:var(--mkt-border);"></div>
@* Action Node *@
<div style="width:280px;padding:16px;background:rgba(59,130,246,0.08);border:1px solid rgba(59,130,246,0.2);border-radius:10px;display:flex;flex-direction:column;gap:8px;">
<div style="display:flex;align-items:center;gap:8px;">
<i data-lucide="message-circle" style="width:14px;height:14px;color:var(--mkt-info);"></i>
<span style="font-size:12px;font-weight:700;color:var(--mkt-info);letter-spacing:0.5px;">ACTION</span>
</div>
<span style="font-size:13px;font-weight:600;color:var(--mkt-text-primary);">Send Welcome Reply</span>
<span style="font-size:11px;color:var(--mkt-text-secondary);">Auto-reply with greeting template</span>
</div>
@* Connector *@
<div style="width:40px;height:2px;background:var(--mkt-border);"></div>
@* Condition Node *@
<div style="width:280px;padding:16px;background:rgba(250,204,21,0.08);border:1px solid rgba(250,204,21,0.2);border-radius:10px;display:flex;flex-direction:column;gap:8px;">
<div style="display:flex;align-items:center;gap:8px;">
<i data-lucide="git-branch" style="width:14px;height:14px;color:var(--mkt-yellow-primary);"></i>
<span style="font-size:12px;font-weight:700;color:var(--mkt-yellow-primary);letter-spacing:0.5px;">CONDITION</span>
</div>
<span style="font-size:13px;font-weight:600;color:var(--mkt-text-primary);">Check Customer Type</span>
<span style="font-size:11px;color:var(--mkt-text-secondary);">Route VIP vs standard customers</span>
</div>
</div>
</div>
@* ═══ RIGHT: Node Palette ═══ *@
<div style="width:260px;background:var(--mkt-bg-card);border-left:1px solid var(--mkt-border);display:flex;flex-direction:column;">
<div style="padding:16px;border-bottom:1px solid var(--mkt-border);display:flex;align-items:center;gap:8px;">
<i data-lucide="box" style="width:16px;height:16px;color:var(--mkt-yellow-primary);"></i>
<span style="font-size:13px;font-weight:700;color:var(--mkt-text-primary);letter-spacing:0.5px;">ADD NODE</span>
</div>
<div style="flex:1;padding:8px;display:flex;flex-direction:column;gap:6px;">
@foreach (var node in _nodeTypes)
{
<div style="display:flex;align-items:center;gap:10px;padding:10px 12px;background:var(--mkt-bg-page);border-radius:6px;cursor:pointer;">
<i data-lucide="@node.Icon" style="width:14px;height:14px;color:@node.Color;"></i>
<div style="display:flex;flex-direction:column;gap:1px;">
<span style="font-size:12px;font-weight:600;color:var(--mkt-text-primary);">@node.Name</span>
<span style="font-size:10px;color:var(--mkt-text-tertiary);">@node.Desc</span>
</div>
</div>
}
</div>
</div>
</div>
@code {
// EN: Demo flow data / VI: Dữ liệu luồng demo
private record FlowInfo(string Name, string Description, string Stat1, string Stat2, bool IsActive);
private record NodeType(string Name, string Desc, string Icon, string Color);
private readonly FlowInfo[] _flows = new[]
{
new FlowInfo("Welcome Message", "Greet new customers", "1.2K sent", "85% open", true),
new FlowInfo("Product Inquiry", "Handle product questions", "890 sent", "72% open", false),
new FlowInfo("Abandoned Cart", "Recover lost sales", "456 sent", "28% conv", false),
new FlowInfo("Order Update", "Shipping notifications", "Draft", "", false),
};
private readonly NodeType[] _nodeTypes = new[]
{
new NodeType("Trigger", "Start automation", "zap", "#22C55E"),
new NodeType("Message", "Send a message", "message-circle", "#3B82F6"),
new NodeType("Condition", "Branch logic", "git-branch", "#FACC15"),
new NodeType("Delay", "Wait before next", "clock", "#8B5CF6"),
new NodeType("AI Response", "Gemini auto-reply", "sparkles", "#EC4899"),
};
}

View File

@@ -0,0 +1,173 @@
@page "/marketing/customers"
@layout MarketingLayout
@inherits MarketingBase
@*
EN: Customer CRM — Manage customer profiles for re-marketing campaigns.
VI: Customer CRM — Quản lý hồ sơ khách hàng cho chiến dịch re-marketing.
Design: pencil-design/src/pages/tMarketing/desktop/customer-crm.pen
*@
<PageTitle>Customers — tMarketing</PageTitle>
@* ═══ TOP BAR ═══ *@
<div class="mkt-topbar">
<div class="mkt-topbar__left">
<h1 class="mkt-topbar__title">CUSTOMERS</h1>
<p class="mkt-topbar__subtitle">Manage customer profiles for re-marketing campaigns</p>
</div>
<div class="mkt-topbar__right">
<div class="mkt-search">
<i data-lucide="search"></i>
<input type="text" placeholder="SEARCH" @bind="SearchQuery" />
</div>
<button class="mkt-btn-ghost">
<i data-lucide="download"></i>
<span>EXPORT</span>
</button>
<button class="mkt-btn-primary">
<i data-lucide="user-plus"></i>
<span>ADD</span>
</button>
</div>
</div>
@* ═══ CONTENT ═══ *@
<div class="mkt-content">
@* ── KPI Row ── *@
<div class="mkt-kpi-row">
<div class="mkt-kpi-card">
<span class="mkt-kpi-card__label">TOTAL CUSTOMERS</span>
<div style="display:flex;align-items:center;gap:8px;">
<span class="mkt-kpi-card__value">12,456</span>
<div class="mkt-kpi-card__badge mkt-kpi-card__badge--up">
<i data-lucide="arrow-up"></i>
+8.2%
</div>
</div>
</div>
<div class="mkt-kpi-card">
<span class="mkt-kpi-card__label">ACTIVE</span>
<div style="display:flex;align-items:center;gap:8px;">
<span class="mkt-kpi-card__value">4,892</span>
<div class="mkt-kpi-card__badge mkt-kpi-card__badge--up">
<i data-lucide="arrow-up"></i>
+12.1%
</div>
</div>
</div>
<div class="mkt-kpi-card">
<span class="mkt-kpi-card__label">NEW TODAY</span>
<div style="display:flex;align-items:center;gap:8px;">
<span class="mkt-kpi-card__value">+342</span>
<span class="mkt-badge mkt-badge--active">NEW</span>
</div>
</div>
<div class="mkt-kpi-card">
<span class="mkt-kpi-card__label">SEGMENTS</span>
<div style="display:flex;align-items:center;gap:8px;">
<span class="mkt-kpi-card__value">8</span>
<span class="mkt-badge mkt-badge--draft">GROUPS</span>
</div>
</div>
</div>
@* ── Customer Table ── *@
<div class="mkt-panel" style="flex:1;">
<div class="mkt-panel__header">
<h3 class="mkt-panel__title">
<i data-lucide="users"></i>
CUSTOMER LIST
</h3>
<div style="display:flex;gap:8px;">
<button class="mkt-btn-ghost" style="padding:4px 8px;">
<span style="font-size:11px;">ALL</span>
</button>
<button class="mkt-btn-ghost" style="padding:4px 8px;">
<span style="font-size:11px;">VIP</span>
</button>
<button class="mkt-btn-ghost" style="padding:4px 8px;">
<span style="font-size:11px;">NEW</span>
</button>
</div>
</div>
<div class="mkt-panel__body" style="padding:0;">
<table class="mkt-table">
<thead>
<tr>
<th style="width:32px;"></th>
<th>NAME</th>
<th>PHONE</th>
<th>CHANNEL</th>
<th>SCORE</th>
<th style="width:80px;">ACTIONS</th>
</tr>
</thead>
<tbody>
@foreach (var customer in _customers)
{
<tr>
<td>
<div style="width:32px;height:32px;border-radius:50%;background:@customer.AvatarBg;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;color:var(--mkt-text-primary);">@customer.Initials</div>
</td>
<td style="font-weight:600;">@customer.Name</td>
<td style="color:var(--mkt-text-secondary);font-size:12px;">@customer.Phone</td>
<td><span class="mkt-badge mkt-badge--info">@customer.Channel</span></td>
<td style="color:var(--mkt-yellow-primary);">@customer.Score</td>
<td>
<div style="display:flex;gap:4px;">
<button style="width:28px;height:28px;border-radius:4px;background:var(--mkt-bg-page);border:none;display:flex;align-items:center;justify-content:center;cursor:pointer;">
<i data-lucide="message-circle" style="width:12px;height:12px;color:var(--mkt-text-secondary);"></i>
</button>
<button style="width:28px;height:28px;border-radius:4px;background:var(--mkt-bg-page);border:none;display:flex;align-items:center;justify-content:center;cursor:pointer;">
<i data-lucide="more-horizontal" style="width:12px;height:12px;color:var(--mkt-text-secondary);"></i>
</button>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
@* ── Bottom Actions + Pagination ── *@
<div style="display:flex;justify-content:space-between;align-items:center;">
<div style="display:flex;gap:8px;">
<button class="mkt-btn-ghost">
<i data-lucide="mail"></i>
<span>EMAIL</span>
</button>
<button class="mkt-btn-ghost">
<i data-lucide="smartphone"></i>
<span>SMS</span>
</button>
<button class="mkt-btn-ghost">
<i data-lucide="target"></i>
<span>CAMPAIGN</span>
</button>
<button class="mkt-btn-primary">
<i data-lucide="download"></i>
<span>EXPORT ALL</span>
</button>
</div>
<div style="display:flex;gap:4px;">
<div style="width:32px;height:32px;border-radius:6px;background:var(--mkt-yellow-primary);display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;color:var(--mkt-bg-card);cursor:pointer;">01</div>
<div style="width:32px;height:32px;border-radius:6px;background:var(--mkt-bg-card);display:flex;align-items:center;justify-content:center;font-size:12px;color:var(--mkt-text-secondary);cursor:pointer;">02</div>
<div style="width:32px;height:32px;border-radius:6px;background:var(--mkt-bg-card);display:flex;align-items:center;justify-content:center;font-size:12px;color:var(--mkt-text-secondary);cursor:pointer;">03</div>
</div>
</div>
</div>
@code {
// EN: Demo customer data / VI: Dữ liệu khách hàng demo
private record CustomerInfo(string Name, string Initials, string Phone, string Channel, string Score, string AvatarBg);
private readonly CustomerInfo[] _customers = new[]
{
new CustomerInfo("Nguyễn Văn A", "NA", "0912 xxx xxx", "ZALO", "★★★★☆", "#3B82F6"),
new CustomerInfo("Trần Thị B", "TB", "0987 xxx xxx", "FB", "★★★☆☆", "#8B5CF6"),
new CustomerInfo("Lê Văn C", "LC", "0909 xxx xxx", "IG", "★★★★★", "#EC4899"),
new CustomerInfo("Phạm Thị D", "PD", "0888 xxx xxx", "WA", "★★☆☆☆", "#22C55E"),
};
}

View File

@@ -0,0 +1,192 @@
@page "/marketing/livechat"
@layout MarketingLayout
@inherits MarketingBase
@*
EN: Livechat Console — Real-time customer chat with 3-panel layout.
VI: Livechat Console — Chat khách hàng thời gian thực với bố cục 3 panel.
Design: pencil-design/src/pages/tMarketing/desktop/livechat-console.pen
*@
<PageTitle>Livechat — tMarketing</PageTitle>
<div class="mkt-chat-layout" style="height:calc(100vh - 0px);">
@* ═══ LEFT: Conversation List ═══ *@
<div class="mkt-chat-list" style="background:var(--mkt-bg-card);">
@* Header *@
<div style="padding:16px;border-bottom:1px solid var(--mkt-border);display:flex;align-items:center;justify-content:space-between;">
<span style="font-size:13px;font-weight:700;color:var(--mkt-text-primary);letter-spacing:0.5px;">CONVERSATIONS</span>
<div style="display:flex;gap:8px;">
<span style="font-size:11px;font-weight:600;color:var(--mkt-yellow-primary);cursor:pointer;">ALL</span>
<span style="font-size:11px;color:var(--mkt-text-tertiary);cursor:pointer;">UNREAD</span>
</div>
</div>
@* Conversation items *@
@foreach (var conv in _conversations)
{
<div class="mkt-chat-item @(conv.IsActive ? "mkt-chat-item--active" : "")" @onclick="@(() => SelectConversation(conv))">
<div style="width:36px;height:36px;border-radius:50%;background:@conv.AvatarBg;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;color:var(--mkt-text-primary);flex-shrink:0;">
@conv.Initials
</div>
<div style="flex:1;min-width:0;display:flex;flex-direction:column;gap:2px;">
<div style="display:flex;justify-content:space-between;align-items:center;">
<span style="font-size:13px;font-weight:600;color:var(--mkt-text-primary);">@conv.Name</span>
<span style="font-size:10px;color:var(--mkt-text-tertiary);">@conv.Time</span>
</div>
<span style="font-size:12px;color:var(--mkt-text-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">@conv.LastMessage</span>
<div style="display:flex;gap:4px;align-items:center;">
<span style="font-size:10px;padding:1px 5px;border-radius:3px;background:@conv.ChannelBg;color:@conv.ChannelColor;font-weight:600;">@conv.Channel</span>
@if (conv.Unread > 0)
{
<span style="font-size:9px;padding:1px 5px;border-radius:8px;background:var(--mkt-yellow-primary);color:var(--mkt-bg-card);font-weight:700;">@conv.Unread</span>
}
</div>
</div>
</div>
}
</div>
@* ═══ CENTER: Chat Area ═══ *@
<div class="mkt-chat-main" style="background:var(--mkt-bg-page);">
@* Chat header *@
<div style="padding:12px 20px;border-bottom:1px solid var(--mkt-border);display:flex;align-items:center;gap:12px;">
<div style="width:40px;height:40px;border-radius:50%;background:var(--mkt-info);display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:700;color:white;">NA</div>
<div style="flex:1;display:flex;flex-direction:column;gap:2px;">
<span style="font-size:14px;font-weight:600;color:var(--mkt-text-primary);">Nguyễn Văn A</span>
<span style="font-size:11px;color:var(--mkt-success);">● Online</span>
</div>
<div style="display:flex;gap:8px;">
<button class="mkt-btn-ghost" style="padding:6px;">
<i data-lucide="phone" style="width:14px;height:14px;"></i>
</button>
<button class="mkt-btn-ghost" style="padding:6px;">
<i data-lucide="more-vertical" style="width:14px;height:14px;"></i>
</button>
</div>
</div>
@* Chat messages *@
<div style="flex:1;overflow-y:auto;padding:20px;display:flex;flex-direction:column;gap:16px;">
@* Customer message *@
<div style="display:flex;gap:8px;max-width:70%;">
<div style="padding:10px 14px;background:var(--mkt-bg-card);border-radius:10px;border-top-left-radius:2px;">
<p style="margin:0;font-size:13px;color:var(--mkt-text-primary);">Xin chào, tôi muốn hỏi về sản phẩm mới ạ 🤗</p>
<span style="font-size:10px;color:var(--mkt-text-tertiary);margin-top:4px;display:block;">10:30 AM</span>
</div>
</div>
@* Agent reply *@
<div style="display:flex;justify-content:flex-end;">
<div style="padding:10px 14px;background:var(--mkt-yellow-dim);border-radius:10px;border-top-right-radius:2px;max-width:70%;">
<p style="margin:0;font-size:13px;color:var(--mkt-text-primary);">Chào bạn! 👋 Cảm ơn bạn đã liên hệ. Bạn quan tâm đến sản phẩm nào ạ?</p>
<span style="font-size:10px;color:var(--mkt-text-tertiary);margin-top:4px;display:block;">10:31 AM</span>
</div>
</div>
@* Customer message *@
<div style="display:flex;gap:8px;max-width:70%;">
<div style="padding:10px 14px;background:var(--mkt-bg-card);border-radius:10px;border-top-left-radius:2px;">
<p style="margin:0;font-size:13px;color:var(--mkt-text-primary);">Tôi muốn hỏi về sản phẩm coffee đặc biệt, giá bao nhiêu vậy shop?</p>
<span style="font-size:10px;color:var(--mkt-text-tertiary);margin-top:4px;display:block;">10:32 AM</span>
</div>
</div>
@* Typing indicator *@
<div style="display:flex;gap:4px;align-items:center;">
<div style="width:6px;height:6px;border-radius:50%;background:var(--mkt-text-tertiary);animation:pulse 1.4s infinite;"></div>
<div style="width:6px;height:6px;border-radius:50%;background:var(--mkt-text-tertiary);animation:pulse 1.4s infinite 0.2s;"></div>
<div style="width:6px;height:6px;border-radius:50%;background:var(--mkt-text-tertiary);animation:pulse 1.4s infinite 0.4s;"></div>
<span style="font-size:11px;color:var(--mkt-text-tertiary);margin-left:4px;">Customer is typing...</span>
</div>
</div>
@* Quick replies *@
<div style="padding:8px 20px;border-top:1px solid var(--mkt-border);display:flex;gap:8px;align-items:center;">
<span style="font-size:11px;font-weight:600;color:var(--mkt-text-tertiary);">QUICK:</span>
@foreach (var reply in _quickReplies)
{
<button style="padding:4px 10px;border-radius:12px;background:var(--mkt-bg-card);border:1px solid var(--mkt-border);color:var(--mkt-text-secondary);font-size:11px;cursor:pointer;">@reply</button>
}
</div>
@* Message input *@
<div style="padding:12px 20px;border-top:1px solid var(--mkt-border);display:flex;align-items:center;gap:8px;">
<button style="width:40px;height:40px;border-radius:8px;background:var(--mkt-bg-card);border:none;display:flex;align-items:center;justify-content:center;cursor:pointer;">
<i data-lucide="paperclip" style="width:16px;height:16px;color:var(--mkt-text-secondary);"></i>
</button>
<div style="flex:1;padding:10px 14px;background:var(--mkt-bg-card);border-radius:8px;border:1px solid var(--mkt-border);">
<input type="text" placeholder="Type your message..." style="width:100%;background:none;border:none;outline:none;color:var(--mkt-text-primary);font-size:13px;" />
</div>
<button style="width:40px;height:40px;border-radius:8px;background:var(--mkt-yellow-primary);border:none;display:flex;align-items:center;justify-content:center;cursor:pointer;">
<i data-lucide="send" style="width:16px;height:16px;color:var(--mkt-bg-card);"></i>
</button>
</div>
</div>
@* ═══ RIGHT: Customer Info ═══ *@
<div class="mkt-chat-info" style="background:var(--mkt-bg-card);">
<div style="padding:20px;text-align:center;border-bottom:1px solid var(--mkt-border);">
<div style="width:56px;height:56px;border-radius:50%;background:var(--mkt-info);display:flex;align-items:center;justify-content:center;font-size:18px;font-weight:700;color:white;margin:0 auto 10px;">NA</div>
<div style="font-size:14px;font-weight:600;color:var(--mkt-text-primary);">Nguyễn Văn A</div>
<div style="font-size:11px;color:var(--mkt-text-secondary);margin-top:2px;">khách hàng VIP</div>
</div>
<div style="padding:16px;display:flex;flex-direction:column;gap:12px;">
<div style="display:flex;justify-content:space-between;">
<span style="font-size:11px;color:var(--mkt-text-tertiary);">PHONE</span>
<span style="font-size:12px;color:var(--mkt-text-primary);">0912 xxx xxx</span>
</div>
<div style="display:flex;justify-content:space-between;">
<span style="font-size:11px;color:var(--mkt-text-tertiary);">CHANNEL</span>
<span class="mkt-badge mkt-badge--info">ZALO</span>
</div>
<div style="display:flex;justify-content:space-between;">
<span style="font-size:11px;color:var(--mkt-text-tertiary);">SCORE</span>
<span style="font-size:12px;color:var(--mkt-yellow-primary);">★★★★☆</span>
</div>
<div style="display:flex;justify-content:space-between;">
<span style="font-size:11px;color:var(--mkt-text-tertiary);">ORDERS</span>
<span style="font-size:12px;color:var(--mkt-text-primary);">12</span>
</div>
<div style="display:flex;justify-content:space-between;">
<span style="font-size:11px;color:var(--mkt-text-tertiary);">TOTAL SPENT</span>
<span style="font-size:12px;color:var(--mkt-text-primary);">2.4M</span>
</div>
</div>
<div style="padding:0 16px;display:flex;flex-direction:column;gap:8px;">
<span style="font-size:11px;font-weight:600;color:var(--mkt-text-tertiary);letter-spacing:0.5px;">TAGS</span>
<div style="display:flex;gap:4px;flex-wrap:wrap;">
<span class="mkt-badge mkt-badge--active">VIP</span>
<span class="mkt-badge mkt-badge--training">COFFEE LOVER</span>
<span class="mkt-badge mkt-badge--info">LOYAL</span>
</div>
</div>
</div>
</div>
@code {
// EN: Demo conversation data / VI: Dữ liệu hội thoại demo
private record ConversationInfo(string Name, string Initials, string LastMessage, string Time, string Channel, string ChannelColor, string ChannelBg, string AvatarBg, int Unread, bool IsActive);
private ConversationInfo _selectedConversation = null!;
private readonly string[] _quickReplies = new[]
{
"Xin chào!", "Cảm ơn", "Đang kiểm tra", "Liên hệ CSKH"
};
private readonly ConversationInfo[] _conversations = new[]
{
new ConversationInfo("Nguyễn Văn A", "NA", "Tôi muốn hỏi về sản phẩm...", "10:32", "ZALO", "#0068FF", "rgba(0,104,255,0.12)", "#3B82F6", 2, true),
new ConversationInfo("Trần Thị B", "TB", "Giá bao nhiêu vậy shop?", "10:15", "FB", "#3B82F6", "rgba(59,130,246,0.12)", "#8B5CF6", 1, false),
new ConversationInfo("Lê Văn C", "LC", "Cảm ơn shop nhé!", "09:45", "IG", "#E1306C", "rgba(225,48,108,0.12)", "#EC4899", 0, false),
new ConversationInfo("David Chen", "DC", "Hi, I want to order...", "09:20", "WA", "#25D366", "rgba(37,211,102,0.12)", "#22C55E", 0, false),
};
private void SelectConversation(ConversationInfo conv)
{
_selectedConversation = conv;
}
}

View File

@@ -0,0 +1,64 @@
using Microsoft.AspNetCore.Components;
namespace WebClientTpos.Client.Pages.Marketing;
/// <summary>
/// EN: Base class for all Marketing pages — provides navigation, formatting helpers.
/// VI: Lớp cơ sở cho tất cả trang Marketing — cung cấp điều hướng, định dạng số liệu.
/// </summary>
public abstract class MarketingBase : ComponentBase
{
[Inject] protected NavigationManager NavigationManager { get; set; } = default!;
/// <summary>
/// EN: Page title shown in topbar. / VI: Tiêu đề trang.
/// </summary>
protected string PageTitle { get; set; } = string.Empty;
/// <summary>
/// EN: Page subtitle. / VI: Phụ đề trang.
/// </summary>
protected string PageSubtitle { get; set; } = string.Empty;
/// <summary>
/// EN: Search query. / VI: Truy vấn tìm kiếm.
/// </summary>
protected string SearchQuery { get; set; } = string.Empty;
/// <summary>
/// EN: Navigate to a marketing sub-page.
/// VI: Điều hướng đến trang con marketing.
/// </summary>
protected void NavigateTo(string path)
{
NavigationManager.NavigateTo($"/marketing/{path}");
}
/// <summary>
/// EN: Format large numbers (1.2K, 45.2K, 1.5M).
/// VI: Định dạng số lớn.
/// </summary>
protected static string FormatNumber(double value)
{
if (value >= 1_000_000) return $"{value / 1_000_000:0.#}M";
if (value >= 1_000) return $"{value / 1_000:0.#}K";
return value.ToString("N0");
}
/// <summary>
/// EN: Format percentage with sign. / VI: Định dạng phần trăm có dấu.
/// </summary>
protected static string FormatPercent(double value)
{
var sign = value >= 0 ? "+" : "";
return $"{sign}{value:0.#}%";
}
/// <summary>
/// EN: Get today formatted. / VI: Ngày hôm nay.
/// </summary>
protected static string GetTodayFormatted()
{
return DateTime.Now.ToString("dd/MM/yyyy");
}
}

View File

@@ -0,0 +1,162 @@
@page "/marketing"
@layout MarketingLayout
@inherits MarketingBase
@*
EN: Social Hub — Manage all social media channels in one place.
VI: Social Hub — Quản lý tất cả kênh mạng xã hội tại một nơi.
Design: pencil-design/src/pages/tMarketing/desktop/social-hub.pen
*@
<PageTitle>Social Hub — tMarketing</PageTitle>
@* ═══ TOP BAR ═══ *@
<div class="mkt-topbar">
<div class="mkt-topbar__left">
<h1 class="mkt-topbar__title">SOCIAL HUB</h1>
<p class="mkt-topbar__subtitle">Manage all your social media channels in one place</p>
</div>
<div class="mkt-topbar__right">
<div class="mkt-search">
<i data-lucide="search"></i>
<input type="text" placeholder="SEARCH" @bind="SearchQuery" />
</div>
<button class="mkt-btn-primary">
<i data-lucide="plus"></i>
<span>NEW POST</span>
</button>
</div>
</div>
@* ═══ CONTENT ═══ *@
<div class="mkt-content">
@* ── ROW 1: Platform Status + Quick Actions ── *@
<div style="display:flex;gap:20px;">
@* Platform Status *@
<div class="mkt-panel" style="flex:1;">
<div class="mkt-panel__header" style="border-bottom:none;padding-bottom:8px;">
<h3 class="mkt-panel__title">
<i data-lucide="globe"></i>
PLATFORM STATUS
</h3>
</div>
<div class="mkt-panel__body" style="display:flex;flex-direction:column;gap:12px;padding-top:0;">
@foreach (var platform in _platforms)
{
<div style="display:flex;align-items:center;gap:12px;padding:10px 12px;background:var(--mkt-bg-page);border-radius:6px;">
<div style="width:8px;height:8px;border-radius:50%;background:@platform.StatusColor;"></div>
<div style="display:flex;align-items:center;gap:8px;flex:1;">
<span style="font-size:12px;font-weight:600;color:@platform.Color;">@platform.Name</span>
</div>
<span class="mkt-badge @(platform.IsConnected ? "mkt-badge--active" : "mkt-badge--draft")">
@(platform.IsConnected ? "CONNECTED" : "DISCONNECTED")
</span>
<span style="font-size:11px;color:var(--mkt-text-secondary);">@platform.Followers followers</span>
</div>
}
</div>
</div>
@* Quick Actions *@
<div class="mkt-panel" style="width:280px;">
<div class="mkt-panel__header" style="border-bottom:none;padding-bottom:8px;">
<h3 class="mkt-panel__title">
<i data-lucide="zap"></i>
QUICK ACTIONS
</h3>
</div>
<div class="mkt-panel__body" style="display:flex;flex-direction:column;gap:10px;padding-top:0;">
@foreach (var action in _quickActions)
{
<div style="display:flex;align-items:center;gap:10px;padding:10px 12px;background:var(--mkt-bg-page);border-radius:6px;cursor:pointer;">
<i data-lucide="@action.Icon" style="width:14px;height:14px;color:var(--mkt-yellow-primary);"></i>
<span style="font-size:12px;font-weight:500;color:var(--mkt-text-primary);">@action.Label</span>
</div>
}
</div>
</div>
</div>
@* ── ROW 2: Unified Feed ── *@
<div class="mkt-panel">
<div class="mkt-panel__header">
<h3 class="mkt-panel__title">
<i data-lucide="rss"></i>
UNIFIED FEED
</h3>
<div style="display:flex;gap:12px;">
<span style="font-size:11px;font-weight:600;color:var(--mkt-yellow-primary);cursor:pointer;">ALL</span>
<span style="font-size:11px;color:var(--mkt-text-tertiary);cursor:pointer;">SCHEDULED</span>
</div>
</div>
<div class="mkt-panel__body" style="display:flex;flex-direction:column;gap:12px;">
@foreach (var post in _feedPosts)
{
<div style="display:flex;gap:12px;align-items:flex-start;padding-bottom:12px;border-bottom:1px solid var(--mkt-border);">
<div style="width:3px;height:40px;border-radius:2px;background:@post.ChannelColor;flex-shrink:0;"></div>
<div style="flex:1;display:flex;flex-direction:column;gap:4px;">
<span style="font-size:13px;font-weight:500;color:var(--mkt-text-primary);">@post.Content</span>
<span style="font-size:11px;color:var(--mkt-text-secondary);">@post.Time • @post.Engagement</span>
</div>
<span style="font-size:10px;font-weight:700;color:@post.ChannelColor;padding:2px 6px;border-radius:3px;background:rgba(255,255,255,0.05);">@post.Channel</span>
</div>
}
</div>
</div>
@* ── ROW 3: KPI Stats ── *@
<div class="mkt-kpi-row">
@foreach (var kpi in _kpis)
{
<div class="mkt-kpi-card">
<span class="mkt-kpi-card__label">@kpi.Label</span>
<span class="mkt-kpi-card__value">@kpi.Value</span>
<div style="display:flex;align-items:center;gap:6px;">
<i data-lucide="arrow-up-right" style="width:12px;height:12px;color:var(--mkt-success);"></i>
<span style="font-size:12px;font-weight:600;color:var(--mkt-success);">@kpi.Change</span>
</div>
</div>
}
</div>
</div>
@code {
// EN: Demo platform data / VI: Dữ liệu nền tảng demo
private record PlatformInfo(string Name, string Color, string StatusColor, bool IsConnected, string Followers);
private record ActionInfo(string Icon, string Label);
private record FeedPost(string Content, string Time, string Engagement, string Channel, string ChannelColor);
private record KpiInfo(string Label, string Value, string Change);
private readonly PlatformInfo[] _platforms = new[]
{
new PlatformInfo("Facebook", "#3B82F6", "#22C55E", true, "12.5K"),
new PlatformInfo("Instagram", "#E1306C", "#22C55E", true, "8.2K"),
new PlatformInfo("Zalo OA", "#0068FF", "#22C55E", true, "15.1K"),
new PlatformInfo("WhatsApp", "#25D366", "#22C55E", true, "3.4K"),
new PlatformInfo("TikTok", "#FF0050", "#EF4444", false, "—"),
};
private readonly ActionInfo[] _quickActions = new[]
{
new ActionInfo("edit-3", "Create New Post"),
new ActionInfo("calendar", "Schedule Content"),
new ActionInfo("bar-chart-2", "View Analytics"),
new ActionInfo("settings", "Channel Settings"),
};
private readonly FeedPost[] _feedPosts = new[]
{
new FeedPost("🎉 Spring Collection ra mắt — Giảm 20% tất cả sản phẩm mới!", "2 giờ trước", "234 likes • 45 comments", "FB", "#3B82F6"),
new FeedPost("Behind the scenes — Quy trình sản xuất coffee đặc biệt", "4 giờ trước", "1.2K views • 89 likes", "IG", "#E1306C"),
new FeedPost("Thông báo: Chương trình khách hàng thân thiết mới", "6 giờ trước", "567 reads", "ZALO", "#0068FF"),
new FeedPost("Try our new seasonal drinks! 🍵 Limited time only", "8 giờ trước", "89 likes • 12 shares", "WA", "#25D366"),
};
private readonly KpiInfo[] _kpis = new[]
{
new KpiInfo("TOTAL REACH", "45.2K", "12.5%"),
new KpiInfo("ENGAGEMENT", "12.8%", "8.2%"),
new KpiInfo("NEW FOLLOWERS", "+1.2K", "24.1%"),
new KpiInfo("MESSAGES", "234", "15.3%"),
};
}

View File

@@ -0,0 +1,633 @@
/* ═══════════════════════════════════════════════════════════════
tMarketing — CSS Design System
EN: Industrial dark theme with yellow accent for marketing module.
VI: Theme công nghiệp tối với điểm nhấn vàng cho module marketing.
Design source: pencil-design/src/pages/tMarketing/desktop/
═══════════════════════════════════════════════════════════════ */
/* ── DESIGN TOKENS ── */
:root {
--mkt-bg-page: #18181B;
--mkt-bg-card: #0F0F10;
--mkt-bg-elevated: #1C1C1F;
--mkt-yellow-primary: #FACC15;
--mkt-yellow-light: #FDE047;
--mkt-yellow-dim: rgba(250, 204, 21, 0.08);
--mkt-yellow-glow: rgba(250, 204, 21, 0.15);
--mkt-text-primary: #FAFAFA;
--mkt-text-secondary: #71717A;
--mkt-text-tertiary: #52525B;
--mkt-border: #27272A;
--mkt-border-light: #3F3F46;
--mkt-success: #22C55E;
--mkt-warning: #FACC15;
--mkt-error: #EF4444;
--mkt-info: #3B82F6;
--mkt-purple: #8B5CF6;
--mkt-pink: #EC4899;
--mkt-sidebar-w: 240px;
--mkt-font: 'Roboto', sans-serif;
}
/* ── LAYOUT ── */
.mkt-layout {
display: flex;
min-height: 100vh;
background: var(--mkt-bg-page);
color: var(--mkt-text-primary);
font-family: var(--mkt-font);
}
.mkt-main {
flex: 1;
display: flex;
flex-direction: column;
overflow-y: auto;
background: var(--mkt-bg-page);
}
/* ── SIDEBAR ── */
.mkt-sidebar {
width: var(--mkt-sidebar-w);
background: var(--mkt-bg-card);
display: flex;
flex-direction: column;
border-right: 1px solid var(--mkt-border);
flex-shrink: 0;
overflow-y: auto;
}
.mkt-sidebar__logo {
display: flex;
align-items: center;
gap: 8px;
padding: 20px 16px;
}
.mkt-sidebar__logo-icon {
width: 32px;
height: 32px;
background: var(--mkt-yellow-primary);
color: var(--mkt-bg-card);
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
font-weight: 700;
}
.mkt-sidebar__logo-text {
font-size: 14px;
font-weight: 700;
letter-spacing: 1px;
color: var(--mkt-text-primary);
}
/* Navigation items */
.mkt-sidebar__nav {
display: flex;
flex-direction: column;
gap: 2px;
padding: 0 12px;
flex: 1;
}
.mkt-nav-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
color: var(--mkt-text-secondary);
text-decoration: none;
transition: all 0.2s ease;
letter-spacing: 0.5px;
}
.mkt-nav-item:hover {
color: var(--mkt-text-primary);
background: var(--mkt-yellow-dim);
}
.mkt-nav-item--active {
color: var(--mkt-text-primary) !important;
background: var(--mkt-yellow-glow) !important;
}
.mkt-nav-item--active .mkt-nav-num {
color: var(--mkt-yellow-primary) !important;
}
.mkt-nav-num {
font-size: 12px;
font-weight: 600;
color: var(--mkt-text-tertiary);
min-width: 20px;
}
/* Sidebar stats card */
.mkt-sidebar__stats {
margin: 16px 12px;
padding: 16px;
background: rgba(250, 204, 21, 0.06);
border-radius: 10px;
display: flex;
flex-direction: column;
gap: 12px;
}
.mkt-sidebar__stats-header {
display: flex;
align-items: center;
gap: 8px;
font-size: 11px;
font-weight: 600;
color: var(--mkt-yellow-primary);
letter-spacing: 0.5px;
}
.mkt-sidebar__stats-header i {
width: 14px;
height: 14px;
}
.mkt-sidebar__stats-value {
font-size: 28px;
font-weight: 700;
color: var(--mkt-text-primary);
}
/* Sidebar user */
.mkt-sidebar__user {
display: flex;
align-items: center;
gap: 10px;
padding: 16px;
border-top: 1px solid var(--mkt-border);
}
.mkt-user-avatar {
width: 28px;
height: 28px;
border-radius: 6px;
background: var(--mkt-border);
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
font-weight: 700;
color: var(--mkt-text-primary);
}
.mkt-user-name {
flex: 1;
font-size: 12px;
font-weight: 500;
color: var(--mkt-text-primary);
letter-spacing: 0.5px;
}
.mkt-sidebar__user button {
background: none;
border: none;
color: var(--mkt-text-tertiary);
cursor: pointer;
padding: 4px;
}
.mkt-sidebar__user button i {
width: 14px;
height: 14px;
}
/* ── TOPBAR ── */
.mkt-topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 28px;
flex-shrink: 0;
}
.mkt-topbar__left {
display: flex;
flex-direction: column;
gap: 4px;
}
.mkt-topbar__title {
font-size: 22px;
font-weight: 700;
color: var(--mkt-text-primary);
margin: 0;
letter-spacing: 1px;
text-transform: uppercase;
}
.mkt-topbar__subtitle {
font-size: 13px;
color: var(--mkt-text-secondary);
margin: 0;
}
.mkt-topbar__right {
display: flex;
align-items: center;
gap: 10px;
}
/* ── BUTTONS ── */
.mkt-btn-primary {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
background: var(--mkt-yellow-primary);
color: var(--mkt-bg-card);
border: none;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
letter-spacing: 0.5px;
transition: background 0.2s;
}
.mkt-btn-primary:hover {
background: var(--mkt-yellow-light);
}
.mkt-btn-primary i {
width: 14px;
height: 14px;
}
.mkt-btn-ghost {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
background: transparent;
color: var(--mkt-text-secondary);
border: 1px solid var(--mkt-border);
border-radius: 6px;
font-size: 12px;
font-weight: 500;
cursor: pointer;
letter-spacing: 0.5px;
transition: all 0.2s;
}
.mkt-btn-ghost:hover {
color: var(--mkt-text-primary);
border-color: var(--mkt-border-light);
}
.mkt-btn-ghost i {
width: 14px;
height: 14px;
}
/* ── CONTENT AREA ── */
.mkt-content {
display: flex;
flex-direction: column;
gap: 24px;
padding: 0 28px 28px;
flex: 1;
}
/* ── KPI CARDS ── */
.mkt-kpi-row {
display: flex;
gap: 16px;
}
.mkt-kpi-card {
flex: 1;
background: var(--mkt-bg-card);
border-radius: 10px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 8px;
}
.mkt-kpi-card__header {
display: flex;
align-items: center;
justify-content: space-between;
}
.mkt-kpi-card__icon {
width: 32px;
height: 32px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
}
.mkt-kpi-card__icon i {
width: 16px;
height: 16px;
}
.mkt-kpi-card__badge {
font-size: 11px;
font-weight: 600;
padding: 2px 6px;
border-radius: 4px;
display: flex;
align-items: center;
gap: 2px;
}
.mkt-kpi-card__badge--up {
color: var(--mkt-success);
background: rgba(34, 197, 94, 0.1);
}
.mkt-kpi-card__badge--down {
color: var(--mkt-error);
background: rgba(239, 68, 68, 0.1);
}
.mkt-kpi-card__badge i {
width: 10px;
height: 10px;
}
.mkt-kpi-card__value {
font-size: 24px;
font-weight: 700;
color: var(--mkt-text-primary);
}
.mkt-kpi-card__label {
font-size: 12px;
color: var(--mkt-text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* ── PANELS / CARDS ── */
.mkt-panel {
background: var(--mkt-bg-card);
border-radius: 10px;
overflow: hidden;
}
.mkt-panel__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid var(--mkt-border);
}
.mkt-panel__title {
font-size: 13px;
font-weight: 700;
color: var(--mkt-text-primary);
display: flex;
align-items: center;
gap: 8px;
letter-spacing: 0.5px;
text-transform: uppercase;
margin: 0;
}
.mkt-panel__title i {
width: 16px;
height: 16px;
color: var(--mkt-yellow-primary);
}
.mkt-panel__action {
font-size: 12px;
color: var(--mkt-yellow-primary);
text-decoration: none;
font-weight: 500;
letter-spacing: 0.3px;
}
.mkt-panel__body {
padding: 16px 20px;
}
/* ── TABLE ── */
.mkt-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.mkt-table th {
text-align: left;
padding: 10px 12px;
font-size: 11px;
font-weight: 600;
color: var(--mkt-text-tertiary);
text-transform: uppercase;
letter-spacing: 0.5px;
border-bottom: 1px solid var(--mkt-border);
}
.mkt-table td {
padding: 12px;
border-bottom: 1px solid var(--mkt-border);
color: var(--mkt-text-primary);
}
.mkt-table tr:last-child td {
border-bottom: none;
}
.mkt-table tr:hover td {
background: var(--mkt-yellow-dim);
}
/* ── STATUS BADGES ── */
.mkt-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.3px;
text-transform: uppercase;
}
.mkt-badge--active {
background: rgba(34, 197, 94, 0.12);
color: var(--mkt-success);
}
.mkt-badge--draft {
background: rgba(113, 113, 122, 0.12);
color: var(--mkt-text-secondary);
}
.mkt-badge--training {
background: rgba(250, 204, 21, 0.12);
color: var(--mkt-warning);
}
.mkt-badge--error {
background: rgba(239, 68, 68, 0.12);
color: var(--mkt-error);
}
.mkt-badge--info {
background: rgba(59, 130, 246, 0.12);
color: var(--mkt-info);
}
/* ── SEARCH ── */
.mkt-search {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: var(--mkt-bg-card);
border: 1px solid var(--mkt-border);
border-radius: 6px;
min-width: 200px;
}
.mkt-search i {
width: 14px;
height: 14px;
color: var(--mkt-text-tertiary);
}
.mkt-search input {
flex: 1;
background: none;
border: none;
outline: none;
color: var(--mkt-text-primary);
font-size: 12px;
font-family: var(--mkt-font);
}
.mkt-search input::placeholder {
color: var(--mkt-text-tertiary);
}
/* ── CHANNEL CARDS (Social Hub) ── */
.mkt-channel-card {
background: var(--mkt-bg-card);
border-radius: 10px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
border: 1px solid var(--mkt-border);
transition: border-color 0.2s;
}
.mkt-channel-card:hover {
border-color: var(--mkt-border-light);
}
/* ── CHAT (Livechat) ── */
.mkt-chat-layout {
display: flex;
flex: 1;
min-height: 0;
}
.mkt-chat-list {
width: 300px;
border-right: 1px solid var(--mkt-border);
display: flex;
flex-direction: column;
overflow-y: auto;
}
.mkt-chat-main {
flex: 1;
display: flex;
flex-direction: column;
}
.mkt-chat-info {
width: 280px;
border-left: 1px solid var(--mkt-border);
display: flex;
flex-direction: column;
overflow-y: auto;
}
.mkt-chat-item {
display: flex;
gap: 10px;
padding: 12px 16px;
cursor: pointer;
border-bottom: 1px solid var(--mkt-border);
transition: background 0.2s;
}
.mkt-chat-item:hover,
.mkt-chat-item--active {
background: var(--mkt-yellow-dim);
}
/* ── MOBILE RESPONSIVE ── */
.mkt-sidebar-overlay {
display: none;
}
@media (max-width: 768px) {
.mkt-sidebar {
position: fixed;
left: -260px;
top: 0;
bottom: 0;
z-index: 1000;
transition: left 0.3s ease;
}
.mkt-sidebar--open {
left: 0;
}
.mkt-sidebar-overlay {
display: block;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
}
.mkt-kpi-row {
flex-wrap: wrap;
}
.mkt-kpi-card {
min-width: calc(50% - 8px);
}
.mkt-topbar {
padding: 16px;
}
.mkt-content {
padding: 0 16px 16px;
}
.mkt-chat-info {
display: none;
}
.mkt-chat-list {
width: 100%;
}
}

View File

@@ -32,6 +32,7 @@
<link rel="stylesheet" href="/css/app.css" />
<link rel="stylesheet" href="/css/auth.css" />
<link rel="stylesheet" href="/css/admin.css" />
<link rel="stylesheet" href="/css/marketing.css" />
<link rel="stylesheet" href="/css/pos.css" />
<link rel="icon" type="image/png" href="/favicon.png" />
<link href="/WebClientTpos.Client.styles.css" rel="stylesheet" />