littleshop/LittleShop/Areas/Admin/Views/Customers/Details.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

446 lines
19 KiB
Plaintext

@model LittleShop.DTOs.CustomerDto
@{
ViewData["Title"] = $"Customer: {Model.TelegramDisplayName}";
var customerOrders = ViewData["CustomerOrders"] as List<LittleShop.DTOs.OrderDto> ?? new List<LittleShop.DTOs.OrderDto>();
}
<div class="row mb-3">
<div class="col">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="@Url.Action("Index", "Dashboard")">Dashboard</a></li>
<li class="breadcrumb-item"><a href="@Url.Action("Index", "Customers")">Customers</a></li>
<li class="breadcrumb-item active">@Model.TelegramDisplayName</li>
</ol>
</nav>
</div>
</div>
<!-- Customer Header -->
<div class="row mb-4">
<div class="col-md-8">
<h1>
<i class="fas fa-user-circle"></i> @Model.TelegramDisplayName
@if (Model.IsBlocked)
{
<span class="badge bg-danger ms-2">
<i class="fas fa-ban"></i> BLOCKED
</span>
}
else if (!Model.IsActive)
{
<span class="badge bg-secondary ms-2">
<i class="fas fa-trash"></i> DELETED
</span>
}
else
{
<span class="badge bg-success ms-2">
<i class="fas fa-check-circle"></i> ACTIVE
</span>
}
</h1>
<p class="text-muted mb-0">
Telegram: @@<strong>@Model.TelegramUsername</strong> | ID: @Model.TelegramUserId
</p>
</div>
<div class="col-md-4 text-end">
<!-- GDPR Data Export Buttons -->
<div class="btn-group me-2" role="group">
<a href="@Url.Action("ExportJson", "Customers", new { id = Model.Id })"
class="btn btn-info"
title="Export all customer data as JSON (GDPR Right to Data Portability)">
<i class="fas fa-file-code"></i> Export JSON
</a>
<a href="@Url.Action("ExportCsv", "Customers", new { id = Model.Id })"
class="btn btn-success"
title="Export all customer data as CSV (GDPR Right to Data Portability)">
<i class="fas fa-file-csv"></i> Export CSV
</a>
</div>
<a href="@Url.Action("Index", "Customers")" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Back to List
</a>
</div>
</div>
<!-- Customer Information Cards -->
<div class="row mb-4">
<!-- Basic Information -->
<div class="col-md-6">
<div class="card">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="fas fa-id-card"></i> Customer Information</h5>
</div>
<div class="card-body">
<table class="table table-sm mb-0">
<tr>
<th style="width: 40%;">Display Name:</th>
<td>@Model.TelegramDisplayName</td>
</tr>
<tr>
<th>Full Name:</th>
<td>
@if (!string.IsNullOrEmpty(Model.TelegramFirstName) || !string.IsNullOrEmpty(Model.TelegramLastName))
{
@Model.TelegramFirstName @Model.TelegramLastName
}
else
{
<span class="text-muted">Not provided</span>
}
</td>
</tr>
<tr>
<th>Telegram Username:</th>
<td>@@<strong>@Model.TelegramUsername</strong></td>
</tr>
<tr>
<th>Telegram User ID:</th>
<td><code>@Model.TelegramUserId</code></td>
</tr>
<tr>
<th>Language:</th>
<td>@Model.Language.ToUpper()</td>
</tr>
<tr>
<th>Timezone:</th>
<td>@Model.Timezone</td>
</tr>
<tr>
<th>Customer Since:</th>
<td>@Model.CreatedAt.ToString("MMMM dd, yyyy")</td>
</tr>
<tr>
<th>Last Active:</th>
<td>
@if (Model.LastActiveAt > DateTime.MinValue)
{
var daysAgo = (DateTime.UtcNow - Model.LastActiveAt).Days;
@Model.LastActiveAt.ToString("MMMM dd, yyyy HH:mm")
@if (daysAgo <= 1)
{
<span class="badge bg-success">Active today</span>
}
else if (daysAgo <= 7)
{
<span class="badge bg-info">@daysAgo days ago</span>
}
else if (daysAgo <= 30)
{
<span class="badge bg-warning">@daysAgo days ago</span>
}
else
{
<span class="badge bg-danger">@daysAgo days ago</span>
}
}
else
{
<span class="text-muted">Never</span>
}
</td>
</tr>
</table>
</div>
</div>
</div>
<!-- Risk Score & Metrics -->
<div class="col-md-6">
<div class="card">
<div class="card-header bg-warning text-dark">
<h5 class="mb-0"><i class="fas fa-shield-alt"></i> Risk Assessment</h5>
</div>
<div class="card-body">
@{
var riskLevel = Model.RiskScore >= 80 ? "Very High Risk" :
Model.RiskScore >= 50 ? "High Risk" :
Model.RiskScore >= 30 ? "Medium Risk" :
"Low Risk";
var riskClass = Model.RiskScore >= 80 ? "danger" :
Model.RiskScore >= 50 ? "warning" :
Model.RiskScore >= 30 ? "info" :
"success";
}
<div class="text-center mb-3">
<h1 class="display-4 text-@riskClass mb-0">
<i class="fas fa-exclamation-triangle"></i> @Model.RiskScore
</h1>
<p class="text-@riskClass fw-bold mb-0">@riskLevel</p>
</div>
<hr>
<table class="table table-sm mb-0">
<tr>
<th style="width: 60%;">Total Orders:</th>
<td class="text-end"><strong>@Model.TotalOrders</strong></td>
</tr>
<tr class="table-success">
<th>Successful Orders:</th>
<td class="text-end text-success"><strong>@Model.SuccessfulOrders</strong></td>
</tr>
<tr class="table-warning">
<th>Cancelled Orders:</th>
<td class="text-end text-warning"><strong>@Model.CancelledOrders</strong></td>
</tr>
<tr class="table-danger">
<th>Disputed Orders:</th>
<td class="text-end text-danger"><strong>@Model.DisputedOrders</strong></td>
</tr>
<tr>
<th>Success Rate:</th>
<td class="text-end">
@if (Model.TotalOrders > 0)
{
var successRate = (Model.SuccessfulOrders * 100.0) / Model.TotalOrders;
@successRate.ToString("F1")<text>%</text>
}
else
{
<span class="text-muted">N/A</span>
}
</td>
</tr>
</table>
<div class="mt-3">
<form method="post" action="@Url.Action("RefreshRiskScore", new { id = Model.Id })" class="d-inline">
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-sm btn-outline-warning w-100">
<i class="fas fa-sync-alt"></i> Recalculate Risk Score
</button>
</form>
</div>
</div>
</div>
</div>
</div>
<!-- Spending Metrics -->
<div class="row mb-4">
<div class="col-md-3">
<div class="card bg-primary text-white">
<div class="card-body text-center">
<h6 class="mb-2">Total Spent</h6>
<h3 class="mb-0">£@Model.TotalSpent.ToString("N2")</h3>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-success text-white">
<div class="card-body text-center">
<h6 class="mb-2">Average Order Value</h6>
<h3 class="mb-0">£@Model.AverageOrderValue.ToString("N2")</h3>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-info text-white">
<div class="card-body text-center">
<h6 class="mb-2">First Order</h6>
<h3 class="mb-0" style="font-size: 1.2rem;">
@if (Model.FirstOrderDate > DateTime.MinValue)
{
@Model.FirstOrderDate.ToString("MMM dd, yyyy")
}
else
{
<span>No orders</span>
}
</h3>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-warning text-dark">
<div class="card-body text-center">
<h6 class="mb-2">Last Order</h6>
<h3 class="mb-0" style="font-size: 1.2rem;">
@if (Model.LastOrderDate > DateTime.MinValue)
{
@Model.LastOrderDate.ToString("MMM dd, yyyy")
}
else
{
<span>No orders</span>
}
</h3>
</div>
</div>
</div>
</div>
<!-- Customer Management Actions -->
<div class="row mb-4">
<div class="col">
<div class="card @(Model.IsBlocked ? "border-danger" : "")">
<div class="card-header @(Model.IsBlocked ? "bg-danger text-white" : "bg-light")">
<h5 class="mb-0"><i class="fas fa-cog"></i> Customer Management Actions</h5>
</div>
<div class="card-body">
@if (Model.IsBlocked)
{
<div class="alert alert-danger">
<h5><i class="fas fa-ban"></i> Customer is Blocked</h5>
<p class="mb-0"><strong>Reason:</strong> @Model.BlockReason</p>
</div>
<form method="post" action="@Url.Action("Unblock", new { id = Model.Id })" class="d-inline">
@Html.AntiForgeryToken()
<button type="submit" class="btn btn-success" onclick="return confirm('Are you sure you want to unblock this customer?')">
<i class="fas fa-check-circle"></i> Unblock Customer
</button>
</form>
}
else
{
<button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#blockModal">
<i class="fas fa-ban"></i> Block Customer
</button>
}
<button type="button" class="btn btn-warning" data-bs-toggle="modal" data-bs-target="#deleteModal">
<i class="fas fa-trash"></i> Delete Customer
</button>
@if (!string.IsNullOrEmpty(Model.CustomerNotes))
{
<hr>
<h6>Admin Notes:</h6>
<p class="text-muted mb-0">@Model.CustomerNotes</p>
}
</div>
</div>
</div>
</div>
<!-- Order History -->
<div class="row">
<div class="col">
<div class="card">
<div class="card-header bg-light">
<h5 class="mb-0">
<i class="fas fa-shopping-cart"></i> Order History
<span class="badge bg-primary">@customerOrders.Count</span>
</h5>
</div>
<div class="card-body p-0">
@if (customerOrders.Any())
{
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>Order ID</th>
<th>Date</th>
<th>Status</th>
<th class="text-end">Items</th>
<th class="text-end">Total</th>
<th class="text-center">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var order in customerOrders)
{
<tr>
<td><code>@order.Id.ToString().Substring(0, 8)</code></td>
<td>@order.CreatedAt.ToString("MMM dd, yyyy HH:mm")</td>
<td>
@{
var statusClass = order.Status.ToString() == "Delivered" ? "success" :
order.Status.ToString() == "Cancelled" ? "danger" :
order.Status.ToString() == "PendingPayment" ? "warning" :
"info";
}
<span class="badge bg-@statusClass">@order.Status</span>
</td>
<td class="text-end">@order.Items.Sum(i => i.Quantity)</td>
<td class="text-end"><strong>£@order.TotalAmount.ToString("N2")</strong></td>
<td class="text-center">
<a href="@Url.Action("Details", "Orders", new { id = order.Id })" class="btn btn-sm btn-primary">
<i class="fas fa-eye"></i> View
</a>
</td>
</tr>
}
</tbody>
</table>
</div>
}
else
{
<div class="alert alert-info mb-0">
<i class="fas fa-info-circle"></i> No orders yet for this customer.
</div>
}
</div>
</div>
</div>
</div>
<!-- Block Customer Modal -->
<div class="modal fade" id="blockModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<form method="post" action="@Url.Action("Block", new { id = Model.Id })">
@Html.AntiForgeryToken()
<div class="modal-header bg-danger text-white">
<h5 class="modal-title"><i class="fas fa-ban"></i> Block Customer</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="alert alert-warning">
<i class="fas fa-exclamation-triangle"></i>
<strong>Warning:</strong> Blocking this customer will prevent them from placing new orders.
</div>
<div class="mb-3">
<label for="blockReason" class="form-label">
<strong>Reason for blocking:</strong> <span class="text-danger">*</span>
</label>
<textarea name="reason" id="blockReason" class="form-control" rows="3"
placeholder="Enter the reason for blocking this customer..." required></textarea>
<small class="text-muted">This reason will be visible to administrators.</small>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-danger">
<i class="fas fa-ban"></i> Block Customer
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Delete Customer Modal -->
<div class="modal fade" id="deleteModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<form method="post" action="@Url.Action("Delete", new { id = Model.Id })">
@Html.AntiForgeryToken()
<div class="modal-header bg-warning text-dark">
<h5 class="modal-title"><i class="fas fa-trash"></i> Delete Customer</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="alert alert-info">
<i class="fas fa-info-circle"></i>
<strong>Note:</strong> This is a soft delete. Customer data will be retained but marked as inactive.
</div>
<p>Are you sure you want to delete customer <strong>@Model.TelegramDisplayName</strong>?</p>
<p class="text-muted mb-0">The customer record and order history will be preserved but hidden from normal views.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-warning">
<i class="fas fa-trash"></i> Delete Customer
</button>
</div>
</form>
</div>
</div>
</div>