Full site redesign, help system, and dashboard stats fixes
Multi-phase visual facelift (design tokens, dashboards, sidebar/navbar shell, per-page help guides, and a layout/content pass across every remaining page) plus backend fixes to the dashboard KPI stats: - Admin/Supervisor dashboard KPIs (revenue, donations, registrations, tickets sold) now use a rolling trailing-month window (today back one calendar month, e.g. 9 May - 8 June if today is 8 June) instead of calendar month-to-date, which under-counted for most of the month. The comparison window shifts the same way, so like is still compared with like. - Reports deep-links from those stat tiles now match the same window (range=trailing_month, replacing range=this_month). - Design tokens (brand-* Tailwind scale + shadcn CSS variables), a site-wide contextual help button, fixed dashboard sidebar/navbar, Admin/Supervisor/Staff/User dashboard rebuilds backed by a new GET /api/stats/overview endpoint, a dedicated Contact page, Site Settings restyle with WhatsApp config folded in, and an Account activity feed backed by a new SecurityEvent model. - Every remaining page (home, events, registration flow, auth, legal, payment results, and every Admin/Supervisor/Staff/User tool page) restyled onto the same design tokens, several with real layout upgrades (home hero, events list/detail, donate page, auth pages). - 20+ new dedicated help guides so the whole site has page-specific help content instead of falling back to a generic guide. - Assorted fixes surfaced along the way: donation-leg double-counting in payment stats, donations not counting toward revenue, refund netting in per-method report breakdowns, and donation over-allocation after a refund. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -631,7 +631,7 @@ export default function SelfServicePage() {
|
||||
type={showChangePassword ? "text" : "password"}
|
||||
value={changePassword}
|
||||
onChange={(e) => setChangePassword(e.target.value)}
|
||||
className="w-full border border-gray-300 rounded-lg px-4 py-3 pr-12 text-base focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
className="w-full border border-gray-300 rounded-lg px-4 py-3 pr-12 text-base focus:outline-none focus:ring-2 focus:ring-brand-500"
|
||||
placeholder="Your password"
|
||||
autoFocus
|
||||
required
|
||||
@@ -651,7 +651,7 @@ export default function SelfServicePage() {
|
||||
<button
|
||||
type="submit"
|
||||
disabled={changeLoading}
|
||||
className="flex-1 bg-blue-600 text-white rounded-lg py-3 text-base font-semibold hover:bg-blue-700 disabled:opacity-60 transition"
|
||||
className="flex-1 bg-brand-600 text-white rounded-lg py-3 text-base font-semibold hover:bg-brand-700 disabled:opacity-60 transition"
|
||||
>
|
||||
{changeLoading ? "Verifying…" : "Confirm"}
|
||||
</button>
|
||||
@@ -678,7 +678,7 @@ export default function SelfServicePage() {
|
||||
type="email"
|
||||
value={setupEmail}
|
||||
onChange={(e) => setSetupEmail(e.target.value)}
|
||||
className="w-full border border-gray-300 rounded-xl px-4 py-3 text-base focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
className="w-full border border-gray-300 rounded-xl px-4 py-3 text-base focus:outline-none focus:ring-2 focus:ring-brand-500"
|
||||
placeholder="supervisor@example.com"
|
||||
required
|
||||
autoComplete="email"
|
||||
@@ -691,7 +691,7 @@ export default function SelfServicePage() {
|
||||
type={showSetupPassword ? "text" : "password"}
|
||||
value={setupPassword}
|
||||
onChange={(e) => setSetupPassword(e.target.value)}
|
||||
className="w-full border border-gray-300 rounded-xl px-4 py-3 pr-12 text-base focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
className="w-full border border-gray-300 rounded-xl px-4 py-3 pr-12 text-base focus:outline-none focus:ring-2 focus:ring-brand-500"
|
||||
placeholder="••••••••"
|
||||
required
|
||||
autoComplete="current-password"
|
||||
@@ -703,7 +703,7 @@ export default function SelfServicePage() {
|
||||
<button
|
||||
type="submit"
|
||||
disabled={setupLoading}
|
||||
className="w-full bg-blue-600 text-white rounded-xl py-3.5 text-lg font-semibold hover:bg-blue-700 disabled:opacity-60 transition mt-2"
|
||||
className="w-full bg-brand-600 text-white rounded-xl py-3.5 text-lg font-semibold hover:bg-brand-700 disabled:opacity-60 transition mt-2"
|
||||
>
|
||||
{setupLoading ? "Signing in…" : "Sign In"}
|
||||
</button>
|
||||
@@ -723,7 +723,7 @@ export default function SelfServicePage() {
|
||||
<select
|
||||
value={selectedEventId}
|
||||
onChange={(e) => setSelectedEventId(e.target.value)}
|
||||
className="w-full border border-gray-300 rounded-xl px-4 py-3 text-base focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
className="w-full border border-gray-300 rounded-xl px-4 py-3 text-base focus:outline-none focus:ring-2 focus:ring-brand-500"
|
||||
>
|
||||
<option value="">— Select an event —</option>
|
||||
{events.map((ev) => (
|
||||
@@ -735,9 +735,9 @@ export default function SelfServicePage() {
|
||||
</div>
|
||||
)}
|
||||
{selectedEvent && (
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-xl px-4 py-3 text-sm text-blue-800">
|
||||
<div className="bg-brand-50 border border-brand-200 rounded-xl px-4 py-3 text-sm text-brand-800">
|
||||
<p className="font-semibold">{selectedEvent.title}</p>
|
||||
<p className="text-blue-600">{fmtDate(selectedEvent.startDate)} — {fmtDate(selectedEvent.endDate)}</p>
|
||||
<p className="text-brand-600">{fmtDate(selectedEvent.startDate)} — {fmtDate(selectedEvent.endDate)}</p>
|
||||
{selectedEvent.form?.isRequired && (
|
||||
<p className="text-orange-600 mt-1 text-xs font-medium">This event requires an attendee form to be completed at registration.</p>
|
||||
)}
|
||||
@@ -746,7 +746,7 @@ export default function SelfServicePage() {
|
||||
<button
|
||||
onClick={handleStartKiosk}
|
||||
disabled={!selectedEventId}
|
||||
className="w-full bg-blue-600 text-white rounded-xl py-3.5 text-lg font-semibold hover:bg-blue-700 disabled:opacity-50 transition"
|
||||
className="w-full bg-brand-600 text-white rounded-xl py-3.5 text-lg font-semibold hover:bg-brand-700 disabled:opacity-50 transition"
|
||||
>
|
||||
Start Kiosk
|
||||
</button>
|
||||
@@ -784,7 +784,7 @@ export default function SelfServicePage() {
|
||||
else if (!v.trim() && visitorPhone.trim()) setNotificationPref("whatsapp");
|
||||
}}
|
||||
onBlur={handleEmailBlur}
|
||||
className="w-full border border-gray-300 rounded-xl px-4 py-3.5 text-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
className="w-full border border-gray-300 rounded-xl px-4 py-3.5 text-lg focus:outline-none focus:ring-2 focus:ring-brand-500"
|
||||
placeholder="john@example.com"
|
||||
/>
|
||||
</div>
|
||||
@@ -804,7 +804,7 @@ export default function SelfServicePage() {
|
||||
else if (!v.trim() && visitorEmail.trim()) setNotificationPref("email");
|
||||
}}
|
||||
onBlur={handlePhoneBlur}
|
||||
className="w-full border border-gray-300 rounded-xl px-4 py-3.5 text-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
className="w-full border border-gray-300 rounded-xl px-4 py-3.5 text-lg focus:outline-none focus:ring-2 focus:ring-brand-500"
|
||||
placeholder="+27 82 000 0000"
|
||||
/>
|
||||
</div>
|
||||
@@ -831,7 +831,7 @@ export default function SelfServicePage() {
|
||||
type="text"
|
||||
value={visitorName}
|
||||
onChange={(e) => setVisitorName(e.target.value)}
|
||||
className="w-full border border-gray-300 rounded-xl px-4 py-3.5 text-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
className="w-full border border-gray-300 rounded-xl px-4 py-3.5 text-lg focus:outline-none focus:ring-2 focus:ring-brand-500"
|
||||
placeholder="John Smith"
|
||||
required
|
||||
/>
|
||||
@@ -850,7 +850,7 @@ export default function SelfServicePage() {
|
||||
className={`flex-1 py-3 transition-colors ${
|
||||
notificationPref === p
|
||||
? p === "whatsapp" ? "bg-green-600 text-white"
|
||||
: p === "both" ? "bg-indigo-600 text-white"
|
||||
: p === "both" ? "bg-brand-600 text-white"
|
||||
: "bg-blue-600 text-white"
|
||||
: "bg-white text-gray-600 hover:bg-gray-50"
|
||||
}`}
|
||||
@@ -901,7 +901,7 @@ export default function SelfServicePage() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setQuantities((q) => ({ ...q, [key]: (q[key] || 0) + 1 }))}
|
||||
className="w-10 h-10 rounded-full border-2 border-blue-500 text-xl font-bold text-blue-600 hover:bg-blue-50 flex items-center justify-center transition"
|
||||
className="w-10 h-10 rounded-full border-2 border-brand-500 text-xl font-bold text-brand-600 hover:bg-brand-50 flex items-center justify-center transition"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
@@ -932,7 +932,7 @@ export default function SelfServicePage() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setQuantities((q) => ({ ...q, [opt.id]: (q[opt.id] || 0) + 1 }))}
|
||||
className="w-10 h-10 rounded-full border-2 border-blue-500 text-xl font-bold text-blue-600 hover:bg-blue-50 flex items-center justify-center transition"
|
||||
className="w-10 h-10 rounded-full border-2 border-brand-500 text-xl font-bold text-brand-600 hover:bg-brand-50 flex items-center justify-center transition"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
@@ -959,7 +959,7 @@ export default function SelfServicePage() {
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<div
|
||||
onClick={() => setCreateAccount((v) => !v)}
|
||||
className={`relative w-12 h-6 rounded-full transition-colors ${createAccount ? "bg-blue-600" : "bg-gray-300"}`}
|
||||
className={`relative w-12 h-6 rounded-full transition-colors ${createAccount ? "bg-brand-600" : "bg-gray-300"}`}
|
||||
>
|
||||
<span className={`absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full shadow transition-transform ${createAccount ? "translate-x-6" : ""}`} />
|
||||
</div>
|
||||
@@ -976,7 +976,7 @@ export default function SelfServicePage() {
|
||||
type={showVisitorPassword ? "text" : "password"}
|
||||
value={visitorPassword}
|
||||
onChange={(e) => setVisitorPassword(e.target.value)}
|
||||
className="w-full border border-gray-300 rounded-xl px-4 py-3 pr-12 text-base focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
className="w-full border border-gray-300 rounded-xl px-4 py-3 pr-12 text-base focus:outline-none focus:ring-2 focus:ring-brand-500"
|
||||
placeholder="Min. 6 characters"
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
@@ -996,7 +996,7 @@ export default function SelfServicePage() {
|
||||
<button
|
||||
type="submit"
|
||||
disabled={formLoading || accountConflict}
|
||||
className="w-full bg-blue-600 text-white rounded-xl py-4 text-xl font-bold hover:bg-blue-700 disabled:opacity-60 transition"
|
||||
className="w-full bg-brand-600 text-white rounded-xl py-4 text-xl font-bold hover:bg-brand-700 disabled:opacity-60 transition"
|
||||
>
|
||||
{formLoading ? "Registering…" : eventHasRequiredForm && mainTicketCount > 0 ? "Next — Fill in Form" : "Register"}
|
||||
</button>
|
||||
@@ -1033,7 +1033,7 @@ export default function SelfServicePage() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setShowUpdateConfirm(false); handleRegister(); }}
|
||||
className="flex-1 bg-blue-600 text-white rounded-lg py-3 text-base font-semibold hover:bg-blue-700 transition"
|
||||
className="flex-1 bg-brand-600 text-white rounded-lg py-3 text-base font-semibold hover:bg-brand-700 transition"
|
||||
>
|
||||
Confirm & Update
|
||||
</button>
|
||||
@@ -1051,7 +1051,7 @@ export default function SelfServicePage() {
|
||||
<h2 className="text-2xl font-bold text-gray-900 mt-1">Attendee Information</h2>
|
||||
{mainTicketCount > 1 && (
|
||||
<div className="mt-3">
|
||||
<p className="text-base font-semibold text-blue-700">
|
||||
<p className="text-base font-semibold text-brand-700">
|
||||
Attendee {currentTicketIndex + 1} of {mainTicketCount}
|
||||
</p>
|
||||
<div className="flex gap-1.5 justify-center mt-2">
|
||||
@@ -1062,7 +1062,7 @@ export default function SelfServicePage() {
|
||||
i < currentTicketIndex
|
||||
? "w-6 bg-green-500"
|
||||
: i === currentTicketIndex
|
||||
? "w-6 bg-blue-600"
|
||||
? "w-6 bg-brand-600"
|
||||
: "w-6 bg-gray-200"
|
||||
}`}
|
||||
/>
|
||||
@@ -1096,7 +1096,7 @@ export default function SelfServicePage() {
|
||||
checked={currentTicketAnswers[field.id] === opt}
|
||||
onChange={() => setCurrentTicketAnswers(a => ({ ...a, [field.id]: opt }))}
|
||||
required={field.isRequired && !currentTicketAnswers[field.id]}
|
||||
className="accent-blue-600"
|
||||
className="accent-brand-600"
|
||||
/>
|
||||
<span className="text-base">{opt}</span>
|
||||
</label>
|
||||
@@ -1108,7 +1108,7 @@ export default function SelfServicePage() {
|
||||
value={currentTicketAnswers[field.id] || ""}
|
||||
onChange={e => setCurrentTicketAnswers(a => ({ ...a, [field.id]: e.target.value }))}
|
||||
required={field.isRequired}
|
||||
className="w-full border border-gray-300 rounded-xl px-4 py-3 text-base focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
className="w-full border border-gray-300 rounded-xl px-4 py-3 text-base focus:outline-none focus:ring-2 focus:ring-brand-500"
|
||||
/>
|
||||
) : field.type === "date" ? (
|
||||
<input
|
||||
@@ -1116,7 +1116,7 @@ export default function SelfServicePage() {
|
||||
value={currentTicketAnswers[field.id] || ""}
|
||||
onChange={e => setCurrentTicketAnswers(a => ({ ...a, [field.id]: e.target.value }))}
|
||||
required={field.isRequired}
|
||||
className="w-full border border-gray-300 rounded-xl px-4 py-3 text-base focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
className="w-full border border-gray-300 rounded-xl px-4 py-3 text-base focus:outline-none focus:ring-2 focus:ring-brand-500"
|
||||
/>
|
||||
) : field.type === "paragraph" ? (
|
||||
<textarea
|
||||
@@ -1124,7 +1124,7 @@ export default function SelfServicePage() {
|
||||
onChange={e => setCurrentTicketAnswers(a => ({ ...a, [field.id]: e.target.value }))}
|
||||
required={field.isRequired}
|
||||
rows={3}
|
||||
className="w-full border border-gray-300 rounded-xl px-4 py-3 text-base focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none"
|
||||
className="w-full border border-gray-300 rounded-xl px-4 py-3 text-base focus:outline-none focus:ring-2 focus:ring-brand-500 resize-none"
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
@@ -1132,7 +1132,7 @@ export default function SelfServicePage() {
|
||||
value={currentTicketAnswers[field.id] || ""}
|
||||
onChange={e => setCurrentTicketAnswers(a => ({ ...a, [field.id]: e.target.value }))}
|
||||
required={field.isRequired}
|
||||
className="w-full border border-gray-300 rounded-xl px-4 py-3 text-base focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
className="w-full border border-gray-300 rounded-xl px-4 py-3 text-base focus:outline-none focus:ring-2 focus:ring-brand-500"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -1147,7 +1147,7 @@ export default function SelfServicePage() {
|
||||
<button
|
||||
type="submit"
|
||||
disabled={formSubmitLoading}
|
||||
className="w-full bg-blue-600 text-white rounded-xl py-4 text-xl font-bold hover:bg-blue-700 disabled:opacity-60 transition"
|
||||
className="w-full bg-brand-600 text-white rounded-xl py-4 text-xl font-bold hover:bg-brand-700 disabled:opacity-60 transition"
|
||||
>
|
||||
{formSubmitLoading
|
||||
? "Submitting…"
|
||||
@@ -1175,7 +1175,7 @@ export default function SelfServicePage() {
|
||||
<p className="text-lg text-gray-600 mb-3">Welcome, <span className="font-semibold">{confirmationName}</span>!</p>
|
||||
)}
|
||||
{selectedEvent && (
|
||||
<p className="text-blue-700 font-medium mb-4">{selectedEvent.title}</p>
|
||||
<p className="text-brand-700 font-medium mb-4">{selectedEvent.title}</p>
|
||||
)}
|
||||
|
||||
{registrationIsFree ? (
|
||||
@@ -1189,7 +1189,7 @@ export default function SelfServicePage() {
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-xl px-4 py-3 text-blue-800 text-sm mb-5 text-left">
|
||||
<div className="bg-brand-50 border border-brand-200 rounded-xl px-4 py-3 text-brand-800 text-sm mb-5 text-left">
|
||||
<p className="font-semibold mb-1">
|
||||
{notificationPref === "whatsapp"
|
||||
? "Payment instructions have been sent to your WhatsApp."
|
||||
@@ -1197,12 +1197,12 @@ export default function SelfServicePage() {
|
||||
? "Payment instructions have been sent via email and WhatsApp."
|
||||
: "Payment instructions have been emailed to you."}
|
||||
</p>
|
||||
<p className="text-blue-700">Payment can also be made at the door — cash and card accepted.</p>
|
||||
<p className="text-brand-700">Payment can also be made at the door — cash and card accepted.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-6">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full border-4 border-blue-200 bg-blue-50 text-2xl font-bold text-blue-700">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full border-4 border-brand-200 bg-brand-50 text-2xl font-bold text-brand-700">
|
||||
{countdown}
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 mt-2">Returning to registration in {countdown}s</p>
|
||||
@@ -1210,7 +1210,7 @@ export default function SelfServicePage() {
|
||||
|
||||
<button
|
||||
onClick={resetAndGoToForm}
|
||||
className="w-full bg-blue-600 text-white rounded-xl py-4 text-xl font-bold hover:bg-blue-700 transition"
|
||||
className="w-full bg-brand-600 text-white rounded-xl py-4 text-xl font-bold hover:bg-brand-700 transition"
|
||||
>
|
||||
OK — Next Person
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user