diff --git a/apps/admin-web/package.json b/apps/admin-web/package.json index 642007e..10fe088 100644 --- a/apps/admin-web/package.json +++ b/apps/admin-web/package.json @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "date-fns": "^4.1.0", "axios": "^1.13.6", "clsx": "^2.1.1", "lucide-react": "^0.563.0", diff --git a/apps/admin-web/src/layouts/AdminLayout.tsx b/apps/admin-web/src/layouts/AdminLayout.tsx index 7ac9416..2fa4177 100644 --- a/apps/admin-web/src/layouts/AdminLayout.tsx +++ b/apps/admin-web/src/layouts/AdminLayout.tsx @@ -68,6 +68,12 @@ const AdminLayout = () => { icon: Users, permission: "users", }, + { + label: "Conference Signups", + path: "/admin/conference-waitlist", + icon: UserCheck, + permission: "users", + }, { label: "Pitch Moderation", path: "/admin/pitches", diff --git a/apps/admin-web/src/pages/ConferenceWaitlist.tsx b/apps/admin-web/src/pages/ConferenceWaitlist.tsx new file mode 100644 index 0000000..6ea61d1 --- /dev/null +++ b/apps/admin-web/src/pages/ConferenceWaitlist.tsx @@ -0,0 +1,83 @@ +import React, { useEffect, useState } from "react"; +import api from "../utils/api"; +import { format } from "date-fns"; + +const ConferenceWaitlist = () => { + const [items, setItems] = useState([]); + const [loading, setLoading] = useState(false); + + const load = async () => { + setLoading(true); + try { + const res = await api.get("/users/admin/conference-waitlist"); + setItems(Array.isArray(res.data) ? res.data : []); + } catch (err) { + console.error(err); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + void load(); + }, []); + + const markNotified = async (id: string) => { + try { + await api.patch(`/users/admin/conference-waitlist/${id}/notify`); + setItems((s) => + s.map((it) => + it.id === id ? { ...it, conferenceNotifiedAt: new Date().toISOString() } : it, + ), + ); + } catch (err) { + console.error(err); + alert("Failed to mark user as notified"); + } + }; + + return ( +
+
+
+

Conference Signups

+

People who signed up in conference mode.

+
+ +
+ +
+ + + + + + + + + + + + {items.map((it) => ( + + + + + + + + ))} + +
EmailRoleSigned UpNotifiedActions
{it.email}{it.role}{it.createdAt ? format(new Date(it.createdAt), "yyyy-MM-dd") : ""}{it.conferenceNotifiedAt ? "Yes" : "No"} + {!it.conferenceNotifiedAt && ( + + )} +
+ {loading &&
Loading...
} + {!loading && items.length === 0 &&
No conference signups yet.
} +
+
+ ); +}; + +export default ConferenceWaitlist; diff --git a/apps/admin-web/src/pages/Waitlist.tsx b/apps/admin-web/src/pages/Waitlist.tsx index 39e96e8..be5003a 100644 --- a/apps/admin-web/src/pages/Waitlist.tsx +++ b/apps/admin-web/src/pages/Waitlist.tsx @@ -5,6 +5,8 @@ import { format } from "date-fns"; const Waitlist = () => { const [items, setItems] = useState([]); const [loading, setLoading] = useState(false); + const [selected, setSelected] = useState>({}); + const [inviteMessage, setInviteMessage] = useState(''); const load = async () => { setLoading(true); @@ -31,6 +33,28 @@ const Waitlist = () => { } }; + const toggleSelect = (id: string) => { + setSelected((s) => ({ ...s, [id]: !s[id] })); + }; + + const sendInvites = async () => { + const ids = Object.entries(selected).filter(([_, v]) => v).map(([k]) => k); + if (ids.length === 0) return alert('Select at least one user to invite'); + if (!confirm(`Send invitations to ${ids.length} user(s)?`)) return; + try { + const res = await api.post('/waitlist/invite', { ids, message: inviteMessage }); + // update items + const updatedIds = (res.data.results || []).map((r: any) => r.id); + setItems((s) => s.map((it) => (updatedIds.includes(it.id) ? { ...it, isNotified: true } : it))); + setSelected({}); + setInviteMessage(''); + alert(`Invitations sent to ${res.data.processed || 0} users.`); + } catch (err) { + console.error(err); + alert('Failed to send invitations'); + } + }; + const exportCsv = () => { const header = ["email", "role", "firstName", "lastName", "location", "isNotified", "createdAt"]; const rows = items.map((it) => [ @@ -66,6 +90,17 @@ const Waitlist = () => { + @@ -78,6 +113,9 @@ const Waitlist = () => { {items.map((it) => ( + @@ -93,6 +131,13 @@ const Waitlist = () => { ))}
+ { + const checked = e.target.checked; + const map: Record = {}; + items.forEach((it) => (map[it.id] = checked)); + setSelected(map); + }} + /> + Email Role Name
+ toggleSelect(it.id)} /> + {it.email} {it.role} {(it.firstName || "") + (it.lastName ? ` ${it.lastName}` : "")}
+
+