253 lines
11 KiB
TypeScript
253 lines
11 KiB
TypeScript
'use client';
|
|
|
|
import React from 'react';
|
|
import { ClubMember, ClubUpcomingRide, UserPortalPermissions } from './types';
|
|
import { EmptyState } from './empty-state';
|
|
|
|
interface AttentionSectionProps {
|
|
pendingApplications: ClubMember[];
|
|
upcomingRides: ClubUpcomingRide[];
|
|
permissions: UserPortalPermissions;
|
|
onApproveApplication?: (uid: string) => void;
|
|
onReviewApplication?: (uid: string) => void;
|
|
onAssignSweep?: (rideId: string) => void;
|
|
onMarkChecked?: () => void;
|
|
}
|
|
|
|
export function AttentionSection({
|
|
pendingApplications,
|
|
upcomingRides,
|
|
permissions,
|
|
onApproveApplication,
|
|
onReviewApplication,
|
|
onAssignSweep,
|
|
onMarkChecked,
|
|
}: AttentionSectionProps) {
|
|
// Find any ride needing assignment
|
|
const rideNeedingSweep = upcomingRides.find(
|
|
(r) => r.sweepStatus === 'needs_assignment' || r.sweepStatus === 'vacant'
|
|
);
|
|
|
|
const pendingAppsCount = pendingApplications.length;
|
|
const totalPendingActions = (pendingAppsCount > 0 ? 1 : 0) + (rideNeedingSweep ? 1 : 0);
|
|
|
|
return (
|
|
<section className="rounded-2xl glass-card border border-brand-border/60 p-5 sm:p-6 space-y-5 shadow-lg">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<span
|
|
className={`w-2 h-2 rounded-full ${
|
|
totalPendingActions > 0 ? 'bg-error animate-pulse' : 'bg-brand-cyan'
|
|
}`}
|
|
/>
|
|
<h2 className="font-headline text-base font-bold text-white tracking-wide">
|
|
Attention Required
|
|
</h2>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<span className="font-mono text-xs text-slate-400">
|
|
{totalPendingActions} pending {totalPendingActions === 1 ? 'action' : 'actions'}
|
|
</span>
|
|
{onMarkChecked && (
|
|
<button
|
|
onClick={onMarkChecked}
|
|
className="px-2.5 py-1 rounded-lg bg-surface-container hover:bg-surface-container-high text-slate-300 hover:text-white font-mono text-xs border border-brand-border/60 transition-colors"
|
|
>
|
|
Mark Checked
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 3 Triage Cards Grid */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
|
{/* Card 1: Membership Applications */}
|
|
<div className="rounded-xl bg-surface-container/60 border border-brand-border/60 p-4 sm:p-5 flex flex-col justify-between shadow-md">
|
|
<div>
|
|
<div className="flex items-center justify-between pb-2.5 border-b border-brand-border/40">
|
|
<div className="flex items-center gap-1.5">
|
|
<span className="material-symbols-outlined text-[18px] text-brand-cyan">
|
|
person_add
|
|
</span>
|
|
<span className="font-body text-xs sm:text-sm font-semibold text-white">
|
|
Membership Applications
|
|
</span>
|
|
</div>
|
|
{pendingAppsCount > 0 ? (
|
|
<span className="px-2 py-0.5 rounded-full bg-error-container text-on-error-container font-mono text-[10px] font-semibold border border-error/30">
|
|
{pendingAppsCount} Pending
|
|
</span>
|
|
) : (
|
|
<span className="font-mono text-[10px] text-slate-400 bg-surface-container px-2 py-0.5 rounded-full border border-brand-border/40">0 Pending</span>
|
|
)}
|
|
</div>
|
|
|
|
<p className="font-body text-xs text-slate-400 mt-2 mb-3">
|
|
Membership applications awaiting review.
|
|
</p>
|
|
|
|
{pendingAppsCount === 0 ? (
|
|
<EmptyState
|
|
icon="how_to_reg"
|
|
title="No Pending Applications"
|
|
description="No membership applications currently require review."
|
|
className="py-4"
|
|
/>
|
|
) : (
|
|
<div className="space-y-2">
|
|
{pendingApplications.slice(0, 3).map((app) => (
|
|
<div
|
|
key={app.uid}
|
|
className="p-2.5 rounded-lg bg-surface-container-high border border-brand-border/60 flex items-center justify-between gap-2"
|
|
>
|
|
<div className="min-w-0">
|
|
<div className="font-body text-xs font-semibold text-white truncate">
|
|
{app.displayName ?? 'New Rider'}
|
|
</div>
|
|
<div className="font-mono text-[10px] text-slate-400">
|
|
{app.requestedAt ?? app.joinedAt ?? 'Recently'}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-1.5 shrink-0">
|
|
{permissions.canApproveApplications && (
|
|
<button
|
|
onClick={() => onApproveApplication?.(app.uid)}
|
|
className="px-2.5 py-1 rounded-lg bg-primary-container text-on-primary-container font-bold text-[11px] hover:brightness-110 shadow-[0_0_8px_rgba(0,242,254,0.2)] transition-all cursor-pointer"
|
|
>
|
|
Approve
|
|
</button>
|
|
)}
|
|
<button
|
|
onClick={() => onReviewApplication?.(app.uid)}
|
|
className="px-2 py-1 rounded-lg bg-surface-container hover:bg-surface-container-high text-slate-300 font-mono text-[10px] border border-brand-border/60 transition-colors cursor-pointer"
|
|
>
|
|
Review
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="mt-3 pt-2 border-t border-brand-border/40 flex items-center justify-between text-slate-400 font-mono text-[10px]">
|
|
<span>Triage Status</span>
|
|
<span>{pendingAppsCount === 0 ? 'All Cleared' : `${pendingAppsCount} Actionable`}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Card 2: Moderation Flag / Wall Posts (Preserving deferred backend architecture) */}
|
|
<div className="rounded-xl bg-surface-container/60 border border-brand-border/60 p-4 sm:p-5 flex flex-col justify-between shadow-md">
|
|
<div>
|
|
<div className="flex items-center justify-between pb-2.5 border-b border-brand-border/40">
|
|
<div className="flex items-center gap-1.5">
|
|
<span className="material-symbols-outlined text-[17px] text-amber-400">gavel</span>
|
|
<span className="font-body text-xs sm:text-sm font-semibold text-white">
|
|
Reported Club Wall Post
|
|
</span>
|
|
</div>
|
|
<span className="font-mono text-[10px] text-slate-400 bg-surface-container px-2 py-0.5 rounded-full border border-brand-border/40">Deferred</span>
|
|
</div>
|
|
|
|
<p className="font-body text-xs text-slate-400 mt-2 mb-3">
|
|
Club Wall moderation and content flagging module.
|
|
</p>
|
|
|
|
<EmptyState
|
|
icon="verified"
|
|
title="No Reported Posts"
|
|
description="No content flags pending. Club Wall moderation backend is scheduled for future release."
|
|
className="py-4"
|
|
/>
|
|
</div>
|
|
|
|
<div className="mt-3 pt-2 border-t border-brand-border/40 flex items-center justify-between text-slate-400 font-mono text-[10px]">
|
|
<span>Moderation Queue</span>
|
|
<span>Cleared</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Card 3: Upcoming Ride Assignment */}
|
|
<div className="rounded-xl bg-surface-container/60 border border-brand-border/60 p-4 sm:p-5 flex flex-col justify-between shadow-md">
|
|
<div>
|
|
<div className="flex items-center justify-between pb-2.5 border-b border-brand-border/40">
|
|
<div className="flex items-center gap-1.5">
|
|
<span className="material-symbols-outlined text-[17px] text-brand-cyan">
|
|
assignment_ind
|
|
</span>
|
|
<span className="font-body text-xs sm:text-sm font-semibold text-white">
|
|
Upcoming Ride Assignment
|
|
</span>
|
|
</div>
|
|
{rideNeedingSweep ? (
|
|
<span className="px-2 py-0.5 rounded-full bg-brand-cyan/15 text-brand-cyan font-mono text-[10px] font-semibold border border-brand-cyan/30">
|
|
Action Needed
|
|
</span>
|
|
) : (
|
|
<span className="font-mono text-[10px] text-slate-400 bg-surface-container px-2 py-0.5 rounded-full border border-brand-border/40">Assigned</span>
|
|
)}
|
|
</div>
|
|
|
|
<p className="font-body text-xs text-slate-400 mt-2 mb-3">
|
|
Safety position check for scheduled group rides:
|
|
</p>
|
|
|
|
{rideNeedingSweep ? (
|
|
<div className="rounded bg-surface-container-high border border-outline-variant/60 p-2.5 space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<span className="font-body text-xs text-on-surface font-semibold truncate max-w-[180px]">
|
|
{rideNeedingSweep.title}
|
|
</span>
|
|
<span className="font-mono text-[10px] text-primary shrink-0">
|
|
{rideNeedingSweep.scheduledStartAt.split('·')[0].trim()}
|
|
</span>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-1.5 font-mono text-[11px] text-outline">
|
|
<div>
|
|
Riders Going: <span className="text-on-surface">{rideNeedingSweep.confirmedCount}</span>
|
|
</div>
|
|
<div>
|
|
Road Captain: <span className="text-on-surface">{rideNeedingSweep.roadCaptainName}</span>
|
|
</div>
|
|
<div className="col-span-2">
|
|
Sweep: <span className="text-error font-medium">Vacant</span>
|
|
</div>
|
|
</div>
|
|
<div className="p-1.5 rounded bg-surface-container-lowest/80 font-body text-[11px] text-on-surface-variant border border-outline-variant/40">
|
|
Sweep position is currently unassigned for this ride.
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<EmptyState
|
|
icon="check_circle"
|
|
title="Rosters Assigned"
|
|
description="All scheduled rides have designated Road Captain and Sweep positions."
|
|
className="py-4"
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
<div className="mt-3 pt-2">
|
|
{rideNeedingSweep && permissions.canPlanRides ? (
|
|
<button
|
|
onClick={() => onAssignSweep?.(rideNeedingSweep.id)}
|
|
className="w-full h-8 rounded-lg bg-secondary-fixed text-on-secondary-fixed font-body text-xs font-bold hover:brightness-110 transition-all flex items-center justify-center gap-1.5 shadow-sm cursor-pointer"
|
|
>
|
|
<span className="material-symbols-outlined text-[16px]">person_pin_circle</span>
|
|
<span>Assign Sweep</span>
|
|
</button>
|
|
) : (
|
|
<div className="font-mono text-[10px] text-outline text-right">
|
|
Positions Stable
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|