Browse Source

Dashboard commit 4

pull/10/head
Benjamin Arhen 2 years ago
parent
commit
9712148d39
  1. 1
      Client/Biskilog Accounting.Client.csproj
  2. 17
      Client/Layouts/MainLayout.razor.cs
  3. 277
      Client/Pages/Dashboard/Dashboard.razor
  4. 145
      Client/Pages/Dashboard/Dashboard.razor.cs
  5. 14
      Client/Pages/Dashboard/Elements/AnalyticsItemSmall.razor
  6. 14
      Client/Pages/Dashboard/Elements/ChartElement.razor
  7. 45
      Client/Pages/Dashboard/Elements/ChartElement.razor.cs
  8. 30
      Client/Pages/Dashboard/Elements/LowStockItems.razor
  9. 11
      Client/Pages/Dashboard/Elements/LowStockItems.razor.cs
  10. 80
      Client/Pages/Dashboard/Elements/MostPurchasedElement.razor
  11. 33
      Client/Pages/Dashboard/Elements/MostPurchasedElement.razor.cs
  12. 31
      Client/Pages/Dashboard/Elements/TransactionCard.razor
  13. 24
      Client/Pages/Dashboard/Elements/TransactionCard.razor.cs
  14. 2
      Client/Pages/Dashboard/Elements/WelcomeCard.razor
  15. 39
      Client/Pages/Dashboard/Elements/WelcomeCard.razor.cs
  16. 2
      Client/_Imports.razor
  17. 2
      Client/wwwroot/index.html
  18. 3
      Server/Controllers/AnalyticsController.cs
  19. 37
      Server/Services/AnalyticalService.cs
  20. 8
      Shared/Interfaces/IAnalytics.cs
  21. 1
      Shared/Interfaces/ICalculator.cs
  22. 26
      Shared/ServiceRepo/CalculatorService.cs

1
Client/Biskilog Accounting.Client.csproj

@ -20,6 +20,7 @@
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="7.0.5" PrivateAssets="all" />
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
<PackageReference Include="Blazor-ApexCharts" Version="0.9.21-beta" />
<PackageReference Include="Radzen.Blazor" Version="4.12.0" />
</ItemGroup>
<ItemGroup>

17
Client/Layouts/MainLayout.razor.cs

@ -4,8 +4,20 @@ namespace Biskilog_Accounting.Client.Layouts
{
public partial class MainLayout
{
protected override async Task OnInitializedAsync()
protected override Task OnInitializedAsync()
{
CheckPermission();
return base.OnInitializedAsync();
}
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
CheckPermission();
}
base.OnAfterRender(firstRender);
}
private async void CheckPermission()
{
//Checks if user token is set else redirect user to login page
if (!await m_tokenService.IsTokenSet())
@ -18,7 +30,6 @@ namespace Biskilog_Accounting.Client.Layouts
var authHeader = new AuthenticationHeaderValue("Bearer", token.Substring(6).Trim());
m_http.DefaultRequestHeaders.Authorization = authHeader;
}
return;
}
}
}

277
Client/Pages/Dashboard/Dashboard.razor

