using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Meezi.API.Models.CustomRoles; using Meezi.Core.Authorization; using Meezi.Core.Entities; using Meezi.Core.Interfaces; using Meezi.Infrastructure.Data; using Meezi.Shared; namespace Meezi.API.Controllers; [Route("api/cafes/{cafeId}/custom-roles")] public class CustomRolesController : CafeApiControllerBase { private readonly AppDbContext _db; public CustomRolesController(AppDbContext db) { _db = db; } [HttpGet] public async Task List(string cafeId, ITenantContext tenant, CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; if (EnsureOwner(tenant) is { } forbidden) return forbidden; var roles = await _db.CustomRoles .AsNoTracking() .Where(r => r.CafeId == cafeId) .OrderBy(r => r.Name) .Select(r => new { r.Id, r.Name, r.Description, r.Color, r.PermissionsJson, EmployeeCount = _db.Employees.Count(e => e.CafeId == cafeId && e.CustomRoleId == r.Id && e.DeletedAt == null), r.CreatedAt, }) .ToListAsync(ct); var dtos = roles.Select(r => new CustomRoleDto( r.Id, r.Name, r.Description, r.Color, CustomRolePermissions.Parse(r.PermissionsJson).Select(p => p.ToString()).OrderBy(p => p).ToList(), r.EmployeeCount, r.CreatedAt)).ToList(); return Ok(new ApiResponse>(true, dtos)); } [HttpGet("{id}")] public async Task Get(string cafeId, string id, ITenantContext tenant, CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; if (EnsureOwner(tenant) is { } forbidden) return forbidden; var r = await _db.CustomRoles.AsNoTracking() .FirstOrDefaultAsync(x => x.Id == id && x.CafeId == cafeId, ct); if (r is null) return NotFoundError("Custom role not found."); var employeeCount = await _db.Employees .CountAsync(e => e.CafeId == cafeId && e.CustomRoleId == id && e.DeletedAt == null, ct); return Ok(new ApiResponse(true, new CustomRoleDto( r.Id, r.Name, r.Description, r.Color, CustomRolePermissions.Parse(r.PermissionsJson).Select(p => p.ToString()).OrderBy(p => p).ToList(), employeeCount, r.CreatedAt))); } [HttpPost] public async Task Create( string cafeId, [FromBody] CreateCustomRoleRequest request, ITenantContext tenant, CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; if (EnsureOwner(tenant) is { } forbidden) return forbidden; var name = request.Name?.Trim() ?? string.Empty; if (string.IsNullOrWhiteSpace(name)) return BadRequest(new ApiResponse(false, null, new ApiError("VALIDATION_ERROR", "Name is required.", "Name"))); var permissions = ParseAndValidatePermissions(request.Permissions); var role = new CustomRole { CafeId = cafeId, Name = name, Description = request.Description?.Trim(), Color = NormalizeColor(request.Color), PermissionsJson = CustomRolePermissions.Serialize(permissions), }; _db.CustomRoles.Add(role); await _db.SaveChangesAsync(ct); return CreatedAtAction(nameof(Get), new { cafeId, id = role.Id }, new ApiResponse(true, ToDto(role, 0))); } [HttpPatch("{id}")] public async Task Update( string cafeId, string id, [FromBody] UpdateCustomRoleRequest request, ITenantContext tenant, CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; if (EnsureOwner(tenant) is { } forbidden) return forbidden; var role = await _db.CustomRoles .FirstOrDefaultAsync(r => r.Id == id && r.CafeId == cafeId, ct); if (role is null) return NotFoundError("Custom role not found."); if (request.Name is not null) { var name = request.Name.Trim(); if (string.IsNullOrWhiteSpace(name)) return BadRequest(new ApiResponse(false, null, new ApiError("VALIDATION_ERROR", "Name cannot be empty.", "Name"))); role.Name = name; } if (request.Description is not null) role.Description = request.Description.Trim().Length > 0 ? request.Description.Trim() : null; if (request.Color is not null) role.Color = NormalizeColor(request.Color); if (request.Permissions is not null) role.PermissionsJson = CustomRolePermissions.Serialize(ParseAndValidatePermissions(request.Permissions)); await _db.SaveChangesAsync(ct); var employeeCount = await _db.Employees .CountAsync(e => e.CafeId == cafeId && e.CustomRoleId == id && e.DeletedAt == null, ct); return Ok(new ApiResponse(true, ToDto(role, employeeCount))); } [HttpDelete("{id}")] public async Task Delete( string cafeId, string id, ITenantContext tenant, CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; if (EnsureOwner(tenant) is { } forbidden) return forbidden; var role = await _db.CustomRoles .FirstOrDefaultAsync(r => r.Id == id && r.CafeId == cafeId, ct); if (role is null) return NotFoundError("Custom role not found."); // Unassign employees before deletion so they fall back to their base role permissions. await _db.Employees .Where(e => e.CafeId == cafeId && e.CustomRoleId == id) .ExecuteUpdateAsync(s => s.SetProperty(e => e.CustomRoleId, (string?)null), ct); role.DeletedAt = DateTime.UtcNow; await _db.SaveChangesAsync(ct); return Ok(new ApiResponse(true, null)); } // ── Employee custom-role assignment ─────────────────────────────────────── [HttpPut("/api/cafes/{cafeId}/employees/{employeeId}/custom-role")] public async Task AssignToEmployee( string cafeId, string employeeId, [FromBody] AssignCustomRoleRequest request, ITenantContext tenant, CancellationToken ct) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; if (EnsureOwner(tenant) is { } forbidden) return forbidden; var employee = await _db.Employees .FirstOrDefaultAsync(e => e.Id == employeeId && e.CafeId == cafeId && e.DeletedAt == null, ct); if (employee is null) return NotFoundError("Employee not found."); if (request.CustomRoleId is not null) { var roleExists = await _db.CustomRoles .AnyAsync(r => r.Id == request.CustomRoleId && r.CafeId == cafeId && r.DeletedAt == null, ct); if (!roleExists) return NotFoundError("Custom role not found."); } employee.CustomRoleId = request.CustomRoleId; await _db.SaveChangesAsync(ct); return Ok(new ApiResponse(true, null)); } // ── Helpers ─────────────────────────────────────────────────────────────── private static CustomRoleDto ToDto(CustomRole r, int employeeCount) => new( r.Id, r.Name, r.Description, r.Color, CustomRolePermissions.Parse(r.PermissionsJson).Select(p => p.ToString()).OrderBy(p => p).ToList(), employeeCount, r.CreatedAt); private static IEnumerable ParseAndValidatePermissions(IReadOnlyList? names) { if (names is null) return []; return names .Where(n => Enum.TryParse(n, ignoreCase: true, out _)) .Select(n => Enum.Parse(n, ignoreCase: true)) .Distinct(); } private static string? NormalizeColor(string? color) { if (string.IsNullOrWhiteSpace(color)) return null; var c = color.Trim(); return c.StartsWith('#') ? c : null; } }