Add tab icons and fix mobile dashboard styling issues
- Add matching lucide-react icons to tab/mode switcher buttons on payments, at-the-door, manual, email-attendees, whatsapp-attendees, and admin cashup pages, mirroring icons already used in their help menus - Fix navbar Logout button sitting lower than other nav links (missing border/padding classes that other links use for their active-underline) - Fix stat card labels getting truncated on mobile by removing the ellipsis-cut label and widening the mobile grid to one column - Fix Revenue trend / Top performing events rendering outside the viewport on mobile by containing horizontal overflow and truncating long event titles in the table
This commit is contained in:
@@ -6,7 +6,7 @@ import { useAuth } from "@/hooks/useAuth";
|
|||||||
import { apiFetch } from "@/lib/api";
|
import { apiFetch } from "@/lib/api";
|
||||||
import { useDismissingState } from "@/hooks/useDismissingState";
|
import { useDismissingState } from "@/hooks/useDismissingState";
|
||||||
import type { EventCost, EventCostType, EventFinancials, CashupMethod } from "@/types";
|
import type { EventCost, EventCostType, EventFinancials, CashupMethod } from "@/types";
|
||||||
import { Wallet } from "lucide-react";
|
import { Wallet, DollarSign, ClipboardCheck, FileBarChart } from "lucide-react";
|
||||||
|
|
||||||
const METHOD_LABELS: Record<CashupMethod, string> = { cash: "Cash", card: "Card", eft: "EFT", other: "Other" };
|
const METHOD_LABELS: Record<CashupMethod, string> = { cash: "Cash", card: "Card", eft: "EFT", other: "Other" };
|
||||||
const METHODS: CashupMethod[] = ["cash", "card", "eft", "other"];
|
const METHODS: CashupMethod[] = ["cash", "card", "eft", "other"];
|
||||||
@@ -76,9 +76,9 @@ export default function EventCashupPage() {
|
|||||||
{error && <div className="text-sm text-red-600 bg-red-50 border border-red-100 rounded p-2">{error}</div>}
|
{error && <div className="text-sm text-red-600 bg-red-50 border border-red-100 rounded p-2">{error}</div>}
|
||||||
|
|
||||||
<div className="flex gap-2 border-b">
|
<div className="flex gap-2 border-b">
|
||||||
<button className={"px-3 py-2 text-sm " + (tab === "costs" ? "border-b-2 border-brand-600 text-brand-700 font-medium" : "text-gray-500")} onClick={() => setTab("costs")}>Costs</button>
|
<button className={"inline-flex items-center gap-1.5 px-3 py-2 text-sm " + (tab === "costs" ? "border-b-2 border-brand-600 text-brand-700 font-medium" : "text-gray-500")} onClick={() => setTab("costs")}><DollarSign className="w-4 h-4" />Costs</button>
|
||||||
<button className={"px-3 py-2 text-sm " + (tab === "reconciliation" ? "border-b-2 border-brand-600 text-brand-700 font-medium" : "text-gray-500")} onClick={() => setTab("reconciliation")}>Reconciliation</button>
|
<button className={"inline-flex items-center gap-1.5 px-3 py-2 text-sm " + (tab === "reconciliation" ? "border-b-2 border-brand-600 text-brand-700 font-medium" : "text-gray-500")} onClick={() => setTab("reconciliation")}><ClipboardCheck className="w-4 h-4" />Reconciliation</button>
|
||||||
<button className={"px-3 py-2 text-sm " + (tab === "report" ? "border-b-2 border-brand-600 text-brand-700 font-medium" : "text-gray-500")} onClick={() => setTab("report")}>Report</button>
|
<button className={"inline-flex items-center gap-1.5 px-3 py-2 text-sm " + (tab === "report" ? "border-b-2 border-brand-600 text-brand-700 font-medium" : "text-gray-500")} onClick={() => setTab("report")}><FileBarChart className="w-4 h-4" />Report</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loading && <div className="text-sm text-gray-400">Loading…</div>}
|
{loading && <div className="text-sm text-gray-400">Loading…</div>}
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ export default function AdminDashboardPage() {
|
|||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-6xl mx-auto w-full">
|
<div className="max-w-6xl mx-auto w-full overflow-x-hidden">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h1 className="text-2xl font-semibold text-gray-900">Welcome back{user ? `, ${user.name}` : ""} 👋</h1>
|
<h1 className="text-2xl font-semibold text-gray-900">Welcome back{user ? `, ${user.name}` : ""} 👋</h1>
|
||||||
<p className="text-sm text-gray-500 mt-1">Here's what's happening with your events today.</p>
|
<p className="text-sm text-gray-500 mt-1">Here's what's happening with your events today.</p>
|
||||||
@@ -132,7 +132,7 @@ export default function AdminDashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid lg:grid-cols-3 gap-6">
|
<div className="grid lg:grid-cols-3 gap-6">
|
||||||
<div className="lg:col-span-2 space-y-6">
|
<div className="lg:col-span-2 space-y-6 min-w-0">
|
||||||
<div className="border rounded-xl p-4 bg-white shadow-sm">
|
<div className="border rounded-xl p-4 bg-white shadow-sm">
|
||||||
<h2 className="text-lg font-semibold mb-3">Revenue trend — past month</h2>
|
<h2 className="text-lg font-semibold mb-3">Revenue trend — past month</h2>
|
||||||
{overview && overview.trend.length > 0 ? (
|
{overview && overview.trend.length > 0 ? (
|
||||||
@@ -157,7 +157,7 @@ export default function AdminDashboardPage() {
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{overview.topEvents.map(e => (
|
{overview.topEvents.map(e => (
|
||||||
<TableRow key={e.eventId}>
|
<TableRow key={e.eventId}>
|
||||||
<TableCell className="font-medium">{e.title}</TableCell>
|
<TableCell className="font-medium max-w-[140px] sm:max-w-[220px] truncate" title={e.title}>{e.title}</TableCell>
|
||||||
<TableCell className="text-right">{formatCount(e.registrations)}</TableCell>
|
<TableCell className="text-right">{formatCount(e.registrations)}</TableCell>
|
||||||
<TableCell className="text-right">{formatRand(e.revenue)}</TableCell>
|
<TableCell className="text-right">{formatRand(e.revenue)}</TableCell>
|
||||||
<TableCell className="text-right">{formatCount(e.ticketsSold)}</TableCell>
|
<TableCell className="text-right">{formatCount(e.ticketsSold)}</TableCell>
|
||||||
@@ -171,7 +171,7 @@ export default function AdminDashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6 min-w-0">
|
||||||
<div className="border rounded-xl p-4 bg-white shadow-sm">
|
<div className="border rounded-xl p-4 bg-white shadow-sm">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<h2 className="text-lg font-semibold">Payments overview</h2>
|
<h2 className="text-lg font-semibold">Payments overview</h2>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { useRouter } from "next/navigation";
|
|||||||
import { apiFetch } from "@/lib/api";
|
import { apiFetch } from "@/lib/api";
|
||||||
import { scoreUser } from "@/lib/fuzzyMatch";
|
import { scoreUser } from "@/lib/fuzzyMatch";
|
||||||
import { useDismissingState } from "@/hooks/useDismissingState";
|
import { useDismissingState } from "@/hooks/useDismissingState";
|
||||||
import { DoorOpen } from "lucide-react";
|
import { DoorOpen, UserPlus, CreditCard, CheckSquare, Ticket, RotateCcw, type LucideIcon } from "lucide-react";
|
||||||
|
|
||||||
type Mode = "registration" | "payment" | "checkin" | "tickets" | "refund";
|
type Mode = "registration" | "payment" | "checkin" | "tickets" | "refund";
|
||||||
|
|
||||||
@@ -18,6 +18,14 @@ const MODE_LABELS: Record<Mode, string> = {
|
|||||||
refund: "REFUND",
|
refund: "REFUND",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MODE_ICONS: Record<Mode, LucideIcon> = {
|
||||||
|
registration: UserPlus,
|
||||||
|
payment: CreditCard,
|
||||||
|
checkin: CheckSquare,
|
||||||
|
tickets: Ticket,
|
||||||
|
refund: RotateCcw,
|
||||||
|
};
|
||||||
|
|
||||||
// ─── Fuzzy search helpers ─────────────────────────────────────────────────────
|
// ─── Fuzzy search helpers ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
function fuzzyFilterRegs(allRegs: any[], search: string): any[] {
|
function fuzzyFilterRegs(allRegs: any[], search: string): any[] {
|
||||||
@@ -390,11 +398,13 @@ export default function AtTheDoorPage() {
|
|||||||
|
|
||||||
{/* Mode Buttons */}
|
{/* Mode Buttons */}
|
||||||
<div className="flex gap-2 mb-4 flex-wrap">
|
<div className="flex gap-2 mb-4 flex-wrap">
|
||||||
{(["registration", "payment", "checkin", "tickets", "refund"] as Mode[]).map(m => (
|
{(["registration", "payment", "checkin", "tickets", "refund"] as Mode[]).map(m => {
|
||||||
|
const Icon = MODE_ICONS[m];
|
||||||
|
return (
|
||||||
<button
|
<button
|
||||||
key={m}
|
key={m}
|
||||||
onClick={() => setMode(m)}
|
onClick={() => setMode(m)}
|
||||||
className={`px-4 py-2 rounded text-sm font-medium border ${
|
className={`inline-flex items-center gap-1.5 px-4 py-2 rounded text-sm font-medium border ${
|
||||||
mode === m
|
mode === m
|
||||||
? m === "refund"
|
? m === "refund"
|
||||||
? "bg-red-600 text-white border-red-600"
|
? "bg-red-600 text-white border-red-600"
|
||||||
@@ -402,9 +412,11 @@ export default function AtTheDoorPage() {
|
|||||||
: "bg-white hover:bg-gray-50"
|
: "bg-white hover:bg-gray-50"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
<Icon className="w-4 h-4" />
|
||||||
{MODE_LABELS[m]}
|
{MODE_LABELS[m]}
|
||||||
</button>
|
</button>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ✅ Panels */}
|
{/* ✅ Panels */}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useAuth } from "@/hooks/useAuth";
|
|||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import { apiFetch, fetchAllUsers } from "@/lib/api";
|
import { apiFetch, fetchAllUsers } from "@/lib/api";
|
||||||
import { useDismissingState } from "@/hooks/useDismissingState";
|
import { useDismissingState } from "@/hooks/useDismissingState";
|
||||||
import { Mail } from "lucide-react";
|
import { Mail, Users, Zap, Clock } from "lucide-react";
|
||||||
|
|
||||||
type Attendee = { id: string; name: string; email: string; pref: string };
|
type Attendee = { id: string; name: string; email: string; pref: string };
|
||||||
|
|
||||||
@@ -503,20 +503,24 @@ function EmailAttendeesPageInner() {
|
|||||||
|
|
||||||
{/* Tabs like on Payments page */}
|
{/* Tabs like on Payments page */}
|
||||||
<div className="mb-4 flex items-center gap-2 flex-wrap">
|
<div className="mb-4 flex items-center gap-2 flex-wrap">
|
||||||
<label className={`px-3 py-1.5 text-sm rounded border ${tab === 'attendees' ? 'bg-brand-600 text-white border-brand-600' : 'bg-white text-gray-800 border-gray-200'} cursor-pointer`}>
|
<label className={`inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded border ${tab === 'attendees' ? 'bg-brand-600 text-white border-brand-600' : 'bg-white text-gray-800 border-gray-200'} cursor-pointer`}>
|
||||||
<input type="radio" name="emailTab" value="attendees" className="hidden" checked={tab==='attendees'} onChange={() => setTab('attendees')} />
|
<input type="radio" name="emailTab" value="attendees" className="hidden" checked={tab==='attendees'} onChange={() => setTab('attendees')} />
|
||||||
|
<Users className="w-4 h-4" />
|
||||||
Attendees
|
Attendees
|
||||||
</label>
|
</label>
|
||||||
<label className={`px-3 py-1.5 text-sm rounded border ${tab === 'automations' ? 'bg-brand-600 text-white border-brand-600' : 'bg-white text-gray-800 border-gray-200'} cursor-pointer`}>
|
<label className={`inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded border ${tab === 'automations' ? 'bg-brand-600 text-white border-brand-600' : 'bg-white text-gray-800 border-gray-200'} cursor-pointer`}>
|
||||||
<input type="radio" name="emailTab" value="automations" className="hidden" checked={tab==='automations'} onChange={() => setTab('automations')} />
|
<input type="radio" name="emailTab" value="automations" className="hidden" checked={tab==='automations'} onChange={() => setTab('automations')} />
|
||||||
|
<Zap className="w-4 h-4" />
|
||||||
Automations
|
Automations
|
||||||
</label>
|
</label>
|
||||||
<label className={`px-3 py-1.5 text-sm rounded border ${tab === 'broadcasts' ? 'bg-brand-600 text-white border-brand-600' : 'bg-white text-gray-800 border-gray-200'} cursor-pointer`}>
|
<label className={`inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded border ${tab === 'broadcasts' ? 'bg-brand-600 text-white border-brand-600' : 'bg-white text-gray-800 border-gray-200'} cursor-pointer`}>
|
||||||
<input type="radio" name="emailTab" value="broadcasts" className="hidden" checked={tab==='broadcasts'} onChange={() => setTab('broadcasts')} />
|
<input type="radio" name="emailTab" value="broadcasts" className="hidden" checked={tab==='broadcasts'} onChange={() => setTab('broadcasts')} />
|
||||||
|
<Mail className="w-4 h-4" />
|
||||||
Broadcasts
|
Broadcasts
|
||||||
</label>
|
</label>
|
||||||
<label className={`px-3 py-1.5 text-sm rounded border ${tab === 'scheduled' ? 'bg-brand-600 text-white border-brand-600' : 'bg-white text-gray-800 border-gray-200'} cursor-pointer`}>
|
<label className={`inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded border ${tab === 'scheduled' ? 'bg-brand-600 text-white border-brand-600' : 'bg-white text-gray-800 border-gray-200'} cursor-pointer`}>
|
||||||
<input type="radio" name="emailTab" value="scheduled" className="hidden" checked={tab==='scheduled'} onChange={() => setTab('scheduled')} />
|
<input type="radio" name="emailTab" value="scheduled" className="hidden" checked={tab==='scheduled'} onChange={() => setTab('scheduled')} />
|
||||||
|
<Clock className="w-4 h-4" />
|
||||||
Scheduled
|
Scheduled
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { useRouter } from "next/navigation";
|
|||||||
import { apiFetch, fetchAllUsers } from "@/lib/api";
|
import { apiFetch, fetchAllUsers } from "@/lib/api";
|
||||||
import { scoreUser } from "@/lib/fuzzyMatch";
|
import { scoreUser } from "@/lib/fuzzyMatch";
|
||||||
import { useDismissingState } from "@/hooks/useDismissingState";
|
import { useDismissingState } from "@/hooks/useDismissingState";
|
||||||
import { UserPlus } from "lucide-react";
|
import { UserPlus, CreditCard } from "lucide-react";
|
||||||
|
|
||||||
// ─── Pricing helpers ─────────────────────────────────────────────────────────
|
// ─── Pricing helpers ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -355,12 +355,14 @@ export default function ManualRegistrationPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mb-4 flex items-center gap-2">
|
<div className="mb-4 flex items-center gap-2">
|
||||||
<label className={`px-3 py-1.5 text-sm rounded border ${tab === 'register' ? 'bg-brand-600 text-white border-brand-600' : 'bg-white text-gray-800 border-gray-200'} cursor-pointer`}>
|
<label className={`inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded border ${tab === 'register' ? 'bg-brand-600 text-white border-brand-600' : 'bg-white text-gray-800 border-gray-200'} cursor-pointer`}>
|
||||||
<input type="radio" name="tab" value="register" className="hidden" checked={tab==='register'} onChange={() => setTab('register')} />
|
<input type="radio" name="tab" value="register" className="hidden" checked={tab==='register'} onChange={() => setTab('register')} />
|
||||||
|
<UserPlus className="w-4 h-4" />
|
||||||
Register
|
Register
|
||||||
</label>
|
</label>
|
||||||
<label className={`px-3 py-1.5 text-sm rounded border ${tab === 'payment' ? 'bg-brand-600 text-white border-brand-600' : 'bg-white text-gray-800 border-gray-200'} cursor-pointer`}>
|
<label className={`inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded border ${tab === 'payment' ? 'bg-brand-600 text-white border-brand-600' : 'bg-white text-gray-800 border-gray-200'} cursor-pointer`}>
|
||||||
<input type="radio" name="tab" value="payment" className="hidden" checked={tab==='payment'} onChange={() => setTab('payment')} />
|
<input type="radio" name="tab" value="payment" className="hidden" checked={tab==='payment'} onChange={() => setTab('payment')} />
|
||||||
|
<CreditCard className="w-4 h-4" />
|
||||||
Record Payment
|
Record Payment
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ export default function SupervisorDashboardPage() {
|
|||||||
}, 15000, !!token);
|
}, 15000, !!token);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-6xl mx-auto w-full">
|
<div className="max-w-6xl mx-auto w-full overflow-x-hidden">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h1 className="text-2xl font-semibold text-gray-900">Welcome back{user ? `, ${user.name}` : ""} 👋</h1>
|
<h1 className="text-2xl font-semibold text-gray-900">Welcome back{user ? `, ${user.name}` : ""} 👋</h1>
|
||||||
<p className="text-sm text-gray-500 mt-1">Here's what's happening with your events today.</p>
|
<p className="text-sm text-gray-500 mt-1">Here's what's happening with your events today.</p>
|
||||||
@@ -131,7 +131,7 @@ export default function SupervisorDashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid lg:grid-cols-3 gap-6">
|
<div className="grid lg:grid-cols-3 gap-6">
|
||||||
<div className="lg:col-span-2 space-y-6">
|
<div className="lg:col-span-2 space-y-6 min-w-0">
|
||||||
<div className="border rounded-xl p-4 bg-white shadow-sm">
|
<div className="border rounded-xl p-4 bg-white shadow-sm">
|
||||||
<h2 className="text-lg font-semibold mb-3">Revenue trend — past month</h2>
|
<h2 className="text-lg font-semibold mb-3">Revenue trend — past month</h2>
|
||||||
{overview && overview.trend.length > 0 ? (
|
{overview && overview.trend.length > 0 ? (
|
||||||
@@ -156,7 +156,7 @@ export default function SupervisorDashboardPage() {
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{overview.topEvents.map(e => (
|
{overview.topEvents.map(e => (
|
||||||
<TableRow key={e.eventId}>
|
<TableRow key={e.eventId}>
|
||||||
<TableCell className="font-medium">{e.title}</TableCell>
|
<TableCell className="font-medium max-w-[140px] sm:max-w-[220px] truncate" title={e.title}>{e.title}</TableCell>
|
||||||
<TableCell className="text-right">{formatCount(e.registrations)}</TableCell>
|
<TableCell className="text-right">{formatCount(e.registrations)}</TableCell>
|
||||||
<TableCell className="text-right">{formatRand(e.revenue)}</TableCell>
|
<TableCell className="text-right">{formatRand(e.revenue)}</TableCell>
|
||||||
<TableCell className="text-right">{formatCount(e.ticketsSold)}</TableCell>
|
<TableCell className="text-right">{formatCount(e.ticketsSold)}</TableCell>
|
||||||
@@ -190,7 +190,7 @@ export default function SupervisorDashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6 min-w-0">
|
||||||
<div className="border rounded-xl p-4 bg-white shadow-sm">
|
<div className="border rounded-xl p-4 bg-white shadow-sm">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<h2 className="text-lg font-semibold">Scanner activity</h2>
|
<h2 className="text-lg font-semibold">Scanner activity</h2>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { useRouter, useSearchParams } from "next/navigation";
|
|||||||
import { apiFetch, fetchAllUsers, fetchAllPayments } from "@/lib/api";
|
import { apiFetch, fetchAllUsers, fetchAllPayments } from "@/lib/api";
|
||||||
import { useDismissingState } from "@/hooks/useDismissingState";
|
import { useDismissingState } from "@/hooks/useDismissingState";
|
||||||
import { scoreUser } from "@/lib/fuzzyMatch";
|
import { scoreUser } from "@/lib/fuzzyMatch";
|
||||||
import { Wallet } from "lucide-react";
|
import { Wallet, RotateCcw, HandHeart, ScanLine, Link2 } from "lucide-react";
|
||||||
|
|
||||||
// A donation is never mutated once created — assigning it to a registration creates a separate
|
// A donation is never mutated once created — assigning it to a registration creates a separate
|
||||||
// "leg" Payment row (isDonation:false, originalPaymentId -> the donation, amount > 0) instead.
|
// "leg" Payment row (isDonation:false, originalPaymentId -> the donation, amount > 0) instead.
|
||||||
@@ -566,24 +566,29 @@ function PaymentsContent() {
|
|||||||
{info && <div className="p-3 mb-3 border rounded bg-emerald-50 text-emerald-800 text-sm">{info}</div>}
|
{info && <div className="p-3 mb-3 border rounded bg-emerald-50 text-emerald-800 text-sm">{info}</div>}
|
||||||
|
|
||||||
<div className="mb-4 flex flex-wrap items-center gap-2">
|
<div className="mb-4 flex flex-wrap items-center gap-2">
|
||||||
<label className={`px-3 py-1.5 text-sm rounded border ${mode === 'payment' ? 'bg-brand-600 text-white border-brand-600' : 'bg-white text-gray-800 border-gray-200'} cursor-pointer`}>
|
<label className={`inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded border ${mode === 'payment' ? 'bg-brand-600 text-white border-brand-600' : 'bg-white text-gray-800 border-gray-200'} cursor-pointer`}>
|
||||||
<input type="radio" name="mode" value="payment" className="hidden" checked={mode==='payment'} onChange={() => setMode('payment')} />
|
<input type="radio" name="mode" value="payment" className="hidden" checked={mode==='payment'} onChange={() => setMode('payment')} />
|
||||||
|
<Wallet className="w-4 h-4" />
|
||||||
Payment
|
Payment
|
||||||
</label>
|
</label>
|
||||||
<label className={`px-3 py-1.5 text-sm rounded border ${mode === 'refund' ? 'bg-brand-600 text-white border-brand-600' : 'bg-white text-gray-800 border-gray-200'} cursor-pointer`}>
|
<label className={`inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded border ${mode === 'refund' ? 'bg-brand-600 text-white border-brand-600' : 'bg-white text-gray-800 border-gray-200'} cursor-pointer`}>
|
||||||
<input type="radio" name="mode" value="refund" className="hidden" checked={mode==='refund'} onChange={() => setMode('refund')} />
|
<input type="radio" name="mode" value="refund" className="hidden" checked={mode==='refund'} onChange={() => setMode('refund')} />
|
||||||
|
<RotateCcw className="w-4 h-4" />
|
||||||
Refund
|
Refund
|
||||||
</label>
|
</label>
|
||||||
<label className={`px-3 py-1.5 text-sm rounded border ${mode === 'donation' ? 'bg-brand-600 text-white border-brand-600' : 'bg-white text-gray-800 border-gray-200'} cursor-pointer`}>
|
<label className={`inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded border ${mode === 'donation' ? 'bg-brand-600 text-white border-brand-600' : 'bg-white text-gray-800 border-gray-200'} cursor-pointer`}>
|
||||||
<input type="radio" name="mode" value="donation" className="hidden" checked={mode==='donation'} onChange={() => setMode('donation')} />
|
<input type="radio" name="mode" value="donation" className="hidden" checked={mode==='donation'} onChange={() => setMode('donation')} />
|
||||||
|
<HandHeart className="w-4 h-4" />
|
||||||
Donations
|
Donations
|
||||||
</label>
|
</label>
|
||||||
<label className={`px-3 py-1.5 text-sm rounded border ${mode === 'reconcile' ? 'bg-brand-600 text-white border-brand-600' : 'bg-white text-gray-800 border-gray-200'} cursor-pointer`}>
|
<label className={`inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded border ${mode === 'reconcile' ? 'bg-brand-600 text-white border-brand-600' : 'bg-white text-gray-800 border-gray-200'} cursor-pointer`}>
|
||||||
<input type="radio" name="mode" value="reconcile" className="hidden" checked={mode==='reconcile'} onChange={() => setMode('reconcile')} />
|
<input type="radio" name="mode" value="reconcile" className="hidden" checked={mode==='reconcile'} onChange={() => setMode('reconcile')} />
|
||||||
|
<ScanLine className="w-4 h-4" />
|
||||||
Reconcile
|
Reconcile
|
||||||
</label>
|
</label>
|
||||||
<label className={`px-3 py-1.5 text-sm rounded border ${mode === 'link' ? 'bg-brand-600 text-white border-brand-600' : 'bg-white text-gray-800 border-gray-200'} cursor-pointer`}>
|
<label className={`inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded border ${mode === 'link' ? 'bg-brand-600 text-white border-brand-600' : 'bg-white text-gray-800 border-gray-200'} cursor-pointer`}>
|
||||||
<input type="radio" name="mode" value="link" className="hidden" checked={mode==='link'} onChange={() => setMode('link')} />
|
<input type="radio" name="mode" value="link" className="hidden" checked={mode==='link'} onChange={() => setMode('link')} />
|
||||||
|
<Link2 className="w-4 h-4" />
|
||||||
Payment Link
|
Payment Link
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,12 +5,19 @@ import { useAuth } from "@/hooks/useAuth";
|
|||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import { apiFetch, fetchAllUsers } from "@/lib/api";
|
import { apiFetch, fetchAllUsers } from "@/lib/api";
|
||||||
import { useDismissingState } from "@/hooks/useDismissingState";
|
import { useDismissingState } from "@/hooks/useDismissingState";
|
||||||
import { MessageCircle } from "lucide-react";
|
import { MessageCircle, Users, Zap, Send, Clock, type LucideIcon } from "lucide-react";
|
||||||
|
|
||||||
// Attendee with preference info
|
// Attendee with preference info
|
||||||
type Attendee = { id: string; name: string; phone: string; pref: string };
|
type Attendee = { id: string; name: string; phone: string; pref: string };
|
||||||
type UserEntry = { id: string; name: string; phone: string; pref: string };
|
type UserEntry = { id: string; name: string; phone: string; pref: string };
|
||||||
|
|
||||||
|
const WA_TAB_ICONS: Record<"attendees" | "automations" | "broadcasts" | "scheduled", LucideIcon> = {
|
||||||
|
attendees: Users,
|
||||||
|
automations: Zap,
|
||||||
|
broadcasts: Send,
|
||||||
|
scheduled: Clock,
|
||||||
|
};
|
||||||
|
|
||||||
function toLocalInputValue(d: Date) {
|
function toLocalInputValue(d: Date) {
|
||||||
const pad = (n: number) => String(n).padStart(2, "0");
|
const pad = (n: number) => String(n).padStart(2, "0");
|
||||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||||
@@ -612,12 +619,16 @@ function WhatsAppAttendeesPageInner() {
|
|||||||
|
|
||||||
{/* Tabs */}
|
{/* Tabs */}
|
||||||
<div className="mb-4 flex items-center gap-2 flex-wrap">
|
<div className="mb-4 flex items-center gap-2 flex-wrap">
|
||||||
{(["attendees", "automations", "broadcasts", "scheduled"] as const).map((t) => (
|
{(["attendees", "automations", "broadcasts", "scheduled"] as const).map((t) => {
|
||||||
<label key={t} className={`px-3 py-1.5 text-sm rounded border cursor-pointer ${tab === t ? "bg-green-600 text-white border-green-600" : "bg-white text-gray-800 border-gray-200"}`}>
|
const Icon = WA_TAB_ICONS[t];
|
||||||
|
return (
|
||||||
|
<label key={t} className={`inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded border cursor-pointer ${tab === t ? "bg-green-600 text-white border-green-600" : "bg-white text-gray-800 border-gray-200"}`}>
|
||||||
<input type="radio" name="waTab" value={t} className="hidden" checked={tab === t} onChange={() => setTab(t)} />
|
<input type="radio" name="waTab" value={t} className="hidden" checked={tab === t} onChange={() => setTab(t)} />
|
||||||
|
<Icon className="w-4 h-4" />
|
||||||
{t.charAt(0).toUpperCase() + t.slice(1)}
|
{t.charAt(0).toUpperCase() + t.slice(1)}
|
||||||
</label>
|
</label>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── Attendees Tab ── */}
|
{/* ── Attendees Tab ── */}
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ export const Navbar = () => {
|
|||||||
<button
|
<button
|
||||||
key={link.label}
|
key={link.label}
|
||||||
onClick={link.onClick}
|
onClick={link.onClick}
|
||||||
className="text-sm text-gray-700 hover:text-brand-600"
|
className="text-sm pb-0.5 border-b-2 border-transparent text-gray-700 hover:text-brand-600 transition-colors"
|
||||||
>
|
>
|
||||||
{link.label}
|
{link.label}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export function StatCard({
|
|||||||
<Icon className={"w-5 h-5 " + t.icon} />
|
<Icon className={"w-5 h-5 " + t.icon} />
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="text-xs text-gray-500 truncate">{label}</div>
|
<div className="text-xs text-gray-500 leading-snug">{label}</div>
|
||||||
<div className="text-xl font-semibold text-gray-900 truncate">{value}</div>
|
<div className="text-xl font-semibold text-gray-900 truncate">{value}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -67,5 +67,5 @@ export function StatCard({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function StatCardRow({ children }: { children: React.ReactNode }) {
|
export function StatCardRow({ children }: { children: React.ReactNode }) {
|
||||||
return <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4">{children}</div>;
|
return <div className="grid grid-cols-1 sm:grid-cols-3 lg:grid-cols-5 gap-4">{children}</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user