@ -7,7 +7,7 @@
<div class="container-xxl flex-grow-1 container-p-y">
<div class="row">
<div class="col-lg-8 mb-4 order-0">
<WelcomeCard CurrentTradeSales="@m_tradeSummary.CurrentTradeSales" PreviousTradeSales="@m_tradeSummary.LastTradeSales" Username="@m_username" />
<WelcomeCard TradeSummary="@m_tradeSummary" Username="@m_username" />
</div>
<div class="col-lg-4 col-md-4 order-1">
<div class="row">
@ -15,13 +15,13 @@
<AnalyticsItemSmall Icon="../assets/img/icons/unicons/chart-success.png" Title="Transactions" Value="@m_tradeSummary.CurrentTradeSales" Percentage="@(((m_tradeSummary.CurrentTradeSales-m_tradeSummary.LastTradeSales)/m_tradeSummary.LastTradeSales)*100)" />
</div>
<div class="col-lg-6 col-md-12 col-6 mb-4">
<AnalyticsItemSmall Icon="../assets/img/icons/unicons/wallet-info.png" Title="Cancelled Sales" Value="4679" Percentage="28.42" />
<AnalyticsItemSmall Icon="../assets/img/icons/unicons/wallet-info.png" Title="Cancelled Sales" Value="@(m_cancelledWeeklySale)" Percentage="@(m_cancelledPercentage)" />
</div>
</div>
</div>
<!-- Total Revenue -->
<div class="col-12 col-lg-8 order-2 order-md-3 order-lg-2 mb-4">
<ChartElement Series="@m_weeklySaleItems" Title="Weekly Revenue" SubTitle="Performance Trend" IsLoading="@loadWeeklySales" />
<ChartElement Series="@m_weeklySaleItems" Title="Weekly Revenue" SubTitle="Sale Trend" IsLoading="@loadWeeklySales" />
</div>
<!--/ Total Revenue -->
<div class="col-12 col-md-8 col-lg-4 order-3 order-md-2">
@ -30,7 +30,7 @@
<AnalyticsItemSmall Icon="../assets/img/icons/unicons/paypal.png" Title="Credit Sales" Value="2456" Percentage="-22" />
</div>
<div class="col-6 mb-4">
<AnalyticsItemSmall Icon="../assets/img/icons/unicons/cc-primary.png" Title="Transactions" Value="2456" Percentage="-22" />
<AnalyticsItemSmall Icon="../assets/img/icons/unicons/cc-primary.png" Title="Debts To Collect" Value="@(m_totalDebt)" Percentage="-22" />
</div>
<!-- </div>
<div class="row"> -->
@ -57,278 +57,21 @@
</div>
</div>
<div class="row">
<!-- Order Statistics -->
<!-- Low stock items -->
<div class="col-md-6 col-lg-4 col-xl-4 order-0 mb-4">
<div class="card h-100">
<div class="card-header d-flex align-items-center justify-content-between pb-0">
<div class="card-title mb-0">
<h5 class="m-0 me-2">Order Statistics</h5>
<small class="text-muted">42.82k Total Sales</small>
</div>
<div class="dropdown">
<button class="btn p-0"
type="button"
id="orederStatistics"
data-bs-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false">
<i class="bx bx-dots-vertical-rounded"></i>
</button>
<div class="dropdown-menu dropdown-menu-end" aria-labelledby="orederStatistics">
<a class="dropdown-item" href="javascript:void(0);">Select All</a>
<a class="dropdown-item" href="javascript:void(0);">Refresh</a>
<a class="dropdown-item" href="javascript:void(0);">Share</a>
</div>
</div>
</div>
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<div class="d-flex flex-column align-items-center gap-1">
<h2 class="mb-2">8,258</h2>
<span>Total Orders</span>
</div>
<div id="orderStatisticsChart"></div>
</div>
<ul class="p-0 m-0">
<li class="d-flex mb-4 pb-1">
<div class="avatar flex-shrink-0 me-3">
<span class="avatar-initial rounded bg-label-primary">
<i class="bx bx-mobile-alt"></i>
</span>
</div>
<div class="d-flex w-100 flex-wrap align-items-center justify-content-between gap-2">
<div class="me-2">
<h6 class="mb-0">Electronic</h6>
<small class="text-muted">Mobile, Earbuds, TV</small>
</div>
<div class="user-progress">
<small class="fw-semibold">82.5k</small>
</div>
</div>
</li>
<li class="d-flex mb-4 pb-1">
<div class="avatar flex-shrink-0 me-3">
<span class="avatar-initial rounded bg-label-success"><i class="bx bx-closet"></i></span>
</div>
<div class="d-flex w-100 flex-wrap align-items-center justify-content-between gap-2">
<div class="me-2">
<h6 class="mb-0">Fashion</h6>
<small class="text-muted">T-shirt, Jeans, Shoes</small>
</div>
<div class="user-progress">
<small class="fw-semibold">23.8k</small>
</div>
</div>
</li>
<li class="d-flex mb-4 pb-1">
<div class="avatar flex-shrink-0 me-3">
<span class="avatar-initial rounded bg-label-info"><i class="bx bx-home-alt"></i></span>
</div>
<div class="d-flex w-100 flex-wrap align-items-center justify-content-between gap-2">
<div class="me-2">
<h6 class="mb-0">Decor</h6>
<small class="text-muted">Fine Art, Dining</small>
</div>
<div class="user-progress">
<small class="fw-semibold">849k</small>
</div>
</div>
</li>
<li class="d-flex">
<div class="avatar flex-shrink-0 me-3">
<span class="avatar-initial rounded bg-label-secondary">
<i class="bx bx-football"></i>
</span>
</div>
<div class="d-flex w-100 flex-wrap align-items-center justify-content-between gap-2">
<div class="me-2">
<h6 class="mb-0">Sports</h6>
<small class="text-muted">Football, Cricket Kit</small>
</div>
<div class="user-progress">
<small class="fw-semibold">99</small>
</div>
</div>
</li>
</ul>
</div>
</div>
<LowStockItems LowStockProducts="@m_lowstock"/>
</div>
<!--/ Order Statistics -->
<!-- Expense Overview -->
<div class="col-md-6 col-lg-4 order-1 mb-4">
<div class="card h-100">
<div class="card-header">
<ul class="nav nav-pills" role="tablist">
<li class="nav-item">
<button type="button"
class="nav-link active"
role="tab"
data-bs-toggle="tab"
data-bs-target="#navs-tabs-line-card-income"
aria-controls="navs-tabs-line-card-income"
aria-selected="true">
Income
</button>
</li>
<li class="nav-item">
<button type="button" class="nav-link" role="tab">Expenses</button>
</li>
<li class="nav-item">
<button type="button" class="nav-link" role="tab">Profit</button>
</li>
</ul>
</div>
<div class="card-body px-0">
<div class="tab-content p-0">
<div class="tab-pane fade show active" id="navs-tabs-line-card-income" role="tabpanel">
<div class="d-flex p-4 pt-3">
<div class="avatar flex-shrink-0 me-3">
<img src="../assets/img/icons/unicons/wallet.png" alt="User" />
</div>
<div>
<small class="text-muted d-block">Total Balance</small>
<div class="d-flex align-items-center">
<h6 class="mb-0 me-1">$459.10</h6>
<small class="text-success fw-semibold">
<i class="bx bx-chevron-up"></i>
42.9%
</small>
</div>
</div>
</div>
<div id="incomeChart"></div>
<div class="d-flex justify-content-center pt-4 gap-2">
<div class="flex-shrink-0">
<div id="expensesOfWeek"></div>
</div>
<div>
<p class="mb-n1 mt-1">Expenses This Week</p>
<small class="text-muted">$39 less than last week</small>
</div>
</div>
</div>
</div>
</div>
</div>
<MostPurchasedElement MostPurchasedItems="@m_mostPurchased" IsLoading="@m_loadingMostPurchased" />
</div>
<!--/ Expense Overview -->
<!-- Transactions -->
<div class="col-md-6 col-lg-4 order-2 mb-4">
<div class="card h-100">
<div class="card-header d-flex align-items-center justify-content-between">
<h5 class="card-title m-0 me-2">Transactions</h5>
<div class="dropdown">
<button class="btn p-0"
type="button"
id="transactionID"
data-bs-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false">
<i class="bx bx-dots-vertical-rounded"></i>
</button>
<div class="dropdown-menu dropdown-menu-end" aria-labelledby="transactionID">
<a class="dropdown-item" href="javascript:void(0);">Last 28 Days</a>
<a class="dropdown-item" href="javascript:void(0);">Last Month</a>
<a class="dropdown-item" href="javascript:void(0);">Last Year</a>
</div>
</div>
</div>
<div class="card-body">
<ul class="p-0 m-0">
<li class="d-flex mb-4 pb-1">
<div class="avatar flex-shrink-0 me-3">
<img src="../assets/img/icons/unicons/paypal.png" alt="User" class="rounded" />
</div>
<div class="d-flex w-100 flex-wrap align-items-center justify-content-between gap-2">
<div class="me-2">
<small class="text-muted d-block mb-1">Paypal</small>
<h6 class="mb-0">Send money</h6>
</div>
<div class="user-progress d-flex align-items-center gap-1">
<h6 class="mb-0">+82.6</h6>
<span class="text-muted">USD</span>
</div>
</div>
</li>
<li class="d-flex mb-4 pb-1">
<div class="avatar flex-shrink-0 me-3">
<img src="../assets/img/icons/unicons/wallet.png" alt="User" class="rounded" />
</div>
<div class="d-flex w-100 flex-wrap align-items-center justify-content-between gap-2">
<div class="me-2">
<small class="text-muted d-block mb-1">Wallet</small>
<h6 class="mb-0">Mac'D</h6>
</div>
<div class="user-progress d-flex align-items-center gap-1">
<h6 class="mb-0">+270.69</h6>
<span class="text-muted">USD</span>
</div>
</div>
</li>
<li class="d-flex mb-4 pb-1">
<div class="avatar flex-shrink-0 me-3">
<img src="../assets/img/icons/unicons/chart.png" alt="User" class="rounded" />
</div>
<div class="d-flex w-100 flex-wrap align-items-center justify-content-between gap-2">
<div class="me-2">
<small class="text-muted d-block mb-1">Transfer</small>
<h6 class="mb-0">Refund</h6>
</div>
<div class="user-progress d-flex align-items-center gap-1">
<h6 class="mb-0">+637.91</h6>
<span class="text-muted">USD</span>
</div>
</div>
</li>
<li class="d-flex mb-4 pb-1">
<div class="avatar flex-shrink-0 me-3">
<img src="../assets/img/icons/unicons/cc-success.png" alt="User" class="rounded" />
</div>
<div class="d-flex w-100 flex-wrap align-items-center justify-content-between gap-2">
<div class="me-2">
<small class="text-muted d-block mb-1">Credit Card</small>
<h6 class="mb-0">Ordered Food</h6>
</div>
<div class="user-progress d-flex align-items-center gap-1">
<h6 class="mb-0">-838.71</h6>
<span class="text-muted">USD</span>
</div>
</div>
</li>
<li class="d-flex mb-4 pb-1">
<div class="avatar flex-shrink-0 me-3">
<img src="../assets/img/icons/unicons/wallet.png" alt="User" class="rounded" />
</div>
<div class="d-flex w-100 flex-wrap align-items-center justify-content-between gap-2">
<div class="me-2">
<small class="text-muted d-block mb-1">Wallet</small>
<h6 class="mb-0">Starbucks</h6>
</div>
<div class="user-progress d-flex align-items-center gap-1">
<h6 class="mb-0">+203.33</h6>
<span class="text-muted">USD</span>
</div>
</div>
</li>
<li class="d-flex">
<div class="avatar flex-shrink-0 me-3">
<img src="../assets/img/icons/unicons/cc-warning.png" alt="User" class="rounded" />
</div>
<div class="d-flex w-100 flex-wrap align-items-center justify-content-between gap-2">
<div class="me-2">
<small class="text-muted d-block mb-1">Mastercard</small>
<h6 class="mb-0">Ordered Food</h6>
</div>
<div class="user-progress d-flex align-items-center gap-1">
<h6 class="mb-0">-92.45</h6>
<span class="text-muted">USD</span>
</div>
</div>
</li>
</ul>
</div>
</div>
<!-- Recent Transactions -->
<TransactionCard Sales="@m_recentTransaction" />
<!--/ Recent Transactions -->
</div>
<!--/ Transactions -->
</div>

