This commit is contained in:
sysadmin 2025-08-27 22:19:39 +01:00
parent 5c6abe5686
commit bbf5acbb6b
22 changed files with 15571 additions and 6 deletions

View File

@ -11,6 +11,7 @@ public class CustomerMessage
public bool IsUrgent { get; set; }
public string? OrderReference { get; set; }
public DateTime CreatedAt { get; set; }
public int Direction { get; set; } // 0 = AdminToCustomer, 1 = CustomerToAdmin
}
public enum MessageType

View File

@ -8,4 +8,5 @@ public interface IMessageService
Task<bool> MarkMessageAsSentAsync(Guid messageId, string? platformMessageId = null);
Task<bool> MarkMessageAsFailedAsync(Guid messageId, string reason);
Task<bool> CreateCustomerMessageAsync(object messageData);
Task<List<CustomerMessage>?> GetCustomerMessagesAsync(Guid customerId);
}

View File

@ -6,6 +6,7 @@ public interface IOrderService
{
Task<ApiResponse<Order>> CreateOrderAsync(CreateOrderRequest request);
Task<ApiResponse<List<Order>>> GetOrdersByIdentityAsync(string identityReference);
Task<ApiResponse<List<Order>>> GetOrdersByCustomerIdAsync(Guid customerId);
Task<ApiResponse<Order>> GetOrderByIdAsync(Guid id);
Task<ApiResponse<CryptoPayment>> CreatePaymentAsync(Guid orderId, int currency);
Task<ApiResponse<List<CryptoPayment>>> GetOrderPaymentsAsync(Guid orderId);

View File

@ -84,4 +84,26 @@ public class MessageService : IMessageService
return false;
}
}
public async Task<List<CustomerMessage>?> GetCustomerMessagesAsync(Guid customerId)
{
try
{
var response = await _httpClient.GetAsync($"api/bot/messages/customer/{customerId}");
if (response.IsSuccessStatusCode)
{
var messages = await response.Content.ReadFromJsonAsync<List<CustomerMessage>>();
return messages ?? new List<CustomerMessage>();
}
_logger.LogWarning("Failed to get customer messages: {StatusCode}", response.StatusCode);
return new List<CustomerMessage>();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting customer messages");
return new List<CustomerMessage>();
}
}
}

View File

