Compare commits

...
Author SHA1 Message Date
joshua 8f69e58aa2 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
2026-08-07 00:41:52 +02:00
joshuaandClaude Sonnet 5 1b2f2a7c79 Stop tracking banner.json runtime data store
Same reasoning as scheduled-emails.json: it's mutated by the running
app (site banner content), not source. readBanner() already falls
back to sane defaults when the file is missing, so untracking it is
safe for fresh checkouts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-07 00:04:45 +02:00
joshuaandClaude Sonnet 5 7906248c67 Stop tracking scheduled-emails.json runtime data store
It's mutated by the running app (scheduled job queue), not source -
committing it churns the repo with live recipient/message content.
Kept on disk, just untracked; a fresh checkout will get an empty
store created on demand by ensureStore().

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-07 00:03:04 +02:00
13 changed files with 91 additions and 62 deletions
+5 -1
View File
@@ -33,4 +33,8 @@ Thumbs.db
# misc scratch / generated files # misc scratch / generated files
temp/ temp/
backend/public/uploads/ backend/public/uploads/
# runtime data stores (mutated by the running app, not source)
backend/data/scheduled-emails.json
backend/data/banner.json
-6
View File
@@ -1,6 +0,0 @@
{
"message": "",
"type": "info",
"liveFrom": null,
"liveTill": null
}
-3
View File
@@ -1,3 +0,0 @@
{
"jobs": []
}
@@ -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>}
+4 -4
View File
@@ -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&apos;s what&apos;s happening with your events today.</p> <p className="text-sm text-gray-500 mt-1">Here&apos;s what&apos;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,21 +398,25 @@ 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 => {
<button const Icon = MODE_ICONS[m];
key={m} return (
onClick={() => setMode(m)} <button
className={`px-4 py-2 rounded text-sm font-medium border ${ key={m}
mode === m onClick={() => setMode(m)}
? m === "refund" className={`inline-flex items-center gap-1.5 px-4 py-2 rounded text-sm font-medium border ${
? "bg-red-600 text-white border-red-600" mode === m
: "bg-brand-600 text-white border-brand-600" ? m === "refund"
: "bg-white hover:bg-gray-50" ? "bg-red-600 text-white border-red-600"
}`} : "bg-brand-600 text-white border-brand-600"
> : "bg-white hover:bg-gray-50"
{MODE_LABELS[m]} }`}
</button> >
))} <Icon className="w-4 h-4" />
{MODE_LABELS[m]}
</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&apos;s what&apos;s happening with your events today.</p> <p className="text-sm text-gray-500 mt-1">Here&apos;s what&apos;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];
<input type="radio" name="waTab" value={t} className="hidden" checked={tab === t} onChange={() => setTab(t)} /> return (
{t.charAt(0).toUpperCase() + t.slice(1)} <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"}`}>
</label> <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)}
</label>
);
})}
</div> </div>
{/* ── Attendees Tab ── */} {/* ── Attendees Tab ── */}
+1 -1
View File
@@ -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>
+2 -2
View File
@@ -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>;
} }