diff --git a/DocAnalytics.Api/Controllers/ErrorCatalogController.cs b/DocAnalytics.Api/Controllers/ErrorCatalogController.cs
new file mode 100644
index 0000000..3d04567
--- /dev/null
+++ b/DocAnalytics.Api/Controllers/ErrorCatalogController.cs
@@ -0,0 +1,81 @@
+using DocAnalytics.Api.Common;
+using DocAnalytics.Service.ErrorCatalog;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+
+namespace DocAnalytics.Api.Controllers;
+
+///
+/// Global error catalog management.
+/// GET is open to all authenticated users; POST and PUT require Admin role.
+///
+[ApiController]
+[Authorize(Policy = "DataAccess")]
+[Route("api/v1/error-catalog")]
+public sealed class ErrorCatalogController : ControllerBase
+{
+ private readonly IErrorCatalogService _service;
+
+ public ErrorCatalogController(IErrorCatalogService service) => _service = service;
+
+ /// Returns all error catalog entries ordered by code.
+ // GET /api/v1/error-catalog
+ [HttpGet]
+ public async Task GetAll(CancellationToken ct)
+ {
+ var items = await _service.GetAllAsync(ct);
+ return Ok(ApiResponse>.Ok(items));
+ }
+
+ /// Creates a new error catalog entry. Admin only.
+ // POST /api/v1/error-catalog
+ [HttpPost]
+ [Authorize(Roles = "Admin")]
+ public async Task Create(
+ [FromBody] CreateErrorCatalogDto dto, CancellationToken ct)
+ {
+ if (string.IsNullOrWhiteSpace(dto.ErrorCode) || string.IsNullOrWhiteSpace(dto.Description))
+ return BadRequest(ApiResponse.Fail(
+ "VALIDATION", "ErrorCode and Description are required."));
+
+ var result = await _service.CreateAsync(dto, ct);
+
+ if (result is null)
+ return Conflict(ApiResponse.Fail(
+ "DUPLICATE_CODE", $"Error code '{dto.ErrorCode.Trim().ToUpperInvariant()}' already exists."));
+
+ return Ok(ApiResponse.Ok(result));
+ }
+
+ /// Updates description and remediation of an existing entry. Admin only.
+ // PUT /api/v1/error-catalog/{id}
+ [HttpPut("{id:guid}")]
+ [Authorize(Roles = "Admin")]
+ public async Task Update(
+ Guid id, [FromBody] UpdateErrorCatalogDto dto, CancellationToken ct)
+ {
+ if (string.IsNullOrWhiteSpace(dto.Description))
+ return BadRequest(ApiResponse.Fail(
+ "VALIDATION", "Description is required."));
+
+ var result = await _service.UpdateAsync(id, dto, ct);
+
+ if (result is null)
+ return NotFound(ApiResponse.Fail(
+ "NOT_FOUND", "Error catalog entry not found."));
+
+ return Ok(ApiResponse.Ok(result));
+ }
+
+ // DELETE /api/v1/error-catalog/{id}
+ [HttpDelete("{id:guid}")]
+ [Authorize(Roles = "Admin")]
+ public async Task Delete(Guid id, CancellationToken ct)
+ {
+ var deleted = await _service.DeleteAsync(id, ct);
+ if (!deleted)
+ return NotFound(ApiResponse
+
-
+
+ @if (svc.retryError()) {
+ ⚠ {{ svc.retryError() }}
+ }
+
+
Processing timeline
@if (history().length === 0) {
@@ -63,7 +80,7 @@ Processing timeline
}
-
+
Invoice
@if (svc.invoiceLoading()) {
@@ -76,7 +93,6 @@ Invoice
} @else {
-
@if (svc.invoice()?.header; as h) {
@@ -96,9 +112,8 @@
Invoice
}
-
@if (items().length === 0) {
- No line items — this file has no extracted invoice items.
+ No line items extracted.
} @else {