145
Client/Pages/Dashboard/Dashboard.razor.cs

@ -8,18 +8,32 @@ namespace Biskilog_Accounting.Client.Pages.Dashboard
{
private TradeSummary m_tradeSummary { get; set; } = new TradeSummary();
private List<WeeklySaleItem> m_weeklySaleItems { get; set; } = new List<WeeklySaleItem> { };
private List<CancelledSales> m_weeklyCancelledSales { get; set; } = new List<CancelledSales> { };
private List<SaleItem> m_recentTransaction { get; set; } = new List<SaleItem> { };
private List<MostPurchasedItem> m_mostPurchased { get; set; } = new List<MostPurchasedItem> { };
private List<ProductItem> m_lowstock { get; set; } = new List<ProductItem> { };
private double m_cancelledWeeklySale { get; set; } = 0;
private double m_cancelledPercentage { get; set; } = 0;
private double m_totalDebt { get; set; } = 0;
private string m_username { get; set; } = string.Empty;
private bool loadWeeklySales = true;
private bool m_loadingMostPurchased { get; set; } = true;
protected override async Task OnInitializedAsync()
{
m_username = m_tokenService.GetUserNameFromToken(await m_tokenService.GetToken())!;
await GetTradeSummary();
await GetWeeklySales();
await GetCancelledSales();
await GetDebtSummary();
await GetRecentTransactions();
await GetMostPurchased();
await GetLowStockItems();
return;
}
// {a_limit
//}
/// <summary>
/// Gets the tade summary
/// Gets the trade summary
/// </summary>
/// <returns></returns>
async Task GetTradeSummary()
@ -42,7 +56,30 @@ namespace Biskilog_Accounting.Client.Pages.Dashboard
}
}
/// <summary>
/// Gets the tade summary
/// Gets the debt summary
/// </summary>
/// <returns></returns>
async Task GetDebtSummary()
{
try
{
var response = await m_http.GetAsync("api/analytics/debtors");
if (response.IsSuccessStatusCode)
{
var jsonContent = await response.Content.ReadAsStringAsync();
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
var debt = JsonSerializer.Deserialize<List<InDebtCustomers>>(jsonContent, options);
m_totalDebt = (double)debt.Sum(c => c.Debt);
StateHasChanged();
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
/// <summary>
/// Gets the weekly sales
/// </summary>
/// <returns></returns>
async Task GetWeeklySales()
@ -65,5 +102,107 @@ namespace Biskilog_Accounting.Client.Pages.Dashboard
Console.WriteLine(ex.Message);
}
}
/// <summary>
/// Gets the summary of cancelled sales for the past week
/// </summary>
/// <returns></returns>
async Task GetCancelledSales()
{
try
{
DateTime start = DateTime.Now.AddDays(-7).Date;
DateTime end = DateTime.Now.Date;
var response = await m_http.GetAsync($"api/analytics/cancelledsales/{start}/{end}");
if (response.IsSuccessStatusCode)
{
var jsonContent = await response.Content.ReadAsStringAsync();
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
var weekly = JsonSerializer.Deserialize<List<CancelledSales>>(jsonContent, options);
m_weeklyCancelledSales = weekly;
var loadCancelledSales = m_weeklyCancelledSales.OrderByDescending(t => t.CancelledTransaction.DateCancelled);
if (loadCancelledSales.Count() > 1)
{
m_cancelledWeeklySale = (double)loadCancelledSales.ToList()[0].Value;
m_cancelledPercentage = (((double)loadCancelledSales.ToList()[0].Value - (double)loadCancelledSales.ToList()[1].Value) / (double)loadCancelledSales.ToList()[0].Value) * 100;
}
else
{
m_cancelledWeeklySale = (double)loadCancelledSales.FirstOrDefault().Value;
m_cancelledPercentage = 0;
}
StateHasChanged();
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
/// <summary>
/// Gets the recent transaction
/// </summary>
/// <returns></returns>
async Task GetRecentTransactions()
{
try
{
var response = await m_http.GetAsync($"api/analytics/sales/recent/{50}");
if (response.IsSuccessStatusCode)
{
var jsonContent = await response.Content.ReadAsStringAsync();
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
var recent = JsonSerializer.Deserialize<List<SaleItem>>(jsonContent, options);
m_recentTransaction = recent;
StateHasChanged();
}
}
catch { }
}
/// <summary>
/// Gets a collection of most purchased items within the past
/// </summary>
/// <returns></returns>
async Task GetMostPurchased()
{
try
{
string start = m_tradeSummary.LastTradeDate.ToString("yyyy-MM-dd");
string end = m_tradeSummary.CurrentTradeDate.ToString("yyyy-MM-dd");
var response = await m_http.GetAsync($"api/analytics/mostpurchaseditem/{start}/{end}");
if (response.IsSuccessStatusCode)
{
var jsonContent = await response.Content.ReadAsStringAsync();
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
var recent = JsonSerializer.Deserialize<List<MostPurchasedItem>>(jsonContent, options);
m_mostPurchased = recent;
m_loadingMostPurchased = false;
StateHasChanged();
}
}
catch { }
}
/// <summary>
/// Gets a collection of items that are low on stock
/// </summary>
/// <returns></returns>
async Task GetLowStockItems()
{
try
{
var response = await m_http.GetAsync($"api/analytics/lowonstock");
if (response.IsSuccessStatusCode)
{
var jsonContent = await response.Content.ReadAsStringAsync();
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
var recent = JsonSerializer.Deserialize<List<ProductItem>>(jsonContent, options);
m_lowstock = recent;
StateHasChanged();
}
}
catch (Exception ex) {
}
}
}
}

14
Client/Pages/Dashboard/Elements/AnalyticsItemSmall.razor

@ -9,20 +9,6 @@
alt="chart success"
class="rounded" />
</div>
<div class="dropdown">
<button class="btn p-0"
type="button"
id="cardOpt3"
data-bs-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false">
<i class="bx bx-dots-vertical-rounded"></i>
</button>
<div class="dropdown-menu dropdown-menu-end" aria-labelledby="cardOpt3">
<a class="dropdown-item" href="javascript:void(0);">View More</a>
<a class="dropdown-item" href="javascript:void(0);">Delete</a>
</div>
</div>
</div>
<span class="fw-semibold d-block mb-1">@Title</span>
<h6 class="card-title mb-2">@(m_calculator.FormatMoneyWithCurrency(Value))</h6>

14
Client/Pages/Dashboard/Elements/ChartElement.razor

@ -6,7 +6,7 @@
{
<div class="card">
<div class="row row-bordered g-0">
<div class="col-md-8">
<div class="col-md-7">
<h5 class="card-header m-0 me-2 pb-3">@Title</h5>
<ApexChart TItem="WeeklySaleItem" Options=m_options>
@ -14,18 +14,14 @@
Items="@Series"
Name="Sales"
YValue="@(e => e.Total)"
XValue="@(e => e.Date.ToShortDateString())"
XValue="@(e => e.Date.ToString("dd MMM"))"
SeriesType="SeriesType.Bar" />
</ApexChart>
</div>
<div class="col-md-4">
<div class="col-md-5">
<div class="text-center fw-semibold pt-3 mb-2">@SubTitle</div>
<ApexChart TItem="WeeklySaleItem">
<ApexPointSeries TItem="WeeklySaleItem"
Items="@Series"
Name="Sales"
YValue="@(e => e.Total)"
XValue="@(e => e.Date.Day)"
<ApexChart TItem="WeeklySaleItem" Options="options">
<ApexPointSeries TItem="WeeklySaleItem" Items="@Series" Name="Sales" YValue="@(e => e.Total)" XValue="@(e => e.Date.ToString("dd MMM"))"
SeriesType="SeriesType.Area" />
</ApexChart>
<div class="d-flex px-xxl-4 px-lg-2 p-4 gap-xxl-3 gap-lg-1 gap-3 justify-content-between">

45
Client/Pages/Dashboard/Elements/ChartElement.razor.cs

@ -1,6 +1,7 @@
using ApexCharts;
using Biskilog_Accounting.Shared.CustomModels;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Options;
namespace Biskilog_Accounting.Client.Pages.Dashboard.Elements
{
@ -15,6 +16,7 @@ namespace Biskilog_Accounting.Client.Pages.Dashboard.Elements
[Parameter]
public List<WeeklySaleItem> Series { get; set; } = new List<WeeklySaleItem>();
private ApexChartOptions<WeeklySaleItem> m_options;
private ApexChartOptions<WeeklySaleItem> options = new ApexChartOptions<WeeklySaleItem>();
private string m_bestDay { get; set; } = string.Empty;
private string m_worstDay { get; set; } = string.Empty;
@ -28,17 +30,54 @@ namespace Biskilog_Accounting.Client.Pages.Dashboard.Elements
{
Bar = new PlotOptionsBar
{
ColumnWidth = "7",
ColumnWidth = "20",
}
},
Colors = new List<string> { "#11726d" },
Yaxis = new List<YAxis>
{
new YAxis
{
Labels = new YAxisLabels
{
Formatter = @"function (value, index, w) {
return Number(value).toLocaleString();}"
}
}
},
};
options.Colors = new List<string> { "#11726d" };
options.Fill = new Fill
{
Type = new List<FillType> { FillType.Gradient, FillType.Gradient },
Gradient = new FillGradient
{
ShadeIntensity = 1,
OpacityFrom = 0.2,
OpacityTo = 0.9,
}
};
options.Yaxis = new List<YAxis>
{
new YAxis
{
Labels = new YAxisLabels
{
Formatter = @"function (value, index, w) {
return Number(value).toLocaleString();}"
}
}
};
if (Series.Count > 1)
{
m_bestDay = Series.OrderByDescending(t => t.Total).First().Date.ToShortDateString();
m_bestSales = m_calculator.FormatMoneyWithCurrency((double)Series.OrderByDescending(t => t.Total).First().Total);
m_bestSales = m_calculator.FormatMoneyWithCurrencyKilo((double)Series.OrderByDescending(t => t.Total).First().Total);
m_worstDay = Series.OrderByDescending(t => t.Total).Last().Date.ToShortDateString();
m_worstSales = m_calculator.FormatMoneyWithCurrency((double)Series.OrderByDescending(t => t.Total).Last().Total);
m_worstSales = m_calculator.FormatMoneyWithCurrencyKilo((double)Series.OrderByDescending(t => t.Total).Last().Total);
}
base.OnParametersSet();
}

