littleshop/LittleShop/Areas/Admin/Views/Customers/Index.cshtml
SysAdmin a2247d7c02
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
feat: Add customer management, payments, and push notifications with security enhancements
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>
2025-11-16 19:33:02 +00:00

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>
}