From 1e9e7f7c4660b2afa827880246d0a678bf68ce31 Mon Sep 17 00:00:00 2001 From: Mahad Ahmed Date: Thu, 12 Feb 2026 01:35:43 -0500 Subject: [PATCH 1/3] Refactor StatsService to calculate revenue based on ticket prices and update API error handling --- src/backend/src/stats/stats.service.ts | 9 +++--- src/frontend/web-admin/app/_lib/api.ts | 35 ++++++++++++++-------- src/frontend/web-admin/app/events/page.tsx | 10 +++++-- src/frontend/web-admin/app/page.tsx | 3 ++ src/frontend/web-admin/package-lock.json | 11 ------- 5 files changed, 39 insertions(+), 29 deletions(-) diff --git a/src/backend/src/stats/stats.service.ts b/src/backend/src/stats/stats.service.ts index e79ae59..18e21eb 100644 --- a/src/backend/src/stats/stats.service.ts +++ b/src/backend/src/stats/stats.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { count, sum, sql, gte, inArray } from 'drizzle-orm'; +import { count, sum, sql, gte, inArray, eq } from 'drizzle-orm'; import { DatabaseService } from '../database/database.service'; import { users, events, tickets, payments } from '../db/schema'; @@ -36,12 +36,13 @@ export class StatsService { db.select({ value: count() }).from(events).where(gte(events.date, today)), db.select({ value: count() }).from(tickets), db.select({ value: sum(events.capacity) }).from(events), + // Calculate revenue from tickets × event price (covers tickets without payment records) db .select({ - value: sql`COALESCE(SUM(${payments.amountPaid} - COALESCE(${payments.refundedAmount}, 0)), 0)::int`, + value: sql`COALESCE(SUM(${events.price}), 0)::int`, }) - .from(payments) - .where(inArray(payments.status, ['succeeded', 'partially_refunded'])), + .from(tickets) + .innerJoin(events, eq(tickets.eventId, events.id)), ]); const userCount = Number(userCountResult[0]?.value ?? 0); diff --git a/src/frontend/web-admin/app/_lib/api.ts b/src/frontend/web-admin/app/_lib/api.ts index 67c29a6..8b9415c 100644 --- a/src/frontend/web-admin/app/_lib/api.ts +++ b/src/frontend/web-admin/app/_lib/api.ts @@ -1,22 +1,33 @@ const getBaseUrl = (): string => { - if (typeof window !== "undefined") { - const url = process.env.NEXT_PUBLIC_API_URL; - if (url && typeof url === "string" && url.trim()) return url.replace(/\/$/, ""); + const url = process.env.NEXT_PUBLIC_API_URL; + if (url && typeof url === "string" && url.trim()) { + return url.replace(/\/$/, ""); } - return process.env.NEXT_PUBLIC_API_URL?.replace(/\/$/, "") ?? "http://localhost:3000"; + return "http://localhost:3000"; }; async function apiFetch(path: string, options?: RequestInit): Promise { const base = getBaseUrl(); - const res = await fetch(`${base}${path}`, { - headers: { "Content-Type": "application/json", ...options?.headers }, - ...options, - }); - if (!res.ok) { - const text = await res.text(); - throw new Error(text || `API error: ${res.status} ${res.statusText}`); + const url = `${base}${path}`; + + try { + const res = await fetch(url, { + headers: { "Content-Type": "application/json", ...options?.headers }, + ...options, + }); + + if (!res.ok) { + const text = await res.text(); + throw new Error(text || `API error: ${res.status} ${res.statusText}`); + } + + return res.json(); + } catch (error) { + if (error instanceof TypeError && error.message.includes("fetch")) { + throw new Error(`Failed to connect to backend at ${url}. Is the backend running?`); + } + throw error; } - return res.json(); } // ---------- Dashboard stats ---------- diff --git a/src/frontend/web-admin/app/events/page.tsx b/src/frontend/web-admin/app/events/page.tsx index 56bdba8..e723a27 100644 --- a/src/frontend/web-admin/app/events/page.tsx +++ b/src/frontend/web-admin/app/events/page.tsx @@ -26,8 +26,14 @@ export default function EventsListPage() { useEffect(() => { getEvents() - .then(setEvents) - .catch((e) => setError(e instanceof Error ? e.message : "Failed to load")) + .then((events) => { + console.log("Events loaded:", events.length); + setEvents(events); + }) + .catch((e) => { + console.error("Events load error:", e); + setError(e instanceof Error ? e.message : "Failed to load"); + }) .finally(() => setLoading(false)); }, []); diff --git a/src/frontend/web-admin/app/page.tsx b/src/frontend/web-admin/app/page.tsx index eb982ad..0c26a3c 100644 --- a/src/frontend/web-admin/app/page.tsx +++ b/src/frontend/web-admin/app/page.tsx @@ -38,13 +38,16 @@ export default function DashboardHome() { async function load() { try { setError(null); + console.log("Loading dashboard data..."); const [statsRes, eventsRes] = await Promise.all([ getDashboardStats(), getEvents(), ]); + console.log("Dashboard data loaded:", { stats: statsRes, eventsCount: eventsRes.length }); setStats(statsRes); setEvents(eventsRes); } catch (e) { + console.error("Dashboard load error:", e); setError(e instanceof Error ? e.message : "Failed to load dashboard"); } finally { setLoading(false); diff --git a/src/frontend/web-admin/package-lock.json b/src/frontend/web-admin/package-lock.json index 35152cb..cbc2cd4 100644 --- a/src/frontend/web-admin/package-lock.json +++ b/src/frontend/web-admin/package-lock.json @@ -955,7 +955,6 @@ "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -1016,7 +1015,6 @@ "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/types": "8.54.0", @@ -1503,7 +1501,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2492,7 +2489,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -2666,7 +2662,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -3852,7 +3847,6 @@ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", - "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -4554,7 +4548,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -4756,7 +4749,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -4769,7 +4761,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -5581,7 +5572,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -5738,7 +5728,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" From c8aeccb6e48cd4a965dfc16c70dee8375df875b0 Mon Sep 17 00:00:00 2001 From: Mahad Ahmed Date: Thu, 12 Feb 2026 02:34:17 -0500 Subject: [PATCH 2/3] Add user management features and event creation functionality --- src/frontend/web-admin/app/_lib/api.ts | 53 ++++ .../app/components/DashboardShell.tsx | 4 +- .../web-admin/app/events/create/page.tsx | 193 ++++++++++++++ .../web-admin/app/users/[id]/page.tsx | 244 ++++++++++++++++++ src/frontend/web-admin/app/users/page.tsx | 116 +++++++++ 5 files changed, 608 insertions(+), 2 deletions(-) create mode 100644 src/frontend/web-admin/app/events/create/page.tsx create mode 100644 src/frontend/web-admin/app/users/[id]/page.tsx create mode 100644 src/frontend/web-admin/app/users/page.tsx diff --git a/src/frontend/web-admin/app/_lib/api.ts b/src/frontend/web-admin/app/_lib/api.ts index 8b9415c..bab083d 100644 --- a/src/frontend/web-admin/app/_lib/api.ts +++ b/src/frontend/web-admin/app/_lib/api.ts @@ -66,6 +66,16 @@ export function getUser(id: number): Promise { return apiFetch(`/users/${id}`); } +export function updateUser( + id: number, + data: Partial, +): Promise { + return apiFetch(`/users/${id}`, { + method: "PUT", + body: JSON.stringify(data), + }); +} + // ---------- Events ---------- export interface EventItem { id: number; @@ -91,3 +101,46 @@ export interface EventItem { export function getEvents(): Promise { return apiFetch("/events"); } + +export interface CreateEventPayload { + name: string; + description?: string; + date: string; + location?: string; + capacity: number; + imageUrl?: string; + price: number; + stripePriceId?: string; + requiresTableSignup?: boolean; + requiresBusSignup?: boolean; + tableCount?: number; + seatsPerTable?: number; + busCount?: number; + busCapacity?: number; +} + +export function createEvent(data: CreateEventPayload): Promise { + return apiFetch("/events", { + method: "POST", + body: JSON.stringify(data), + }); +} + +// ---------- Tickets ---------- +export interface Ticket { + ticketId: number; + eventId: number; + checkedIn: boolean; + busSeat: string | null; + tableSeat: string | null; + createdAt: string; + eventName: string; + eventDate: string; + eventLocation: string | null; + eventPrice: number; + eventImageUrl: string | null; +} + +export function getUserTickets(userId: number): Promise { + return apiFetch(`/events/user/${userId}/tickets`); +} diff --git a/src/frontend/web-admin/app/components/DashboardShell.tsx b/src/frontend/web-admin/app/components/DashboardShell.tsx index f3fd5e2..4eaed5d 100644 --- a/src/frontend/web-admin/app/components/DashboardShell.tsx +++ b/src/frontend/web-admin/app/components/DashboardShell.tsx @@ -6,7 +6,7 @@ import { usePathname } from "next/navigation"; const navItems = [ { href: "/", label: "Dashboard" }, { href: "/events", label: "Events" }, - { href: "/manage-roles", label: "Manage roles" }, + { href: "/users", label: "Users" }, ]; export default function DashboardShell({ @@ -66,7 +66,7 @@ export default function DashboardShell({
Create event diff --git a/src/frontend/web-admin/app/events/create/page.tsx b/src/frontend/web-admin/app/events/create/page.tsx new file mode 100644 index 0000000..fbc6237 --- /dev/null +++ b/src/frontend/web-admin/app/events/create/page.tsx @@ -0,0 +1,193 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { createEvent, type CreateEventPayload } from "../../_lib/api"; + +export default function CreateEventPage() { + const router = useRouter(); + const [form, setForm] = useState({ + name: "", + description: "", + date: "", + location: "", + capacity: "", + priceDollars: "", + }); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + + function update(key: K, value: string) { + setForm((prev) => ({ ...prev, [key]: value })); + } + + async function onSubmit(e: React.FormEvent) { + e.preventDefault(); + setError(null); + + if (!form.name.trim()) { + setError("Name is required"); + return; + } + if (!form.date) { + setError("Date is required"); + return; + } + + const capacity = Number(form.capacity); + if (!Number.isFinite(capacity) || capacity <= 0) { + setError("Capacity must be a positive number"); + return; + } + + const priceDollars = form.priceDollars.trim() + ? Number(form.priceDollars) + : 0; + if (!Number.isFinite(priceDollars) || priceDollars < 0) { + setError("Price must be a non‑negative number"); + return; + } + + const payload: CreateEventPayload = { + name: form.name.trim(), + description: form.description.trim() || undefined, + date: new Date(form.date).toISOString(), + location: form.location.trim() || undefined, + capacity, + imageUrl: undefined, + price: Math.round(priceDollars * 100), + }; + + try { + setSubmitting(true); + await createEvent(payload); + router.push("/events"); + router.refresh(); + } catch (err) { + console.error("Create event error:", err); + setError( + err instanceof Error ? err.message : "Failed to create event", + ); + } finally { + setSubmitting(false); + } + } + + return ( +
+

Create event

+

+ Add a new event. It will show up in the mobile app and dashboard. +

+ +
+ {error && ( +
+ {error} +
+ )} + +
+ + update("name", e.target.value)} + required + /> +
+ +
+ +