30
Client/Pages/Dashboard/Elements/LowStockItems.razor

@ -0,0 +1,30 @@
@using Biskilog_Accounting.Shared.CustomModels;
@using Biskilog_Accounting.Shared.Interfaces;
@inject ICalculator m_calculator
<div class="card h-100">
<div class="card-header d-flex align-items-center justify-content-between pb-0">
<div class="card-title mb-0">
<h5 class="m-0 me-2">Low Stock Items</h5>
</div>
</div>
<div class="card-body" style="padding:5px !important;">
<RadzenDataGrid Data="@LowStockProducts" TItem="ProductItem" AllowPaging="true" AllowSorting="true" PageSize="5">
<Columns>
<RadzenDataGridColumn TItem="ProductItem" Property="Product" Title="Product">
<Template Context="detail">
<RadzenRow>
<RadzenColumn>
<span> <b>@detail.Product!.ProductName</b></span> <br />
<span> @detail.Product.Pcode</span>
</RadzenColumn>
<RadzenColumn style="justify-content:end !important;">
<span><b>Quantity left:</b> @(detail.Stock.Quantity) @(detail.BaseUnit)</span>
</RadzenColumn>
</RadzenRow>
</Template>
</RadzenDataGridColumn>
</Columns>
</RadzenDataGrid>
</div>
</div>

