MotorSignal-App/website/components/founder-portal/club-hierarchy-builder.tsx

340 lines
14 KiB
TypeScript

'use client';
import React, { useState } from 'react';
import { ClubMember, ClubRankDefinition } from './types';
interface ClubHierarchyBuilderProps {
clubId: string;
ranks: ClubRankDefinition[];
members: ClubMember[];
selectedRankId: string | null;
onSelectRank: (rankId: string) => void;
onUpdateMemberRank: (memberUid: string, newRank: string) => Promise<void>;
canManageRanks: boolean;
}
export function ClubHierarchyBuilder({
ranks,
members,
selectedRankId,
onSelectRank,
onUpdateMemberRank,
canManageRanks,
}: ClubHierarchyBuilderProps) {
const [assignModalRank, setAssignModalRank] = useState<ClubRankDefinition | null>(null);
const [selectedMemberUid, setSelectedMemberUid] = useState<string>('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [actionMessage, setActionMessage] = useState<{ text: string; type: 'success' | 'error' } | null>(null);
// Group ranks into organizational tiers
const tier1 = ranks.filter((r) => r.level === 1);
const tier2 = ranks.filter((r) => r.level === 2);
const tier3 = ranks.filter((r) => r.level === 3 || r.level === 4);
const tier4 = ranks.filter((r) => r.level >= 5);
const getRidersForRank = (rankTitle: string) => {
return members.filter((m) => {
const currentRank = m.displayRank?.toLowerCase().trim();
const targetRank = rankTitle.toLowerCase().trim();
if (targetRank === 'president' && (m.role === 'owner' || currentRank === 'president' || currentRank === 'founder')) {
return true;
}
return currentRank === targetRank;
});
};
async function handleAssignMember() {
if (!assignModalRank || !selectedMemberUid) return;
setIsSubmitting(true);
setActionMessage(null);
try {
await onUpdateMemberRank(selectedMemberUid, assignModalRank.title);
setActionMessage({ text: `Assigned ${assignModalRank.title} rank successfully.`, type: 'success' });
setSelectedMemberUid('');
setTimeout(() => {
setAssignModalRank(null);
setActionMessage(null);
}, 1200);
} catch {
setActionMessage({ text: 'Failed to update member rank. Try again.', type: 'error' });
} finally {
setIsSubmitting(false);
}
}
const renderRankNode = (rank: ClubRankDefinition) => {
const isSelected = selectedRankId === rank.id;
const assignedRiders = getRidersForRank(rank.title);
return (
<div
key={rank.id}
onClick={() => onSelectRank(rank.id)}
className={`group relative cursor-pointer rounded-xl p-4 transition-all duration-200 border ${
isSelected
? 'bg-surface-container-high/90 border-brand-cyan shadow-[0_0_15px_rgba(0,242,254,0.2)] ring-1 ring-brand-cyan/50'
: 'bg-surface-container/70 border-brand-border/60 hover:border-brand-border hover:bg-surface-container-high/60'
}`}
>
{/* Header */}
<div className="flex items-start justify-between gap-3">
<div className="flex items-center gap-2.5">
<div
className="w-8 h-8 rounded-lg flex items-center justify-center shrink-0 border"
style={{
backgroundColor: rank.badgeBg,
borderColor: `${rank.badgeColor}40`,
color: rank.badgeColor,
}}
>
<span className="material-symbols-outlined text-[18px]">{rank.icon}</span>
</div>
<div>
<div className="flex items-center gap-2">
<span className="font-headline font-bold text-sm text-white">{rank.title}</span>
<span
className="font-mono text-[9px] uppercase px-1.5 py-0.5 rounded font-semibold border"
style={{
backgroundColor: rank.badgeBg,
borderColor: `${rank.badgeColor}30`,
color: rank.badgeColor,
}}
>
Level {rank.level}
</span>
</div>
<span className="font-mono text-[10px] text-slate-400 capitalize">
{rank.category} • Default: {rank.defaultRole}
</span>
</div>
</div>
<div className="flex items-center gap-1.5 shrink-0">
<span className="px-2 py-0.5 rounded-full bg-slate-800/80 border border-slate-700/80 text-slate-300 font-mono text-[11px] font-medium">
{assignedRiders.length} {assignedRiders.length === 1 ? 'rider' : 'riders'}
</span>
</div>
</div>
{/* Description */}
<p className="font-body text-xs text-slate-400 mt-2.5 line-clamp-2 leading-relaxed">
{rank.description}
</p>
{/* Assigned Riders Preview */}
{assignedRiders.length > 0 ? (
<div className="mt-3 pt-2.5 border-t border-brand-border/40 flex flex-wrap gap-1.5 items-center">
{assignedRiders.slice(0, 3).map((rider) => (
<span
key={rider.uid}
className="inline-flex items-center gap-1 px-2 py-0.5 rounded-md bg-surface-container border border-brand-border/60 text-slate-300 text-[11px] font-mono"
>
<span className="w-1.5 h-1.5 rounded-full bg-brand-cyan" />
{rider.displayName || rider.handle || rider.uid.slice(0, 6)}
</span>
))}
{assignedRiders.length > 3 && (
<span className="text-[10px] font-mono text-slate-400">
+{assignedRiders.length - 3} more
</span>
)}
</div>
) : (
<div className="mt-3 pt-2.5 border-t border-brand-border/40 text-[11px] font-mono text-slate-400 italic">
No active riders currently assigned
</div>
)}
{/* Quick Assign Button */}
{canManageRanks && (
<div className="mt-3 flex justify-end">
<button
type="button"
onClick={(e) => {
e.stopPropagation();
setAssignModalRank(rank);
setActionMessage(null);
}}
className="inline-flex items-center gap-1 text-[11px] font-mono font-medium text-brand-cyan hover:text-white px-2 py-1 rounded bg-brand-cyan/10 hover:bg-brand-cyan/20 border border-brand-cyan/30 transition-colors"
>
<span className="material-symbols-outlined text-[14px]">person_add</span>
Assign Rider
</button>
</div>
)}
</div>
);
};
return (
<section className="rounded-2xl glass-card border border-brand-border/60 p-5 sm:p-6 space-y-6 shadow-lg">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 border-b border-brand-border/60 pb-4">
<div>
<div className="flex items-center gap-2">
<span className="material-symbols-outlined text-brand-cyan text-[20px]">
account_tree
</span>
<h2 className="font-headline text-lg sm:text-xl font-bold text-white tracking-tight">
Club Hierarchy Builder
</h2>
</div>
<p className="font-body text-xs sm:text-sm text-slate-400 mt-1">
Visual organizational structure. Select any squad rank to view operational privileges or reassign riders.
</p>
</div>
<div className="flex items-center gap-2 self-start sm:self-center">
<span className="px-2.5 py-0.5 rounded-full bg-brand-cyan/10 border border-brand-cyan/30 text-brand-cyan font-mono text-[11px] font-semibold">
{ranks.length} RANKS
</span>
<span className="px-2.5 py-0.5 rounded-full bg-slate-800 border border-slate-700 text-slate-300 font-mono text-[11px]">
{members.length} MEMBERS
</span>
</div>
</div>
{/* Visual Hierarchy Tree Canvas */}
<div className="space-y-6">
{/* Tier 1: Executive Command */}
<div>
<div className="flex items-center gap-2 mb-3">
<span className="font-mono text-xs text-amber-400 font-bold uppercase tracking-wider">
Tier 1 • Executive Command
</span>
<div className="flex-1 h-px bg-amber-400/20" />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{tier1.map(renderRankNode)}
</div>
</div>
{/* Tier 2: Field Leadership & Governance */}
<div>
<div className="flex items-center gap-2 mb-3">
<span className="font-mono text-xs text-brand-cyan font-bold uppercase tracking-wider">
Tier 2 • Leadership &amp; Administration
</span>
<div className="flex-1 h-px bg-brand-cyan/20" />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{tier2.map(renderRankNode)}
</div>
</div>
{/* Tier 3: Tactical Operations */}
<div>
<div className="flex items-center gap-2 mb-3">
<span className="font-mono text-xs text-purple-400 font-bold uppercase tracking-wider">
Tier 3 • Tactical Operations &amp; Pack Safety
</span>
<div className="flex-1 h-px bg-purple-400/20" />
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{tier3.map(renderRankNode)}
</div>
</div>
{/* Tier 4: Squad Formation */}
<div>
<div className="flex items-center gap-2 mb-3">
<span className="font-mono text-xs text-slate-400 font-bold uppercase tracking-wider">
Tier 4 • Squad Formation &amp; Prospects
</span>
<div className="flex-1 h-px bg-slate-400/20" />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{tier4.map(renderRankNode)}
</div>
</div>
</div>
{/* Assign Member Modal */}
{assignModalRank && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm animate-fade-in">
<div className="bg-surface-container border border-brand-border/80 rounded-2xl p-6 max-w-md w-full shadow-2xl space-y-4">
<div className="flex items-start justify-between">
<div>
<h3 className="font-headline font-bold text-lg text-white">
Assign to {assignModalRank.title}
</h3>
<p className="font-body text-xs text-slate-400 mt-0.5">
Select an active member to grant this honorary squad rank.
</p>
</div>
<button
type="button"
onClick={() => setAssignModalRank(null)}
className="text-slate-400 hover:text-white p-1 rounded-lg hover:bg-surface-container-high transition-colors"
>
<span className="material-symbols-outlined text-[20px]">close</span>
</button>
</div>
<div className="space-y-3">
<label className="block font-mono text-xs text-slate-300">
Select Active Member
</label>
<select
value={selectedMemberUid}
onChange={(e) => setSelectedMemberUid(e.target.value)}
className="w-full bg-surface-container-high border border-brand-border/80 rounded-xl px-3 py-2.5 text-sm text-white font-body focus:outline-none focus:border-brand-cyan"
>
<option value="">-- Choose member --</option>
{members.map((m) => (
<option key={m.uid} value={m.uid}>
{m.displayName || m.handle || m.uid.slice(0, 8)} ({m.displayRank || m.role})
</option>
))}
</select>
<div className="p-3 rounded-lg bg-surface-container-high/60 border border-brand-border/40 text-xs text-slate-400">
<span className="font-semibold text-white">Rank Notice:</span> Setting this rank updates the member&apos;s title displayed on the mobile app, Club Wall, and group rides.
</div>
{actionMessage && (
<div
className={`p-3 rounded-lg text-xs font-mono border ${
actionMessage.type === 'success'
? 'bg-emerald-500/10 border-emerald-500/30 text-emerald-400'
: 'bg-rose-500/10 border-rose-500/30 text-rose-400'
}`}
>
{actionMessage.text}
</div>
)}
</div>
<div className="flex items-center justify-end gap-3 pt-3 border-t border-brand-border/60">
<button
type="button"
onClick={() => setAssignModalRank(null)}
className="px-4 py-2 rounded-xl text-xs font-body text-slate-400 hover:text-white hover:bg-surface-container-high transition-colors"
>
Cancel
</button>
<button
type="button"
disabled={!selectedMemberUid || isSubmitting}
onClick={handleAssignMember}
className="inline-flex items-center gap-2 px-4 py-2 rounded-xl text-xs font-headline font-bold bg-brand-cyan text-black hover:bg-brand-cyan/90 disabled:opacity-50 disabled:cursor-not-allowed transition-all"
>
{isSubmitting ? (
<>
<span className="w-3.5 h-3.5 border-2 border-black border-t-transparent rounded-full animate-spin" />
Saving...
</>
) : (
<>
<span className="material-symbols-outlined text-[16px]">check</span>
Assign Rank
</>
)}
</button>
</div>
</div>
</div>
)}
</section>
);
}