250 lines
9.6 KiB
TypeScript
250 lines
9.6 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import {
|
|
Club,
|
|
ClubMetrics,
|
|
ClubUpcomingRide,
|
|
ClubMember,
|
|
ClubActivityItem,
|
|
UserPortalPermissions,
|
|
ClubRole,
|
|
} from './types';
|
|
import { ClubMasthead } from './club-masthead';
|
|
import { ExecutiveMetrics } from './metric-card';
|
|
import { AttentionSection } from './attention-card';
|
|
import { UpcomingRidesSection } from './club-ride-card';
|
|
import { RolesAndRanksSummary } from './roles-and-ranks-summary';
|
|
import { ClubActivityList } from './club-activity-list';
|
|
|
|
interface FounderOverviewProps {
|
|
club: Club;
|
|
metrics: ClubMetrics;
|
|
upcomingRides: ClubUpcomingRide[];
|
|
pendingApplications: ClubMember[];
|
|
activities: ClubActivityItem[];
|
|
permissions: UserPortalPermissions;
|
|
onRefresh?: () => void;
|
|
}
|
|
|
|
export function FounderOverview({
|
|
club,
|
|
metrics: initialMetrics,
|
|
upcomingRides: initialRides,
|
|
pendingApplications: initialApps,
|
|
activities,
|
|
permissions,
|
|
onRefresh,
|
|
}: FounderOverviewProps) {
|
|
const router = useRouter();
|
|
const [pendingApps, setPendingApps] = useState<ClubMember[]>(initialApps);
|
|
const [rides] = useState<ClubUpcomingRide[]>(initialRides);
|
|
const [metrics, setMetrics] = useState<ClubMetrics>(initialMetrics);
|
|
const [notification, setNotification] = useState<string | null>(null);
|
|
|
|
// Modal dialog state
|
|
const [activeModal, setActiveModal] = useState<'plan_ride' | 'invite_member' | 'settings' | 'announcement' | 'switch_club' | null>(null);
|
|
|
|
function showToast(message: string) {
|
|
setNotification(message);
|
|
setTimeout(() => {
|
|
setNotification((curr) => (curr === message ? null : curr));
|
|
}, 4000);
|
|
}
|
|
|
|
function handleApproveApplication(uid: string) {
|
|
if (!permissions.canApproveApplications) {
|
|
showToast('Administrative privileges are required to approve applications.');
|
|
return;
|
|
}
|
|
const target = pendingApps.find((a) => a.uid === uid);
|
|
setPendingApps((prev) => prev.filter((a) => a.uid !== uid));
|
|
setMetrics((prev) => ({
|
|
...prev,
|
|
members: prev.members + 1,
|
|
pendingApplications: Math.max(0, prev.pendingApplications - 1),
|
|
}));
|
|
showToast(`Approved ${target?.displayName ?? 'Applicant'}'s membership.`);
|
|
}
|
|
|
|
function handleReviewApplication(uid: string) {
|
|
if (uid === 'all') {
|
|
showToast('Navigating to Applications triage management queue.');
|
|
} else {
|
|
const target = pendingApps.find((a) => a.uid === uid);
|
|
showToast(`Reviewing applicant profile: ${target?.displayName ?? 'Rider'}`);
|
|
}
|
|
}
|
|
|
|
function handleAssignSweep(rideId: string) {
|
|
if (!permissions.canPlanRides) {
|
|
showToast('Road Captain or administrative role required to assign sweep positions.');
|
|
return;
|
|
}
|
|
showToast('Sweep assignment modal opened. Designate qualified Road Marshal.');
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-col space-y-6">
|
|
{/* Toast Notification */}
|
|
{notification && (
|
|
<div
|
|
role="status"
|
|
className="fixed bottom-6 right-6 z-50 rounded-lg bg-surface-container-high border border-outline-variant px-4 py-3 shadow-xl flex items-center gap-2.5 text-xs text-on-surface font-body animate-fadeIn"
|
|
>
|
|
<span className="material-symbols-outlined text-secondary text-[18px]">info</span>
|
|
<span>{notification}</span>
|
|
<button
|
|
onClick={() => setNotification(null)}
|
|
className="text-outline hover:text-on-surface ml-2"
|
|
>
|
|
<span className="material-symbols-outlined text-[16px]">close</span>
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Member Access Notice if current role is member */}
|
|
{!permissions.canManageSettings && (
|
|
<div className="p-3.5 rounded-lg bg-surface-container-high border border-outline-variant text-on-surface-variant flex items-center justify-between gap-3 text-xs font-body">
|
|
<div className="flex items-center gap-2">
|
|
<span className="material-symbols-outlined text-primary text-[18px]">visibility</span>
|
|
<span>
|
|
Viewing Founder Portal in <strong className="text-on-surface">Member View Mode</strong>. Administrative actions and mutations are restricted to Club Owner and Admins.
|
|
</span>
|
|
</div>
|
|
<span className="font-mono text-[11px] text-outline shrink-0">View-Only</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* 1. CLUB IDENTITY MASTHEAD */}
|
|
<ClubMasthead
|
|
club={club}
|
|
permissions={permissions}
|
|
onPlanRide={() => setActiveModal('plan_ride')}
|
|
onInviteMember={() => setActiveModal('invite_member')}
|
|
onOpenSettings={() => setActiveModal('settings')}
|
|
/>
|
|
|
|
{/* 2. EXECUTIVE METRICS */}
|
|
<ExecutiveMetrics metrics={metrics} />
|
|
|
|
{/* 3. ATTENTION REQUIRED */}
|
|
<AttentionSection
|
|
pendingApplications={pendingApps}
|
|
upcomingRides={rides}
|
|
permissions={permissions}
|
|
onApproveApplication={handleApproveApplication}
|
|
onReviewApplication={handleReviewApplication}
|
|
onAssignSweep={handleAssignSweep}
|
|
onMarkChecked={() => showToast('All pending review alerts acknowledged.')}
|
|
/>
|
|
|
|
{/* 4. UPCOMING CLUB RIDES */}
|
|
<UpcomingRidesSection
|
|
rides={rides}
|
|
permissions={permissions}
|
|
clubId={club.slug || club.id}
|
|
onPlanRide={() => setActiveModal('plan_ride')}
|
|
onManageRide={(rideId) => showToast(`Opening management cockpit for Ride ID: ${rideId}`)}
|
|
onViewRoster={(rideId) => {
|
|
if (rideId === 'all') {
|
|
showToast('Full ride schedule is planned for a future release.');
|
|
} else {
|
|
showToast('Member roster details will be available in an upcoming release.');
|
|
}
|
|
}}
|
|
onViewRoute={(routeId) => showToast(`Opening route navigation map: ${routeId ?? 'default'}`)}
|
|
/>
|
|
|
|
{/* 5. ROLES & RANKS */}
|
|
<RolesAndRanksSummary
|
|
ownerName={club.founderName || 'Club Founder'}
|
|
memberCount={club.memberCount}
|
|
/>
|
|
|
|
|
|
{/* 6. CLUB ACTIVITY */}
|
|
<ClubActivityList activities={activities} />
|
|
|
|
{/* Modal Dialogs */}
|
|
{activeModal && (
|
|
<div
|
|
role="dialog"
|
|
aria-modal="true"
|
|
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm"
|
|
>
|
|
<div className="w-full max-w-md rounded-xl bg-surface-container-lowest border border-outline-variant p-6 shadow-2xl space-y-4">
|
|
<div className="flex items-center justify-between pb-3 border-b border-outline-variant">
|
|
<h3 className="font-headline text-base font-semibold text-on-surface">
|
|
{activeModal === 'plan_ride' && 'Plan Club Ride'}
|
|
{activeModal === 'invite_member' && 'Invite New Member'}
|
|
{activeModal === 'settings' && 'Club Administration Settings'}
|
|
{activeModal === 'announcement' && 'Post Club Announcement'}
|
|
{activeModal === 'switch_club' && 'Switch Club Roster'}
|
|
</h3>
|
|
<button
|
|
onClick={() => setActiveModal(null)}
|
|
className="text-outline hover:text-on-surface p-1"
|
|
aria-label="Close dialog"
|
|
>
|
|
<span className="material-symbols-outlined text-[20px]">close</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div className="font-body text-xs text-on-surface-variant space-y-3">
|
|
{activeModal === 'plan_ride' && (
|
|
<p>
|
|
Create a new scheduled Group Ride associated with <strong>{club.name}</strong>. Designated routes and checkpoints will sync with ride participants.
|
|
</p>
|
|
)}
|
|
{activeModal === 'invite_member' && (
|
|
<p>
|
|
Send an official membership invitation to a verified rider handle. Once accepted, they will join the club roster.
|
|
</p>
|
|
)}
|
|
{activeModal === 'settings' && (
|
|
<p>
|
|
Club metadata, visibility policy, admission policy, and regional preferences are administered here.
|
|
</p>
|
|
)}
|
|
{activeModal === 'announcement' && (
|
|
<p>
|
|
Broadcast an administrative update to all active members of {club.name}.
|
|
</p>
|
|
)}
|
|
{activeModal === 'switch_club' && (
|
|
<p>
|
|
Select another registered Motorcycle Club from your active memberships.
|
|
</p>
|
|
)}
|
|
|
|
<div className="p-3 rounded bg-surface-container border border-outline-variant text-outline font-mono text-[11px]">
|
|
FEATURE CONSOLE // STAGE FP-1 PROD FOUNDATION
|
|
</div>
|
|
</div>
|
|
|
|
<div className="pt-3 border-t border-outline-variant/50 flex items-center justify-end gap-2">
|
|
<button
|
|
onClick={() => setActiveModal(null)}
|
|
className="h-9 px-4 rounded-xl bg-surface-container text-slate-200 border border-outline-variant/60 font-body text-xs hover:bg-surface-container-high transition-colors cursor-pointer"
|
|
>
|
|
Close
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
showToast('Action acknowledged.');
|
|
setActiveModal(null);
|
|
}}
|
|
className="h-9 px-4 rounded-xl bg-primary-container text-on-primary-container font-body text-xs font-bold hover:brightness-110 shadow-[0_0_12px_rgba(0,242,254,0.25)] transition-all cursor-pointer"
|
|
>
|
|
Confirm
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|