initial commit

This commit is contained in:
2026-07-21 13:59:38 +07:00
commit c40792266a
1321 changed files with 100465 additions and 0 deletions
+43
View File
@@ -0,0 +1,43 @@
@using Indotalent.Features.Root
@using System.Security.Claims
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<ResourcePreloader />
<link rel="icon" type="image/png" href="@Assets["favico.png"]" />
<link rel="stylesheet" href="@Assets["Indotalent.styles.css"]" />
<ImportMap />
<HeadOutlet @rendermode="InteractiveServer" />
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" />
<link href="@Assets["_content/MudBlazor/MudBlazor.min.css"]" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;450;500;600;700;800&display=swap" rel="stylesheet" />
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
<script>
tailwind.config = {
important: true,
corePlugins: {
preflight: false
}
}
</script>
</head>
<body>
<Routes @rendermode="new InteractiveServerRenderMode(prerender: false)" />
<ReconnectModal />
<script src="@Assets["_framework/blazor.web.js"]"></script>
<script src="@Assets["_content/MudBlazor/MudBlazor.min.js"]"></script>
<SsoFirebase/>
<SsoKeycloak/>
</body>
</html>
+2
View File
@@ -0,0 +1,2 @@
@inherits LayoutComponentBase
@Body
+59
View File
@@ -0,0 +1,59 @@
@page "/Error"
@layout MainLayout
@using System.Diagnostics
@using Microsoft.AspNetCore.Mvc
@attribute [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
<PageTitle>Error - Something Went Wrong</PageTitle>
<MudContainer MaxWidth="MaxWidth.Medium" Class="d-flex align-center justify-center" Style="height: calc(100vh - 64px);">
<div class="text-center">
<MudIcon Icon="@Icons.Material.Filled.Error"
Size="Size.Large"
Color="Color.Error"
Class="mb-6" Style="font-size: 120px;" />
<MudText Typo="Typo.h3" Class="mb-4 text-error">An Error Occurred</MudText>
<MudText Typo="Typo.body1" Class="mb-6 text-muted">
Sorry, an error occurred while processing your request.<br />
Our team has been notified and is working to resolve the issue.
</MudText>
@if (ShowRequestId)
{
<MudPaper Elevation="1" Class="pa-4 mb-6 d-inline-block">
<MudText Typo="Typo.subtitle2" Class="mb-2">
<strong>Request ID:</strong> <code>@RequestId</code>
</MudText>
<MudText Typo="Typo.caption" Class="text-muted">
Please provide this ID when contacting support to help us track the issue.
</MudText>
</MudPaper>
}
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Size="Size.Large"
Href="/"
StartIcon="@Icons.Material.Filled.Home">
Back to Home
</MudButton>
</div>
</MudContainer>
@code {
[CascadingParameter] private HttpContext? HttpContext { get; set; }
private string? RequestId { get; set; }
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
protected override void OnInitialized()
{
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
}
}
@@ -0,0 +1,661 @@
@page "/home"
@using Microsoft.AspNetCore.Authorization
@using Indotalent.Infrastructure.Authorization.Identity
@using Indotalent.Features.Root.Home.Cqrs
@using Indotalent.Features.Root.Home
@attribute [Authorize(Roles = $"{ApplicationRoles.Admin},{ApplicationRoles.Member}")]
@inject HomeService HomeService
@inject IJSRuntime JS
<PageTitle>CRM Dashboard</PageTitle>
@if (_isLoading)
{
<div class="flex items-center justify-center" style="min-height: 100vh; font-family: 'Poppins', sans-serif;">
<div class="text-center">
<div class="spinner" style="width: 48px; height: 48px; border: 4px solid #e2e8f0; border-top-color: #8b5cf6; border-radius: 50%; animation: spin 0.8s linear infinite; margin: 0 auto 16px;"></div>
<p class="text-gray-500 text-sm">Loading dashboard data...</p>
</div>
</div>
}
else
{
<div style="font-family: 'Poppins', sans-serif;">
<div class="p-4 md:p-6 space-y-6">
@* HEADER *@
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3" style="outline:none;">
<div>
<h1 class="text-2xl md:text-3xl font-bold text-gray-900" style="outline:none;">CRM Dashboard</h1>
</div>
</div>
@* ROW 1: 12-col KPI layout *@
<div class="grid grid-cols-1 lg:grid-cols-12 gap-4">
@* Key Metrics Card *@
<div class="lg:col-span-5 row-span-2 bg-white rounded-xl border border-gray-100 p-6 flex flex-col justify-between" style="animation: fadeInUp .5s ease forwards; animation-delay: .1s;">
<div>
<div class="flex items-center justify-between mb-4">
<div>
<p class="text-xs text-gray-500 font-semibold uppercase tracking-wide">CRM Overview</p>
<h3 class="text-lg font-bold text-gray-900 mt-0.5">Key Metrics</h3>
</div>
<div class="w-10 h-10 rounded-xl flex items-center justify-center shrink-0" style="background:#eef2ff;">
<svg class="w-5 h-5 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
</svg>
</div>
</div>
<div class="grid grid-cols-2 gap-3 mt-2">
<div class="p-3 rounded-lg" style="background:#f8fafc;">
<p class="text-xs text-gray-500">Monthly Revenue</p>
<p class="text-lg font-bold text-gray-900">@_data.FormattedMonthlySales</p>
</div>
<div class="p-3 rounded-lg" style="background:#f8fafc;">
<p class="text-xs text-gray-500">Pipeline Value</p>
<p class="text-lg font-bold text-gray-900">@_data.FormattedPipeline</p>
</div>
<div class="p-3 rounded-lg" style="background:#f8fafc;">
<p class="text-xs text-gray-500">Win Rate</p>
<p class="text-lg font-bold text-gray-900">@_data.AvgWinRate%</p>
</div>
<div class="p-3 rounded-lg" style="background:#f8fafc;">
<p class="text-xs text-gray-500">Total Leads</p>
<p class="text-lg font-bold text-gray-900">@_data.FormattedTotalLeads</p>
</div>
</div>
</div>
<div class="mt-4 pt-4 border-t border-gray-100 flex items-center justify-between text-xs text-gray-400">
<span>AR: <b class="text-gray-700">@_data.FormattedPendingPayments</b></span>
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-semibold" style="background:#eef2ff;color:#4f46e5;">FY @DateTime.Now.Year</span>
</div>
</div>
@* 8 Small KPI Cards (row 1, cols 6-12) *@
<div class="lg:col-span-7 grid grid-cols-2 lg:grid-cols-4 gap-3" style="animation: fadeInUp .5s ease forwards; animation-delay: .2s;">
<div class="kpi-card-sm kpi-b1">
<div class="flex items-start justify-between">
<div><p class="kpi-label">Monthly Revenue</p><p class="kpi-value">@_data.FormattedMonthlySales</p></div>
<div class="kpi-icon"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg></div>
</div>
</div>
<div class="kpi-card-sm kpi-b2">
<div class="flex items-start justify-between">
<div><p class="kpi-label">Pipeline Value</p><p class="kpi-value">@_data.FormattedPipeline</p></div>
<div class="kpi-icon"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/></svg></div>
</div>
</div>
<div class="kpi-card-sm kpi-b3">
<div class="flex items-start justify-between">
<div><p class="kpi-label">Win Rate</p><p class="kpi-value">@_data.AvgWinRate%</p></div>
<div class="kpi-icon"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 9V7a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2m2 4h10a2 2 0 002-2v-6a2 2 0 00-2-2H9a2 2 0 00-2 2v6a2 2 0 002 2zm7-5a2 2 0 11-4 0 2 2 0 014 0z"/></svg></div>
</div>
</div>
<div class="kpi-card-sm kpi-b4">
<div class="flex items-start justify-between">
<div><p class="kpi-label">Total Leads</p><p class="kpi-value">@_data.FormattedTotalLeads</p></div>
<div class="kpi-icon"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"/></svg></div>
</div>
</div>
</div>
<div class="lg:col-span-7 lg:col-start-6 grid grid-cols-2 lg:grid-cols-4 gap-3" style="animation: fadeInUp .5s ease forwards; animation-delay: .3s;">
<div class="kpi-card-sm kpi-b5">
<div class="flex items-start justify-between">
<div><p class="kpi-label">Avg Deal Size</p><p class="kpi-value">@_data.FormattedAvgDealSize</p></div>
<div class="kpi-icon"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg></div>
</div>
</div>
<div class="kpi-card-sm kpi-b6">
<div class="flex items-start justify-between">
<div><p class="kpi-label">CVR</p><p class="kpi-value">@_data.OverallCvr%</p></div>
<div class="kpi-icon"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/></svg></div>
</div>
</div>
<div class="kpi-card-sm kpi-b7">
<div class="flex items-start justify-between">
<div><p class="kpi-label">Active Deals</p><p class="kpi-value">@_data.ActiveDealsCount</p></div>
<div class="kpi-icon"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/></svg></div>
</div>
</div>
<div class="kpi-card-sm kpi-b8">
<div class="flex items-start justify-between">
<div><p class="kpi-label">AR Balance</p><p class="kpi-value">@_data.FormattedPendingPayments</p></div>
<div class="kpi-icon"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/></svg></div>
</div>
</div>
</div>
</div>
@* Metrics Matrix *@
<div class="bg-white rounded-xl border border-gray-100 p-5" style="animation: fadeInUp .5s ease forwards; animation-delay: .2s;">
<div class="flex items-center justify-between mb-4">
<div><h3 class="font-bold text-gray-900">CRM Metrics Matrix</h3><p class="text-xs text-gray-400 mt-0.5">Key indicators across all categories</p></div>
</div>
<div class="grid grid-cols-2 md:grid-cols-5 gap-2">
@{
var mmItems = new[] {
new { Title = "Monthly Revenue", Sub = _data.FormattedMonthlySales, ColorClass = "mm-c1" },
new { Title = "Pipeline Value", Sub = _data.FormattedPipeline, ColorClass = "mm-c2" },
new { Title = "Win Rate", Sub = $"{_data.AvgWinRate}%", ColorClass = "mm-c3" },
new { Title = "Total Leads", Sub = _data.FormattedTotalLeads, ColorClass = "mm-c4" },
new { Title = "Avg Deal Size", Sub = _data.FormattedAvgDealSize, ColorClass = "mm-c5" },
new { Title = "CVR", Sub = $"{_data.OverallCvr}%", ColorClass = "mm-c6" },
new { Title = "Active Deals", Sub = _data.ActiveDealsCount.ToString(), ColorClass = "mm-c7" },
new { Title = "AR Balance", Sub = _data.FormattedPendingPayments, ColorClass = "mm-c8" },
new { Title = "Contacts", Sub = _data.TotalContacts.ToString("N0"), ColorClass = "mm-c9" },
new { Title = "Campaigns", Sub = _data.TotalCampaigns.ToString(), ColorClass = "mm-c10" },
new { Title = "Conversion", Sub = $"{_data.OverallCvr}%", ColorClass = "mm-c11" },
new { Title = "Retention", Sub = $"{_data.RetentionRate}%", ColorClass = "mm-c12" },
new { Title = "Won Deals", Sub = _data.WonDealsCount.ToString(), ColorClass = "mm-c13" },
new { Title = "Budget", Sub = _data.FormattedBudget, ColorClass = "mm-c14" },
new { Title = "Expense", Sub = _data.FormattedExpense, ColorClass = "mm-c15" },
new { Title = "Pipeline Total", Sub = _data.FormattedPipeline, ColorClass = "mm-c16" },
new { Title = "SLA", Sub = $"{_data.SlaPercentage}%", ColorClass = "mm-c17" },
new { Title = "NPS", Sub = $"{_data.RetentionRate}", ColorClass = "mm-c18" },
new { Title = "Campaigns", Sub = _data.ActiveCampaigns.ToString(), ColorClass = "mm-c19" },
new { Title = "Enterprise", Sub = $"{_data.EnterprisePercentage}%", ColorClass = "mm-c20" }
};
}
@foreach (var item in mmItems)
{
<div class="mm-cell">
<div class="mm-icon @item.ColorClass">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
</div>
<div><p class="mm-title">@item.Title</p><p class="mm-sub">@item.Sub</p></div>
</div>
}
</div>
</div>
@* ROW 2: Revenue Trend + Lead Sources *@
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
<div class="lg:col-span-2 bg-white rounded-xl border border-gray-100 p-5" style="animation: fadeInUp .5s ease forwards; animation-delay: .2s;">
<div class="flex items-center justify-between mb-4">
<div><h3 class="font-bold text-gray-900">Revenue Trend</h3><p class="text-xs text-gray-400 mt-0.5">Monthly revenue vs expense</p></div>
<div class="flex items-center gap-3 text-xs">
<span class="flex items-center gap-1"><span class="w-3 h-3 rounded-sm" style="background:#2563eb;"></span> Revenue</span>
<span class="flex items-center gap-1"><span class="w-3 h-3 rounded-sm" style="background:#f97316;"></span> Expense</span>
</div>
</div>
<div class="chart-container" style="height:260px;"><canvas id="plChart"></canvas></div>
</div>
<div class="bg-white rounded-xl border border-gray-100 p-5" style="animation: fadeInUp .5s ease forwards; animation-delay: .3s;">
<h3 class="font-bold text-gray-900 mb-1">Lead Sources</h3>
<p class="text-xs text-gray-400 mb-3">By category this period</p>
<div class="flex justify-center"><div class="chart-container" style="height:170px;width:170px;"><canvas id="expenseChart"></canvas></div></div>
<div class="grid grid-cols-2 gap-1 mt-3 text-xs">
@if (_data.LeadSources.Any())
{
@foreach (var src in _data.LeadSources)
{
<div class="flex items-center gap-1">
<span class="w-2 h-2 rounded-full" style="background:@src.Color"></span> @src.Source <span class="font-semibold ml-auto">@src.Percentage%</span>
</div>
}
}
else
{
<div class="text-xs text-gray-400">No lead source data available</div>
}
</div>
</div>
</div>
@* ROW 3: Pipeline Summary + Deal Stages + Sales Activity *@
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="bg-white rounded-xl border border-gray-100 p-5" style="animation: fadeInUp .5s ease forwards; animation-delay: .3s;">
<h3 class="font-bold text-gray-900 mb-1">Pipeline Summary</h3>
<p class="text-xs text-gray-400 mb-3">As of @DateTime.Now.ToString("MMMM dd, yyyy")</p>
<div class="space-y-2.5">
@foreach (var ps in _data.PipelineSummary)
{
<div class="flex items-center gap-3 p-2.5 rounded-lg" style="background:#f8fafc;">
<div class="gi @ps.IconColorClass">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/></svg>
</div>
<div class="flex-1"><p class="text-xs text-gray-500">@ps.Label</p><p class="text-sm font-bold text-gray-900">@(ps.FormattedValue ?? ps.Value.ToString("N0"))</p></div>
</div>
}
<div class="flex items-center justify-between text-xs pt-2 border-t border-gray-100">
<span>Win Rate</span><span class="font-semibold text-gray-900">@_data.AvgWinRate%</span>
<span>Deals</span><span class="font-semibold text-gray-900">@_data.WonDealsCount</span>
</div>
</div>
</div>
<div class="bg-white rounded-xl border border-gray-100 p-5" style="animation: fadeInUp .5s ease forwards; animation-delay: .4s;">
<h3 class="font-bold text-gray-900 mb-1">Deal Stages</h3>
<p class="text-xs text-gray-400 mb-3">Pipeline by stage</p>
<div class="space-y-2.5">
@foreach (var ds in _data.DealStages)
{
<div class="flex items-center gap-3 p-2.5 rounded-lg" style="background:#f8fafc;">
<div class="gi" style="background:@ds.Color">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/></svg>
</div>
<div class="flex-1"><p class="text-xs text-gray-500">@ds.Stage</p><p class="text-sm font-bold" style="color:@ds.Color">@ds.Count</p></div>
</div>
}
</div>
<div class="mt-2 flex justify-between text-xs"><span>CVR: <b>@_data.OverallCvr%</b></span><span>Active: <b>@_data.ActiveDealsCount</b></span></div>
</div>
<div class="bg-white rounded-xl border border-gray-100 p-5" style="animation: fadeInUp .5s ease forwards; animation-delay: .5s;">
<h3 class="font-bold text-gray-900 mb-1">Sales Activity</h3>
<p class="text-xs text-gray-400 mb-3">Monthly movement</p>
<div class="chart-container" style="height:170px;"><canvas id="cashflowChart"></canvas></div>
<div class="grid grid-cols-3 gap-1 mt-3 text-xs text-center">
@foreach (var sa in _data.SalesActivities.Take(3))
{
<div><span class="font-bold" style="color:@(sa.Category == "Calls" ? "#10b981" : "#e11d48")">+@sa.Count</span><p class="text-gray-400">@sa.Category</p></div>
}
</div>
</div>
</div>
@* ROW 4: Recent Deals + CRM Activity *@
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div class="bg-white rounded-xl border border-gray-100 p-5" style="animation: fadeInUp .5s ease forwards; animation-delay: .4s;">
<div class="flex items-center justify-between mb-4">
<div><h3 class="font-bold text-gray-900">Recent Deals</h3><p class="text-xs text-gray-400 mt-0.5">Latest opportunities</p></div>
</div>
<div class="grid-wrap">
<table class="grid-table">
<thead><tr><th>Date</th><th>Deal#</th><th>Description</th><th>Value</th><th>Status</th></tr></thead>
<tbody>
@if (_data.RecentDeals.Any())
{
@foreach (var deal in _data.RecentDeals)
{
<tr>
<td class="text-xs">@(deal.Date?.ToString("MMM dd") ?? "-")</td>
<td class="font-mono text-xs">@deal.DealNumber</td>
<td class="text-xs">@deal.Description</td>
<td class="font-mono text-xs">@deal.Value.ToString("N0")</td>
<td><span class="chip @(deal.Status == "Posted" ? "chip-indigo" : "chip-amber")">@deal.Status</span></td>
</tr>
}
}
else
{
<tr><td colspan="5" class="text-xs text-center text-gray-400 py-4">No recent deals available</td></tr>
}
</tbody>
</table>
</div>
<div class="mt-3 flex justify-between text-xs">
<span>Active Deals: <b class="text-amber-600">@_data.ActiveDealsCount</b></span>
<span>Total Pipeline: <b>@_data.FormattedPipeline</b></span>
</div>
</div>
<div class="bg-white rounded-xl border border-gray-100 p-5" style="animation: fadeInUp .5s ease forwards; animation-delay: .5s;">
<div class="flex items-center justify-between mb-4">
<div><h3 class="font-bold text-gray-900">CRM Activity</h3><p class="text-xs text-gray-400 mt-0.5">Real-time updates</p></div>
<span class="flex items-center gap-1.5 text-xs"><span class="w-2 h-2 rounded-full bg-indigo-400" style="animation: pulse 2s infinite;"></span> <span class="text-indigo-600 font-medium">Live</span></span>
</div>
<div class="activity-feed divide-y divide-gray-100 pr-1">
@if (_data.ActivityFeeds.Any())
{
@foreach (var af in _data.ActivityFeeds)
{
<div class="flex gap-3 p-2 rounded-lg hover:bg-gray-50 transition">
<div class="w-8 h-8 rounded-full flex items-center justify-center text-white text-xs font-bold shrink-0" style="background:@af.AvatarBg">@af.AvatarText</div>
<div class="min-w-0 flex-1"><p class="text-sm text-gray-700"><b>@af.Title</b></p><p class="text-xs text-gray-400 mt-0.5">@af.Detail</p><p class="text-xs text-gray-400">@af.TimeAgo</p></div>
<span class="chip @af.ChipColor self-start">@af.ChipLabel</span>
</div>
}
}
else
{
<div class="flex gap-3 p-2"><p class="text-xs text-gray-400">No recent activity</p></div>
}
</div>
</div>
</div>
@* ROW 5: Conversion Metrics + Top Performers + Support Metrics *@
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="bg-white rounded-xl border border-gray-100 p-5" style="animation: fadeInUp .5s ease forwards; animation-delay: .4s;">
<h3 class="font-bold text-gray-900 mb-1">Conversion Metrics</h3>
<p class="text-xs text-gray-400 mb-3">@DateTime.Now.Year Performance</p>
<div class="space-y-2.5">
@foreach (var cm in _data.ConversionMetrics)
{
<div class="p-3 rounded-lg" style="background:#f8fafc;">
<div class="flex items-center gap-3">
<div class="gi" style="background:@cm.BarColor">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
</div>
<div class="flex-1 min-w-0">
<div class="flex justify-between items-center">
<p class="text-xs text-gray-500 font-semibold">@cm.Label</p>
<span class="text-xs font-semibold">@cm.Current / @cm.Target</span>
</div>
<div class="mt-2 progress-bar"><div class="fill" style="width:@($"{cm.PctValue}%");background:@cm.BarColor;"></div></div>
</div>
</div>
</div>
}
</div>
<div class="mt-3 pt-3 border-t border-gray-100 flex justify-between text-xs">
<span>Target CVR: <b class="text-green-600">@_data.TargetCvr%</b></span>
<span class="text-indigo-600">@(_data.OverallCvr >= 5 ? "On Track" : "Improving")</span>
</div>
</div>
<div class="bg-white rounded-xl border border-gray-100 p-5" style="animation: fadeInUp .5s ease forwards; animation-delay: .5s;">
<h3 class="font-bold text-gray-900 mb-1">Top Performers</h3>
<p class="text-xs text-gray-400 mb-3">This quarter ranking</p>
<div class="space-y-2.5">
@if (_data.SalesLeaderboard.Any())
{
var perfColors = new[] { "#f97316", "#f59e0b", "#6366f1", "#8b5cf6" };
@for (int i = 0; i < Math.Min(_data.SalesLeaderboard.Count, 4); i++)
{
var p = _data.SalesLeaderboard[i];
<div class="flex items-center gap-3 p-2.5 rounded-lg" style="background:#f8fafc;">
<div class="gi" style="background:@(perfColors[i % perfColors.Length])">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/></svg>
</div>
<div class="flex-1"><p class="text-xs text-gray-500">@p.Name</p><p class="text-sm font-bold text-gray-900">@p.Achievement.ToString("N0")</p></div>
<span class="text-xs font-semibold text-green-600">@p.WinRate.ToString("F0")%</span>
</div>
}
}
else
{
<p class="text-xs text-gray-400">No performance data available</p>
}
</div>
<div class="mt-2 flex justify-between text-xs"><span>Top Deals: <b>@_data.WonDealsCount</b></span><span>Avg Win: <b>@_data.AvgWinRate%</b></span></div>
</div>
<div class="bg-white rounded-xl border border-gray-100 p-5" style="animation: fadeInUp .5s ease forwards; animation-delay: .6s;">
<h3 class="font-bold text-gray-900 mb-1">Support Metrics</h3>
<p class="text-xs text-gray-400 mb-3">Customer service KPIs</p>
<div class="space-y-2.5">
<div class="flex items-center gap-3 p-2.5 rounded-lg" style="background:#f8fafc;">
<div class="gi gc4"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/></svg></div>
<div class="flex-1"><p class="text-xs text-gray-500">Active Deals</p><p class="text-xs text-gray-400">Total in progress</p></div>
<div class="text-right"><span class="chip chip-blue">@_data.ActiveDealsCount</span></div>
</div>
<div class="flex items-center gap-3 p-2.5 rounded-lg" style="background:#f8fafc;">
<div class="gi gc8"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"/></svg></div>
<div class="flex-1"><p class="text-xs text-gray-500">Total Contacts</p><p class="text-xs text-gray-400">Active contacts</p></div>
<div class="text-right"><span class="chip chip-purple">@_data.TotalContacts.ToString("N0")</span></div>
</div>
<div class="flex items-center gap-3 p-2.5 rounded-lg" style="background:#f8fafc;">
<div class="gi gc11"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/></svg></div>
<div class="flex-1"><p class="text-xs text-gray-500">Retention Rate</p><p class="text-xs text-gray-400">Customer loyalty</p></div>
<div class="text-right"><span class="chip chip-orange">@_data.RetentionRate%</span></div>
</div>
<div class="flex items-center gap-3 p-2.5 rounded-lg" style="background:#f8fafc;">
<div class="gi gc10"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg></div>
<div class="flex-1"><p class="text-xs text-gray-500">Budget</p><p class="text-xs text-gray-400">Total allocated</p></div>
<div class="text-right"><span class="chip chip-red">@_data.FormattedBudget</span></div>
</div>
</div>
</div>
</div>
@* ROW 6: Lead Overview + Upcoming Renewals *@
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div class="bg-white rounded-xl border border-gray-100 p-5" style="animation: fadeInUp .5s ease forwards; animation-delay: .5s;">
<div class="flex items-center justify-between mb-4">
<div><h3 class="font-bold text-gray-900">Lead Overview</h3><p class="text-xs text-gray-400 mt-0.5">As of @DateTime.Now.ToString("MMM dd, yyyy") · Recent leads</p></div>
</div>
<div class="p-3 rounded-lg" style="background:#f8fafc;">
<div class="grid-wrap">
<table class="grid-table" style="background:transparent;">
<thead><tr><th>Lead</th><th>Company</th><th>Amount</th><th>Stage</th></tr></thead>
<tbody>
@if (_data.RecentLeads.Any())
{
@foreach (var lead in _data.RecentLeads)
{
<tr>
<td class="text-xs">@lead.LeadTitle</td>
<td class="text-xs">@lead.Company</td>
<td class="font-mono text-xs text-green-600">@lead.Amount.ToString("N0")</td>
<td class="text-xs">@lead.Stage</td>
</tr>
}
}
else
{
<tr><td colspan="4" class="text-xs text-center text-gray-400 py-4">No leads available</td></tr>
}
</tbody>
</table>
</div>
</div>
<div class="mt-3 pt-3 border-t border-gray-100 flex justify-between text-xs font-semibold">
<span>Total Leads: <span class="text-green-600">@_data.FormattedTotalLeads</span></span>
<span>Avg CVR: <span class="text-indigo-600">@_data.OverallCvr%</span></span>
</div>
</div>
<div class="bg-white rounded-xl border border-gray-100 p-5" style="animation: fadeInUp .5s ease forwards; animation-delay: .6s;">
<div class="flex items-center justify-between mb-4">
<div><h3 class="font-bold text-gray-900">Overdue Invoices</h3><p class="text-xs text-gray-400 mt-0.5">Pending payments</p></div>
</div>
<div class="p-3 rounded-lg" style="background:#f8fafc;">
<div class="grid-wrap">
<table class="grid-table" style="background:transparent;">
<thead><tr><th>Invoice</th><th>Customer</th><th>Amount</th><th>Status</th></tr></thead>
<tbody>
@if (_data.OverdueInvoices.Any())
{
@foreach (var inv in _data.OverdueInvoices)
{
<tr>
<td class="font-mono text-xs">@inv.Number</td>
<td class="text-xs">@inv.Customer</td>
<td class="font-mono text-xs">@inv.Amount.ToString("N0")</td>
<td>
<span class="chip @(inv.Status.Contains("Full") ? "chip-green" : inv.Status.Contains("Partial") ? "chip-amber" : "chip-red")">
@inv.Status
</span>
</td>
</tr>
}
}
else
{
<tr><td colspan="4" class="text-xs text-center text-gray-400 py-4">No overdue invoices</td></tr>
}
</tbody>
</table>
</div>
</div>
<div class="mt-3 pt-3 border-t border-gray-100 flex justify-between text-xs">
<span>Total AR: <b class="text-gray-900">@_data.FormattedPendingPayments</b></span>
<span>Count: <b class="text-gray-900">@_data.OverdueInvoices.Count</b></span>
</div>
</div>
</div>
</div>
</div>
}
@code {
private DashboardCrmResponse _data = new();
private bool _isLoading = true;
private bool _chartsInitialized = false;
private bool _dataReady = false;
protected override async Task OnInitializedAsync()
{
_isLoading = true;
try
{
var res = await HomeService.GetCrmDashboardStatsAsync();
if (res?.IsSuccess == true && res.Value != null)
{
_data = res.Value;
_dataReady = true;
StateHasChanged();
}
}
finally
{
_isLoading = false;
}
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (_dataReady && !_chartsInitialized)
{
_chartsInitialized = true;
await InitCharts();
}
}
private async Task InitCharts()
{
var revenueLabels = _data.RevenueTrend.Select(x => x.Label).ToArray();
var revenueData = _data.RevenueTrend.Select(x => x.SalesAmount).ToArray();
var expenseData = _data.RevenueTrend.Select(x => x.PurchaseAmount).ToArray();
var sourceLabels = _data.LeadSources.Select(x => x.Source).ToArray();
var sourceData = _data.LeadSources.Select(x => x.Percentage).Cast<object>().ToArray();
var sourceColors = _data.LeadSources.Select(x => x.Color).ToArray();
var activityLabels = _data.SalesActivities.Take(4).Select(x => x.Category).ToArray();
var activityCounts = _data.SalesActivities.Take(4).Select(x => (double)x.Count).ToArray();
var activityColors = _data.SalesActivities.Take(4).Select(x => x.Color).ToArray();
await JS.InvokeVoidAsync("initDashboardCharts",
revenueLabels, revenueData, expenseData,
sourceLabels, sourceData, sourceColors,
activityLabels, activityCounts, activityColors);
}
}
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
@@keyframes fadeInUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
@@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
@@keyframes spin { to { transform: rotate(360deg); } }
.chart-container { position: relative; width: 100%; }
.progress-bar { height: 8px; border-radius: 999px; background: #e2e8f0; overflow: hidden; }
.progress-bar .fill { height: 100%; border-radius: 999px; transition: width .8s ease; }
.grid-table th { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .05em; color: #64748b; padding: 10px 12px; text-align: left; border-bottom: 1px solid #e2e8f0; }
.grid-table td { padding: 10px 12px; font-size: 13px; color: #334155; border-bottom: 1px solid #f1f5f9; }
.grid-table tr:hover td { background: #f8fafc; }
.grid-table { width: 100%; border-collapse: collapse; }
.grid-wrap { max-height: 340px; overflow-y: auto; scrollbar-width: thin; }
.grid-wrap::-webkit-scrollbar { width: 4px; }
.grid-wrap::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; }
.chip { display: inline-flex; align-items: center; padding: 3px 10px; border-radius: 999px; font-size: 10px; font-weight: 600; color: #fff; }
.chip-blue { background: #2563eb; }
.chip-green { background: #10b981; }
.chip-amber { background: #f59e0b; }
.chip-red { background: #e11d48; }
.chip-indigo { background: #6366f1; }
.chip-purple { background: #8b5cf6; }
.chip-pink { background: #ec4899; }
.chip-cyan { background: #06b6d4; }
.chip-orange { background: #f97316; }
.chip-teal { background: #14b8a6; }
.activity-feed { max-height: 320px; overflow-y: auto; scrollbar-width: thin; }
.activity-feed::-webkit-scrollbar { width: 4px; }
.activity-feed::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; }
.kpi-badge { display: inline-flex; align-items: center; padding: 2px 8px; border-radius: 999px; font-size: 10px; font-weight: 600; }
.kpi-card-sm { border-radius: 14px; padding: 18px; color: #fff; position: relative; overflow: hidden; min-height: 110px; display: flex; flex-direction: column; justify-content: space-between; }
.kpi-card-sm::after { content: ''; position: absolute; top: -30%; right: -20%; width: 80px; height: 80px; border-radius: 50%; background: rgba(255,255,255,.07); pointer-events: none; }
.kpi-card-sm .kpi-label { font-size: 9.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; opacity: .75; }
.kpi-card-sm .kpi-value { font-size: 20px; font-weight: 800; line-height: 1.1; margin-top: 2px; }
.kpi-card-sm .kpi-delta { font-size: 10px; font-weight: 600; }
.kpi-card-sm .kpi-icon { width: 32px; height: 32px; border-radius: 10px; display: flex; align-items: center; justify-content: center; background: rgba(255,255,255,.15); flex-shrink: 0; }
.kpi-card-sm .kpi-icon svg { width: 16px; height: 16px; stroke: #fff; stroke-width: 2; }
.kpi-b1 { background: #2563eb; } .kpi-b2 { background: #10b981; } .kpi-b3 { background: #ec4899; } .kpi-b4 { background: #8b5cf6; }
.kpi-b5 { background: #f59e0b; } .kpi-b6 { background: #06b6d4; } .kpi-b7 { background: #f97316; } .kpi-b8 { background: #14b8a6; }
.mm-cell { display: flex; align-items: center; gap: 12px; padding: 14px 16px; border-radius: 10px; transition: background .2s; }
.mm-cell:hover { background: #f8fafc; }
.mm-icon { width: 40px; height: 40px; border-radius: 10px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.mm-icon svg { width: 18px; height: 18px; stroke: #fff; stroke-width: 2; }
.mm-title { font-size: 12px; font-weight: 600; color: #334155; line-height: 1.2; }
.mm-sub { font-size: 10px; color: #94a3b8; margin-top: 1px; }
.mm-c1 { background: #2563eb; } .mm-c2 { background: #10b981; } .mm-c3 { background: #ec4899; } .mm-c4 { background: #8b5cf6; }
.mm-c5 { background: #f59e0b; } .mm-c6 { background: #06b6d4; } .mm-c7 { background: #f97316; } .mm-c8 { background: #14b8a6; }
.mm-c9 { background: #6366f1; } .mm-c10 { background: #e11d48; } .mm-c11 { background: #0891b2; } .mm-c12 { background: #7c3aed; }
.mm-c13 { background: #059669; } .mm-c14 { background: #d97706; } .mm-c15 { background: #db2777; } .mm-c16 { background: #0284c7; }
.mm-c17 { background: #65a30d; } .mm-c18 { background: #9333ea; } .mm-c19 { background: #ea580c; } .mm-c20 { background: #0d9488; }
.gi { width: 32px; height: 32px; border-radius: 8px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.gi svg { width: 16px; height: 16px; stroke: #fff; stroke-width: 2; }
.gc1 { background: #2563eb; } .gc2 { background: #10b981; } .gc3 { background: #ec4899; } .gc4 { background: #8b5cf6; }
.gc5 { background: #f59e0b; } .gc6 { background: #06b6d4; } .gc7 { background: #f97316; } .gc8 { background: #14b8a6; }
.gc9 { background: #6366f1; } .gc10 { background: #e11d48; } .gc11 { background: #0891b2; }
</style>
@* Chart initialization script *@
<script suppress-error="BL9992">
window.initDashboardCharts = function(revenueLabels, revenueData, expenseData, sourceLabels, sourceData, sourceColors, activityLabels, activityCounts, activityColors) {
var grayText = '#94a3b8';
var ctx1 = document.getElementById('plChart');
if (ctx1) {
new Chart(ctx1, {
type: 'bar',
data: {
labels: revenueLabels,
datasets: [
{ label: 'Revenue', data: revenueData, backgroundColor: '#2563eb', borderRadius: 4, barPercentage: 0.4 },
{ label: 'Expense', data: expenseData, backgroundColor: '#f97316', borderRadius: 4, barPercentage: 0.4 }
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
y: { beginAtZero: true, grid: { color: '#f1f5f9' }, ticks: { color: grayText, font: { size: 10 }, callback: function(v) { return '$' + v + 'K'; } } },
x: { grid: { display: false }, ticks: { color: grayText, font: { size: 9 } } }
}
}
});
}
var ctx2 = document.getElementById('expenseChart');
if (ctx2) {
new Chart(ctx2, {
type: 'doughnut',
data: {
labels: sourceLabels,
datasets: [{ data: sourceData, backgroundColor: sourceColors, borderWidth: 0 }]
},
options: {
responsive: true,
maintainAspectRatio: false,
cutout: '72%',
plugins: { legend: { display: false } }
}
});
}
var ctx3 = document.getElementById('cashflowChart');
if (ctx3) {
new Chart(ctx3, {
type: 'bar',
data: {
labels: activityLabels,
datasets: [{ label: 'Activity', data: activityCounts, backgroundColor: activityColors, borderRadius: 4, barPercentage: 0.5 }]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
y: { beginAtZero: true, grid: { color: '#f1f5f9' }, ticks: { color: grayText, font: { size: 10 } } },
x: { grid: { display: false }, ticks: { color: grayText, font: { size: 9 } } }
}
}
});
}
};
</script>
+411
View File
@@ -0,0 +1,411 @@
using Indotalent.ConfigBackEnd.Extensions;
using Indotalent.Infrastructure.Database;
using Indotalent.Data.Entities;
using Indotalent.Data.Enums;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace Indotalent.Features.Root.Home.Cqrs;
public class ChartDataPoint
{
public string? Label { get; set; }
public double SalesAmount { get; set; }
public double PurchaseAmount { get; set; }
}
public class ScmMetricDto
{
public string? Label { get; set; }
public string? Value { get; set; }
public string? Color { get; set; }
public double Percentage { get; set; }
}
public class SalesPipelineDto { public string? Stage { get; set; } public int Count { get; set; } public decimal Value { get; set; } }
public class LeadActivityDto { public string? Summary { get; set; } public string? LeadName { get; set; } public string? Type { get; set; } public DateTime? Date { get; set; } }
public class CampaignRoiDto { public string? Label { get; set; } public decimal Cost { get; set; } public decimal Revenue { get; set; } public double Roi { get; set; } }
public class SalesPerformanceDto { public string? Name { get; set; } public decimal Achievement { get; set; } public int Deals { get; set; } public double WinRate { get; set; } }
public class PendingInvoiceDto { public string? Number { get; set; } public string? Customer { get; set; } public decimal Amount { get; set; } public string? Status { get; set; } }
public class RecentLeadDto { public string? LeadTitle { get; set; } public string? Company { get; set; } public decimal Amount { get; set; } public string? Stage { get; set; } }
public class BantScoreDto { public string? Label { get; set; } public string? Value { get; set; } public string? Color { get; set; } }
public class UpcomingFollowUpDto { public string? LeadName { get; set; } public string? Summary { get; set; } public DateTime? ScheduledDate { get; set; } public string? Type { get; set; } }
// New DTOs for dashboard3 reference layout
public class DealStageDto { public string? Stage { get; set; } public int Count { get; set; } public string? Color { get; set; } }
public class LeadSourceDto { public string? Source { get; set; } public int Count { get; set; } public double Percentage { get; set; } public string? Color { get; set; } }
public class ConversionMetricDto { public string? Label { get; set; } public double Current { get; set; } public double Target { get; set; } public double PctValue { get; set; } public string? BarColor { get; set; } }
public class SalesActivityDto { public string? Category { get; set; } public int Count { get; set; } public string? Color { get; set; } }
public class RecentDealDto { public string? DealNumber { get; set; } public string? Description { get; set; } public decimal Value { get; set; } public string? Stage { get; set; } public string? Status { get; set; } public DateTimeOffset? Date { get; set; } }
public class ActivityFeedDto { public string? Title { get; set; } public string? Detail { get; set; } public string? TimeAgo { get; set; } public string? ChipLabel { get; set; } public string? ChipColor { get; set; } public string? AvatarBg { get; set; } public string? AvatarText { get; set; } }
public class PipelineSummaryDto { public string? Label { get; set; } public decimal Value { get; set; } public string? IconColorClass { get; set; } public string? FormattedValue { get; set; } }
public class DashboardCrmResponse
{
public int TotalLeads { get; set; }
public decimal TotalPipelineValue { get; set; }
public decimal MonthlySales { get; set; }
public double AvgWinRate { get; set; }
public int ActiveCampaigns { get; set; }
public int WonDealsCount { get; set; }
public decimal PendingPayments { get; set; }
public decimal TotalBudget { get; set; }
public decimal ActualExpense { get; set; }
public double RetentionRate { get; set; }
public decimal AvgDealSize { get; set; }
public double OverallCvr { get; set; }
public int ActiveDealsCount { get; set; }
public int TotalContacts { get; set; }
public int TotalCampaigns { get; set; }
public double SlaPercentage { get; set; }
public double EnterprisePercentage { get; set; }
public double TargetCvr { get; set; }
public List<SalesPipelineDto> PipelineBreakdown { get; set; } = new();
public List<LeadActivityDto> RecentActivities { get; set; } = new();
public List<CampaignRoiDto> TopCampaigns { get; set; } = new();
public List<SalesPerformanceDto> SalesLeaderboard { get; set; } = new();
public List<PendingInvoiceDto> OverdueInvoices { get; set; } = new();
public List<RecentLeadDto> RecentLeads { get; set; } = new();
public List<BantScoreDto> BantMetrics { get; set; } = new();
public List<UpcomingFollowUpDto> UpcomingFollowUps { get; set; } = new();
public List<ChartDataPoint> MonthlySalesVsPurchase { get; set; } = new();
public List<ChartDataPoint> WeeklySalesVsPurchase { get; set; } = new();
// New properties for dashboard3 layout
public List<DealStageDto> DealStages { get; set; } = new();
public List<LeadSourceDto> LeadSources { get; set; } = new();
public List<ConversionMetricDto> ConversionMetrics { get; set; } = new();
public List<SalesActivityDto> SalesActivities { get; set; } = new();
public List<RecentDealDto> RecentDeals { get; set; } = new();
public List<ActivityFeedDto> ActivityFeeds { get; set; } = new();
public List<PipelineSummaryDto> PipelineSummary { get; set; } = new();
public List<ChartDataPoint> RevenueTrend { get; set; } = new();
public string FormattedPipeline => FormatNumber(TotalPipelineValue);
public string FormattedMonthlySales => FormatNumber(MonthlySales);
public string FormattedPendingPayments => FormatNumber(PendingPayments);
public string FormattedBudget => FormatNumber(TotalBudget);
public string FormattedExpense => FormatNumber(ActualExpense);
public string FormattedAvgDealSize => FormatNumber(AvgDealSize);
public string FormattedTotalLeads => TotalLeads.ToString("N0");
private string FormatNumber(decimal value)
{
if (value >= 1000000)
return (value / 1000000m).ToString("F1") + "M";
if (value >= 1000)
return (value / 1000m).ToString("F1") + "K";
return value.ToString("N0");
}
}
public record GetCrmDashboardQuery() : IRequest<DashboardCrmResponse>;
public class DashboardHandler : IRequestHandler<GetCrmDashboardQuery, DashboardCrmResponse>
{
private readonly AppDbContext _context;
public DashboardHandler(AppDbContext context) => _context = context;
public async Task<DashboardCrmResponse> Handle(GetCrmDashboardQuery request, CancellationToken ct)
{
var today = DateTime.Today;
var monthStart = new DateTime(today.Year, today.Month, 1);
var pipeline = await _context.Lead.NotDeletedOnly()
.GroupBy(x => x.PipelineStage)
.Select(g => new SalesPipelineDto
{
Stage = g.Key.ToString(),
Count = g.Count(),
Value = g.Sum(x => x.AmountTargeted ?? 0)
}).ToListAsync(ct);
var leaders = await _context.SalesRepresentative.NotDeletedOnly()
.Take(5)
.Select(x => new SalesPerformanceDto
{
Name = x.Name,
Achievement = _context.Lead
.Where(l => l.SalesTeamId == x.SalesTeamId && l.ClosingStatus == ClosingStatus.ClosedWon)
.Sum(l => l.AmountClosed ?? 0),
Deals = _context.Lead
.Count(l => l.SalesTeamId == x.SalesTeamId && l.ClosingStatus == ClosingStatus.ClosedWon),
WinRate = _context.Lead.Where(l => l.SalesTeamId == x.SalesTeamId).Any()
? (double)_context.Lead.Count(l => l.SalesTeamId == x.SalesTeamId && l.ClosingStatus == ClosingStatus.ClosedWon)
/ _context.Lead.Count(l => l.SalesTeamId == x.SalesTeamId) * 100
: 0
}).OrderByDescending(x => x.Achievement).ToListAsync(ct);
var campaigns = await _context.Campaign.NotDeletedOnly()
.Take(3)
.Select(x => new CampaignRoiDto
{
Label = x.Title,
Cost = x.ExpenseList.Sum(e => e.Amount ?? 0),
Revenue = x.LeadList.Where(l => l.ClosingStatus == ClosingStatus.ClosedWon).Sum(l => l.AmountClosed ?? 0),
Roi = x.ExpenseList.Sum(e => e.Amount ?? 0) > 0
? (double)(x.LeadList.Where(l => l.ClosingStatus == ClosingStatus.ClosedWon).Sum(l => l.AmountClosed ?? 0) / x.ExpenseList.Sum(e => e.Amount ?? 0))
: 0
}).ToListAsync(ct);
var overdue = await _context.Invoice.NotDeletedOnly()
.Where(x => x.InvoiceStatus != InvoiceStatus.FullPaid)
.OrderByDescending(x => x.CreatedAt).Take(5)
.Select(x => new PendingInvoiceDto
{
Number = x.AutoNumber,
Customer = x.SalesOrder!.Customer!.Name,
Amount = x.SalesOrder.AfterTaxAmount ?? 0,
Status = x.InvoiceStatus.ToString()
}).ToListAsync(ct);
var recentLeads = await _context.Lead.NotDeletedOnly()
.OrderByDescending(x => x.CreatedAt).Take(5)
.Select(x => new RecentLeadDto
{
LeadTitle = x.Title,
Company = x.CompanyName,
Amount = x.AmountTargeted ?? 0,
Stage = x.PipelineStage.ToString()
}).ToListAsync(ct);
var recentActivities = await _context.LeadActivity.NotDeletedOnly()
.OrderByDescending(x => x.FromDate).Take(10)
.Select(x => new LeadActivityDto
{
Summary = x.Summary,
LeadName = x.Lead!.Title,
Type = x.Type.ToString(),
Date = x.FromDate
}).ToListAsync(ct);
var followUps = await _context.LeadActivity.NotDeletedOnly()
.Where(x => x.FromDate >= today && (x.Type == LeadActivityType.Phone || x.Type == LeadActivityType.Meeting))
.OrderBy(x => x.FromDate)
.Take(5)
.Select(x => new UpcomingFollowUpDto
{
LeadName = x.Lead!.Title,
Summary = x.Summary,
ScheduledDate = x.FromDate,
Type = x.Type.ToString()
}).ToListAsync(ct);
var totalLeadsCount = await _context.Lead.NotDeletedOnly().CountAsync(ct);
var wonLeadsCount = await _context.Lead.NotDeletedOnly().CountAsync(x => x.ClosingStatus == ClosingStatus.ClosedWon, ct);
var bantMetrics = new List<BantScoreDto>
{
new() { Label = "Hot Deals", Value = await _context.Lead.CountAsync(x => x.BudgetScore > 80 && x.ClosingStatus == ClosingStatus.OnProgress, ct).ContinueWith(t => t.Result.ToString()), Color = "#f59e0b" },
new() { Label = "Budget Ready", Value = await _context.Lead.CountAsync(x => x.BudgetScore >= 70, ct).ContinueWith(t => t.Result.ToString()), Color = "#f59e0b" },
new() { Label = "Decision Maker", Value = await _context.Lead.CountAsync(x => x.AuthorityScore >= 70, ct).ContinueWith(t => t.Result.ToString()), Color = "#10b981" },
new() { Label = "Timeline Ready", Value = await _context.Lead.CountAsync(x => x.TimelineScore >= 70, ct).ContinueWith(t => t.Result.ToString()), Color = "#3b82f6" },
new() { Label = "SQL", Value = await _context.Lead.CountAsync(x => x.PipelineStage >= PipelineStage.Qualification, ct).ContinueWith(t => t.Result.ToString()), Color = "#64748b" },
new() { Label = "MQL", Value = totalLeadsCount.ToString(), Color = "#1e293b" }
};
var monthlyLabels = new[] { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
var monthlyData = new List<ChartDataPoint>();
var soMonths = await _context.SalesOrder
.Where(x => x.OrderDate != null)
.Select(x => x.OrderDate!.Value)
.ToListAsync(ct);
var poMonths = await _context.PurchaseOrder
.Where(x => x.OrderDate != null)
.Select(x => x.OrderDate!.Value)
.ToListAsync(ct);
var allDates = soMonths.Concat(poMonths)
.Select(d => new { d.Year, d.Month })
.Distinct()
.OrderByDescending(x => x.Year).ThenByDescending(x => x.Month)
.Take(6)
.Reverse()
.ToList();
foreach (var ym in allDates)
{
var monthStartDt = new DateTime(ym.Year, ym.Month, 1);
var monthEndDt = monthStartDt.AddMonths(1);
var salesAmount = await _context.SalesOrder
.Where(x => x.OrderDate >= monthStartDt && x.OrderDate < monthEndDt)
.SumAsync(x => (double?)x.AfterTaxAmount ?? 0, ct);
var purchaseAmount = await _context.PurchaseOrder
.Where(x => x.OrderDate >= monthStartDt && x.OrderDate < monthEndDt)
.SumAsync(x => (double?)x.AfterTaxAmount ?? 0, ct);
monthlyData.Add(new ChartDataPoint
{
Label = monthlyLabels[ym.Month - 1],
SalesAmount = Math.Round(salesAmount / 1000.0, 1),
PurchaseAmount = Math.Round(purchaseAmount / 1000.0, 1)
});
}
var weeklyData = new List<ChartDataPoint>();
var soWeeks = await _context.SalesOrder
.Where(x => x.OrderDate != null)
.Select(x => x.OrderDate!.Value)
.ToListAsync(ct);
var poWeeks = await _context.PurchaseOrder
.Where(x => x.OrderDate != null)
.Select(x => x.OrderDate!.Value)
.ToListAsync(ct);
var allWeekStarts = soWeeks.Concat(poWeeks)
.Select(d => d.AddDays(-(int)d.DayOfWeek))
.Distinct()
.OrderByDescending(w => w)
.Take(6)
.Reverse()
.ToList();
foreach (var weekStart in allWeekStarts)
{
var weekEnd = weekStart.AddDays(7);
var salesAmount = await _context.SalesOrder
.Where(x => x.OrderDate >= weekStart && x.OrderDate < weekEnd)
.SumAsync(x => (double?)x.AfterTaxAmount ?? 0, ct);
var purchaseAmount = await _context.PurchaseOrder
.Where(x => x.OrderDate >= weekStart && x.OrderDate < weekEnd)
.SumAsync(x => (double?)x.AfterTaxAmount ?? 0, ct);
weeklyData.Add(new ChartDataPoint
{
Label = $"Wk {weekStart:MM/dd}",
SalesAmount = Math.Round(salesAmount / 1000.0, 1),
PurchaseAmount = Math.Round(purchaseAmount / 1000.0, 1)
});
}
// New queries for dashboard3 layout
var totalPipeline = pipeline.Sum(x => x.Value);
var totalClosedWon = await _context.Lead.NotDeletedOnly()
.Where(x => x.ClosingStatus == ClosingStatus.ClosedWon)
.SumAsync(x => x.AmountClosed ?? 0, ct);
var dealStages = new List<DealStageDto>
{
new() { Stage = "Prospecting", Count = await _context.Lead.NotDeletedOnly().CountAsync(x => x.PipelineStage == PipelineStage.Prospecting, ct), Color = "#8b5cf6" },
new() { Stage = "Qualification", Count = await _context.Lead.NotDeletedOnly().CountAsync(x => x.PipelineStage == PipelineStage.Qualification, ct), Color = "#f59e0b" },
new() { Stage = "Negotiation", Count = await _context.Lead.NotDeletedOnly().CountAsync(x => x.PipelineStage == PipelineStage.Negotiation, ct), Color = "#e11d48" },
new() { Stage = "Closed Won", Count = wonLeadsCount, Color = "#6366f1" }
};
var pipelineSummaryList = new List<PipelineSummaryDto>
{
new() { Label = "Total Pipeline", Value = totalPipeline, FormattedValue = totalPipeline >= 1000000 ? $"${totalPipeline / 1000000:F2}M" : totalPipeline >= 1000 ? $"${totalPipeline / 1000:F1}K" : $"${totalPipeline:N0}", IconColorClass = "gc1" },
new() { Label = "Closed Won", Value = totalClosedWon, FormattedValue = totalClosedWon >= 1000000 ? $"${totalClosedWon / 1000000:F2}M" : totalClosedWon >= 1000 ? $"${totalClosedWon / 1000:F1}K" : $"${totalClosedWon:N0}", IconColorClass = "gc10" },
new() { Label = "Avg Deal Size", Value = wonLeadsCount > 0 ? totalClosedWon / wonLeadsCount : 0, IconColorClass = "gc2" }
};
var leadSourceRaw = await _context.Lead.NotDeletedOnly()
.Select(x => x.Campaign!.Title ?? "Organic")
.ToListAsync(ct);
var leadSourceGroups = leadSourceRaw
.GroupBy(x => x)
.Select(g => new { Source = g.Key, Count = g.Count() })
.OrderByDescending(x => x.Count)
.Take(5)
.ToList();
var totalSource = leadSourceGroups.Sum(x => x.Count);
var sourceColors = new[] { "#2563eb", "#f97316", "#ec4899", "#8b5cf6", "#06b6d4" };
var leadSources = leadSourceGroups.Select((g, i) => new LeadSourceDto
{
Source = g.Source,
Count = g.Count,
Percentage = totalSource > 0 ? Math.Round((double)g.Count / totalSource * 100, 1) : 0,
Color = i < sourceColors.Length ? sourceColors[i] : "#94a3b8"
}).ToList();
var prospectingCount = await _context.Lead.NotDeletedOnly().CountAsync(x => x.PipelineStage == PipelineStage.Prospecting, ct);
var qualifiedCount = await _context.Lead.NotDeletedOnly().CountAsync(x => x.PipelineStage == PipelineStage.Qualification, ct);
var mqlCount = totalLeadsCount;
var sqlCount = await _context.Lead.NotDeletedOnly().CountAsync(x => x.PipelineStage >= PipelineStage.Qualification, ct);
var conversionMetrics = new List<ConversionMetricDto>
{
new() { Label = "Leads→MQL", Current = sqlCount, Target = totalLeadsCount, PctValue = totalLeadsCount > 0 ? Math.Round((double)sqlCount / totalLeadsCount * 100, 1) : 0, BarColor = "#2563eb" },
new() { Label = "MQL→SQL", Current = qualifiedCount, Target = totalLeadsCount, PctValue = totalLeadsCount > 0 ? Math.Round((double)qualifiedCount / totalLeadsCount * 100, 1) : 0, BarColor = "#f97316" },
new() { Label = "SQL→Won", Current = wonLeadsCount, Target = sqlCount > 0 ? sqlCount : 1, PctValue = sqlCount > 0 ? Math.Round((double)wonLeadsCount / sqlCount * 100, 1) : 0, BarColor = "#ec4899" },
new() { Label = "Overall CVR", Current = wonLeadsCount, Target = totalLeadsCount, PctValue = totalLeadsCount > 0 ? Math.Round((double)wonLeadsCount / totalLeadsCount * 100, 1) : 0, BarColor = "#10b981" }
};
var recentDeals = await _context.Lead.NotDeletedOnly()
.OrderByDescending(x => x.CreatedAt).Take(4)
.Select(x => new RecentDealDto
{
DealNumber = x.AutoNumber,
Description = x.Title,
Value = x.AmountTargeted ?? 0,
Stage = x.PipelineStage.ToString(),
Status = x.ClosingStatus == ClosingStatus.ClosedWon ? "Posted" : x.ClosingStatus == ClosingStatus.ClosedLost ? "Lost" : "Pending",
Date = x.CreatedAt
}).ToListAsync(ct);
var activityFeed = recentActivities.Take(4).Select((a, i) => new ActivityFeedDto
{
Title = a.Summary ?? a.LeadName,
Detail = $"Type: {a.Type} · Lead: {a.LeadName}",
TimeAgo = a.Date.HasValue ? (DateTime.Now - a.Date.Value).TotalMinutes < 60 ? $"{(int)(DateTime.Now - a.Date.Value).TotalMinutes} min ago" : $"{(int)(DateTime.Now - a.Date.Value).TotalHours} hour ago" : "",
ChipLabel = a.Type,
ChipColor = i % 2 == 0 ? "chip-green" : "chip-indigo",
AvatarBg = i switch { 0 => "#10b981", 1 => "#2563eb", 2 => "#6366f1", _ => "#f97316" },
AvatarText = a.Type?.Substring(0, Math.Min(a.Type.Length, 2)).ToUpper()
}).ToList();
var salesActivities = new List<SalesActivityDto>
{
new() { Category = "Calls", Count = await _context.LeadActivity.NotDeletedOnly().CountAsync(x => x.Type == LeadActivityType.Phone, ct), Color = "#10b981" },
new() { Category = "Emails", Count = await _context.LeadActivity.NotDeletedOnly().CountAsync(x => x.Type == LeadActivityType.Email, ct), Color = "#e11d48" },
new() { Category = "Meetings", Count = await _context.LeadActivity.NotDeletedOnly().CountAsync(x => x.Type == LeadActivityType.Meeting, ct), Color = "#e11d48" },
new() { Category = "Total", Count = await _context.LeadActivity.NotDeletedOnly().CountAsync(ct), Color = "#6366f1" }
};
var slaPercentage = totalLeadsCount > 0 ? Math.Round((double)wonLeadsCount / totalLeadsCount * 100, 1) : 94.2;
var enterprisePercentage = totalPipeline > 0 ? Math.Round((double)(totalClosedWon / totalPipeline) * 100, 1) : 35;
var targetCvr = 5.0;
var revenueTrend = monthlyData;
var avgDealSize = wonLeadsCount > 0 ? totalClosedWon / wonLeadsCount : 0;
var overallCvr = totalLeadsCount > 0 ? Math.Round((double)wonLeadsCount / totalLeadsCount * 100, 1) : 0;
var activeDeals = await _context.Lead.NotDeletedOnly().CountAsync(x => x.ClosingStatus == ClosingStatus.OnProgress, ct);
var totalContacts = await _context.LeadContact.NotDeletedOnly().CountAsync(ct);
var totalCampaigns = await _context.Campaign.NotDeletedOnly().CountAsync(ct);
return new DashboardCrmResponse
{
TotalLeads = totalLeadsCount,
TotalPipelineValue = totalPipeline,
MonthlySales = await _context.SalesOrder.NotDeletedOnly().Where(x => x.OrderDate >= monthStart).SumAsync(x => x.AfterTaxAmount ?? 0, ct),
AvgWinRate = totalLeadsCount > 0 ? Math.Round((double)wonLeadsCount / totalLeadsCount * 100, 1) : 0,
ActiveCampaigns = await _context.Campaign.NotDeletedOnly().CountAsync(x => x.Status == CampaignStatus.OnProgress, ct),
WonDealsCount = wonLeadsCount,
PendingPayments = await _context.Invoice.NotDeletedOnly().Where(x => x.InvoiceStatus != InvoiceStatus.FullPaid).SumAsync(x => x.SalesOrder!.AfterTaxAmount ?? 0, ct),
TotalBudget = await _context.Budget.NotDeletedOnly().SumAsync(x => x.Amount ?? 0, ct),
ActualExpense = await _context.Expense.NotDeletedOnly().SumAsync(x => x.Amount ?? 0, ct),
RetentionRate = 92.4,
SlaPercentage = slaPercentage,
EnterprisePercentage = enterprisePercentage,
TargetCvr = targetCvr,
AvgDealSize = avgDealSize,
OverallCvr = overallCvr,
ActiveDealsCount = activeDeals,
TotalContacts = totalContacts,
TotalCampaigns = totalCampaigns,
PipelineBreakdown = pipeline,
SalesLeaderboard = leaders,
TopCampaigns = campaigns,
OverdueInvoices = overdue,
RecentLeads = recentLeads,
RecentActivities = recentActivities,
BantMetrics = bantMetrics,
UpcomingFollowUps = followUps,
MonthlySalesVsPurchase = monthlyData,
WeeklySalesVsPurchase = weeklyData,
DealStages = dealStages,
LeadSources = leadSources,
ConversionMetrics = conversionMetrics,
SalesActivities = salesActivities,
RecentDeals = recentDeals,
ActivityFeeds = activityFeed,
PipelineSummary = pipelineSummaryList,
RevenueTrend = revenueTrend
};
}
}
+23
View File
@@ -0,0 +1,23 @@
using Indotalent.ConfigBackEnd.Extensions;
using Indotalent.Features.Root.Home.Cqrs;
using MediatR;
using Microsoft.AspNetCore.Authentication.JwtBearer;
namespace Indotalent.Features.Root.Home;
public static class HomeEndpoint
{
public static void MapHomeEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/dashboard").WithTags("Dashboard")
.RequireAuthorization(policy => policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser());
group.MapGet("/crm-stats", async (IMediator mediator) =>
{
var result = await mediator.Send(new GetCrmDashboardQuery());
return result.ToApiResponse("CRM Dashboard data retrieved successfully");
})
.WithName("GetCrmDashboardStats");
}
}
+28
View File
@@ -0,0 +1,28 @@
using Indotalent.ConfigBackEnd.Interfaces;
using Indotalent.ConfigFrontEnd.Common;
using Indotalent.Features.Root.Home.Cqrs;
using Indotalent.Infrastructure.Authentication.Identity;
using Indotalent.Shared.Models;
using Microsoft.AspNetCore.Components;
using MudBlazor;
using RestSharp;
namespace Indotalent.Features.Root.Home;
public class HomeService : BaseService
{
private readonly RestClient _client;
public HomeService(IHttpClientFactory clientFactory, NavigationManager nav, ISnackbar snackbar, ICurrentUserService currentUserService, TokenProvider tokenProvider)
: base(clientFactory, nav, snackbar, currentUserService, tokenProvider)
{
_client = new RestClient(nav.BaseUri);
}
public async Task<ApiResponse<DashboardCrmResponse>?> GetCrmDashboardStatsAsync()
{
var request = new RestRequest("api/dashboard/crm-stats", Method.Get);
request.AddHeader("Accept", "application/json");
return await ExecuteWithResponseAsync<DashboardCrmResponse>(_client, request);
}
}
+258
View File
@@ -0,0 +1,258 @@
@using Indotalent.ConfigBackEnd.Extensions
@using Indotalent.ConfigBackEnd.Interfaces
@using Indotalent.Infrastructure.Authentication
@using Indotalent.Infrastructure.Authorization.Identity
@using Indotalent.Shared.Consts
@using Microsoft.AspNetCore.Components.Authorization
@inherits LayoutComponentBase
@inject NavigationManager NavigationManager
@inject ICurrentUserService CurrentUserService
<MudThemeProvider />
<MudDialogProvider />
<MudSnackbarProvider />
<MudPopoverProvider />
<MudLayout>
<AuthorizeView>
<MudAppBar Elevation="1">
<MudSpacer />
<MudMenu Icon="@Icons.Material.Filled.MoreVert" Color="Color.Inherit" Edge="Edge.End" Dense="true"
ActivationEvent="@MouseEvent.LeftClick" AnchorOrigin="Origin.BottomCenter" TransformOrigin="Origin.TopCenter">
<MudMenuItem OnClick="GoToProfile">Profile</MudMenuItem>
<MudMenuItem OnClick="Logout" Color="Color.Error">Logout</MudMenuItem>
</MudMenu>
</MudAppBar>
<MudDrawer @bind-Open="_drawerOpen"
Elevation="0"
Variant="@DrawerVariant.Mini"
OpenMiniOnHover="false"
MiniWidth="80px"
Width="280px"
Class="v6-drawer">
<MudIconButton Icon="@(_drawerOpen? Icons.Material.Rounded.ChevronLeft : Icons.Material.Rounded.ChevronRight)"
OnClick="@DrawerToggle"
Class="v6-toggle-btn"
Size="Size.Small" />
<MudDrawerHeader Class="pa-6 mb-2">
<div class="d-flex align-center">
<MudIcon Icon="@Icons.Material.Filled.Apps" Size="Size.Medium" Color="Color.Primary" />
@if (_drawerOpen)
{
<MudText Typo="Typo.h5" Class="ml-3" Style="font-weight:900">@GlobalConsts.AppInitial</MudText>
}
</div>
</MudDrawerHeader>
<MudNavMenu Class="v6-nav">
@* Member & Admin *@
<AuthorizeView Roles="@($"{ApplicationRoles.Admin},{ApplicationRoles.Member}")" Context="commonAuth">
<Authorized>
<MudNavLink Href="/home" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Home">@(_drawerOpen ? "Home" : "")</MudNavLink>
<MudNavLink Href="/profile" Icon="@Icons.Material.Filled.AccountCircle">@(_drawerOpen ? "Profile" : "")</MudNavLink>
<MudNavLink Href="/pipeline" Icon="@Icons.Material.Filled.AccountTree">@(_drawerOpen ? "Pipeline" : "")</MudNavLink>
<MudNavLink Href="/thirdparty" Icon="@Icons.Material.Filled.Groups">@(_drawerOpen ? "Third Party" : "")</MudNavLink>
<MudNavLink Href="/sales" Icon="@Icons.Material.Filled.MonetizationOn">@(_drawerOpen ? "Sales" : "")</MudNavLink>
<MudNavLink Href="/purchase" Icon="@Icons.Material.Filled.ShoppingCart">@(_drawerOpen ? "Purchase" : "")</MudNavLink>
<MudNavLink Href="/inventory" Icon="@Icons.Material.Filled.Inventory2">@(_drawerOpen ? "Inventory" : "")</MudNavLink>
<MudNavLink Href="/utilities" Icon="@Icons.Material.Filled.Build">@(_drawerOpen ? "Utilities" : "")</MudNavLink>
</Authorized>
</AuthorizeView>
@* Admin Only *@
<AuthorizeView Roles="@ApplicationRoles.Admin" Context="adminAuth">
<Authorized>
<MudNavLink Href="/serilogs" Icon="@Icons.Material.Filled.HistoryEdu">@(_drawerOpen ? "System Logs" : "")</MudNavLink>
<MudNavLink Href="/setting" Icon="@Icons.Material.Filled.Settings">@(_drawerOpen ? "App Settings" : "")</MudNavLink>
<MudNavLink Href="/appsettings/json" Icon="@Icons.Material.Filled.SettingsEthernet">@(_drawerOpen ? "System Settings" : "")</MudNavLink>
</Authorized>
</AuthorizeView>
</MudNavMenu>
<div class="v6-footer d-flex align-center @(_drawerOpen ? "" : "justify-center")">
<MudBadge Color="Color.Success" Overlap="true" Dot="true" Bordered="true" Placement="Placement.BottomRight">
<MudAvatar Color="Color.Primary" Size="Size.Medium">@CurrentUserService.FullName.ToInitial()</MudAvatar>
</MudBadge>
@if (_drawerOpen)
{
<div class="ml-3">
<MudText Typo="Typo.caption" Style="font-weight:700; display: block; color: #1e293b;">@CurrentUserService.FullName</MudText>
<MudText Typo="Typo.caption" Style="color: #64748b; font-size: 10px;">Online</MudText>
</div>
}
</div>
</MudDrawer>
</AuthorizeView>
<MudMainContent Style="background-color: #f1f5f9;">
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pa-3 md-pa-6 lg-pa-8 pt-3 pb-12">
@Body
</MudContainer>
</MudMainContent>
</MudLayout>
<script>
function downloadFile(fileName, contentType, base64String) {
const link = document.createElement('a');
link.download = fileName;
link.href = `data:${contentType};base64,${base64String}`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
</script>
@code {
private bool _drawerOpen = true;
private void DrawerToggle() => _drawerOpen = !_drawerOpen;
private void GoToProfile() => NavigationManager.NavigateTo("/profile");
private void Logout() => NavigationManager.NavigateTo("/account/logout");
}
<style>
.mud-input-control:has(.mud-input-adornment-start) {
margin-top: 0 !important;
height: 35px !important;
}
.mud-input-control:has(.mud-input-adornment-start) .mud-input-outlined-border {
height: 35px !important;
}
.mud-input-control:has(.mud-input-adornment-start) input {
padding-top: 0px !important;
padding-bottom: 0px !important;
height: 30px !important;
font-size: 13px !important;
}
.mud-input-control:has(.mud-input-adornment-start) .mud-input-adornment {
margin-top: 0 !important;
margin-bottom: 0 !important;
height: 35px !important;
}
div:has(> .mud-input-control:has(.mud-input-adornment-start)) > .mud-button-root {
height: 35px !important;
box-shadow: none !important;
}
</style>
<style>
.mud-input-outlined-border {
border-radius: 4px !important;
}
.custom-select-dense .mud-input-control {
margin-top: 0 !important;
}
.custom-select-dense .mud-input-slot {
padding-top: 4px !important;
padding-bottom: 4px !important;
padding-left: 8px !important;
font-size: 12px !important;
}
</style>
<style>
.mud-drawer.v6-drawer {
overflow: visible !important;
z-index: 1350 !important;
background-color: #f8fafc !important;
border-right: 1px solid var(--mud-palette-divider) !important;
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.v6-drawer .mud-drawer-content {
overflow-y: auto !important;
overflow-x: hidden !important;
display: flex;
flex-direction: column;
height: 100%;
}
.v6-toggle-btn {
position: absolute !important;
right: -16px !important;
top: 18px !important;
z-index: 1400 !important;
background-color: white !important;
border: 1px solid var(--mud-palette-divider) !important;
width: 32px !important;
height: 32px !important;
min-width: 32px !important;
border-radius: 50% !important;
box-shadow: 0 4px 6px rgba(0,0,0,0.1) !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
.v6-nav .mud-nav-link,
.v6-nav .mud-nav-group {
max-width: 100% !important;
box-sizing: border-box;
}
.v6-drawer.mud-drawer--mini .v6-nav .mud-nav-link,
.v6-drawer.mud-drawer--mini .v6-nav .mud-nav-group-button {
padding-left: 0px !important;
padding-right: 0px !important;
display: flex !important;
justify-content: center !important;
width: 80px !important;
}
.v6-drawer.mud-drawer--mini .v6-nav .mud-nav-link-icon,
.v6-drawer.mud-drawer--mini .v6-nav .mud-nav-group-icon {
margin-right: 0px !important;
margin-inline-start: 0px !important;
display: flex !important;
justify-content: center !important;
}
.v6-nav .mud-nav-link {
border-radius: 12px;
margin: 4px 12px;
color: #64748b;
}
.v6-nav .mud-nav-link.active {
background-color: white !important;
color: var(--mud-palette-primary) !important;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
font-weight: 600;
}
.v6-footer {
margin-top: auto;
padding: 16px;
border-top: 1px solid var(--mud-palette-divider);
background-color: #f8fafc;
flex-shrink: 0;
}
.mud-badge-dot {
border: 2px solid white !important;
}
</style>
+22
View File
@@ -0,0 +1,22 @@
#blazor-error-ui {
color-scheme: light only;
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
box-sizing: border-box;
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
+33
View File
@@ -0,0 +1,33 @@
@page "/not-found"
@using Microsoft.AspNetCore.Mvc
@layout MainLayout
@attribute [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
<MudContainer MaxWidth="MaxWidth.Medium" Class="d-flex align-center justify-center" Style="height: calc(100vh - 64px);">
<div class="text-center">
<MudIcon Icon="@Icons.Material.Filled.ErrorOutline"
Size="Size.Large"
Color="Color.Error"
Class="mb-6" Style="font-size: 120px;" />
<MudText Typo="Typo.h3" Class="mb-4">Page Not Found (404)</MudText>
<MudText Typo="Typo.body1" Class="mb-6 text-muted">
Sorry, the page you are looking for is unavailable or has been moved.
<br />
Please double-check the URL or return to the home page.
</MudText>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Size="Size.Large"
Href="/"
StartIcon="@Icons.Material.Filled.Home">
Back to Home
</MudButton>
</div>
</MudContainer>
+380
View File
@@ -0,0 +1,380 @@
@page "/"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CRM - Blazor Enterprise CRM Source Code</title>
<script src="https://cdn.tailwindcss.com"></script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.13.0/dist/cdn.min.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;450;500;600;700;800&display=swap" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html { scroll-behavior: smooth; }
body { font-family: "Poppins", sans-serif; background: #ffffff; color: #1e293b; }
.nav-link { position: relative; color: #64748b; font-size: 0.875rem; font-weight: 500; transition: color 0.2s; cursor: pointer; text-decoration: none; }
.nav-link:hover { color: #6366F1; }
.nav-link.active { color: #6366F1; font-weight: 600; }
.nav-link.active::after { content: ''; position: absolute; bottom: -2px; left: 0; width: 100%; height: 2px; background: #6366F1; border-radius: 1px; }
.btn-primary { display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.75rem 1.75rem; background: #6366F1; color: #fff; font-size: 0.875rem; font-weight: 600; border-radius: 0.5rem; transition: all 0.2s; cursor: pointer; border: none; }
.btn-primary:hover { background: #4F46E5; transform: translateY(-1px); box-shadow: 0 4px 12px rgba(99,102,241,0.25); }
.btn-outline { display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.75rem 1.75rem; background: transparent; color: #6366F1; font-size: 0.875rem; font-weight: 600; border-radius: 0.5rem; border: 1.5px solid #C7D2FE; transition: all 0.2s; cursor: pointer; }
.btn-outline:hover { border-color: #6366F1; background: #EEF2FF; }
.tech-card { padding: 1.5rem; background: #ffffff; border: 1px solid #f1f5f9; border-radius: 0.75rem; transition: all 0.25s; cursor: default; }
.tech-card:hover { border-color: #C7D2FE; box-shadow: 0 4px 16px rgba(99,102,241,0.06); transform: translateY(-2px); }
.tech-card .ico { width: 2.5rem; height: 2.5rem; display: flex; align-items: center; justify-content: center; border-radius: 0.5rem; background: #EEF2FF; color: #6366F1; margin-bottom: 0.75rem; }
.tech-card .ico svg { width: 1.25rem; height: 1.25rem; }
.feature-icon { width: 3rem; height: 3rem; display: flex; align-items: center; justify-content: center; border-radius: 0.75rem; background: #EEF2FF; color: #6366F1; flex-shrink: 0; }
.feature-icon svg { width: 1.5rem; height: 1.5rem; }
.footer-link { color: #94a3b8; font-size: 0.8125rem; transition: color 0.2s; cursor: pointer; text-decoration: none; }
.footer-link:hover { color: #ffffff; }
.gradient-text { background: linear-gradient(135deg, #6366F1 0%, #A5B4FC 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
.hero-badge { display: inline-flex; align-items: center; gap: 0.375rem; padding: 0.375rem 1rem; background: #EEF2FF; border: 1px solid #C7D2FE; border-radius: 9999px; font-size: 0.8125rem; color: #6366F1; font-weight: 500; }
#fab-top { position: fixed; bottom: 2rem; right: 2rem; z-index: 100; width: 3rem; height: 3rem; display: flex; align-items: center; justify-content: center; background: #6366F1; color: #fff; border: none; border-radius: 9999px; cursor: pointer; box-shadow: 0 4px 12px rgba(99,102,241,0.3); transition: all 0.25s; opacity: 0; visibility: hidden; transform: translateY(10px); }
#fab-top.show { opacity: 1; visibility: visible; transform: translateY(0); }
#fab-top:hover { background: #4F46E5; transform: translateY(-2px); box-shadow: 0 6px 16px rgba(99,102,241,0.35); }
#fab-top svg { width: 1.25rem; height: 1.25rem; }
[x-cloak] { display: none !important; }
</style>
</head>
<body x-data="{ atTop: true, activeSection: 'home' }" x-init="initScrollSpy(); window.addEventListener('scroll', () => { atTop = window.scrollY < 300; })">
<!-- ===== NAV ===== -->
<!-- NAV -->
<nav class="sticky top-0 z-50 bg-white/95 backdrop-blur border-b border-indigo-50" x-data="{ mobileOpen: false }">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-16">
<div class="flex items-center gap-10">
<a class="flex items-center gap-2 text-indigo-600 font-bold text-lg no-underline" href="#">
<svg class="w-7 h-7 text-indigo-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg>
<span>CRM</span>
</a>
<div class="hidden md:flex items-center gap-1">
<a class="nav-link active px-3 py-1.5 rounded-md bg-indigo-50" href="#home">Home</a>
<a class="nav-link px-3 py-1.5 rounded-md" href="#features">Features</a>
<a class="nav-link px-3 py-1.5 rounded-md" href="#modules">Modules</a>
<a class="nav-link px-3 py-1.5 rounded-md" href="#tech">Technology</a>
<a class="nav-link px-3 py-1.5 rounded-md" href="#demo">Live Demo</a>
</div>
</div>
<div class="hidden md:flex items-center gap-3">
<a class="nav-link" href="/home">Sign In</a>
<a href="https://indotalent.com" target="_blank" class="btn-primary no-underline">Get Source Code</a>
</div>
<button class="md:hidden p-2 text-indigo-500 hover:text-indigo-700" @@click="mobileOpen = !mobileOpen">
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16"/></svg>
</button>
</div>
<div x-show="mobileOpen" x-cloak class="md:hidden pb-4 space-y-2">
<a class="block px-3 py-2 text-sm bg-indigo-50 rounded-md font-medium" style="color: #6366F1;" href="#home">Home</a>
<a class="block px-3 py-2 text-sm text-gray-600 hover:text-indigo-600 rounded-md hover:bg-indigo-50" href="#features">Features</a>
<a class="block px-3 py-2 text-sm text-gray-600 hover:text-indigo-600 rounded-md hover:bg-indigo-50" href="#modules">Modules</a>
<a class="block px-3 py-2 text-sm text-gray-600 hover:text-indigo-600 rounded-md hover:bg-indigo-50" href="#tech">Technology</a>
<a class="block px-3 py-2 text-sm text-gray-600 hover:text-indigo-600 rounded-md hover:bg-indigo-50" href="#demo">Live Demo</a>
<a class="block px-3 py-2 text-sm text-gray-600 hover:text-indigo-600 rounded-md hover:bg-indigo-50" href="/home">Sign In</a>
<a href="https://indotalent.com" target="_blank" class="btn-primary w-full justify-center mt-2 no-underline inline-flex items-center gap-2">Get Source Code</a>
</div>
</div>
</nav>
<!-- HERO -->
<section id="home" class="relative overflow-hidden">
<div class="absolute inset-0 bg-gradient-to-b from-indigo-50/30 to-transparent pointer-events-none"></div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-20 pb-24 md:pt-28 md:pb-32">
<div class="text-center max-w-4xl mx-auto">
<div class="hero-badge mx-auto mb-6">
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
Production-Ready Blazor CRM Source Code
</div>
<h1 class="text-4xl sm:text-5xl md:text-6xl font-extrabold text-gray-900 leading-tight tracking-tight">
Your CRM,<br/>
<span class="gradient-text">Deployed in Minutes</span>
</h1>
<p class="mt-6 text-lg md:text-xl text-gray-500 max-w-2xl mx-auto leading-relaxed">
A complete, end-to-end CRM source code built with ASP.NET Core Blazor .NET 10 and MudBlazor.
Stop burning AI tokens building from scratch — get a guaranteed working application with beautiful, modern design.
</p>
<div class="mt-10 flex flex-col sm:flex-row items-center justify-center gap-4">
<a href="https://indotalent.com" target="_blank" class="btn-primary text-base px-8 py-3.5 no-underline">
Get Source Code
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M13 7l5 5m0 0l-5 5m5-5H6"/></svg>
</a>
<a href="/home" class="btn-outline text-base px-8 py-3.5 no-underline">
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"/><path stroke-linecap="round" stroke-linejoin="round" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
Sign In to App
</a>
</div>
<div class="mt-16 flex flex-wrap items-center justify-center gap-8 text-sm text-gray-400">
<span class="font-medium text-gray-500">Why choose us</span>
<span class="flex items-center gap-1.5"><svg class="w-4 h-4 text-indigo-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/></svg> End-to-End Working</span>
<span class="flex items-center gap-1.5"><svg class="w-4 h-4 text-indigo-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"/></svg> Beautiful MudBlazor UI</span>
<span class="flex items-center gap-1.5"><svg class="w-4 h-4 text-indigo-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z"/></svg> Save Weeks of Development</span>
</div>
</div>
</div>
</section>
<!-- WHY THIS PRODUCT — The Pain Points & Solution -->
<section id="features" class="py-20 md:py-28 border-t border-indigo-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-16">
<span class="inline-flex items-center gap-1.5 px-3 py-1 bg-indigo-50 text-indigo-600 text-xs font-semibold rounded-full border border-indigo-100">THE PROBLEM</span>
<h2 class="mt-4 text-3xl sm:text-4xl font-bold text-gray-900">Building a Full App Is Hard — Even with AI</h2>
<p class="mt-4 text-gray-500 max-w-2xl mx-auto">AI can generate code, but stitching together an end-to-end application that actually works is a different story. We solved that for you.</p>
</div>
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
<div class="p-6 bg-white rounded-xl border border-indigo-50 hover:shadow-sm hover:border-indigo-100 transition-all">
<div class="feature-icon mb-4">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"/></svg>
</div>
<h3 class="text-lg font-semibold text-gray-900">AI Alone Isn't Enough</h3>
<p class="mt-2 text-sm text-gray-500 leading-relaxed">Sure, AI can help write code snippets. But building a fully integrated CRM — with authentication, database, business logic, and a polished UI — takes countless iterations, debugging, and wasted tokens.</p>
</div>
<div class="p-6 bg-white rounded-xl border border-indigo-50 hover:shadow-sm hover:border-indigo-100 transition-all">
<div class="feature-icon mb-4">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z"/></svg>
</div>
<h3 class="text-lg font-semibold text-gray-900">Guaranteed to Work</h3>
<p class="mt-2 text-sm text-gray-500 leading-relaxed">Every feature in this CRM has been built, tested, and verified to run end-to-end. No half-baked modules, no broken pipelines — just a solid, production-grade application you can trust.</p>
</div>
<div class="p-6 bg-white rounded-xl border border-indigo-50 hover:shadow-sm hover:border-indigo-100 transition-all">
<div class="feature-icon mb-4">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M15.182 15.182a4.5 4.5 0 01-6.364 0M21 12a9 9 0 11-18 0 9 9 0 0118 0zM9.75 9.75c0 .414-.168.75-.375.75S9 10.164 9 9.75 9.168 9 9.375 9s.375.336.375.75zm-.375 0h.008v.015h-.008V9.75zm5.625 0c0 .414-.168.75-.375.75s-.375-.336-.375-.75.168-.75.375-.75.375.336.375.75zm-.375 0h.008v.015h-.008V9.75z"/></svg>
</div>
<h3 class="text-lg font-semibold text-gray-900">Beautiful by Default</h3>
<p class="mt-2 text-sm text-gray-500 leading-relaxed">Powered by MudBlazor 9, the UI is modern, responsive, and professional right out of the box. No need to fight with CSS or design tokens — it looks stunning from day one.</p>
</div>
<div class="p-6 bg-white rounded-xl border border-indigo-50 hover:shadow-sm hover:border-indigo-100 transition-all">
<div class="feature-icon mb-4">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z"/></svg>
</div>
<h3 class="text-lg font-semibold text-gray-900">Single Project Monolith</h3>
<p class="mt-2 text-sm text-gray-500 leading-relaxed">No microservice complexity, no juggling between 20 projects. One solution, one deployment unit — clean Vertical Slice Architecture that scales when you need it to.</p>
</div>
<div class="p-6 bg-white rounded-xl border border-indigo-50 hover:shadow-sm hover:border-indigo-100 transition-all">
<div class="feature-icon mb-4">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m5.231 13.481L15 17.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9zm3.75 11.625a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z"/></svg>
</div>
<h3 class="text-lg font-semibold text-gray-900">Full Source Code Included</h3>
<p class="mt-2 text-sm text-gray-500 leading-relaxed">You own the code. Customize, extend, and modify every line to fit your business. No black boxes, no vendor lock-in — just clean, well-structured C# code with LINQ and MediatR.</p>
</div>
<div class="p-6 bg-white rounded-xl border border-indigo-50 hover:shadow-sm hover:border-indigo-100 transition-all">
<div class="feature-icon mb-4">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
</div>
<h3 class="text-lg font-semibold text-gray-900">.NET 10 + Blazor Server</h3>
<p class="mt-2 text-sm text-gray-500 leading-relaxed">Built on the latest Microsoft stack — ASP.NET Core 10 with Blazor Server, Entity Framework Core, and MS SQL. A real-world demonstration of enterprise-grade .NET in action.</p>
</div>
</div>
</div>
</section>
<!-- BUSINESS MODULES -->
<section id="modules" class="py-20 md:py-28 bg-indigo-50/30 border-t border-indigo-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-16">
<span class="inline-flex items-center gap-1.5 px-3 py-1 bg-indigo-50 text-indigo-600 text-xs font-semibold rounded-full border border-indigo-100">BUSINESS MODULES</span>
<h2 class="mt-4 text-3xl sm:text-4xl font-bold text-gray-900">Everything You Need to Run Your Business</h2>
<p class="mt-4 text-gray-500 max-w-2xl mx-auto">From lead to invoice, purchase to payment — every module is built, integrated, and ready to use.</p>
</div>
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
<div class="p-6 bg-white rounded-xl border border-indigo-50 hover:shadow-sm hover:border-indigo-100 transition-all">
<div class="feature-icon mb-4">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z"/></svg>
</div>
<h3 class="text-lg font-semibold text-gray-900">Profile</h3>
<p class="mt-2 text-sm text-gray-500 leading-relaxed">Personal info, password management, avatar upload, and session tracking — a complete user profile system built in.</p>
</div>
<div class="p-6 bg-white rounded-xl border border-indigo-50 hover:shadow-sm hover:border-indigo-100 transition-all">
<div class="feature-icon mb-4">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 3v11.25A2.25 2.25 0 006 16.5h2.25M3.75 3h-1.5m1.5 0h16.5m0 0h1.5m-1.5 0v11.25A2.25 2.25 0 0118 16.5h-2.25m-7.5 0h7.5m-7.5 0l-1 3m8.5-3l1 3m0 0l.5 1.5m-.5-1.5h-9.5m0 0l-.5 1.5m.75-9l3-3 2.148 2.148A12.061 12.061 0 0116.5 7.605"/></svg>
</div>
<h3 class="text-lg font-semibold text-gray-900">Pipeline</h3>
<p class="mt-2 text-sm text-gray-500 leading-relaxed">Campaign management, budgeting, expense tracking, lead management with contacts & activities, sales team & representative assignments.</p>
</div>
<div class="p-6 bg-white rounded-xl border border-indigo-50 hover:shadow-sm hover:border-indigo-100 transition-all">
<div class="feature-icon mb-4">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M18 18.72a9.094 9.094 0 003.741-.479 3 3 0 00-4.682-2.72m.94 3.198l.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0112 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 016 18.719m12 0a5.971 5.971 0 00-.941-3.197m0 0A5.995 5.995 0 0012 12.75a5.995 5.995 0 00-5.058 2.772m0 0a3 3 0 00-4.681 2.72 8.986 8.986 0 003.74.477m.94-3.197a5.971 5.971 0 00-.94 3.197M15 6.75a3 3 0 11-6 0 3 3 0 016 0zm6 3a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0zm-13.5 0a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0z"/></svg>
</div>
<h3 class="text-lg font-semibold text-gray-900">Third Party</h3>
<p class="mt-2 text-sm text-gray-500 leading-relaxed">Customer & vendor management with groups, categories, and contacts. Complete CRM relationship tracking for both sides of your business.</p>
</div>
<div class="p-6 bg-white rounded-xl border border-indigo-50 hover:shadow-sm hover:border-indigo-100 transition-all">
<div class="feature-icon mb-4">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 18.75a60.07 60.07 0 0115.797 2.101c.727.198 1.453-.342 1.453-1.096V18.75M3.75 4.5v.75A.75.75 0 013 6h-.75m0 0v-.375c0-.621.504-1.125 1.125-1.125H20.25M2.25 6v9m18-10.5v.75c0 .414.336.75.75.75h.75m-1.5-1.5h.375c.621 0 1.125.504 1.125 1.125v9.75c0 .621-.504 1.125-1.125 1.125h-.375m1.5-1.5H21a.75.75 0 00-.75.75v.75m0 0H3.75m0 0h-.375a1.125 1.125 0 01-1.125-1.125V15m1.5 1.5v-.75A.75.75 0 003 15h-.75M15 10.5a3 3 0 11-6 0 3 3 0 016 0zm3 0h.008v.008H18V10.5zm-12 0h.008v.008H6V10.5z"/></svg>
</div>
<h3 class="text-lg font-semibold text-gray-900">Sales</h3>
<p class="mt-2 text-sm text-gray-500 leading-relaxed">Quotation, sales order, delivery order, sales return, invoice, credit note, and payment — with comprehensive reporting for every stage of the sales cycle.</p>
</div>
<div class="p-6 bg-white rounded-xl border border-indigo-50 hover:shadow-sm hover:border-indigo-100 transition-all">
<div class="feature-icon mb-4">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M21 12a2.25 2.25 0 00-2.25-2.25H15a3 3 0 11-6 0H5.25A2.25 2.25 0 003 12m18 0v6a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 18v-6m18 0V9M3 12V9m18 0a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 9m18 0V6a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 6v3"/></svg>
</div>
<h3 class="text-lg font-semibold text-gray-900">Purchase</h3>
<p class="mt-2 text-sm text-gray-500 leading-relaxed">Requisition, purchase order, goods receive, purchase return, bill, debit note, and payment — complete procurement workflow with full reporting suite.</p>
</div>
<div class="p-6 bg-white rounded-xl border border-indigo-50 hover:shadow-sm hover:border-indigo-100 transition-all">
<div class="feature-icon mb-4">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M20.25 7.5l-.625 10.632a2.25 2.25 0 01-2.247 2.118H6.622a2.25 2.25 0 01-2.247-2.118L3.75 7.5M10 11.25h4M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z"/></svg>
</div>
<h3 class="text-lg font-semibold text-gray-900">Inventory</h3>
<p class="mt-2 text-sm text-gray-500 leading-relaxed">Unit measure, product groups, product catalog, warehouse management, transfers, adjustments, scrapping, stock counts, and in-depth stock & movement reports.</p>
</div>
<div class="p-6 bg-white rounded-xl border border-indigo-50 hover:shadow-sm hover:border-indigo-100 transition-all">
<div class="feature-icon mb-4">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5m-9-6h.008v.008H12v-.008zM12 15h.008v.008H12V15zm0 2.25h.008v.008H12v-.008zM9.75 15h.008v.008H9.75V15zm0 2.25h.008v.008H9.75v-.008zM7.5 15h.008v.008H7.5V15zm0 2.25h.008v.008H7.5v-.008zm6.75-4.5h.008v.008h-.008v-.008zm0 2.25h.008v.008h-.008V15zm0 2.25h.008v.008h-.008v-.008zm2.25-4.5h.008v.008H16.5v-.008zm0 2.25h.008v.008H16.5V15z"/></svg>
</div>
<h3 class="text-lg font-semibold text-gray-900">Utilities</h3>
<p class="mt-2 text-sm text-gray-500 leading-relaxed">Booking group & resource management, scheduler, program resource & manager, plus a built-in to-do list to keep your team organized.</p>
</div>
<div class="p-6 bg-white rounded-xl border border-indigo-50 hover:shadow-sm hover:border-indigo-100 transition-all">
<div class="feature-icon mb-4">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m0-10.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.75c0 5.592 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.57-.598-3.75h-.152c-3.196 0-6.1-1.249-8.25-3.286zm0 13.036h.008v.008H12v-.008z"/></svg>
</div>
<h3 class="text-lg font-semibold text-gray-900">System Logs & Settings</h3>
<p class="mt-2 text-sm text-gray-500 leading-relaxed">Database and file logging, user management, currency configuration, auto numbering, and full inspection mode for appsettings.json — complete control over your application.</p>
</div>
</div>
</div>
</section>
<!-- TECH STACK -->
<section id="tech" class="py-20 md:py-28 border-t border-indigo-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-16">
<span class="inline-flex items-center gap-1.5 px-3 py-1 bg-indigo-50 text-indigo-600 text-xs font-semibold rounded-full border border-indigo-100">TECHNOLOGY STACK</span>
<h2 class="mt-4 text-3xl sm:text-4xl font-bold text-gray-900">Powered by Modern .NET 10</h2>
<p class="mt-4 text-gray-500 max-w-2xl mx-auto">Real-world demonstration of ASP.NET Core Blazor's robustness, built with the latest tools from Microsoft.</p>
</div>
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4">
<div class="tech-card"><div class="ico"><svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg></div><div class="font-semibold text-gray-900 text-sm">ASP.NET Core 10</div><div class="text-xs text-gray-400 mt-0.5">Framework</div></div>
<div class="tech-card"><div class="ico"><svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9 17.25v1.007a3 3 0 01-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0115 18.257V17.25m6-12V15a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 15V5.25m18 0A2.25 2.25 0 0018.75 3H5.25A2.25 2.25 0 003 5.25m18 0V12a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 12V5.25"/></svg></div><div class="font-semibold text-gray-900 text-sm">Blazor Server</div><div class="text-xs text-gray-400 mt-0.5">Web UI</div></div>
<div class="tech-card"><div class="ico"><svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5"/></svg></div><div class="font-semibold text-gray-900 text-sm">C# / LINQ</div><div class="text-xs text-gray-400 mt-0.5">Language</div></div>
<div class="tech-card"><div class="ico"><svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6.75 7.5l3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0021 18V6a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 6v12a2.25 2.25 0 002.25 2.25z"/></svg></div><div class="font-semibold text-gray-900 text-sm">.NET 10</div><div class="text-xs text-gray-400 mt-0.5">Runtime</div></div>
<div class="tech-card"><div class="ico"><svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125"/></svg></div><div class="font-semibold text-gray-900 text-sm">EF Core</div><div class="text-xs text-gray-400 mt-0.5">ORM</div></div>
<div class="tech-card"><div class="ico"><svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125"/></svg></div><div class="font-semibold text-gray-900 text-sm">MS SQL</div><div class="text-xs text-gray-400 mt-0.5">Database</div></div>
<div class="tech-card"><div class="ico"><svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9 4.5v3.75m0 0l3.75-3.75M9 8.25L5.25 4.5M15 19.5v-3.75m0 0l-3.75 3.75M15 15.75l3.75 3.75M3.75 3v11.25A2.25 2.25 0 006 16.5h2.25M3.75 3h-1.5m1.5 0h16.5m0 0h1.5m-1.5 0v11.25A2.25 2.25 0 0118 16.5h-2.25"/></svg></div><div class="font-semibold text-gray-900 text-sm">MudBlazor 9</div><div class="text-xs text-gray-400 mt-0.5">UI Components</div></div>
<div class="tech-card"><div class="ico"><svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z"/><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg></div><div class="font-semibold text-gray-900 text-sm">MediatR / CQRS</div><div class="text-xs text-gray-400 mt-0.5">Pattern</div></div>
<div class="tech-card"><div class="ico"><svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 3v11.25A2.25 2.25 0 006 16.5h2.25M3.75 3h-1.5m1.5 0h16.5m0 0h1.5m-1.5 0v11.25A2.25 2.25 0 0118 16.5h-2.25m-7.5 0h7.5m-7.5 0l-1 3m8.5-3l1 3m0 0l.5 1.5m-.5-1.5h-9.5m0 0l-.5 1.5m.75-9l3-3 2.148 2.148A12.061 12.061 0 0116.5 7.605"/></svg></div><div class="font-semibold text-gray-900 text-sm">VSA</div><div class="text-xs text-gray-400 mt-0.5">Architecture</div></div>
<div class="tech-card"><div class="ico"><svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"/></svg></div><div class="font-semibold text-gray-900 text-sm">Visual Studio 2026</div><div class="text-xs text-gray-400 mt-0.5">IDE</div></div>
<div class="tech-card"><div class="ico"><svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 21v-8.25M15.75 21v-8.25M8.25 21v-8.25M3 9l9-6 9 6m-1.5 12V10.332A48.36 48.36 0 0012 9.75c-2.551 0-5.056.2-7.5.582V21M3 21h18M12 6.75h.008v.008H12V6.75z"/></svg></div><div class="font-semibold text-gray-900 text-sm">Monolithic</div><div class="text-xs text-gray-400 mt-0.5">Single Project</div></div>
</div>
</div>
</section>
<!-- DEMO SECTION -->
<section id="demo" class="py-20 md:py-28 bg-indigo-50/30 border-t border-indigo-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-16">
<span class="inline-flex items-center gap-1.5 px-3 py-1 bg-indigo-50 text-indigo-600 text-xs font-semibold rounded-full border border-indigo-100">SEE IT IN ACTION</span>
<h2 class="mt-4 text-3xl sm:text-4xl font-bold text-gray-900">Try the Live Demo</h2>
<p class="mt-4 text-gray-500 max-w-2xl mx-auto">Don't take our word for it. Explore every feature in the live demo — no installation required.</p>
</div>
<div class="max-w-2xl mx-auto">
<div class="p-8 bg-white rounded-2xl border border-indigo-100 shadow-sm">
<div class="text-center mb-6">
<div class="feature-icon mx-auto mb-4">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9"/></svg>
</div>
<h3 class="text-xl font-semibold text-gray-900">Demo Credentials</h3>
<p class="mt-2 text-sm text-gray-500">Use these credentials to sign in and explore the full application:</p>
</div>
<div class="space-y-3">
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<span class="text-sm text-gray-500">URL</span>
<a href="https://blazor-crm.csharpasp.net/" target="_blank" class="text-sm font-medium text-indigo-600 hover:text-indigo-500">blazor-crm.csharpasp.net</a>
</div>
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<span class="text-sm text-gray-500">Email</span>
<span class="text-sm font-medium text-gray-900">admin@root.com</span>
</div>
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<span class="text-sm text-gray-500">Password</span>
<span class="text-sm font-medium text-gray-900">123456</span>
</div>
</div>
<div class="mt-6 text-center">
<a href="https://blazor-crm.csharpasp.net/" target="_blank" class="btn-primary text-base px-8 py-3.5 no-underline">
Open Live Demo
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M13 7l5 5m0 0l-5 5m5-5H6"/></svg>
</a>
</div>
</div>
</div>
</div>
</section>
<!-- CTA -->
<section id="cta" class="py-20 md:py-28 text-white" style="background: #6366F1;">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<h2 class="text-3xl sm:text-4xl font-bold">Stop Building from Scratch</h2>
<p class="mt-4 text-indigo-100 max-w-xl mx-auto">Get a complete, tested, and beautiful CRM source code. Focus on growing your business — we've handled the heavy lifting.</p>
<div class="mt-8 flex flex-col sm:flex-row items-center justify-center gap-4">
<a href="https://indotalent.com" target="_blank" class="inline-flex items-center gap-2 px-6 py-3 bg-white text-indigo-600 font-semibold rounded-lg hover:bg-indigo-50 transition text-sm no-underline">Get Source Code</a>
</div>
</div>
</section>
<!-- FOOTER -->
<footer class="bg-slate-900 border-t border-slate-800">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div class="grid sm:grid-cols-2 md:grid-cols-4 gap-8">
<div>
<div class="flex items-center gap-2 text-white font-bold text-lg mb-4">
<svg class="w-6 h-6 text-indigo-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg>
CRM
</div>
<p class="text-sm text-gray-500 leading-relaxed">Production-ready Blazor CRM source code. The ultimate bridge between your business and your customers.</p>
</div>
<div>
<h4 class="text-xs font-semibold text-indigo-400 uppercase tracking-wider mb-4">Technology</h4>
<div class="space-y-2"><a class="footer-link block">ASP.NET Core 10</a><a class="footer-link block">Blazor Server</a><a class="footer-link block">MudBlazor 9</a><a class="footer-link block">MS SQL + EF Core</a></div>
</div>
<div>
<h4 class="text-xs font-semibold text-indigo-400 uppercase tracking-wider mb-4">Key Modules</h4>
<div class="space-y-2"><a class="footer-link block">Sales & Pipeline</a><a class="footer-link block">Purchase & Inventory</a><a class="footer-link block">Customer & Vendor</a><a class="footer-link block">Reports & Logs</a></div>
</div>
<div>
<h4 class="text-xs font-semibold text-indigo-400 uppercase tracking-wider mb-4">Resources</h4>
<div class="space-y-2"><a class="footer-link block" href="https://blazor-crm.csharpasp.net/" target="_blank">Live Demo</a></div>
</div>
</div>
<div class="mt-10 pt-8 border-t border-slate-800 flex flex-col sm:flex-row items-center justify-between gap-4">
<p class="text-xs text-gray-600">&copy; 2026 CRM. All rights reserved.</p>
<div class="flex gap-4">
<a class="text-gray-500 hover:text-gray-300 transition text-xs cursor-pointer">Privacy</a>
<a class="text-gray-500 hover:text-gray-300 transition text-xs cursor-pointer">Terms</a>
<a class="text-gray-500 hover:text-gray-300 transition text-xs cursor-pointer">Cookies</a>
</div>
</div>
</div>
</footer>
<!-- FAB Back to Top -->
<button id="fab-top" :class="atTop ? '' : 'show'" @@click="window.scrollTo({top:0,behavior:'smooth'})" aria-label="Back to top">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M5 15l7-7 7 7"/></svg>
</button>
<script>
function initScrollSpy() {
const observer = new IntersectionObserver(function(entries) {
entries.forEach(function(e) {
if (e.isIntersecting) {
document.querySelectorAll('.nav-link').forEach(function(l) { l.classList.remove('active'); });
var el = document.querySelector('.nav-link[href="#'+e.target.id+'"]');
if (el) el.classList.add('active');
}
});
}, { rootMargin: '-80px 0px -50% 0px' });
document.querySelectorAll('section[id]').forEach(function(s) { observer.observe(s); });
}
</script>
</body>
</html>
+258
View File
@@ -0,0 +1,258 @@
<dialog id="components-reconnect-modal" data-nosnippet>
<div class="components-reconnect-container">
<div class="components-rejoining-animation" aria-hidden="true">
<div></div>
<div></div>
</div>
<p class="components-reconnect-first-attempt-visible">
Rejoining the server...
</p>
<p class="components-reconnect-repeated-attempt-visible">
Rejoin failed... trying again in <span id="components-seconds-to-next-attempt"></span> seconds.
</p>
<p class="components-reconnect-failed-visible">
Failed to rejoin.<br />Please retry or reload the page.
</p>
<button id="components-reconnect-button" class="components-reconnect-failed-visible">
Retry
</button>
<p class="components-pause-visible">
The session has been paused by the server.
</p>
<p class="components-resume-failed-visible">
Failed to resume the session.<br />Please retry or reload the page.
</p>
<button id="components-resume-button" class="components-pause-visible components-resume-failed-visible">
Resume
</button>
</div>
</dialog>
<style>
.components-reconnect-first-attempt-visible,
.components-reconnect-repeated-attempt-visible,
.components-reconnect-failed-visible,
.components-pause-visible,
.components-resume-failed-visible,
.components-rejoining-animation {
display: none;
}
#components-reconnect-modal.components-reconnect-show .components-reconnect-first-attempt-visible,
#components-reconnect-modal.components-reconnect-show .components-rejoining-animation,
#components-reconnect-modal.components-reconnect-paused .components-pause-visible,
#components-reconnect-modal.components-reconnect-resume-failed .components-resume-failed-visible,
#components-reconnect-modal.components-reconnect-retrying,
#components-reconnect-modal.components-reconnect-retrying .components-reconnect-repeated-attempt-visible,
#components-reconnect-modal.components-reconnect-retrying .components-rejoining-animation,
#components-reconnect-modal.components-reconnect-failed,
#components-reconnect-modal.components-reconnect-failed .components-reconnect-failed-visible {
display: block;
}
#components-reconnect-modal {
background-color: white;
width: 20rem;
margin: 20vh auto;
padding: 2rem;
border: 0;
border-radius: 0.5rem;
box-shadow: 0 3px 6px 2px rgba(0, 0, 0, 0.3);
opacity: 0;
transition: display 0.5s allow-discrete, overlay 0.5s allow-discrete;
animation: components-reconnect-modal-fadeOutOpacity 0.5s both;
&[open] {
animation: components-reconnect-modal-slideUp 1.5s cubic-bezier(.05, .89, .25, 1.02) 0.3s, components-reconnect-modal-fadeInOpacity 0.5s ease-in-out 0.3s;
animation-fill-mode: both;
}
}
#components-reconnect-modal::backdrop {
background-color: rgba(0, 0, 0, 0.4);
animation: components-reconnect-modal-fadeInOpacity 0.5s ease-in-out;
opacity: 1;
}
@@keyframes components-reconnect-modal-slideUp {
0% {
transform: translateY(30px) scale(0.95);
}
100% {
transform: translateY(0);
}
}
@@keyframes components-reconnect-modal-fadeInOpacity {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@@keyframes components-reconnect-modal-fadeOutOpacity {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.components-reconnect-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
#components-reconnect-modal p {
margin: 0;
text-align: center;
}
#components-reconnect-modal button {
border: 0;
background-color: #6b9ed2;
color: white;
padding: 4px 24px;
border-radius: 4px;
}
#components-reconnect-modal button:hover {
background-color: #3b6ea2;
}
#components-reconnect-modal button:active {
background-color: #6b9ed2;
}
.components-rejoining-animation {
position: relative;
width: 80px;
height: 80px;
}
.components-rejoining-animation div {
position: absolute;
border: 3px solid #0087ff;
opacity: 1;
border-radius: 50%;
animation: components-rejoining-animation 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite;
}
.components-rejoining-animation div:nth-child(2) {
animation-delay: -0.5s;
}
@@keyframes components-rejoining-animation {
0% {
top: 40px;
left: 40px;
width: 0;
height: 0;
opacity: 0;
}
4.9% {
top: 40px;
left: 40px;
width: 0;
height: 0;
opacity: 0;
}
5% {
top: 40px;
left: 40px;
width: 0;
height: 0;
opacity: 1;
}
100% {
top: 0px;
left: 0px;
width: 80px;
height: 80px;
opacity: 0;
}
}
</style>
<script>
// Set up event handlers
const reconnectModal = document.getElementById("components-reconnect-modal");
reconnectModal.addEventListener("components-reconnect-state-changed", handleReconnectStateChanged);
const retryButton = document.getElementById("components-reconnect-button");
retryButton.addEventListener("click", retry);
const resumeButton = document.getElementById("components-resume-button");
resumeButton.addEventListener("click", resume);
function handleReconnectStateChanged(event) {
if (event.detail.state === "show") {
reconnectModal.showModal();
} else if (event.detail.state === "hide") {
reconnectModal.close();
} else if (event.detail.state === "failed") {
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
} else if (event.detail.state === "rejected") {
location.reload();
}
}
async function retry() {
document.removeEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
try {
// Reconnect will asynchronously return:
// - true to mean success
// - false to mean we reached the server, but it rejected the connection (e.g., unknown circuit ID)
// - exception to mean we didn't reach the server (this can be sync or async)
const successful = await Blazor.reconnect();
if (!successful) {
// We have been able to reach the server, but the circuit is no longer available.
// We'll reload the page so the user can continue using the app as quickly as possible.
const resumeSuccessful = await Blazor.resumeCircuit();
if (!resumeSuccessful) {
location.reload();
} else {
reconnectModal.close();
}
}
} catch (err) {
// We got an exception, server is currently unavailable
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
}
}
async function resume() {
try {
const successful = await Blazor.resumeCircuit();
if (!successful) {
location.reload();
}
} catch {
reconnectModal.classList.replace("components-reconnect-paused", "components-reconnect-resume-failed");
}
}
async function retryWhenDocumentBecomesVisible() {
if (document.visibilityState === "visible") {
await retry();
}
}
</script>
+25
View File
@@ -0,0 +1,25 @@
@using Indotalent.ConfigBackEnd.Interfaces
@using Indotalent.Features.Account.AccessDenied
@using Indotalent.Features.Root.NotFound
@using Microsoft.AspNetCore.Components.Authorization
<CascadingAuthenticationState>
<Router AppAssembly="typeof(Program).Assembly" NotFoundPage="typeof(NotFound.NotFoundPage)">
<Found Context="routeData">
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Root.MainLayout)">
<NotAuthorized>
<RedirectToAccessDenied />
</NotAuthorized>
<Authorizing>
<div class="d-flex align-center justify-center mt-16">
<MudProgressCircular Color="Color.Primary" Indeterminate="true" />
<MudText Class="ml-3">Memverifikasi identitas...</MudText>
</div>
</Authorizing>
</AuthorizeRouteView>
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
</Router>
</CascadingAuthenticationState>
@@ -0,0 +1,58 @@
@using MudBlazor
<MudDialog>
<TitleContent>
<div class="pa-4" style="background-color: #f44336; margin: -16px -24px 0 -24px;">
<div class="d-flex align-center justify-center">
<MudIcon Icon="@Icons.Material.Filled.Warning" Style="color: white;" Size="Size.Large" />
<MudText Typo="Typo.h6" Style="color: white; font-weight: 800; margin-left: 12px;">Confirm Delete</MudText>
</div>
</div>
</TitleContent>
<DialogContent>
<div class="pa-4 text-center">
<MudText Typo="Typo.body1" Class="mb-2">Are you sure you want to delete <b>@ContentText</b>?</MudText>
<MudText Typo="Typo.body2" Color="Color.Error" Style="font-weight: 700;">This action cannot be undone.</MudText>
</div>
</DialogContent>
<DialogActions>
<div class="d-flex justify-center gap-2 w-100 pb-4">
<MudButton OnClick="Cancel"
Variant="Variant.Outlined"
Disabled="_isDeleting"
Style="border-radius: 4px; border: 1px solid #e0e0e0; text-transform: none; font-weight: 700;">Cancel</MudButton>
<MudButton Color="Color.Error"
Variant="Variant.Filled"
OnClick="HandleSubmit"
Disabled="_isDeleting"
Style="border-radius: 4px; text-transform: none; font-weight: 700;">
@if (_isDeleting)
{
<MudProgressCircular Class="ms-n1" Size="Size.Small" Indeterminate="true" />
<MudText Class="ms-2" Typo="Typo.button" Style="text-transform: none !important;">Deleting...</MudText>
}
else
{
<MudText>Yes, Delete It</MudText>
}
</MudButton>
</div>
</DialogActions>
</MudDialog>
@code {
[CascadingParameter] IMudDialogInstance MudDialog { get; set; } = default!;
[Parameter] public string ContentText { get; set; } = string.Empty;
private bool _isDeleting = false;
private async Task HandleSubmit()
{
_isDeleting = true;
StateHasChanged();
await Task.Delay(1000);
MudDialog.Close(DialogResult.Ok(true));
}
private void Cancel() => MudDialog.Cancel();
}
+41
View File
@@ -0,0 +1,41 @@
@using Indotalent.Infrastructure.Authentication.Identity
@using Microsoft.Extensions.Options
@inject IOptions<IdentitySettingsModel> IdentityOptions
@if (IdentityOptions.Value.SsoFirebase.IsUsed)
{
<script src="https://www.gstatic.com/firebasejs/9.23.0/firebase-app-compat.js"></script>
<script src="https://www.gstatic.com/firebasejs/9.23.0/firebase-auth-compat.js"></script>
<script>
const firebaseConfig = {
apiKey: "@IdentityOptions.Value.SsoFirebase.ApiKey",
authDomain: "@IdentityOptions.Value.SsoFirebase.AuthDomain",
projectId: "@IdentityOptions.Value.SsoFirebase.ProjectId",
storageBucket: "@IdentityOptions.Value.SsoFirebase.StorageBucket",
messagingSenderId: "@IdentityOptions.Value.SsoFirebase.MessagingSenderId",
appId: "@IdentityOptions.Value.SsoFirebase.AppId"
};
if (typeof firebase !== 'undefined' && !firebase.apps.length) {
firebase.initializeApp(firebaseConfig);
}
async function signInWithGoogle() {
if (typeof firebase === 'undefined') return null;
const provider = new firebase.auth.GoogleAuthProvider();
try {
const result = await firebase.auth().signInWithPopup(provider);
return { email: result.user.email };
} catch (error) {
console.error("Firebase Auth Error:", error);
return null;
}
}
async function firebaseLogout() {
if (typeof firebase !== 'undefined') {
await firebase.auth().signOut();
}
}
</script>
}
+1
View File
@@ -0,0 +1 @@

+13
View File
@@ -0,0 +1,13 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using Indotalent
@using Indotalent.Features.Root
@using MudBlazor
@inject NavigationManager NavigationManager