'use client'; import React, { useState } from 'react'; import { ClubCapability, ClubCapabilityDefinition, ClubRankDefinition } from './types'; interface ClubPermissionMatrixProps { ranks: ClubRankDefinition[]; capabilities: ClubCapabilityDefinition[]; permissions: Record>; selectedRankId: string | null; onTogglePermission: (rankId: string, capability: ClubCapability) => void; onResetDefaults?: () => void; onSavePermissions?: () => Promise; isSaving?: boolean; canEdit: boolean; /** * Whether a server-side persistence contract for per-rank capability * permissions actually exists. Defaults to false. * * The enforced club authorization model is role-based (Owner / Admin), not * rank-based (see getPortalPermissions in lib/club-data.ts). No Firestore * document or API persists this capability matrix today, so by default the * matrix renders as a read-only reference of the standard governance model. * The editable + save path is retained for when a backend contract ships * (see website/docs/backend-contracts/club-rank-permissions.md). */ persistenceAvailable?: boolean; } export function ClubPermissionMatrix({ ranks, capabilities, permissions, selectedRankId, onTogglePermission, onResetDefaults, onSavePermissions, isSaving = false, canEdit, persistenceAvailable = false, }: ClubPermissionMatrixProps) { const [saveSuccess, setSaveSuccess] = useState(false); // Edit + save controls are only live when the caller both grants edit rights // AND a real persistence backend is wired. Without persistence the matrix is // a truthful read-only reference: it never mutates and never reports a save. const editable = canEdit && persistenceAvailable && typeof onSavePermissions === 'function'; async function handleSave() { if (!editable || !onSavePermissions) return; await onSavePermissions(); setSaveSuccess(true); setTimeout(() => setSaveSuccess(false), 2500); } return (
{/* Header */}
tune

Operational Permission Matrix

Standard delegated operational privileges per squad rank. Administrative roles enforce backend limits.

{editable ? (
{onResetDefaults && ( )}
) : ( visibility View-only reference )}
{/* Truthful availability notice: shown whenever persistence is not wired. */} {!editable && (
info This matrix shows MotorSignal’s standard rank conventions for reference. Custom per-rank permissions are not saved yet. Who can actually manage this club is enforced by the{' '} Owner and{' '} Admin roles in the member roster — see Ranks & Roles above. Per-rank customization is planned.
)} {/* Desktop / Tablet Matrix Table */}
{capabilities.map((cap) => ( ))} {ranks.map((rank) => { const isSelected = selectedRankId === rank.id; const rankPerms = permissions[rank.id] || {}; return ( {capabilities.map((cap) => { const isGranted = rankPerms[cap.id] ?? false; const isImmutableOwner = rank.id === 'president'; const toggleDisabled = !editable || isImmutableOwner; return ( ); })} ); })}
Squad Rank
{cap.label} {cap.category}
{rank.icon}
{rank.title}
Level {rank.level}
{/* Legend & Architecture Note */}
Active Toggle: Rank delegates authority to lead and coordinate squad members.
shield Software RBAC: Owner and Admin roles enforce immutable security boundaries in Firestore.
); }