|
| 1 | +// Copyright © 2024–2026 Jasper Ford |
| 2 | +// SPDX-License-Identifier: AGPL-3.0-or-later |
| 3 | +// Author: Jasper Ford |
| 4 | +// See NOTICE file for attribution and acknowledgements. |
| 5 | + |
| 6 | +using System.ComponentModel.DataAnnotations; |
| 7 | +using Microsoft.AspNetCore.Authorization; |
| 8 | +using Microsoft.AspNetCore.Mvc; |
| 9 | +using Nexus.Api.Data; |
| 10 | +using Nexus.Api.Extensions; |
| 11 | +using Nexus.Api.Services; |
| 12 | + |
| 13 | +namespace Nexus.Api.Controllers; |
| 14 | + |
| 15 | +[ApiController] |
| 16 | +[Route("api/admin/deliverables")] |
| 17 | +[Authorize(Policy = "AdminOnly")] |
| 18 | +public class DeliverablesController : ControllerBase |
| 19 | +{ |
| 20 | + private readonly DeliverableService _svc; |
| 21 | + private readonly TenantContext _tenantContext; |
| 22 | + |
| 23 | + public DeliverablesController(DeliverableService svc, TenantContext tenantContext) |
| 24 | + { |
| 25 | + _svc = svc; |
| 26 | + _tenantContext = tenantContext; |
| 27 | + } |
| 28 | + |
| 29 | + [HttpGet] |
| 30 | + public async Task<IActionResult> List( |
| 31 | + [FromQuery] string? status, [FromQuery] string? priority, |
| 32 | + [FromQuery] int? assigned_to, [FromQuery] int page = 1, [FromQuery] int limit = 20) |
| 33 | + { |
| 34 | + if (!_tenantContext.TenantId.HasValue) return BadRequest(new { error = "Tenant context not resolved" }); |
| 35 | + page = Math.Max(1, page); limit = Math.Clamp(limit, 1, 100); |
| 36 | + var (items, total) = await _svc.ListDeliverablesAsync(_tenantContext.TenantId.Value, status, priority, assigned_to, page, limit); |
| 37 | + var data = items.Select(d => new { |
| 38 | + id = d.Id, title = d.Title, status = d.Status.ToString(), priority = d.Priority.ToString(), |
| 39 | + due_date = d.DueDate, completed_at = d.CompletedAt, tags = d.Tags, |
| 40 | + assigned_to = d.AssignedTo == null ? null : new { id = d.AssignedTo.Id, first_name = d.AssignedTo.FirstName, last_name = d.AssignedTo.LastName }, |
| 41 | + created_by = d.CreatedBy == null ? null : new { id = d.CreatedBy.Id, first_name = d.CreatedBy.FirstName, last_name = d.CreatedBy.LastName }, |
| 42 | + created_at = d.CreatedAt |
| 43 | + }); |
| 44 | + return Ok(new { data, pagination = new { page, limit, total, pages = (int)Math.Ceiling((double)total / limit) } }); |
| 45 | + } |
| 46 | + |
| 47 | + [HttpGet("dashboard")] |
| 48 | + public async Task<IActionResult> Dashboard() |
| 49 | + { |
| 50 | + if (!_tenantContext.TenantId.HasValue) return BadRequest(new { error = "Tenant context not resolved" }); |
| 51 | + var result = await _svc.GetDashboardAsync(_tenantContext.TenantId.Value); |
| 52 | + return Ok(result); |
| 53 | + } |
| 54 | + |
| 55 | + [HttpGet("analytics")] |
| 56 | + public async Task<IActionResult> Analytics() |
| 57 | + { |
| 58 | + if (!_tenantContext.TenantId.HasValue) return BadRequest(new { error = "Tenant context not resolved" }); |
| 59 | + var result = await _svc.GetAnalyticsAsync(_tenantContext.TenantId.Value); |
| 60 | + return Ok(result); |
| 61 | + } |
| 62 | + |
| 63 | + [HttpGet("{id:int}")] |
| 64 | + public async Task<IActionResult> Get(int id) |
| 65 | + { |
| 66 | + if (!_tenantContext.TenantId.HasValue) return BadRequest(new { error = "Tenant context not resolved" }); |
| 67 | + var d = await _svc.GetDeliverableAsync(_tenantContext.TenantId.Value, id); |
| 68 | + if (d == null) return NotFound(new { error = "Deliverable not found" }); |
| 69 | + return Ok(new { |
| 70 | + id = d.Id, title = d.Title, description = d.Description, status = d.Status.ToString(), |
| 71 | + priority = d.Priority.ToString(), due_date = d.DueDate, completed_at = d.CompletedAt, tags = d.Tags, |
| 72 | + assigned_to = d.AssignedTo == null ? null : new { id = d.AssignedTo.Id, first_name = d.AssignedTo.FirstName, last_name = d.AssignedTo.LastName }, |
| 73 | + comments = d.Comments.Select(c => new { id = c.Id, content = c.Content, created_at = c.CreatedAt, user = c.User == null ? null : new { id = c.User.Id, first_name = c.User.FirstName } }), |
| 74 | + created_at = d.CreatedAt |
| 75 | + }); |
| 76 | + } |
| 77 | + |
| 78 | + [HttpPost] |
| 79 | + public async Task<IActionResult> Create([FromBody] CreateDeliverableRequest req) |
| 80 | + { |
| 81 | + var userId = User.GetUserId(); |
| 82 | + if (userId == null) return Unauthorized(); |
| 83 | + if (!_tenantContext.TenantId.HasValue) return BadRequest(new { error = "Tenant context not resolved" }); |
| 84 | + var (d, error) = await _svc.CreateDeliverableAsync(_tenantContext.TenantId.Value, userId.Value, req.Title, req.Description, req.AssignedToUserId, req.Priority, req.DueDate, req.Tags); |
| 85 | + if (error != null) return BadRequest(new { error }); |
| 86 | + return CreatedAtAction(nameof(Get), new { id = d!.Id }, new { id = d.Id, title = d.Title }); |
| 87 | + } |
| 88 | + |
| 89 | + [HttpPut("{id:int}")] |
| 90 | + public async Task<IActionResult> Update(int id, [FromBody] UpdateDeliverableRequest req) |
| 91 | + { |
| 92 | + if (!_tenantContext.TenantId.HasValue) return BadRequest(new { error = "Tenant context not resolved" }); |
| 93 | + var (d, error) = await _svc.UpdateDeliverableAsync(_tenantContext.TenantId.Value, id, req.Title, req.Description, req.AssignedToUserId, req.Status, req.Priority, req.DueDate, req.Tags); |
| 94 | + if (error != null) return BadRequest(new { error }); |
| 95 | + return Ok(new { id = d!.Id, title = d.Title, status = d.Status.ToString() }); |
| 96 | + } |
| 97 | + |
| 98 | + [HttpDelete("{id:int}")] |
| 99 | + public async Task<IActionResult> Delete(int id) |
| 100 | + { |
| 101 | + if (!_tenantContext.TenantId.HasValue) return BadRequest(new { error = "Tenant context not resolved" }); |
| 102 | + var (ok, error) = await _svc.DeleteDeliverableAsync(_tenantContext.TenantId.Value, id); |
| 103 | + if (!ok) return BadRequest(new { error }); |
| 104 | + return Ok(new { message = "Deleted" }); |
| 105 | + } |
| 106 | + |
| 107 | + [HttpPost("{id:int}/comments")] |
| 108 | + public async Task<IActionResult> AddComment(int id, [FromBody] AddDeliverableCommentRequest req) |
| 109 | + { |
| 110 | + var userId = User.GetUserId(); |
| 111 | + if (userId == null) return Unauthorized(); |
| 112 | + if (!_tenantContext.TenantId.HasValue) return BadRequest(new { error = "Tenant context not resolved" }); |
| 113 | + var (comment, error) = await _svc.AddCommentAsync(_tenantContext.TenantId.Value, id, userId.Value, req.Content); |
| 114 | + if (error != null) return BadRequest(new { error }); |
| 115 | + return Ok(new { id = comment!.Id, content = comment.Content, created_at = comment.CreatedAt }); |
| 116 | + } |
| 117 | +} |
| 118 | + |
| 119 | +public record CreateDeliverableRequest([property: Required] string Title, string? Description, int? AssignedToUserId, string? Priority, DateTime? DueDate, string? Tags); |
| 120 | +public record UpdateDeliverableRequest(string? Title, string? Description, int? AssignedToUserId, string? Status, string? Priority, DateTime? DueDate, string? Tags); |
| 121 | +public record AddDeliverableCommentRequest([property: Required] string Content); |
0 commit comments