11
Client/Pages/Dashboard/Elements/LowStockItems.razor.cs

@ -0,0 +1,11 @@
using Biskilog_Accounting.Shared.CustomModels;
using Microsoft.AspNetCore.Components;
namespace Biskilog_Accounting.Client.Pages.Dashboard.Elements
{
public partial class LowStockItems
{
[Parameter]
public IEnumerable<ProductItem> LowStockProducts { get;set; } = new List<ProductItem>();
}
}

80
Client/Pages/Dashboard/Elements/MostPurchasedElement.razor

@ -0,0 +1,80 @@
@using Biskilog_Accounting.Shared.CustomModels;
@using Biskilog_Accounting.Shared.Interfaces;
@inject ICalculator m_calculator
@if (!IsLoading)
{
@if (MostPurchasedItems.Count() > 5)
{
<div class="card h-100">
<div class="card-body" style="padding:5px !important;">
<div class="d-flex justify-content-between flex-sm-row flex-column gap-3">
<div class="d-flex flex-sm-column flex-row align-items-start justify-content-between">
<div class="card-title" style="padding:10px;">
<h5 class="text-nowrap mb-2">Top 50 most purchased items</h5>
</div>
<div class="mt-sm-auto">
@foreach (MostPurchasedItem purchasedItem in m_items.Take(5))
{
<small class="text-nowrap fw-semibold">
@($"{purchasedItem.ProductName} x{purchasedItem.NbrTimesSold}")
</small> <br />
}
</div>
</div>
<ApexChart TItem="MostPurchasedItem" Options="@m_options" Width="150" Height="150">
<ApexPointSeries TItem="MostPurchasedItem"
Items="m_items"
SeriesType="SeriesType.Donut"
Name="Top 5 performers"
XValue="@(e => e.ProductName)"
YValue="@(e => e.NbrTimesSold)"
OrderByDescending="e=>e.Y" />
</ApexChart>
</div>
<div class="card-body" style="padding:5px !important;">
<RadzenDataGrid Data="@MostPurchasedItems" TItem="MostPurchasedItem" AllowPaging="true" AllowSorting="true" PageSize="5">
<Columns>
<RadzenDataGridColumn TItem="MostPurchasedItem" Property="Revenue" Title="Product">
<Template Context="detail">
<RadzenRow>
<RadzenColumn>
<span> <b>@detail.ProductName</b></span> <br />
<span> @detail.ProductId</span>
</RadzenColumn>
<RadzenColumn style="justify-content:end !important;">
<span><b>Revenue:</b> @m_calculator.FormatMoneyWithCurrencyKilo((double)detail.Revenue)</span> <br />
<span><b>Quantity Sold:</b> @detail.NbrTimesSold</span>
</RadzenColumn>
</RadzenRow>
</Template>
</RadzenDataGridColumn>
</Columns>
</RadzenDataGrid>
</div>
</div>
</div>
}
}
else
{
<div class="card h-100">
<div class="card-body" style="padding:5px !important;">
</div>
</div>
}
<style>
.rz-datatable-thead th, .rz-grid-table thead th {
background-color: #a4d5d3;
padding: 0;
color: white;
}
.rz-paginator {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
}
</style>