@ -64,6 +64,30 @@ public class OrderService : IOrderService
System.Net.HttpStatusCode.InternalServerError);
}
}
public async Task<ApiResponse<List<Order>>> GetOrdersByCustomerIdAsync(Guid customerId)
{
try
{
var response = await _httpClient.GetAsync($"api/orders/by-customer/{customerId}");
if (response.IsSuccessStatusCode)
{
var orders = await response.Content.ReadFromJsonAsync<List<Order>>();
return ApiResponse<List<Order>>.Success(orders ?? new List<Order>());
}
var error = await response.Content.ReadAsStringAsync();
return ApiResponse<List<Order>>.Failure(error, response.StatusCode);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to get orders for customer {CustomerId}", customerId);
return ApiResponse<List<Order>>.Failure(
ex.Message,
System.Net.HttpStatusCode.InternalServerError);
}
}
public async Task<ApiResponse<Order>> GetOrderByIdAsync(Guid id)
{

View File

@ -11,11 +11,13 @@ namespace LittleShop.Areas.Admin.Controllers;
public class MessagesController : Controller
{
private readonly ICustomerMessageService _messageService;
private readonly ICustomerService _customerService;
private readonly ILogger<MessagesController> _logger;
public MessagesController(ICustomerMessageService messageService, ILogger<MessagesController> logger)
public MessagesController(ICustomerMessageService messageService, ICustomerService customerService, ILogger<MessagesController> logger)
{
_messageService = messageService;
_customerService = customerService;
_logger = logger;
}
@ -28,9 +30,42 @@ public class MessagesController : Controller
public async Task<IActionResult> Customer(Guid id)
{
var conversation = await _messageService.GetMessageThreadAsync(id);
// If no conversation exists yet, create an empty one for this customer
if (conversation == null)
{
return NotFound();
// Check if customer exists
var customerExists = await _messageService.ValidateCustomerExistsAsync(id);
if (!customerExists)
{
TempData["Error"] = "Customer not found.";
return RedirectToAction("Index");
}
// Get customer information
var customer = await _customerService.GetCustomerByIdAsync(id);
if (customer == null)
{
TempData["Error"] = "Customer information not available.";
return RedirectToAction("Index");
}
// Create empty conversation structure for new conversation
conversation = new MessageThreadDto
{
ThreadId = id,
Subject = customer.DisplayName,
CustomerId = id,
CustomerName = customer.DisplayName,
OrderId = null,
OrderReference = null,
StartedAt = DateTime.UtcNow,
LastMessageAt = DateTime.UtcNow,
MessageCount = 0,
HasUnreadMessages = false,
RequiresResponse = false,
Messages = new List<CustomerMessageDto>()
};
}
return View(conversation);

View File

@ -4,6 +4,15 @@
ViewData["Title"] = $"Conversation with {Model.CustomerName}";
}
@{
// Get customer info if name is not loaded
if (Model.CustomerName == "Loading...")
{
// This would need to be loaded via a service call
Model.CustomerName = "Customer"; // Fallback
}
}
<div class="container-fluid">
<div class="row">
<div class="col-12">
@ -41,8 +50,19 @@
</small>
</div>
<div class="card-body" style="max-height: 500px; overflow-y: auto;">
@foreach (var message in Model.Messages.OrderBy(m => m.CreatedAt))
@if (!Model.Messages.Any())
{
<div class="text-center text-muted py-4">
<i class="fas fa-comments fa-3x mb-3"></i>
<h5>No messages yet</h5>
<p>This is the start of your conversation with @Model.CustomerName.</p>
<p class="small">Send a message below to begin the conversation.</p>
</div>
}
else
{
@foreach (var message in Model.Messages.OrderBy(m => m.CreatedAt))
{
<div class="mb-3 @(message.Direction == 0 ? "ms-4" : "me-4")">
<div class="d-flex @(message.Direction == 0 ? "justify-content-end" : "justify-content-start")">
<div class="card @(message.Direction == 0 ? "bg-primary text-white" : "bg-light") @(message.Direction == 0 ? "ms-auto" : "me-auto")" style="max-width: 70%;">
@ -84,6 +104,7 @@
</div>
</div>
</div>
}
}
</div>
</div>
@ -118,7 +139,7 @@
<!-- Reply Form -->
<div class="card">
<div class="card-header">
<h6><i class="fas fa-reply"></i> Send Reply</h6>
<h6><i class="fas fa-@(Model.MessageCount == 0 ? "comment" : "reply")"></i> @(Model.MessageCount == 0 ? "Start Conversation" : "Send Reply")</h6>
</div>
<div class="card-body">
<form method="post" action="@Url.Action("Reply")">
@ -126,7 +147,7 @@
<div class="mb-3">
<label for="content" class="form-label">Message</label>
<textarea class="form-control" id="content" name="content" rows="4" required placeholder="Type your reply here..."></textarea>
<textarea class="form-control" id="content" name="content" rows="4" required placeholder="@(Model.MessageCount == 0 ? "Start the conversation with this customer..." : "Type your reply here...")"></textarea>
</div>
<div class="mb-3 form-check">
@ -137,7 +158,7 @@
</div>
<button type="submit" class="btn btn-primary">
<i class="fas fa-paper-plane"></i> Send Reply
<i class="fas fa-paper-plane"></i> @(Model.MessageCount == 0 ? "Send Message" : "Send Reply")
</button>
</form>
</div>

View File

@ -110,6 +110,13 @@ public class BotMessagesController : ControllerBase
return BadRequest($"Error creating customer message: {ex.Message}");
}
}
[HttpGet("customer/{customerId}")]
public async Task<ActionResult<IEnumerable<CustomerMessageDto>>> GetCustomerMessages(Guid customerId)
{
var messages = await _messageService.GetCustomerMessagesAsync(customerId);
return Ok(messages);
}
}
// TEMPORARY DTO FOR TESTING

