initial: standalone repo from monorepo split

This commit is contained in:
kevin-asprec
2026-04-13 09:37:07 +08:00
commit 5382f3b4e5
87 changed files with 5278 additions and 0 deletions

View File

@@ -0,0 +1,224 @@
import { Role } from './roles';
/**
* All modules available in the permission matrix.
* Each module can have: view, create, update, archive, approve, export actions.
*/
export const MODULES = [
'dashboard',
'clients',
'subscriptions',
'invoices',
'payments',
'tickets',
'employees',
'payroll',
'expenses',
'assets',
'accounts',
'fund_transfers',
'accounting',
'reports',
'areas',
'plans',
'settings',
'users',
] as const;
export type Module = (typeof MODULES)[number];
export const MODULE_LABELS: Record<Module, string> = {
dashboard: 'Dashboard',
clients: 'Clients',
subscriptions: 'Subscriptions',
invoices: 'Invoices',
payments: 'Payments',
tickets: 'Tickets',
employees: 'Employees',
payroll: 'Payroll',
expenses: 'Expenses',
assets: 'Assets',
accounts: 'Company Accounts',
fund_transfers: 'Fund Transfers',
accounting: 'Accounting',
reports: 'Reports',
areas: 'Areas',
plans: 'Plans',
settings: 'Settings',
users: 'Users',
};
export const ACTIONS = ['canView', 'canCreate', 'canUpdate', 'canArchive', 'canApprove', 'canExport'] as const;
export type Action = (typeof ACTIONS)[number];
export const ACTION_LABELS: Record<Action, string> = {
canView: 'View',
canCreate: 'Create',
canUpdate: 'Update',
canArchive: 'Archive',
canApprove: 'Approve',
canExport: 'Export',
};
/**
* Defines which actions are applicable per module.
* Only these checkboxes should be shown/enforced in the matrix.
*/
export const MODULE_ACTIONS: Record<Module, readonly Action[]> = {
dashboard: ['canView'],
clients: ['canView', 'canCreate', 'canUpdate', 'canArchive', 'canExport'],
subscriptions: ['canView', 'canCreate', 'canUpdate', 'canArchive', 'canExport'],
invoices: ['canView', 'canCreate', 'canUpdate', 'canArchive', 'canApprove', 'canExport'],
payments: ['canView', 'canCreate', 'canUpdate', 'canApprove', 'canExport'],
tickets: ['canView', 'canCreate', 'canUpdate', 'canArchive', 'canExport'],
employees: ['canView', 'canCreate', 'canUpdate', 'canArchive', 'canExport'],
payroll: ['canView', 'canCreate', 'canUpdate', 'canApprove', 'canExport'],
expenses: ['canView', 'canCreate', 'canUpdate', 'canArchive', 'canApprove', 'canExport'],
assets: ['canView', 'canCreate', 'canUpdate', 'canArchive', 'canExport'],
accounts: ['canView', 'canCreate', 'canUpdate', 'canApprove', 'canExport'],
fund_transfers: ['canView', 'canCreate', 'canApprove', 'canExport'],
accounting: ['canView', 'canExport'],
reports: ['canView', 'canExport'],
areas: ['canView', 'canCreate', 'canUpdate', 'canArchive'],
plans: ['canView', 'canCreate', 'canUpdate', 'canArchive'],
settings: ['canView', 'canUpdate'],
users: ['canView', 'canCreate', 'canUpdate', 'canArchive'],
};
/**
* Permission matrix type — one row per module with boolean actions.
*/
export interface PermissionRow {
module: Module;
canView: boolean;
canCreate: boolean;
canUpdate: boolean;
canArchive: boolean;
canApprove: boolean;
canExport: boolean;
}
/**
* Default permission matrices for system roles seeded per tenant.
*/
function allTrue(modules: readonly Module[], actions: readonly Action[]): PermissionRow[] {
return MODULES.map((mod) => ({
module: mod,
canView: actions.includes('canView') && modules.includes(mod),
canCreate: actions.includes('canCreate') && modules.includes(mod),
canUpdate: actions.includes('canUpdate') && modules.includes(mod),
canArchive: actions.includes('canArchive') && modules.includes(mod),
canApprove: actions.includes('canApprove') && modules.includes(mod),
canExport: actions.includes('canExport') && modules.includes(mod),
}));
}
const ALL_MODULES = [...MODULES] as Module[];
export const DEFAULT_ROLE_PERMISSIONS: Record<string, PermissionRow[]> = {
tenant_admin: MODULES.map((mod) => ({
module: mod,
canView: true,
canCreate: true,
canUpdate: true,
canArchive: true,
canApprove: true,
canExport: true,
})),
manager: MODULES.map((mod) => {
const noAccess: Module[] = ['users'];
const viewOnly: Module[] = ['dashboard', 'accounting', 'settings'];
if (noAccess.includes(mod)) return { module: mod, canView: false, canCreate: false, canUpdate: false, canArchive: false, canApprove: false, canExport: false };
if (viewOnly.includes(mod)) return { module: mod, canView: true, canCreate: false, canUpdate: false, canArchive: false, canApprove: false, canExport: mod === 'accounting' };
return {
module: mod,
canView: true,
canCreate: true,
canUpdate: true,
canArchive: true,
canApprove: ['invoices', 'payments', 'expenses', 'payroll', 'fund_transfers'].includes(mod),
canExport: true,
};
}),
technician: MODULES.map((mod) => {
// Technicians can create/update tickets and record payments in the field.
// They can VIEW clients, subscriptions, invoices for context but creating/updating
// those requires manager-level API access (@Roles('manager') on POST/PATCH).
// Assets require manager via class-level @Roles('manager').
// Subscriptions require manager for all endpoints.
const viewOnly: Module[] = ['clients', 'subscriptions', 'invoices', 'dashboard'];
const fullAccess: Module[] = ['tickets', 'payments'];
if (viewOnly.includes(mod)) return { module: mod, canView: true, canCreate: false, canUpdate: false, canArchive: false, canApprove: false, canExport: false };
if (fullAccess.includes(mod)) return { module: mod, canView: true, canCreate: true, canUpdate: true, canArchive: false, canApprove: false, canExport: false };
return { module: mod, canView: false, canCreate: false, canUpdate: false, canArchive: false, canApprove: false, canExport: false };
}),
collector: MODULES.map((mod) => {
const canWrite: Module[] = ['payments'];
const canView: Module[] = ['dashboard', 'clients', 'invoices', 'payments'];
if (!canView.includes(mod)) return { module: mod, canView: false, canCreate: false, canUpdate: false, canArchive: false, canApprove: false, canExport: false };
return {
module: mod,
canView: true,
canCreate: canWrite.includes(mod),
canUpdate: false,
canArchive: false,
canApprove: false,
canExport: false,
};
}),
};
// ─── Legacy support (for super_admin which uses old UserRole system) ───
export const Permission = {
READ_DASHBOARD: 'read:dashboard',
READ_REPORTS: 'read:reports',
READ_CLIENTS: 'read:clients',
WRITE_CLIENTS: 'write:clients',
DELETE_CLIENTS: 'delete:clients',
READ_SUBSCRIPTIONS: 'read:subscriptions',
WRITE_SUBSCRIPTIONS: 'write:subscriptions',
READ_INVOICES: 'read:invoices',
WRITE_INVOICES: 'write:invoices',
VOID_INVOICES: 'void:invoices',
READ_PAYMENTS: 'read:payments',
WRITE_PAYMENTS: 'write:payments',
APPROVE_REMITTANCES: 'approve:remittances',
READ_TICKETS: 'read:tickets',
WRITE_TICKETS: 'write:tickets',
ASSIGN_TICKETS: 'assign:tickets',
READ_EMPLOYEES: 'read:employees',
WRITE_EMPLOYEES: 'write:employees',
READ_PAYROLL: 'read:payroll',
WRITE_PAYROLL: 'write:payroll',
READ_EXPENSES: 'read:expenses',
WRITE_EXPENSES: 'write:expenses',
APPROVE_EXPENSES: 'approve:expenses',
READ_ASSETS: 'read:assets',
WRITE_ASSETS: 'write:assets',
READ_ACCOUNTS: 'read:accounts',
WRITE_ACCOUNTS: 'write:accounts',
TRANSFER_ACCOUNTS: 'transfer:accounts',
READ_ACCOUNTING: 'read:accounting',
WRITE_ACCOUNTING: 'write:accounting',
READ_SETTINGS: 'read:settings',
WRITE_SETTINGS: 'write:settings',
MANAGE_USERS: 'manage:users',
MANAGE_TENANTS: 'manage:tenants',
} as const;
export type PermissionString = (typeof Permission)[keyof typeof Permission];
/**
* Get permissions for super_admin (all permissions).
*/
export function getPermissionsForRoles(roles: string[]): string[] {
if (roles.includes(Role.SUPER_ADMIN)) {
return Object.values(Permission);
}
// For tenant users, permissions come from TenantRole → RolePermission in DB
return [];
}