33
Client/Pages/Dashboard/Elements/MostPurchasedElement.razor.cs

@ -0,0 +1,33 @@
using ApexCharts;
using Biskilog_Accounting.Shared.CustomModels;
using Microsoft.AspNetCore.Components;
using Radzen.Blazor.Rendering;
using Legend = ApexCharts.Legend;
namespace Biskilog_Accounting.Client.Pages.Dashboard.Elements
{
public partial class MostPurchasedElement
{
[Parameter]
public IEnumerable<MostPurchasedItem> MostPurchasedItems { get; set; } = new List<MostPurchasedItem>();
[Parameter]
public bool IsLoading { get; set; } = true;
private ApexChartOptions<MostPurchasedItem> m_options { get; set; } = new();
private IEnumerable<MostPurchasedItem> m_items { get; set; } = new List<MostPurchasedItem>();
protected override void OnInitialized()
{
m_options.Legend = new Legend()
{
Show = false,
};
}
protected override void OnParametersSet()
{
m_items = MostPurchasedItems.OrderByDescending(t => t.NbrTimesSold);
IsLoading = false;
base.OnParametersSet();
}
}
}

31
Client/Pages/Dashboard/Elements/TransactionCard.razor

@ -0,0 +1,31 @@
@using Biskilog_Accounting.Shared.CustomModels;
@using Biskilog_Accounting.Shared.Interfaces;
@inject ICalculator m_calculator
<div class="card h-100">
<div class="card-header d-flex align-items-center justify-content-between">
<h5 class="card-title m-0 me-2">Last 50 Transactions</h5>
</div>
<div class="card-body" style="padding:5px !important;">
<RadzenDataGrid Data="@Sales" TItem="SaleItem" AllowPaging="true" AllowSorting="true">
<Columns>
<RadzenDataGridColumn TItem="SaleItem" Property="Transno" Title="Receipt#" />
<RadzenDataGridColumn TItem="SaleItem" Property="Date" Title="Timestamp" />
<RadzenDataGridColumn TItem="SaleItem" Property="Total" Title="Total">
<Template Context="detail">
@(m_calculator.FormatMoneyWithCurrencyKilo((double)detail.Total))
</Template>
</RadzenDataGridColumn>
</Columns>
</RadzenDataGrid>
</div>
</div>
<style>
.rz-datatable-thead th, .rz-grid-table thead th {
background-color: #a4d5d3;
padding: 0;
color: white;
}
</style>

24
Client/Pages/Dashboard/Elements/TransactionCard.razor.cs

@ -0,0 +1,24 @@
using Biskilog_Accounting.Shared.CustomModels;
using Microsoft.AspNetCore.Components;
namespace Biskilog_Accounting.Client.Pages.Dashboard.Elements
{
public partial class TransactionCard
{
[Parameter]
public List<SaleItem> Sales { get; set; } = new List<SaleItem> { };
[Parameter]
public bool IsLoading { get; set; } = true;
protected override Task OnInitializedAsync()
{
IsLoading = true;
return base.OnInitializedAsync();
}
protected override void OnParametersSet()
{
IsLoading = false;
base.OnParametersSet();
}
}
}

