Initial commit of LittleShop project (excluding large archives)
- BTCPay Server integration - TeleBot Telegram bot - Review system - Admin area - Docker deployment configuration 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
185
LittleShop/Areas/Admin/Controllers/ReviewsController.cs
Normal file
185
LittleShop/Areas/Admin/Controllers/ReviewsController.cs
Normal file
@@ -0,0 +1,185 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using LittleShop.Services;
|
||||
using LittleShop.DTOs;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace LittleShop.Areas.Admin.Controllers;
|
||||
|
||||
[Area("Admin")]
|
||||
[Authorize(AuthenticationSchemes = "Cookies", Roles = "Admin")]
|
||||
public class ReviewsController : Controller
|
||||
{
|
||||
private readonly IReviewService _reviewService;
|
||||
private readonly ILogger<ReviewsController> _logger;
|
||||
|
||||
public ReviewsController(IReviewService reviewService, ILogger<ReviewsController> logger)
|
||||
{
|
||||
_reviewService = reviewService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> Index()
|
||||
{
|
||||
try
|
||||
{
|
||||
var pendingReviews = await _reviewService.GetPendingReviewsAsync();
|
||||
return View(pendingReviews);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error loading reviews index");
|
||||
TempData["ErrorMessage"] = "Error loading reviews";
|
||||
return View(new List<ReviewDto>());
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IActionResult> Details(Guid id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var review = await _reviewService.GetReviewByIdAsync(id);
|
||||
if (review == null)
|
||||
{
|
||||
TempData["ErrorMessage"] = "Review not found";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
return View(review);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error loading review {ReviewId}", id);
|
||||
TempData["ErrorMessage"] = "Error loading review details";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Approve(Guid id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var userIdClaim = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
if (!Guid.TryParse(userIdClaim, out var userId))
|
||||
{
|
||||
TempData["ErrorMessage"] = "Authentication error";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
var success = await _reviewService.ApproveReviewAsync(id, userId);
|
||||
if (success)
|
||||
{
|
||||
TempData["SuccessMessage"] = "Review approved successfully";
|
||||
}
|
||||
else
|
||||
{
|
||||
TempData["ErrorMessage"] = "Failed to approve review";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error approving review {ReviewId}", id);
|
||||
TempData["ErrorMessage"] = "Error approving review";
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Delete(Guid id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var success = await _reviewService.DeleteReviewAsync(id);
|
||||
if (success)
|
||||
{
|
||||
TempData["SuccessMessage"] = "Review deleted successfully";
|
||||
}
|
||||
else
|
||||
{
|
||||
TempData["ErrorMessage"] = "Failed to delete review";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error deleting review {ReviewId}", id);
|
||||
TempData["ErrorMessage"] = "Error deleting review";
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
public async Task<IActionResult> Edit(Guid id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var review = await _reviewService.GetReviewByIdAsync(id);
|
||||
if (review == null)
|
||||
{
|
||||
TempData["ErrorMessage"] = "Review not found";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
var updateDto = new UpdateReviewDto
|
||||
{
|
||||
Rating = review.Rating,
|
||||
Title = review.Title,
|
||||
Comment = review.Comment,
|
||||
IsApproved = review.IsApproved,
|
||||
IsActive = review.IsActive
|
||||
};
|
||||
|
||||
ViewBag.ReviewId = id;
|
||||
ViewBag.ProductName = review.ProductName;
|
||||
ViewBag.CustomerName = review.CustomerDisplayName;
|
||||
|
||||
return View(updateDto);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error loading review {ReviewId} for edit", id);
|
||||
TempData["ErrorMessage"] = "Error loading review for edit";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Edit(Guid id, UpdateReviewDto updateDto)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
var review = await _reviewService.GetReviewByIdAsync(id);
|
||||
ViewBag.ReviewId = id;
|
||||
ViewBag.ProductName = review?.ProductName ?? "";
|
||||
ViewBag.CustomerName = review?.CustomerDisplayName ?? "";
|
||||
return View(updateDto);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var success = await _reviewService.UpdateReviewAsync(id, updateDto);
|
||||
if (success)
|
||||
{
|
||||
TempData["SuccessMessage"] = "Review updated successfully";
|
||||
return RedirectToAction(nameof(Details), new { id });
|
||||
}
|
||||
else
|
||||
{
|
||||
TempData["ErrorMessage"] = "Review not found";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error updating review {ReviewId}", id);
|
||||
TempData["ErrorMessage"] = "Error updating review";
|
||||
|
||||
var reviewDetails = await _reviewService.GetReviewByIdAsync(id);
|
||||
ViewBag.ReviewId = id;
|
||||
ViewBag.ProductName = reviewDetails?.ProductName ?? "";
|
||||
ViewBag.CustomerName = reviewDetails?.CustomerDisplayName ?? "";
|
||||
return View(updateDto);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,104 +1,104 @@
|
||||
@model IEnumerable<LittleShop.DTOs.OrderDto>
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Orders";
|
||||
}
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<h1><i class="fas fa-shopping-cart"></i> Orders</h1>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<a href="@Url.Action("Create")" class="btn btn-primary">
|
||||
<i class="fas fa-plus"></i> Create Order
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
@if (Model.Any())
|
||||
{
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Order ID</th>
|
||||
<th>Customer</th>
|
||||
<th>Shipping To</th>
|
||||
<th>Status</th>
|
||||
<th>Total</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var order in Model)
|
||||
{
|
||||
<tr>
|
||||
<td><code>@order.Id.ToString().Substring(0, 8)...</code></td>
|
||||
<td>
|
||||
@if (order.Customer != null)
|
||||
{
|
||||
<div>
|
||||
<strong>@order.Customer.DisplayName</strong>
|
||||
@if (!string.IsNullOrEmpty(order.Customer.TelegramUsername))
|
||||
{
|
||||
<br><small class="text-muted">@@@order.Customer.TelegramUsername</small>
|
||||
}
|
||||
<br><small class="badge bg-info">@order.Customer.CustomerType</small>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">@order.ShippingName</span>
|
||||
@if (!string.IsNullOrEmpty(order.IdentityReference))
|
||||
{
|
||||
<br><small class="text-muted">(@order.IdentityReference)</small>
|
||||
}
|
||||
}
|
||||
</td>
|
||||
<td>@order.ShippingCity, @order.ShippingCountry</td>
|
||||
<td>
|
||||
@{
|
||||
var badgeClass = order.Status switch
|
||||
{
|
||||
LittleShop.Enums.OrderStatus.PendingPayment => "bg-warning",
|
||||
LittleShop.Enums.OrderStatus.PaymentReceived => "bg-success",
|
||||
LittleShop.Enums.OrderStatus.Processing => "bg-info",
|
||||
LittleShop.Enums.OrderStatus.Shipped => "bg-primary",
|
||||
LittleShop.Enums.OrderStatus.Delivered => "bg-success",
|
||||
LittleShop.Enums.OrderStatus.Cancelled => "bg-danger",
|
||||
_ => "bg-secondary"
|
||||
};
|
||||
}
|
||||
<span class="badge @badgeClass">@order.Status</span>
|
||||
</td>
|
||||
<td><strong>£@order.TotalAmount</strong></td>
|
||||
<td>@order.CreatedAt.ToString("MMM dd, yyyy HH:mm")</td>
|
||||
<td>
|
||||
<a href="@Url.Action("Details", new { id = order.Id })" class="btn btn-sm btn-outline-primary">
|
||||
<i class="fas fa-eye"></i> View
|
||||
</a>
|
||||
@if (order.Customer != null)
|
||||
{
|
||||
<a href="@Url.Action("Details", new { id = order.Id })" class="btn btn-sm btn-success ms-1" title="Message Customer">
|
||||
<i class="fas fa-comment"></i>
|
||||
</a>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="text-center py-4">
|
||||
<i class="fas fa-shopping-cart fa-3x text-muted mb-3"></i>
|
||||
<p class="text-muted">No orders found yet.</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@model IEnumerable<LittleShop.DTOs.OrderDto>
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Orders";
|
||||
}
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<h1><i class="fas fa-shopping-cart"></i> Orders</h1>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<a href="@Url.Action("Create")" class="btn btn-primary">
|
||||
<i class="fas fa-plus"></i> Create Order
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
@if (Model.Any())
|
||||
{
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Order ID</th>
|
||||
<th>Customer</th>
|
||||
<th>Shipping To</th>
|
||||
<th>Status</th>
|
||||
<th>Total</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var order in Model)
|
||||
{
|
||||
<tr>
|
||||
<td><code>@order.Id.ToString().Substring(0, 8)...</code></td>
|
||||
<td>
|
||||
@if (order.Customer != null)
|
||||
{
|
||||
<div>
|
||||
<strong>@order.Customer.DisplayName</strong>
|
||||
@if (!string.IsNullOrEmpty(order.Customer.TelegramUsername))
|
||||
{
|
||||
<br><small class="text-muted">@@@order.Customer.TelegramUsername</small>
|
||||
}
|
||||
<br><small class="badge bg-info">@order.Customer.CustomerType</small>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">@order.ShippingName</span>
|
||||
@if (!string.IsNullOrEmpty(order.IdentityReference))
|
||||
{
|
||||
<br><small class="text-muted">(@order.IdentityReference)</small>
|
||||
}
|
||||
}
|
||||
</td>
|
||||
<td>@order.ShippingCity, @order.ShippingCountry</td>
|
||||
<td>
|
||||
@{
|
||||
var badgeClass = order.Status switch
|
||||
{
|
||||
LittleShop.Enums.OrderStatus.PendingPayment => "bg-warning",
|
||||
LittleShop.Enums.OrderStatus.PaymentReceived => "bg-success",
|
||||
LittleShop.Enums.OrderStatus.Processing => "bg-info",
|
||||
LittleShop.Enums.OrderStatus.Shipped => "bg-primary",
|
||||
LittleShop.Enums.OrderStatus.Delivered => "bg-success",
|
||||
LittleShop.Enums.OrderStatus.Cancelled => "bg-danger",
|
||||
_ => "bg-secondary"
|
||||
};
|
||||
}
|
||||
<span class="badge @badgeClass">@order.Status</span>
|
||||
</td>
|
||||
<td><strong>£@order.TotalAmount</strong></td>
|
||||
<td>@order.CreatedAt.ToString("MMM dd, yyyy HH:mm")</td>
|
||||
<td>
|
||||
<a href="@Url.Action("Details", new { id = order.Id })" class="btn btn-sm btn-outline-primary">
|
||||
<i class="fas fa-eye"></i> View
|
||||
</a>
|
||||
@if (order.Customer != null)
|
||||
{
|
||||
<a href="@Url.Action("Details", new { id = order.Id })" class="btn btn-sm btn-success ms-1" title="Message Customer">
|
||||
<i class="fas fa-comment"></i>
|
||||
</a>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="text-center py-4">
|
||||
<i class="fas fa-shopping-cart fa-3x text-muted mb-3"></i>
|
||||
<p class="text-muted">No orders found yet.</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
149
LittleShop/Areas/Admin/Views/Reviews/Details.cshtml
Normal file
149
LittleShop/Areas/Admin/Views/Reviews/Details.cshtml
Normal file
@@ -0,0 +1,149 @@
|
||||
@model LittleShop.DTOs.ReviewDto
|
||||
@{
|
||||
ViewData["Title"] = "Review Details";
|
||||
}
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h2><i class="fas fa-star me-2"></i>Review Details</h2>
|
||||
<a asp-action="Index" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i>Back to Reviews
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@if (TempData["SuccessMessage"] != null)
|
||||
{
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
@TempData["SuccessMessage"]
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="card shadow">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-comment me-2"></i>Customer Review
|
||||
@if (Model.IsVerifiedPurchase)
|
||||
{
|
||||
<span class="badge bg-success ms-2">
|
||||
<i class="fas fa-check-circle"></i> Verified Purchase
|
||||
</span>
|
||||
}
|
||||
@if (Model.IsApproved)
|
||||
{
|
||||
<span class="badge bg-info ms-2">
|
||||
<i class="fas fa-check"></i> Approved
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-warning ms-2">
|
||||
<i class="fas fa-clock"></i> Pending Approval
|
||||
</span>
|
||||
}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h6><i class="fas fa-box me-2"></i>Product Information</h6>
|
||||
<p><strong>Product:</strong> @Model.ProductName</p>
|
||||
<p><strong>Product ID:</strong> <code>@Model.ProductId</code></p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6><i class="fas fa-user me-2"></i>Customer Information</h6>
|
||||
<p><strong>Customer:</strong> @Model.CustomerDisplayName</p>
|
||||
<p><strong>Customer ID:</strong> <code>@Model.CustomerId</code></p>
|
||||
<p><strong>Order ID:</strong> <code>@Model.OrderId</code></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h6><i class="fas fa-star me-2"></i>Review Details</h6>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><strong>Rating:</strong></label>
|
||||
<div class="d-flex align-items-center">
|
||||
@for (int i = 1; i <= 5; i++)
|
||||
{
|
||||
<i class="fas fa-star @(i <= Model.Rating ? "text-warning" : "text-muted") me-1"></i>
|
||||
}
|
||||
<span class="ms-2 h5 mb-0">@Model.Rating out of 5 stars</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrEmpty(Model.Title))
|
||||
{
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><strong>Title:</strong></label>
|
||||
<p class="form-control-plaintext">@Model.Title</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrEmpty(Model.Comment))
|
||||
{
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><strong>Comment:</strong></label>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<p class="mb-0">@Model.Comment</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h6><i class="fas fa-info-circle me-2"></i>Review Metadata</h6>
|
||||
<p><strong>Created:</strong> @Model.CreatedAt.ToString("MMM dd, yyyy 'at' h:mm tt")</p>
|
||||
<p><strong>Updated:</strong> @Model.UpdatedAt.ToString("MMM dd, yyyy 'at' h:mm tt")</p>
|
||||
|
||||
@if (Model.IsApproved && Model.ApprovedAt.HasValue)
|
||||
{
|
||||
<p><strong>Approved:</strong> @Model.ApprovedAt.Value.ToString("MMM dd, yyyy 'at' h:mm tt")</p>
|
||||
@if (!string.IsNullOrEmpty(Model.ApprovedByUsername))
|
||||
{
|
||||
<p><strong>Approved By:</strong> @Model.ApprovedByUsername</p>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6><i class="fas fa-cogs me-2"></i>Actions</h6>
|
||||
<div class="d-grid gap-2">
|
||||
@if (!Model.IsApproved)
|
||||
{
|
||||
<form asp-action="Approve" asp-route-id="@Model.Id" method="post" class="d-inline">
|
||||
<button type="submit" class="btn btn-success w-100"
|
||||
onclick="return confirm('Approve this review for public display?')">
|
||||
<i class="fas fa-check me-2"></i>Approve Review
|
||||
</button>
|
||||
</form>
|
||||
}
|
||||
|
||||
<a asp-action="Edit" asp-route-id="@Model.Id" class="btn btn-outline-primary">
|
||||
<i class="fas fa-edit me-2"></i>Edit Review
|
||||
</a>
|
||||
|
||||
<form asp-action="Delete" asp-route-id="@Model.Id" method="post" class="d-inline">
|
||||
<button type="submit" class="btn btn-outline-danger w-100"
|
||||
onclick="return confirm('Are you sure you want to delete this review?')">
|
||||
<i class="fas fa-trash me-2"></i>Delete Review
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
136
LittleShop/Areas/Admin/Views/Reviews/Index.cshtml
Normal file
136
LittleShop/Areas/Admin/Views/Reviews/Index.cshtml
Normal file
@@ -0,0 +1,136 @@
|
||||
@model IEnumerable<LittleShop.DTOs.ReviewDto>
|
||||
@{
|
||||
ViewData["Title"] = "Reviews";
|
||||
}
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3 d-flex justify-content-between align-items-center">
|
||||
<h6 class="m-0 font-weight-bold text-primary">
|
||||
<i class="fas fa-star me-2"></i>Customer Reviews
|
||||
</h6>
|
||||
<span class="badge badge-warning">@Model.Count() Pending Approval</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if (TempData["SuccessMessage"] != null)
|
||||
{
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
@TempData["SuccessMessage"]
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (TempData["ErrorMessage"] != null)
|
||||
{
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
@TempData["ErrorMessage"]
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!Model.Any())
|
||||
{
|
||||
<div class="text-center py-4">
|
||||
<div class="mb-3">
|
||||
<i class="fas fa-star fa-3x text-muted"></i>
|
||||
</div>
|
||||
<h5 class="text-muted">No pending reviews</h5>
|
||||
<p class="text-muted">New customer reviews will appear here for approval.</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Product</th>
|
||||
<th>Customer</th>
|
||||
<th>Rating</th>
|
||||
<th>Review</th>
|
||||
<th>Order ID</th>
|
||||
<th>Date</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var review in Model.OrderByDescending(r => r.CreatedAt))
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
<strong>@review.ProductName</strong>
|
||||
</td>
|
||||
<td>
|
||||
<span class="text-muted">@review.CustomerDisplayName</span>
|
||||
@if (review.IsVerifiedPurchase)
|
||||
{
|
||||
<i class="fas fa-check-circle text-success ms-1" title="Verified Purchase"></i>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
@for (int i = 1; i <= 5; i++)
|
||||
{
|
||||
<i class="fas fa-star @(i <= review.Rating ? "text-warning" : "text-muted")"></i>
|
||||
}
|
||||
<span class="ms-2 text-muted">(@review.Rating/5)</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
@if (!string.IsNullOrEmpty(review.Title))
|
||||
{
|
||||
<strong class="d-block">@review.Title</strong>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(review.Comment))
|
||||
{
|
||||
<span class="text-muted">
|
||||
@(review.Comment.Length > 100 ? review.Comment.Substring(0, 100) + "..." : review.Comment)
|
||||
</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<small class="text-monospace">@review.OrderId.ToString().Substring(0, 8)...</small>
|
||||
</td>
|
||||
<td>
|
||||
<small>@review.CreatedAt.ToString("MMM dd, yyyy")</small>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group" role="group">
|
||||
<a asp-action="Details" asp-route-id="@review.Id"
|
||||
class="btn btn-sm btn-outline-primary" title="View Details">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
@if (!review.IsApproved)
|
||||
{
|
||||
<form asp-action="Approve" asp-route-id="@review.Id" method="post" class="d-inline">
|
||||
<button type="submit" class="btn btn-sm btn-success"
|
||||
onclick="return confirm('Approve this review?')" title="Approve">
|
||||
<i class="fas fa-check"></i>
|
||||
</button>
|
||||
</form>
|
||||
}
|
||||
<a asp-action="Edit" asp-route-id="@review.Id"
|
||||
class="btn btn-sm btn-outline-secondary" title="Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<form asp-action="Delete" asp-route-id="@review.Id" method="post" class="d-inline">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger"
|
||||
onclick="return confirm('Delete this review?')" title="Delete">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,123 +1,128 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||
<title>@ViewData["Title"] - LittleShop Admin</title>
|
||||
|
||||
<!-- PWA Meta Tags -->
|
||||
<meta name="application-name" content="LittleShop Admin" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||
<meta name="apple-mobile-web-app-title" content="LittleShop" />
|
||||
<meta name="description" content="Modern e-commerce admin panel" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="theme-color" content="#2563eb" />
|
||||
<meta name="msapplication-TileColor" content="#2563eb" />
|
||||
<meta name="msapplication-tap-highlight" content="no" />
|
||||
|
||||
<!-- PWA Manifest -->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<!-- Icons -->
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
<link rel="apple-touch-icon" href="/icons/icon-152x152.png" />
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="/icons/icon-72x72.png" />
|
||||
<link rel="apple-touch-icon" sizes="96x96" href="/icons/icon-96x96.png" />
|
||||
<link rel="apple-touch-icon" sizes="128x128" href="/icons/icon-128x128.png" />
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="/icons/icon-144x144.png" />
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="/icons/icon-152x152.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="/icons/icon-192x192.png" />
|
||||
<link rel="apple-touch-icon" sizes="384x384" href="/icons/icon-384x384.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="/icons/icon-512x512.png" />
|
||||
<link href="/lib/bootstrap/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="/lib/fontawesome/css/all.min.css" rel="stylesheet">
|
||||
<link href="/lib/bootstrap-icons/bootstrap-icons.css" rel="stylesheet">
|
||||
<link href="/css/modern-admin.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<nav class="navbar navbar-expand-sm navbar-light bg-white">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="@Url.Action("Index", "Dashboard", new { area = "Admin" })">
|
||||
<i class="fas fa-store"></i> LittleShop Admin
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
|
||||
<ul class="navbar-nav flex-grow-1">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="@Url.Action("Index", "Dashboard", new { area = "Admin" })">
|
||||
<i class="fas fa-tachometer-alt"></i> Dashboard
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="@Url.Action("Index", "Categories", new { area = "Admin" })">
|
||||
<i class="fas fa-tags"></i> Categories
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="@Url.Action("Index", "Products", new { area = "Admin" })">
|
||||
<i class="fas fa-box"></i> Products
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="@Url.Action("Index", "Orders", new { area = "Admin" })">
|
||||
<i class="fas fa-shopping-cart"></i> Orders
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="@Url.Action("Index", "Messages", new { area = "Admin" })">
|
||||
<i class="fas fa-comments"></i> Messages
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="@Url.Action("Index", "ShippingRates", new { area = "Admin" })">
|
||||
<i class="fas fa-truck"></i> Shipping
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="@Url.Action("Index", "Users", new { area = "Admin" })">
|
||||
<i class="fas fa-users"></i> Users
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="@Url.Action("Index", "Bots", new { area = "Admin" })">
|
||||
<i class="fas fa-robot"></i> Bots
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-user"></i> @User.Identity?.Name
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<form method="post" action="@Url.Action("Logout", "Account", new { area = "Admin" })">
|
||||
<button type="submit" class="dropdown-item">
|
||||
<i class="fas fa-sign-out-alt"></i> Logout
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
<div class="container-fluid">
|
||||
<main role="main" class="pb-3">
|
||||
@RenderBody()
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/lib/jquery/jquery.min.js"></script>
|
||||
<script src="/lib/bootstrap/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/js/pwa.js"></script>
|
||||
<script src="/js/modern-mobile.js"></script>
|
||||
@await RenderSectionAsync("Scripts", required: false)
|
||||
</body>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||
<title>@ViewData["Title"] - LittleShop Admin</title>
|
||||
|
||||
<!-- PWA Meta Tags -->
|
||||
<meta name="application-name" content="LittleShop Admin" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||
<meta name="apple-mobile-web-app-title" content="LittleShop" />
|
||||
<meta name="description" content="Modern e-commerce admin panel" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="theme-color" content="#2563eb" />
|
||||
<meta name="msapplication-TileColor" content="#2563eb" />
|
||||
<meta name="msapplication-tap-highlight" content="no" />
|
||||
|
||||
<!-- PWA Manifest -->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<!-- Icons -->
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
<link rel="apple-touch-icon" href="/icons/icon-152x152.png" />
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="/icons/icon-72x72.png" />
|
||||
<link rel="apple-touch-icon" sizes="96x96" href="/icons/icon-96x96.png" />
|
||||
<link rel="apple-touch-icon" sizes="128x128" href="/icons/icon-128x128.png" />
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="/icons/icon-144x144.png" />
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="/icons/icon-152x152.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="/icons/icon-192x192.png" />
|
||||
<link rel="apple-touch-icon" sizes="384x384" href="/icons/icon-384x384.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="/icons/icon-512x512.png" />
|
||||
<link href="/lib/bootstrap/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="/lib/fontawesome/css/all.min.css" rel="stylesheet">
|
||||
<link href="/lib/bootstrap-icons/bootstrap-icons.css" rel="stylesheet">
|
||||
<link href="/css/modern-admin.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<nav class="navbar navbar-expand-sm navbar-light bg-white">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="@Url.Action("Index", "Dashboard", new { area = "Admin" })">
|
||||
<i class="fas fa-store"></i> LittleShop Admin
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
|
||||
<ul class="navbar-nav flex-grow-1">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="@Url.Action("Index", "Dashboard", new { area = "Admin" })">
|
||||
<i class="fas fa-tachometer-alt"></i> Dashboard
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="@Url.Action("Index", "Categories", new { area = "Admin" })">
|
||||
<i class="fas fa-tags"></i> Categories
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="@Url.Action("Index", "Products", new { area = "Admin" })">
|
||||
<i class="fas fa-box"></i> Products
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="@Url.Action("Index", "Orders", new { area = "Admin" })">
|
||||
<i class="fas fa-shopping-cart"></i> Orders
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="@Url.Action("Index", "Reviews", new { area = "Admin" })">
|
||||
<i class="fas fa-star"></i> Reviews
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="@Url.Action("Index", "Messages", new { area = "Admin" })">
|
||||
<i class="fas fa-comments"></i> Messages
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="@Url.Action("Index", "ShippingRates", new { area = "Admin" })">
|
||||
<i class="fas fa-truck"></i> Shipping
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="@Url.Action("Index", "Users", new { area = "Admin" })">
|
||||
<i class="fas fa-users"></i> Users
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="@Url.Action("Index", "Bots", new { area = "Admin" })">
|
||||
<i class="fas fa-robot"></i> Bots
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-user"></i> @User.Identity?.Name
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<form method="post" action="@Url.Action("Logout", "Account", new { area = "Admin" })">
|
||||
<button type="submit" class="dropdown-item">
|
||||
<i class="fas fa-sign-out-alt"></i> Logout
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
<div class="container-fluid">
|
||||
<main role="main" class="pb-3">
|
||||
@RenderBody()
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/lib/jquery/jquery.min.js"></script>
|
||||
<script src="/lib/bootstrap/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/js/pwa.js"></script>
|
||||
<script src="/js/modern-mobile.js"></script>
|
||||
@await RenderSectionAsync("Scripts", required: false)
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user