Some checks failed
Build and Deploy LittleShop / Build TeleBot Docker Image (push) Failing after 11s
Build and Deploy LittleShop / Build LittleShop Docker Image (push) Failing after 15s
Build and Deploy LittleShop / Deploy to Production VPS (Manual Only) (push) Has been skipped
Build and Deploy LittleShop / Deploy to Pre-Production (CT109) (push) Has been skipped
Major Feature Additions: - Customer management: Full CRUD with data export and privacy compliance - Payment management: Centralized payment tracking and administration - Push notification subscriptions: Manage and track web push subscriptions Security Enhancements: - IP whitelist middleware for administrative endpoints - Data retention service with configurable policies - Enhanced push notification security documentation - Security fixes progress tracking (2025-11-14) UI/UX Improvements: - Enhanced navigation with improved mobile responsiveness - Updated admin dashboard with order status counts - Improved product CRUD forms - New customer and payment management interfaces Backend Improvements: - Extended customer service with data export capabilities - Enhanced order service with status count queries - Improved crypto payment service with better error handling - Updated validators and configuration Documentation: - DEPLOYMENT_NGINX_GUIDE.md: Nginx deployment instructions - IP_STORAGE_ANALYSIS.md: IP storage security analysis - PUSH_NOTIFICATION_SECURITY.md: Push notification security guide - UI_UX_IMPROVEMENT_PLAN.md: Planned UI/UX enhancements - UI_UX_IMPROVEMENTS_COMPLETED.md: Completed improvements Cleanup: - Removed temporary database WAL files - Removed stale commit message file 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
233 lines
11 KiB
Plaintext
233 lines
11 KiB
Plaintext
@model IEnumerable<LittleShop.DTOs.CustomerDto>
|
|
@{
|
|
ViewData["Title"] = "Customers";
|
|
var searchTerm = ViewData["SearchTerm"] as string ?? "";
|
|
}
|
|
|
|
<div class="row mb-3">
|
|
<div class="col">
|
|
<h1><i class="fas fa-users"></i> Customer Management</h1>
|
|
<p class="text-muted mb-0">Manage customer accounts, view order history, and monitor risk scores</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Search Bar -->
|
|
<div class="row mb-4">
|
|
<div class="col-md-6">
|
|
<form method="get" action="@Url.Action("Index")" class="input-group">
|
|
<input type="text" name="searchTerm" value="@searchTerm"
|
|
class="form-control"
|
|
placeholder="Search by name, username, Telegram ID, or email...">
|
|
<button class="btn btn-primary" type="submit">
|
|
<i class="fas fa-search"></i> Search
|
|
</button>
|
|
@if (!string.IsNullOrEmpty(searchTerm))
|
|
{
|
|
<a href="@Url.Action("Index")" class="btn btn-secondary">
|
|
<i class="fas fa-times"></i> Clear
|
|
</a>
|
|
}
|
|
</form>
|
|
</div>
|
|
<div class="col-md-6 text-end">
|
|
<span class="text-muted">
|
|
<strong>@Model.Count()</strong> customer@(Model.Count() != 1 ? "s" : "") found
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Statistics Cards -->
|
|
<div class="row mb-4">
|
|
<div class="col-md-3">
|
|
<div class="card border-primary">
|
|
<div class="card-body">
|
|
<h6 class="text-muted mb-2">Total Customers</h6>
|
|
<h3 class="mb-0">@Model.Count()</h3>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card border-success">
|
|
<div class="card-body">
|
|
<h6 class="text-muted mb-2">Active</h6>
|
|
<h3 class="mb-0 text-success">@Model.Count(c => c.IsActive && !c.IsBlocked)</h3>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card border-danger">
|
|
<div class="card-body">
|
|
<h6 class="text-muted mb-2">Blocked</h6>
|
|
<h3 class="mb-0 text-danger">@Model.Count(c => c.IsBlocked)</h3>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card border-warning">
|
|
<div class="card-body">
|
|
<h6 class="text-muted mb-2">High Risk (>70)</h6>
|
|
<h3 class="mb-0 text-warning">@Model.Count(c => c.RiskScore > 70)</h3>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Customers Table -->
|
|
@if (Model.Any())
|
|
{
|
|
<div class="card">
|
|
<div class="card-header bg-light">
|
|
<h5 class="mb-0"><i class="fas fa-list"></i> Customer List</h5>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover mb-0">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>Customer</th>
|
|
<th>Telegram</th>
|
|
<th class="text-center">Status</th>
|
|
<th class="text-end">Orders</th>
|
|
<th class="text-end">Total Spent</th>
|
|
<th class="text-center">Risk Score</th>
|
|
<th>Last Active</th>
|
|
<th class="text-center">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach (var customer in Model)
|
|
{
|
|
<tr class="@(customer.IsBlocked ? "table-danger" : "")">
|
|
<td>
|
|
<div>
|
|
<strong>@customer.TelegramDisplayName</strong>
|
|
@if (!string.IsNullOrEmpty(customer.TelegramFirstName) && !string.IsNullOrEmpty(customer.TelegramLastName))
|
|
{
|
|
<br><small class="text-muted">@customer.TelegramFirstName @customer.TelegramLastName</small>
|
|
}
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<div>
|
|
@@<strong>@customer.TelegramUsername</strong>
|
|
<br><small class="text-muted">ID: @customer.TelegramUserId</small>
|
|
</div>
|
|
</td>
|
|
<td class="text-center">
|
|
@if (customer.IsBlocked)
|
|
{
|
|
<span class="badge bg-danger">
|
|
<i class="fas fa-ban"></i> Blocked
|
|
</span>
|
|
}
|
|
else if (!customer.IsActive)
|
|
{
|
|
<span class="badge bg-secondary">
|
|
<i class="fas fa-trash"></i> Deleted
|
|
</span>
|
|
}
|
|
else
|
|
{
|
|
<span class="badge bg-success">
|
|
<i class="fas fa-check-circle"></i> Active
|
|
</span>
|
|
}
|
|
</td>
|
|
<td class="text-end">
|
|
<div>
|
|
<strong>@customer.TotalOrders</strong>
|
|
@if (customer.SuccessfulOrders > 0)
|
|
{
|
|
<br><small class="text-success">@customer.SuccessfulOrders successful</small>
|
|
}
|
|
@if (customer.CancelledOrders > 0)
|
|
{
|
|
<br><small class="text-muted">@customer.CancelledOrders cancelled</small>
|
|
}
|
|
@if (customer.DisputedOrders > 0)
|
|
{
|
|
<br><small class="text-danger">@customer.DisputedOrders disputed</small>
|
|
}
|
|
</div>
|
|
</td>
|
|
<td class="text-end">
|
|
<strong>£@customer.TotalSpent.ToString("N2")</strong>
|
|
@if (customer.AverageOrderValue > 0)
|
|
{
|
|
<br><small class="text-muted">Avg: £@customer.AverageOrderValue.ToString("N2")</small>
|
|
}
|
|
</td>
|
|
<td class="text-center">
|
|
@{
|
|
var riskBadgeClass = customer.RiskScore >= 80 ? "bg-danger" :
|
|
customer.RiskScore >= 50 ? "bg-warning" :
|
|
customer.RiskScore >= 30 ? "bg-info" :
|
|
"bg-success";
|
|
var riskIcon = customer.RiskScore >= 80 ? "fa-exclamation-triangle" :
|
|
customer.RiskScore >= 50 ? "fa-exclamation-circle" :
|
|
customer.RiskScore >= 30 ? "fa-info-circle" :
|
|
"fa-check-circle";
|
|
}
|
|
<span class="badge @riskBadgeClass" style="font-size: 1.1em;">
|
|
<i class="fas @riskIcon"></i> @customer.RiskScore
|
|
</span>
|
|
</td>
|
|
<td>
|
|
@if (customer.LastActiveAt > DateTime.MinValue)
|
|
{
|
|
var daysAgo = (DateTime.UtcNow - customer.LastActiveAt).Days;
|
|
<span>@customer.LastActiveAt.ToString("MMM dd, yyyy")</span>
|
|
@if (daysAgo <= 1)
|
|
{
|
|
<br><small class="text-success">Today</small>
|
|
}
|
|
else if (daysAgo <= 7)
|
|
{
|
|
<br><small class="text-muted">@daysAgo days ago</small>
|
|
}
|
|
else if (daysAgo <= 30)
|
|
{
|
|
<br><small class="text-warning">@daysAgo days ago</small>
|
|
}
|
|
else
|
|
{
|
|
<br><small class="text-danger">@daysAgo days ago</small>
|
|
}
|
|
}
|
|
else
|
|
{
|
|
<span class="text-muted">Never</span>
|
|
}
|
|
</td>
|
|
<td class="text-center">
|
|
<a href="@Url.Action("Details", new { id = customer.Id })"
|
|
class="btn btn-sm btn-primary"
|
|
title="View Details">
|
|
<i class="fas fa-eye"></i>
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="alert alert-info">
|
|
<i class="fas fa-info-circle"></i>
|
|
@if (!string.IsNullOrEmpty(searchTerm))
|
|
{
|
|
<strong>No customers found matching "@searchTerm"</strong>
|
|
<p class="mb-0">Try a different search term or <a href="@Url.Action("Index")" class="alert-link">view all customers</a>.</p>
|
|
}
|
|
else
|
|
{
|
|
<strong>No customers yet</strong>
|
|
<p class="mb-0">Customers will appear here automatically when they place their first order through the TeleBot.</p>
|
|
}
|
|
</div>
|
|
}
|