2
Client/Pages/Dashboard/Elements/WelcomeCard.razor

@ -5,7 +5,7 @@
<div class="d-flex align-items-end row">
<div class="col-sm-7">
<div class="card-body">
<h5 class="card-title text-primary">@(CurrentTradeSales > PreviousTradeSales ? "Congratulations 🎉" : PreviousTradeSales > CurrentTradeSales ? "Tough shift there" : "Well Done") @Username!</h5>
<h5 class="card-title text-primary">@(TradeSummary.CurrentTradeSales > TradeSummary.LastTradeSales ? "Congratulations 🎉" : TradeSummary.LastTradeSales > TradeSummary.CurrentTradeSales ? "Tough shift there" : "Well Done") @Username!</h5>
<p class="mb-4">
@m_remarks
</p>

39
Client/Pages/Dashboard/Elements/WelcomeCard.razor.cs

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Components;
using Biskilog_Accounting.Shared.CustomModels;
using Microsoft.AspNetCore.Components;
namespace Biskilog_Accounting.Client.Pages.Dashboard.Elements
{
@ -8,11 +9,11 @@ namespace Biskilog_Accounting.Client.Pages.Dashboard.Elements
public string Username { get; set; } = string.Empty;
[Parameter]
public double CurrentTradeSales { get; set; } = 0;
[Parameter]
public double PreviousTradeSales { get; set; } = 0;
public TradeSummary TradeSummary { get; set; } = new TradeSummary();
private string m_remarks { get; set; } = "";
private string m_currentTrade { get; set; } = string.Empty;
private string m_previousTrade { get; set; } = string.Empty;
protected override void OnParametersSet()
{
@ -20,20 +21,38 @@ namespace Biskilog_Accounting.Client.Pages.Dashboard.Elements
}
private void CalculateStatistic()
{
double change = ((CurrentTradeSales - PreviousTradeSales) / PreviousTradeSales) * 100;
if (CurrentTradeSales > PreviousTradeSales)
if(TradeSummary.CurrentTradeDate != DateTime.Today)
{
m_currentTrade = $"most recent trade date ({TradeSummary.CurrentTradeDate.ToString("dd MMM,yyy")})";
}
else
{
m_currentTrade = "current trade";
}
if (TradeSummary.LastTradeDate != DateTime.Today.AddDays(-1))
{
m_previousTrade = $"previous trade date ({TradeSummary.LastTradeDate.ToString("dd MMM,yyy")})";
}
else
{
m_previousTrade = "previous trade";
}
double change = ((TradeSummary.CurrentTradeSales - TradeSummary.LastTradeSales) / TradeSummary.LastTradeSales) * 100;
if (TradeSummary.CurrentTradeSales > TradeSummary.LastTradeSales)
{
string changePercent = change.ToString("0.00") + "%";
m_remarks = $"You made a total of {m_calculator.FormatMoneyWithCurrency(CurrentTradeSales)} in the current trade, {changePercent} more than the previous trade sales";
m_remarks = $"You made a total of {m_calculator.FormatMoneyWithCurrency(TradeSummary.CurrentTradeSales)} in the {m_currentTrade}, {changePercent} more than the sales in the {m_previousTrade}";
}
else if (PreviousTradeSales > CurrentTradeSales)
else if (TradeSummary.LastTradeSales > TradeSummary.CurrentTradeSales)
{
string changePercent = (change * -1).ToString("0.00") + "%";
m_remarks = $"You made a total of {m_calculator.FormatMoneyWithCurrency(CurrentTradeSales)} in the current trade, {changePercent} less than the previous trade sales";
m_remarks = $"You made a total of {m_calculator.FormatMoneyWithCurrency(TradeSummary.CurrentTradeSales)} in the {m_currentTrade}, {changePercent} less than the sales in the {m_previousTrade}";
}
else
{
m_remarks = $"You made a total of {m_calculator.FormatMoneyWithCurrency(CurrentTradeSales)} in the current trade, same as the previous trade sales";
m_remarks = $"You made a total of {m_calculator.FormatMoneyWithCurrency(TradeSummary.CurrentTradeSales)} in the {m_currentTrade}, same as the sales in the {m_previousTrade}";
}
StateHasChanged();
}

2
Client/_Imports.razor

@ -6,3 +6,5 @@
@using Microsoft.JSInterop
@using Biskilog_Accounting.Client
@using ApexCharts;
@using Radzen
@using Radzen.Blazor

2
Client/wwwroot/index.html

@ -7,6 +7,7 @@
<base href="/" />
<link href="css/app.css" rel="stylesheet" />
<link href="manifest.json" rel="manifest" />
<link rel="stylesheet" href="_content/Radzen.Blazor/css/material-base.css">
<!-- Font Awesome -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"
rel="stylesheet" />
@ -49,6 +50,7 @@
<script src="_framework/blazor.webassembly.js"></script>
<script src="_content/Blazor-ApexCharts/js/apex-charts.min.js"></script>
<script src="_content/Blazor-ApexCharts/js/blazor-apex-charts.js"></script>
<script src="_content/Radzen.Blazor/Radzen.Blazor.js"></script>
<script>navigator.serviceWorker.register('service-worker.js');</script>
</body>

3
Server/Controllers/AnalyticsController.cs

@ -91,7 +91,6 @@ namespace Biskilog_Accounting.Server.Controllers
[HttpGet, Route("lowonstock")]
public IEnumerable<ProductItem> GetLowOnStockItems()
{
string token = Request.Headers[HeaderNames.Authorization]!;
return m_analyticService.GetOutOfStockItems();
}
@ -120,7 +119,7 @@ namespace Biskilog_Accounting.Server.Controllers
/// </summary>
[Authorize]
[HttpGet, Route("sales/recent/{a_limit}")]
public IEnumerable<Tblcart> GetRecentTransactions(int a_limit)
public IEnumerable<SaleItem> GetRecentTransactions(int a_limit)
{
return m_analyticService.GetRecentSales(a_limit);
}

37
Server/Services/AnalyticalService.cs

@ -7,6 +7,7 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Net.Http.Headers;
using MySqlConnector;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Drawing.Drawing2D;
@ -145,6 +146,7 @@ namespace Biskilog_Accounting.Server.Services
from au in AltUnit.DefaultIfEmpty()
join rs in m_context.Restocklevels on item.Pcode equals rs.ProductId
join un in m_context.Unitofmeasures on p.BaseUnit equals un.UnitCode
orderby item.Quantity
where p.Status!.ToLower() != "inactive" && accessiblebranches.Contains(item.BranchId) &&
((rs.WarnLevel >= item.Quantity && rs.Unit == p.BaseUnit) || (rs.WarnLevel >= (item.Quantity / au.QuantityUnit) &&
rs.Unit == au.UnitCode)
@ -177,6 +179,11 @@ namespace Biskilog_Accounting.Server.Services
};
}
public IEnumerable<ProductPriceChange> GetProductPriceChangeHistory(int a_limit)
{
throw new NotImplementedException();
}
public IEnumerable<ProductPriceChange> GetRecentPriceChanges(int a_limit)
{
string token = m_httpContext.Request.Headers[HeaderNames.Authorization]!;
@ -198,16 +205,40 @@ namespace Biskilog_Accounting.Server.Services
}).Take(a_limit);
}
public IEnumerable<Tblcart> GetRecentSales(int a_limit)
public IEnumerable<SaleItem> GetRecentSales(int a_limit)
{
string token = m_httpContext.Request.Headers[HeaderNames.Authorization]!;
if (AuthEnums.Valid == m_tokenService.ValidateToken(token))
{
IEnumerable<string> accessiblebranches = m_tokenService.BranchIds(token);
return m_context.Tblcarts.Where(t => accessiblebranches.Contains(t.BranchId)).OrderByDescending(t => t.Date).Take(a_limit);
using (var command = m_context.Database.GetDbConnection().CreateCommand())
{
command.CommandText = "CALL GetRecentTransactions(@p0,@p1)";
command.Parameters.Add(new MySqlParameter("@p0", string.Join(", ", accessiblebranches.ToArray())));
command.Parameters.Add(new MySqlParameter("@p1", a_limit));
m_context.Database.OpenConnection();
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
yield return new SaleItem
{
Transno = reader.GetString(0),
Total = (decimal)reader.GetDouble(1),
Date = reader.GetDateTime(2),
Cashier = reader.GetString(3),
BranchId = reader.GetString(4),
Customer = reader.GetString(5),
Status = reader.GetString(6),
};
}
}
}
}
return new List<Tblcart>();
}
public IEnumerable<Tblcart> GetSalesTransaction(DateTime a_start, DateTime a_end)