View File

@ -64,6 +64,14 @@ public class OrdersController : ControllerBase
return Ok(orders);
}
[HttpGet("by-customer/{customerId}")]
[AllowAnonymous]
public async Task<ActionResult<IEnumerable<OrderDto>>> GetOrdersByCustomerId(Guid customerId)
{
var orders = await _orderService.GetOrdersByCustomerIdAsync(customerId);
return Ok(orders);
}
[HttpGet("by-identity/{identityReference}/{id}")]
[AllowAnonymous]
public async Task<ActionResult<OrderDto>> GetOrderByIdentity(string identityReference, Guid id)

View File

@ -6,6 +6,7 @@ public interface IOrderService
{
Task<IEnumerable<OrderDto>> GetAllOrdersAsync();
Task<IEnumerable<OrderDto>> GetOrdersByIdentityAsync(string identityReference);
Task<IEnumerable<OrderDto>> GetOrdersByCustomerIdAsync(Guid customerId);
Task<OrderDto?> GetOrderByIdAsync(Guid id);
Task<OrderDto> CreateOrderAsync(CreateOrderDto createOrderDto);
Task<bool> UpdateOrderStatusAsync(Guid id, UpdateOrderStatusDto updateOrderStatusDto);

View File

@ -46,6 +46,20 @@ public class OrderService : IOrderService
return orders.Select(MapToDto);
}
public async Task<IEnumerable<OrderDto>> GetOrdersByCustomerIdAsync(Guid customerId)
{
var orders = await _context.Orders
.Include(o => o.Customer)
.Include(o => o.Items)
.ThenInclude(oi => oi.Product)
.Include(o => o.Payments)
.Where(o => o.CustomerId == customerId)
.OrderByDescending(o => o.CreatedAt)
.ToListAsync();
return orders.Select(MapToDto);
}
public async Task<OrderDto?> GetOrderByIdAsync(Guid id)
{
var order = await _context.Orders

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,31 @@
{
"ProjectPath": "C:\\Production\\Source\\LittleShop\\LittleShop",
"ProjectType": "Project (ASP.NET Core)",
"TotalEndpoints": 115,
"AuthenticatedEndpoints": 78,
"TestableStates": 3,
"IdentifiedGaps": 224,
"SuggestedTests": 190,
"DeadLinks": 0,
"HttpErrors": 97,
"VisualIssues": 0,
"SecurityInsights": 1,
"PerformanceInsights": 1,
"OverallTestCoverage": 16.956521739130434,
"VisualConsistencyScore": 0,
"CriticalRecommendations": [
"CRITICAL: Test coverage is only 17.0% - implement comprehensive test suite",
"HIGH: Address 97 HTTP errors in the application",
"MEDIUM: Improve visual consistency - current score 0.0%",
"HIGH: Address 224 testing gaps for comprehensive coverage"
],
"GeneratedFiles": [
"C:\\Production\\Source\\LittleShop\\LittleShop\\TestAgent_Results\\project_structure.json",
"C:\\Production\\Source\\LittleShop\\LittleShop\\TestAgent_Results\\authentication_analysis.json",
"C:\\Production\\Source\\LittleShop\\LittleShop\\TestAgent_Results\\endpoint_discovery.json",
"C:\\Production\\Source\\LittleShop\\LittleShop\\TestAgent_Results\\coverage_analysis.json",
"C:\\Production\\Source\\LittleShop\\LittleShop\\TestAgent_Results\\error_detection.json",
"C:\\Production\\Source\\LittleShop\\LittleShop\\TestAgent_Results\\visual_testing.json",
"C:\\Production\\Source\\LittleShop\\LittleShop\\TestAgent_Results\\intelligent_analysis.json"
]
}

View File

@ -0,0 +1,79 @@
{
"BusinessLogicInsights": [
{
"Component": "Claude CLI Integration",
"Insight": "Error analyzing business logic: Failed to execute Claude CLI: An error occurred trying to start process \u0027claude\u0027 with working directory \u0027C:\\Production\\Source\\TestAgent\u0027. The system cannot find the file specified.",
"Complexity": "Unknown",
"PotentialIssues": [],
"TestingRecommendations": [],
"Priority": "Medium"
}
],
"TestScenarioSuggestions": [
{
"ScenarioName": "Claude CLI Integration Error",
"Description": "Error generating test scenarios: Failed to execute Claude CLI: An error occurred trying to start process \u0027claude\u0027 with working directory \u0027C:\\Production\\Source\\TestAgent\u0027. The system cannot find the file specified.",
"TestType": "",
"Steps": [],
"ExpectedOutcomes": [],
"Priority": "Medium",
"RequiredData": [],
"Dependencies": []
}
],
"SecurityInsights": [
{
"VulnerabilityType": "Analysis Error",
"Location": "",
"Description": "Error analyzing security: Failed to execute Claude CLI: An error occurred trying to start process \u0027claude\u0027 with working directory \u0027C:\\Production\\Source\\TestAgent\u0027. The system cannot find the file specified.",
"Severity": "Medium",
"Recommendations": [],
"TestingApproaches": []
}
],
"PerformanceInsights": [
{
"Component": "Analysis Error",
"PotentialBottleneck": "Error analyzing performance: Failed to execute Claude CLI: An error occurred trying to start process \u0027claude\u0027 with working directory \u0027C:\\Production\\Source\\TestAgent\u0027. The system cannot find the file specified.",
"Impact": "Unknown",
"OptimizationSuggestions": [],
"TestingStrategies": []
}
],
"ArchitecturalRecommendations": [
{
"Category": "Analysis Error",
"Recommendation": "Error generating architectural recommendations: Failed to execute Claude CLI: An error occurred trying to start process \u0027claude\u0027 with working directory \u0027C:\\Production\\Source\\TestAgent\u0027. The system cannot find the file specified.",
"Rationale": "",
"Impact": "Unknown",
"ImplementationSteps": []
}
],
"GeneratedTestCases": [
{
"TestName": "Claude CLI Integration Error",
"TestCategory": "Error",
"Description": "Error generating test cases: Failed to execute Claude CLI: An error occurred trying to start process \u0027claude\u0027 with working directory \u0027C:\\Production\\Source\\TestAgent\u0027. The system cannot find the file specified.",
"TestCode": "",
"TestData": [],
"ExpectedOutcome": "",
"Reasoning": ""
}
],
"Summary": {
"TotalInsights": 4,
"HighPriorityItems": 0,
"GeneratedTestCases": 1,
"SecurityIssuesFound": 1,
"PerformanceOptimizations": 1,
"KeyFindings": [
"Performance optimization opportunities identified"
],
"NextSteps": [
"Review and prioritize security recommendations",
"Implement generated test cases",
"Address high-priority business logic testing gaps",
"Consider architectural improvements for better testability"
]
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,17 @@
{
"ConsistencyTests": [],
"AuthStateComparisons": [],
"ResponsiveTests": [],
"ComponentTests": [],
"Regressions": [],
"Summary": {
"TotalTests": 0,
"PassedTests": 0,
"FailedTests": 0,
"ConsistencyViolations": 0,
"ResponsiveIssues": 0,
"VisualRegressions": 0,
"OverallScore": 0,
"Recommendations": []
}
}

Binary file not shown.

Binary file not shown.