|
| 1 | +using Microsoft.AspNetCore.Mvc; |
| 2 | +using Microsoft.EntityFrameworkCore; |
| 3 | +using Tasklog.Api.Data; |
| 4 | +using Tasklog.Api.Models; |
| 5 | + |
| 6 | +namespace Tasklog.Api.Controllers |
| 7 | +{ |
| 8 | + [ApiController] |
| 9 | + [Route("api/labels")] |
| 10 | + public class LabelsController : ControllerBase |
| 11 | + { |
| 12 | + private readonly TasklogDbContext _context; |
| 13 | + |
| 14 | + public LabelsController(TasklogDbContext context) |
| 15 | + { |
| 16 | + _context = context; |
| 17 | + } |
| 18 | + |
| 19 | + // GET /api/labels |
| 20 | + // Returns all labels ordered alphabetically by name. |
| 21 | + [HttpGet] |
| 22 | + public async Task<IActionResult> GetAll() |
| 23 | + { |
| 24 | + var labels = await _context.Labels |
| 25 | + .OrderBy(l => l.Name) |
| 26 | + .ToListAsync(); |
| 27 | + |
| 28 | + return Ok(labels); |
| 29 | + } |
| 30 | + |
| 31 | + // POST /api/labels |
| 32 | + // Creates a new label. Expects { name: string, colorIndex: int }. |
| 33 | + [HttpPost] |
| 34 | + public async Task<IActionResult> Create([FromBody] CreateLabelRequest request) |
| 35 | + { |
| 36 | + if (string.IsNullOrWhiteSpace(request.Name)) |
| 37 | + return BadRequest(new { message = "Label name is required." }); |
| 38 | + |
| 39 | + if (request.ColorIndex < 0 || request.ColorIndex > 9) |
| 40 | + return BadRequest(new { message = "ColorIndex must be between 0 and 9." }); |
| 41 | + |
| 42 | + // Reject the request if a label with the same name already exists (case-insensitive). |
| 43 | + var trimmedName = request.Name.Trim(); |
| 44 | + var nameExists = await _context.Labels |
| 45 | + .AnyAsync(l => l.Name.ToLower() == trimmedName.ToLower()); |
| 46 | + |
| 47 | + if (nameExists) |
| 48 | + return Conflict(new { message = $"A label named \"{trimmedName}\" already exists." }); |
| 49 | + |
| 50 | + var label = new Label |
| 51 | + { |
| 52 | + Name = trimmedName, |
| 53 | + ColorIndex = request.ColorIndex, |
| 54 | + CreatedAt = DateTime.UtcNow |
| 55 | + }; |
| 56 | + |
| 57 | + _context.Labels.Add(label); |
| 58 | + await _context.SaveChangesAsync(); |
| 59 | + |
| 60 | + return CreatedAtAction(nameof(GetAll), new { id = label.Id }, label); |
| 61 | + } |
| 62 | + |
| 63 | + // PATCH /api/labels/{id} |
| 64 | + // Updates a label's name and/or color. Returns the updated label. |
| 65 | + [HttpPatch("{id:int}")] |
| 66 | + public async Task<IActionResult> Update(int id, [FromBody] UpdateLabelRequest request) |
| 67 | + { |
| 68 | + if (string.IsNullOrWhiteSpace(request.Name)) |
| 69 | + return BadRequest(new { message = "Label name is required." }); |
| 70 | + |
| 71 | + if (request.ColorIndex < 0 || request.ColorIndex > 9) |
| 72 | + return BadRequest(new { message = "ColorIndex must be between 0 and 9." }); |
| 73 | + |
| 74 | + var label = await _context.Labels.FindAsync(id); |
| 75 | + |
| 76 | + if (label is null) |
| 77 | + return NotFound(new { message = $"Label {id} not found." }); |
| 78 | + |
| 79 | + // Reject the rename if another label already has the same name (case-insensitive). |
| 80 | + // The current label is excluded from the check so saving with no name change still works. |
| 81 | + var trimmedName = request.Name.Trim(); |
| 82 | + var nameConflict = await _context.Labels |
| 83 | + .AnyAsync(l => l.Id != id && l.Name.ToLower() == trimmedName.ToLower()); |
| 84 | + |
| 85 | + if (nameConflict) |
| 86 | + return Conflict(new { message = $"A label named \"{trimmedName}\" already exists." }); |
| 87 | + |
| 88 | + label.Name = trimmedName; |
| 89 | + label.ColorIndex = request.ColorIndex; |
| 90 | + await _context.SaveChangesAsync(); |
| 91 | + |
| 92 | + return Ok(label); |
| 93 | + } |
| 94 | + |
| 95 | + // DELETE /api/labels/{id} |
| 96 | + // Deletes a label. The join table entries are cascade-deleted by the database, |
| 97 | + // so this safely unlinks the label from all tasks without deleting those tasks. |
| 98 | + // Returns 204 No Content on success, 404 if not found. |
| 99 | + [HttpDelete("{id:int}")] |
| 100 | + public async Task<IActionResult> Delete(int id) |
| 101 | + { |
| 102 | + var label = await _context.Labels.FindAsync(id); |
| 103 | + |
| 104 | + if (label is null) |
| 105 | + return NotFound(new { message = $"Label {id} not found." }); |
| 106 | + |
| 107 | + _context.Labels.Remove(label); |
| 108 | + await _context.SaveChangesAsync(); |
| 109 | + |
| 110 | + return NoContent(); |
| 111 | + } |
| 112 | + } |
| 113 | + |
| 114 | + // Request body for label creation. |
| 115 | + public record CreateLabelRequest(string Name, int ColorIndex); |
| 116 | + |
| 117 | + // Request body for label update (name and/or color). |
| 118 | + public record UpdateLabelRequest(string Name, int ColorIndex); |
| 119 | +} |
0 commit comments