8
Shared/Interfaces/IAnalytics.cs

@ -64,12 +64,18 @@ namespace Biskilog_Accounting.Shared.Interfaces
/// </summary>
/// <param name="a_limit">The number of rows to return </param>
/// <returns></returns>
IEnumerable<Tblcart> GetRecentSales(int a_limit);
IEnumerable<SaleItem> GetRecentSales(int a_limit);
/// <summary>
/// Fetches a collection of product price changes recently made
/// </summary>
/// <param name="a_limit">the number of rows to return</param>
/// <returns></returns>
IEnumerable<ProductPriceChange> GetRecentPriceChanges(int a_limit);
/// <summary>
/// Fetches a collection of price change history per product
/// </summary>
/// <param name="a_limit">the number of products to fetch history</param>
/// <returns></returns>
IEnumerable<ProductPriceChange> GetProductPriceChangeHistory(int a_limit);
}
}

1
Shared/Interfaces/ICalculator.cs

@ -11,6 +11,7 @@ namespace Biskilog_Accounting.Shared.Interfaces
{
double CalculatePercentage();
string FormatMoneyWithCurrency(double a_amount);
string FormatMoneyWithCurrencyKilo(double a_amount);
NumberFormatInfo GetCurrencyCode();
}
}

26
Shared/ServiceRepo/CalculatorService.cs

@ -19,7 +19,10 @@ namespace Biskilog_Accounting.Shared.ServiceRepo
{
return string.Format(GetCurrencyCode(), " {0:C2}", a_amount);
}
public string FormatMoneyWithCurrencyKilo(double a_amount)
{
return GetCurrencyCode().CurrencySymbol + FormatNumber(a_amount);
}
public NumberFormatInfo GetCurrencyCode()
{
//TODO have a better implementation
@ -35,5 +38,26 @@ namespace Biskilog_Accounting.Shared.ServiceRepo
return numberFormatInfo;
}
private string FormatNumber(double a_amount)
{
if (a_amount >= 100000000)
{
return (a_amount / 1000000D).ToString("0.#M");
}
if (a_amount >= 1000000)
{
return (a_amount / 1000000D).ToString("0.##M");
}
if (a_amount >= 100000)
{
return (a_amount / 1000D).ToString("0.#k");
}
if (a_amount >= 10000)
{
return (a_amount / 1000D).ToString("0.##k");
}
return a_amount.ToString("#,0");
}
}
}

Loading…
Cancel
Save