feat: custom roles with per-permission matrix for café owners
- Owner can define named custom roles (e.g. Barista, Supervisor) with
color, description, and a fine-grained permission set (21 permissions
across 7 categories: admin, menu, staff, customer, reports, ops, kitchen)
- Employee assigned a custom role gets its permissions embedded in the
JWT at login (customPerms claim) and parsed by TenantMiddleware —
overrides the static EmployeeRole matrix for all API permission checks
- New endpoints: GET/POST/PATCH/DELETE /api/cafes/{id}/custom-roles and
PUT /api/cafes/{id}/employees/{id}/custom-role for assignment
- Dashboard Settings → Team & Staff → Custom Roles panel with grouped
checkbox matrix, group-level toggles, color preset picker, CRUD forms,
and employee-count display; translations in fa/en/ar
- EF migration adds CustomRoles table + nullable CustomRoleId FK on Employees
- POS slip now shows per-item notes on both thermal print and bill preview
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Meezi.Core.Authorization;
|
||||
|
||||
/// <summary>Helpers for serialising/deserialising a custom role's permission set.</summary>
|
||||
public static class CustomRolePermissions
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOpts = new(JsonSerializerDefaults.Web);
|
||||
|
||||
/// <summary>Parse the stored JSON array of permission names into a set.</summary>
|
||||
public static IReadOnlySet<Permission> Parse(string? json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json)) return new HashSet<Permission>();
|
||||
try
|
||||
{
|
||||
var names = JsonSerializer.Deserialize<string[]>(json, JsonOpts) ?? [];
|
||||
var set = new HashSet<Permission>();
|
||||
foreach (var name in names)
|
||||
if (Enum.TryParse<Permission>(name, ignoreCase: true, out var p))
|
||||
set.Add(p);
|
||||
return set;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new HashSet<Permission>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Serialise a permission set to JSON for storage.</summary>
|
||||
public static string Serialize(IEnumerable<Permission> permissions) =>
|
||||
JsonSerializer.Serialize(permissions.Select(p => p.ToString()).ToArray(), JsonOpts);
|
||||
}
|
||||
@@ -9,6 +9,9 @@ public static class MeeziClaimTypes
|
||||
public const string BranchId = "branchId";
|
||||
public const string Actor = "actor";
|
||||
public const string Phone = "phone";
|
||||
/// <summary>Comma-separated list of <see cref="Meezi.Core.Authorization.Permission"/> names
|
||||
/// embedded when the employee has a custom role. Presence overrides the role-based matrix.</summary>
|
||||
public const string CustomPermissions = "customPerms";
|
||||
}
|
||||
|
||||
public static class MeeziActorKinds
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace Meezi.Core.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// A café-defined role template that overrides the standard <see cref="Meezi.Core.Enums.EmployeeRole"/>
|
||||
/// permission set. The owner creates named roles (e.g. "Barista", "Floor Supervisor") and assigns
|
||||
/// specific <see cref="Meezi.Core.Authorization.Permission"/> values to each one.
|
||||
/// When an employee has a <see cref="CustomRoleId"/>, their effective permissions come from here
|
||||
/// instead of the static <see cref="Meezi.Core.Authorization.RolePermissions"/> matrix.
|
||||
/// </summary>
|
||||
public class CustomRole : TenantEntity
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
/// <summary>Optional hex color (e.g. "#F59E0B") for badge display in the UI.</summary>
|
||||
public string? Color { get; set; }
|
||||
/// <summary>JSON array of <see cref="Meezi.Core.Authorization.Permission"/> enum names.</summary>
|
||||
public string PermissionsJson { get; set; } = "[]";
|
||||
|
||||
public Cafe Cafe { get; set; } = null!;
|
||||
public ICollection<Employee> Employees { get; set; } = [];
|
||||
}
|
||||
@@ -26,6 +26,14 @@ public class Employee : TenantEntity
|
||||
public ICollection<EmployeeSchedule> Schedules { get; set; } = [];
|
||||
public ICollection<LeaveRequest> LeaveRequests { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Optional custom role defined by the café owner. When set, this role's permission set
|
||||
/// overrides the standard <see cref="RolePermissions"/> matrix for this employee.
|
||||
/// The base <see cref="Role"/> enum still controls café-wide vs. branch-scoped behaviour.
|
||||
/// </summary>
|
||||
public string? CustomRoleId { get; set; }
|
||||
public CustomRole? CustomRole { get; set; }
|
||||
|
||||
/// <summary>Per-branch role assignments (multi-branch staff). Owners are café-wide and may have none.</summary>
|
||||
public ICollection<EmployeeBranchRole> BranchRoles { get; set; } = [];
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Meezi.Core.Authorization;
|
||||
using Meezi.Core.Enums;
|
||||
|
||||
namespace Meezi.Core.Interfaces;
|
||||
@@ -14,4 +15,10 @@ public interface ITenantContext
|
||||
bool IsSystemAdmin { get; }
|
||||
bool IsAuthenticated { get; }
|
||||
bool IsCafeOwner => Role == EmployeeRole.Owner;
|
||||
/// <summary>
|
||||
/// When non-null the employee has a custom role. These permissions override the static
|
||||
/// <see cref="RolePermissions"/> matrix; the base <see cref="Role"/> still governs
|
||||
/// café-wide vs. branch-scoped behaviour.
|
||||
/// </summary>
|
||||
IReadOnlySet<Permission>? CustomPermissions { get; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user