using Management.Data; using Management.Security; using Microsoft.AspNetCore.Mvc; using Microsoft.Data.SqlClient; using System.Data; using System.Text.Json; namespace Management.Controllers.Admin; /// /// Client document management (admin view). /// Admins can list, upload, download, and delete documents for any client. /// All documents created here have scope='client' and a required clientId. /// /// POST /api/admin/client-documents/list - List docs for a client /// POST /api/admin/client-documents/upload - Upload doc for a client /// GET /api/admin/client-documents/{id}/download /// DELETE /api/admin/client-documents/{id} /// [ApiController] [Route("api/admin/client-documents")] public sealed class AdminClientDocumentsController : AdminControllerBase { private readonly IConfiguration _config; public AdminClientDocumentsController(SqlService sql, ClientContext client, IConfiguration config, ILogger log) : base(sql, client, log) { _config = config; } // ── POST /api/admin/client-documents/list ──────────────────────────────── [HttpPost("list")] public async Task List([FromBody] JsonElement body, CancellationToken ct) { try { var clientId = body.TryGetProperty("clientId", out var cid) ? cid.GetString() : null; if (string.IsNullOrWhiteSpace(clientId)) return BadRequest(new { ok = false, error = "clientId is required" }); var rqst = JsonSerializer.Serialize(new { scope = "client", clientId }); var result = await Sql.ExecProcAsync("dbo.usp_DocumentList", "list", rqst, ct: ct); return Content(result, "application/json"); } catch (Exception ex) { Logger.LogError(ex, "Client document list failed"); return StatusCode(500, new { ok = false, message = ex.Message }); } } // ── POST /api/admin/client-documents/upload ────────────────────────────── [HttpPost("upload")] [RequestSizeLimit(52_428_800)] public async Task Upload( IFormFile file, [FromForm] string clientId, [FromForm] string category, [FromForm] string? description = null, CancellationToken ct = default) { if (file == null || file.Length == 0) return BadRequest(new { ok = false, message = "No file provided" }); if (string.IsNullOrWhiteSpace(clientId)) return BadRequest(new { ok = false, message = "clientId is required" }); try { byte[] fileBytes; using (var ms = new MemoryStream()) { await file.CopyToAsync(ms, ct); fileBytes = ms.ToArray(); } var rqst = JsonSerializer.Serialize(new { docFileName = file.FileName, docMimeType = file.ContentType, docFileSize = file.Length, docCategory = category, docDescription = description, docUploadedBy = Client.Email, docScope = "client", docCltId = clientId }); Logger.LogInformation("[ClientDocs] Upload {FileName} for client {ClientId} by {User}", file.FileName, clientId, Client.Email); var result = await ExecUploadAsync(rqst, fileBytes, ct); return Content(result, "application/json"); } catch (Exception ex) { Logger.LogError(ex, "Client document upload failed: {FileName}", file?.FileName); return StatusCode(500, new { ok = false, message = ex.Message }); } } // ── GET /api/admin/client-documents/{id}/download ──────────────────────── [HttpGet("{id:long}/download")] public async Task Download(long id, CancellationToken ct = default) { try { var cs = _config.GetConnectionString("Sql") ?? throw new InvalidOperationException("Missing ConnectionStrings:Sql"); await using var conn = new SqlConnection(cs); await conn.OpenAsync(ct); await using var cmd = new SqlCommand("dbo.usp_Document", conn) { CommandType = CommandType.StoredProcedure, CommandTimeout = 60 }; cmd.Parameters.Add(new SqlParameter("@action", SqlDbType.VarChar, 50) { Value = "document.download" }); cmd.Parameters.Add(new SqlParameter("@rqst", SqlDbType.NVarChar, -1) { Value = JsonSerializer.Serialize(new { docId = id }) }); await using var reader = await cmd.ExecuteReaderAsync(ct); if (!await reader.ReadAsync(ct)) return NotFound(new { ok = false, message = "Document not found" }); var fileName = reader.GetString(reader.GetOrdinal("docFileName")); var mimeType = reader.GetString(reader.GetOrdinal("docMimeType")); var content = (byte[])reader["docContent"]; return File(content, mimeType, fileName); } catch (Exception ex) { Logger.LogError(ex, "Client document download failed: docId={DocId}", id); return StatusCode(500, new { ok = false, message = ex.Message }); } } // ── DELETE /api/admin/client-documents/{id} ────────────────────────────── [HttpDelete("{id:long}")] public async Task Delete(long id, CancellationToken ct = default) { try { Logger.LogInformation("[ClientDocs] Delete docId={DocId} by {User}", id, Client.Email); var rqst = JsonSerializer.Serialize(new { docId = id }); var result = await Sql.ExecProcAsync("dbo.usp_Document", "document.delete", rqst, ct: ct); return Content(result, "application/json"); } catch (Exception ex) { Logger.LogError(ex, "Client document delete failed: docId={DocId}", id); return StatusCode(500, new { ok = false, message = ex.Message }); } } // ─── Upload helper ──────────────────────────────────────────────────────── private async Task ExecUploadAsync(string rqst, byte[] fileContent, CancellationToken ct) { var cs = _config.GetConnectionString("Sql") ?? throw new InvalidOperationException("Missing ConnectionStrings:Sql"); await using var conn = new SqlConnection(cs); await conn.OpenAsync(ct); await using var cmd = new SqlCommand("dbo.usp_Document", conn) { CommandType = CommandType.StoredProcedure, CommandTimeout = 60 }; cmd.Parameters.Add(new SqlParameter("@action", SqlDbType.VarChar, 50) { Value = "document.upload" }); cmd.Parameters.Add(new SqlParameter("@rqst", SqlDbType.NVarChar, -1) { Value = rqst }); cmd.Parameters.Add(new SqlParameter("@filecontent", SqlDbType.VarBinary, -1) { Value = fileContent }); var pResp = new SqlParameter("@resp", SqlDbType.NVarChar, -1) { Direction = ParameterDirection.Output }; cmd.Parameters.Add(pResp); await cmd.ExecuteNonQueryAsync(ct); return pResp.Value as string ?? JsonSerializer.Serialize(new { ok = false, message = "No response from database" }); } }