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 bool IsUrgent { get; set; }
public string? OrderReference { get; set; } public string? OrderReference { get; set; }
public DateTime CreatedAt { get; set; } public DateTime CreatedAt { get; set; }
public int Direction { get; set; } // 0 = AdminToCustomer, 1 = CustomerToAdmin
} }
public enum MessageType public enum MessageType

View File

@ -8,4 +8,5 @@ public interface IMessageService
Task<bool> MarkMessageAsSentAsync(Guid messageId, string? platformMessageId = null); Task<bool> MarkMessageAsSentAsync(Guid messageId, string? platformMessageId = null);
Task<bool> MarkMessageAsFailedAsync(Guid messageId, string reason); Task<bool> MarkMessageAsFailedAsync(Guid messageId, string reason);
Task<bool> CreateCustomerMessageAsync(object messageData); 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<Order>> CreateOrderAsync(CreateOrderRequest request);
Task<ApiResponse<List<Order>>> GetOrdersByIdentityAsync(string identityReference); Task<ApiResponse<List<Order>>> GetOrdersByIdentityAsync(string identityReference);
Task<ApiResponse<List<Order>>> GetOrdersByCustomerIdAsync(Guid customerId);
Task<ApiResponse<Order>> GetOrderByIdAsync(Guid id); Task<ApiResponse<Order>> GetOrderByIdAsync(Guid id);
Task<ApiResponse<CryptoPayment>> CreatePaymentAsync(Guid orderId, int currency); Task<ApiResponse<CryptoPayment>> CreatePaymentAsync(Guid orderId, int currency);
Task<ApiResponse<List<CryptoPayment>>> GetOrderPaymentsAsync(Guid orderId); Task<ApiResponse<List<CryptoPayment>>> GetOrderPaymentsAsync(Guid orderId);

View File

@ -84,4 +84,26 @@ public class MessageService : IMessageService
return false; 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

@ -65,6 +65,30 @@ public class OrderService : IOrderService
} }
} }
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) public async Task<ApiResponse<Order>> GetOrderByIdAsync(Guid id)
{ {
try try

View File

@ -11,11 +11,13 @@ namespace LittleShop.Areas.Admin.Controllers;
public class MessagesController : Controller public class MessagesController : Controller
{ {
private readonly ICustomerMessageService _messageService; private readonly ICustomerMessageService _messageService;
private readonly ICustomerService _customerService;
private readonly ILogger<MessagesController> _logger; private readonly ILogger<MessagesController> _logger;
public MessagesController(ICustomerMessageService messageService, ILogger<MessagesController> logger) public MessagesController(ICustomerMessageService messageService, ICustomerService customerService, ILogger<MessagesController> logger)
{ {
_messageService = messageService; _messageService = messageService;
_customerService = customerService;
_logger = logger; _logger = logger;
} }
@ -28,9 +30,42 @@ public class MessagesController : Controller
public async Task<IActionResult> Customer(Guid id) public async Task<IActionResult> Customer(Guid id)
{ {
var conversation = await _messageService.GetMessageThreadAsync(id); var conversation = await _messageService.GetMessageThreadAsync(id);
// If no conversation exists yet, create an empty one for this customer
if (conversation == null) 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); return View(conversation);

View File

@ -4,6 +4,15 @@
ViewData["Title"] = $"Conversation with {Model.CustomerName}"; 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="container-fluid">
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
@ -41,8 +50,19 @@
</small> </small>
</div> </div>
<div class="card-body" style="max-height: 500px; overflow-y: auto;"> <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="mb-3 @(message.Direction == 0 ? "ms-4" : "me-4")">
<div class="d-flex @(message.Direction == 0 ? "justify-content-end" : "justify-content-start")"> <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%;"> <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> </div>
}
} }
</div> </div>
</div> </div>
@ -118,7 +139,7 @@
<!-- Reply Form --> <!-- Reply Form -->
<div class="card"> <div class="card">
<div class="card-header"> <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>
<div class="card-body"> <div class="card-body">
<form method="post" action="@Url.Action("Reply")"> <form method="post" action="@Url.Action("Reply")">
@ -126,7 +147,7 @@
<div class="mb-3"> <div class="mb-3">
<label for="content" class="form-label">Message</label> <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>
<div class="mb-3 form-check"> <div class="mb-3 form-check">
@ -137,7 +158,7 @@
</div> </div>
<button type="submit" class="btn btn-primary"> <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> </button>
</form> </form>
</div> </div>

View File

@ -110,6 +110,13 @@ public class BotMessagesController : ControllerBase
return BadRequest($"Error creating customer message: {ex.Message}"); 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 // TEMPORARY DTO FOR TESTING

View File

@ -64,6 +64,14 @@ public class OrdersController : ControllerBase
return Ok(orders); 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}")] [HttpGet("by-identity/{identityReference}/{id}")]
[AllowAnonymous] [AllowAnonymous]
public async Task<ActionResult<OrderDto>> GetOrderByIdentity(string identityReference, Guid id) 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>> GetAllOrdersAsync();
Task<IEnumerable<OrderDto>> GetOrdersByIdentityAsync(string identityReference); Task<IEnumerable<OrderDto>> GetOrdersByIdentityAsync(string identityReference);
Task<IEnumerable<OrderDto>> GetOrdersByCustomerIdAsync(Guid customerId);
Task<OrderDto?> GetOrderByIdAsync(Guid id); Task<OrderDto?> GetOrderByIdAsync(Guid id);
Task<OrderDto> CreateOrderAsync(CreateOrderDto createOrderDto); Task<OrderDto> CreateOrderAsync(CreateOrderDto createOrderDto);
Task<bool> UpdateOrderStatusAsync(Guid id, UpdateOrderStatusDto updateOrderStatusDto); Task<bool> UpdateOrderStatusAsync(Guid id, UpdateOrderStatusDto updateOrderStatusDto);

View File

@ -46,6 +46,20 @@ public class OrderService : IOrderService
return orders.Select(MapToDto); 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) public async Task<OrderDto?> GetOrderByIdAsync(Guid id)
{ {
var order = await _context.Orders 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.