Merge branch 'fix/dashboard-mobile-styling' into main
This commit is contained in:
@@ -6,7 +6,7 @@ import { useAuth } from "@/hooks/useAuth";
|
||||
import { apiFetch } from "@/lib/api";
|
||||
import { useDismissingState } from "@/hooks/useDismissingState";
|
||||
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 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>}
|
||||
|
||||
<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={"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={"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 === "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={"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={"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>
|
||||
|
||||
{loading && <div className="text-sm text-gray-400">Loading…</div>}
|
||||
|
||||
@@ -102,7 +102,7 @@ export default function AdminDashboardPage() {
|
||||
: 0;
|
||||
|
||||
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">
|
||||
<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>
|
||||
@@ -132,7 +132,7 @@ export default function AdminDashboardPage() {
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<h2 className="text-lg font-semibold mb-3">Revenue trend — past month</h2>
|
||||
{overview && overview.trend.length > 0 ? (
|
||||
@@ -157,7 +157,7 @@ export default function AdminDashboardPage() {
|
||||
<TableBody>
|
||||
{overview.topEvents.map(e => (
|
||||
<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">{formatRand(e.revenue)}</TableCell>
|
||||
<TableCell className="text-right">{formatCount(e.ticketsSold)}</TableCell>
|
||||
@@ -171,7 +171,7 @@ export default function AdminDashboardPage() {
|
||||
</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="flex items-center justify-between mb-3">
|
||||
<h2 className="text-lg font-semibold">Payments overview</h2>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useRouter } from "next/navigation";
|
||||
import { apiFetch } from "@/lib/api";
|
||||
import { scoreUser } from "@/lib/fuzzyMatch";
|
||||
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";
|
||||
|
||||
@@ -18,6 +18,14 @@ const MODE_LABELS: Record<Mode, string> = {
|
||||
refund: "REFUND",
|
||||
};
|
||||
|
||||
const MODE_ICONS: Record<Mode, LucideIcon> = {
|
||||
registration: UserPlus,
|
||||
payment: CreditCard,
|
||||
checkin: CheckSquare,
|
||||
tickets: Ticket,
|
||||
refund: RotateCcw,
|
||||
};
|
||||
|
||||
// ─── Fuzzy search helpers ─────────────────────────────────────────────────────
|
||||
|
||||
function fuzzyFilterRegs(allRegs: any[], search: string): any[] {
|
||||
@@ -390,11 +398,13 @@ export default function AtTheDoorPage() {
|
||||
|
||||
{/* Mode Buttons */}
|
||||
<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
|
||||
key={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
|
||||
? m === "refund"
|
||||
? "bg-red-600 text-white border-red-600"
|
||||
@@ -402,9 +412,11 @@ export default function AtTheDoorPage() {
|
||||
: "bg-white hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
<Icon className="w-4 h-4" />
|
||||
{MODE_LABELS[m]}
|
||||
</button>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* ✅ Panels */}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useAuth } from "@/hooks/useAuth";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { apiFetch, fetchAllUsers } from "@/lib/api";
|
||||
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 };
|
||||
|
||||
@@ -503,20 +503,24 @@ function EmailAttendeesPageInner() {
|
||||
|
||||
{/* Tabs like on Payments page */}
|
||||
<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')} />
|
||||
<Users className="w-4 h-4" />
|
||||
Attendees
|
||||
</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')} />
|
||||
<Zap className="w-4 h-4" />
|
||||
Automations
|
||||
</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')} />
|
||||
<Mail className="w-4 h-4" />
|
||||
Broadcasts
|
||||
</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')} />
|
||||
<Clock className="w-4 h-4" />
|
||||
Scheduled
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useRouter } from "next/navigation";
|
||||
import { apiFetch, fetchAllUsers } from "@/lib/api";
|
||||
import { scoreUser } from "@/lib/fuzzyMatch";
|
||||
import { useDismissingState } from "@/hooks/useDismissingState";
|
||||
import { UserPlus } from "lucide-react";
|
||||
import { UserPlus, CreditCard } from "lucide-react";
|
||||
|
||||
// ─── Pricing helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
@@ -355,12 +355,14 @@ export default function ManualRegistrationPage() {
|
||||
)}
|
||||
|
||||
<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')} />
|
||||
<UserPlus className="w-4 h-4" />
|
||||
Register
|
||||
</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')} />
|
||||
<CreditCard className="w-4 h-4" />
|
||||
Record Payment
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -101,7 +101,7 @@ export default function SupervisorDashboardPage() {
|
||||
}, 15000, !!token);
|
||||
|
||||
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">
|
||||
<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>
|
||||
@@ -131,7 +131,7 @@ export default function SupervisorDashboardPage() {
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<h2 className="text-lg font-semibold mb-3">Revenue trend — past month</h2>
|
||||
{overview && overview.trend.length > 0 ? (
|
||||
@@ -156,7 +156,7 @@ export default function SupervisorDashboardPage() {
|
||||
<TableBody>
|
||||
{overview.topEvents.map(e => (
|
||||
<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">{formatRand(e.revenue)}</TableCell>
|
||||
<TableCell className="text-right">{formatCount(e.ticketsSold)}</TableCell>
|
||||
@@ -190,7 +190,7 @@ export default function SupervisorDashboardPage() {
|
||||
</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="flex items-center justify-between mb-3">
|
||||
<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 { useDismissingState } from "@/hooks/useDismissingState";
|
||||
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
|
||||
// "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>}
|
||||
|
||||
<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')} />
|
||||
<Wallet className="w-4 h-4" />
|
||||
Payment
|
||||
</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')} />
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
Refund
|
||||
</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')} />
|
||||
<HandHeart className="w-4 h-4" />
|
||||
Donations
|
||||
</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')} />
|
||||
<ScanLine className="w-4 h-4" />
|
||||
Reconcile
|
||||
</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')} />
|
||||
<Link2 className="w-4 h-4" />
|
||||
Payment Link
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -5,12 +5,19 @@ import { useAuth } from "@/hooks/useAuth";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { apiFetch, fetchAllUsers } from "@/lib/api";
|
||||
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
|
||||
type Attendee = { 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) {
|
||||
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())}`;
|
||||
@@ -612,12 +619,16 @@ function WhatsAppAttendeesPageInner() {
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="mb-4 flex items-center gap-2 flex-wrap">
|
||||
{(["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"}`}>
|
||||
{(["attendees", "automations", "broadcasts", "scheduled"] as const).map((t) => {
|
||||
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)} />
|
||||
<Icon className="w-4 h-4" />
|
||||
{t.charAt(0).toUpperCase() + t.slice(1)}
|
||||
</label>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* ── Attendees Tab ── */}
|
||||
|
||||
@@ -108,7 +108,7 @@ export const Navbar = () => {
|
||||
<button
|
||||
key={link.label}
|
||||
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}
|
||||
</button>
|
||||
|
||||
@@ -39,7 +39,7 @@ export function StatCard({
|
||||
<Icon className={"w-5 h-5 " + t.icon} />
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
@@ -67,5 +67,5 @@ export function StatCard({
|
||||
}
|
||||
|
||||
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