feat: Refine admin guest counting logic, increase stats refresh rate, and add guest count utility scripts.

This commit is contained in:
2026-01-29 08:40:30 +02:00
parent 8cb95f3ddf
commit 641bef46cb
6 changed files with 70 additions and 24 deletions

20
check-counts.js Normal file
View File

@@ -0,0 +1,20 @@
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
async function main() {
const total = await prisma.guest.count();
const confirmed = await prisma.guest.count({
where: {
OR: [
{ rsvpStatus: 'ACCEPTED' },
{ AND: [{ rsvpStatus: null }, { isAttending: true }] }
]
}
});
console.log(`Total Guests: ${total}`);
console.log(`Confirmed Guests: ${confirmed}`);
}
main()
.catch(e => console.error(e))
.finally(async () => await prisma.$disconnect());

25
check-counts.ts Normal file
View File

@@ -0,0 +1,25 @@
import { prisma } from "@/lib/db";
async function main() {
console.log('Checking database counts...');
try {
const total = await prisma.guest.count();
const confirmed = await prisma.guest.count({
where: {
OR: [
{ rsvpStatus: 'ACCEPTED' },
{ AND: [{ rsvpStatus: 'PENDING' }, { isAttending: true }] }
]
}
});
console.log(`Total Guests: ${total}`);
console.log(`Confirmed Guests: ${confirmed}`);
} catch (error) {
console.error('Error querying database:', error);
}
}
main()
.catch(e => console.error(e))
.finally(async () => await prisma.$disconnect());

View File

@@ -13,7 +13,7 @@ const AdminDashboard = dynamic(
// This prevents server-only code from being evaluated during bundling // This prevents server-only code from being evaluated during bundling
if (typeof window === 'undefined') { if (typeof window === 'undefined') {
// On server, return a no-op component that won't be used (ssr: false) // On server, return a no-op component that won't be used (ssr: false)
return Promise.resolve({ return Promise.resolve({
default: () => { default: () => {
if (typeof window === 'undefined') { if (typeof window === 'undefined') {
return null; return null;
@@ -43,8 +43,8 @@ const AdminDashboard = dynamic(
const AdminLayout = dynamic( const AdminLayout = dynamic(
() => { () => {
if (typeof window === 'undefined') { if (typeof window === 'undefined') {
return Promise.resolve({ return Promise.resolve({
default: () => null default: () => null
}); });
} }
return import("@/components/features/admin/admin-layout").then((mod) => ({ default: mod.AdminLayout })); return import("@/components/features/admin/admin-layout").then((mod) => ({ default: mod.AdminLayout }));
@@ -84,7 +84,7 @@ function AdminContent() {
method: 'GET', method: 'GET',
credentials: 'include', credentials: 'include',
}); });
if (response.ok) { if (response.ok) {
let data; let data;
try { try {
@@ -126,14 +126,14 @@ function AdminContent() {
// Use API route for all authentication (handles both email and username) // Use API route for all authentication (handles both email and username)
// This avoids client-side network issues and ensures proper cookie handling // This avoids client-side network issues and ensures proper cookie handling
const isEmail = trimmedUsername.includes('@'); const isEmail = trimmedUsername.includes('@');
const response = await fetch('/api/admin/auth', { const response = await fetch('/api/admin/auth', {
method: 'POST', method: 'POST',
credentials: 'include', credentials: 'include',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
[isEmail ? 'email' : 'username']: trimmedUsername, [isEmail ? 'email' : 'username']: trimmedUsername,
password: trimmedPassword password: trimmedPassword
}), }),
}); });
@@ -141,24 +141,24 @@ function AdminContent() {
try { try {
// Read response body once // Read response body once
const text = await response.text(); const text = await response.text();
// Check content type // Check content type
const contentType = response.headers.get('content-type'); const contentType = response.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) { if (!contentType || !contentType.includes('application/json')) {
// Handle non-JSON responses (e.g., HTML error pages, plain text errors) // Handle non-JSON responses (e.g., HTML error pages, plain text errors)
const errorMessage = text.includes('Internal Server Error') const errorMessage = text.includes('Internal Server Error')
? 'Server error occurred. Please try again or check server logs.' ? 'Server error occurred. Please try again or check server logs.'
: text.substring(0, 200) || `Server error: ${response.status} ${response.statusText}`; : text.substring(0, 200) || `Server error: ${response.status} ${response.statusText}`;
// Only log full error in development // Only log full error in development
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
console.error('Non-JSON response received:', text.substring(0, 200)); console.error('Non-JSON response received:', text.substring(0, 200));
} }
setError(errorMessage); setError(errorMessage);
return; return;
} }
// Parse JSON // Parse JSON
data = text ? JSON.parse(text) : {}; data = text ? JSON.parse(text) : {};
} catch (parseError) { } catch (parseError) {
@@ -166,11 +166,11 @@ function AdminContent() {
const errorMessage = response.status >= 500 const errorMessage = response.status >= 500
? 'Server error occurred. Please try again.' ? 'Server error occurred. Please try again.'
: 'Invalid response from server. Please check your connection.'; : 'Invalid response from server. Please check your connection.';
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
console.error('Failed to parse response:', parseError); console.error('Failed to parse response:', parseError);
} }
setError(errorMessage); setError(errorMessage);
return; return;
} }
@@ -201,7 +201,7 @@ function AdminContent() {
const handleLogout = async () => { const handleLogout = async () => {
try { try {
// Call API logout (handles Supabase sign out server-side) // Call API logout (handles Supabase sign out server-side)
await fetch('/api/admin/auth', { await fetch('/api/admin/auth', {
method: 'DELETE', method: 'DELETE',
credentials: 'include', credentials: 'include',
}); });
@@ -365,16 +365,12 @@ function AdminContent() {
); );
} }
const handleSettingsClick = () => {
// Navigate to settings tab
window.location.href = '/admin?tab=settings';
};
return ( return (
<AdminStatsProvider> <AdminStatsProvider>
<AdminLayout onLogout={handleLogout}> <AdminLayout onLogout={handleLogout}>
<Suspense key="admin-dashboard" fallback={<div className="flex items-center justify-center min-h-screen"><div className="text-wedding-evergreen font-body">Loading dashboard...</div></div>}> <Suspense key="admin-dashboard" fallback={<div className="flex items-center justify-center min-h-screen"><div className="text-wedding-evergreen font-body">Loading dashboard...</div></div>}>
<AdminDashboard defaultTab={activeTab} onSettingsClick={handleSettingsClick} /> <AdminDashboard defaultTab={activeTab} />
</Suspense> </Suspense>
</AdminLayout> </AdminLayout>
</AdminStatsProvider> </AdminStatsProvider>

View File

@@ -35,7 +35,7 @@ async function fetchStatsFromDatabase() {
{ rsvpStatus: 'ACCEPTED' }, { rsvpStatus: 'ACCEPTED' },
{ {
AND: [ AND: [
{ OR: [{ rsvpStatus: null }, { rsvpStatus: 'PENDING' }] }, { rsvpStatus: 'PENDING' },
{ isAttending: true } { isAttending: true }
] ]
} }
@@ -48,7 +48,7 @@ async function fetchStatsFromDatabase() {
{ rsvpStatus: 'DECLINED' }, { rsvpStatus: 'DECLINED' },
{ {
AND: [ AND: [
{ OR: [{ rsvpStatus: null }, { rsvpStatus: 'PENDING' }] }, { rsvpStatus: 'PENDING' },
{ isAttending: false } { isAttending: false }
] ]
} }
@@ -62,7 +62,7 @@ async function fetchStatsFromDatabase() {
{ rsvpStatus: 'ACCEPTED' }, { rsvpStatus: 'ACCEPTED' },
{ {
AND: [ AND: [
{ OR: [{ rsvpStatus: null }, { rsvpStatus: 'PENDING' }] }, { rsvpStatus: 'PENDING' },
{ isAttending: true } { isAttending: true }
] ]
} }

View File

@@ -64,6 +64,11 @@ export function AdminDashboard({
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const tabFromUrl = searchParams.get("tab") || defaultTab; const tabFromUrl = searchParams.get("tab") || defaultTab;
const [activeTab, setActiveTab] = useState(tabFromUrl); const [activeTab, setActiveTab] = useState(tabFromUrl);
useEffect(() => {
console.log("AdminDashboard mounted");
}, []);
const { toast } = useToast(); const { toast } = useToast();
// Unified Data Hooks // Unified Data Hooks

View File

@@ -105,7 +105,7 @@ export function AdminStatsProvider({ children }: AdminStatsProviderProps) {
useEffect(() => { useEffect(() => {
const interval = setInterval(() => { const interval = setInterval(() => {
refresh(true); // Silent refresh refresh(true); // Silent refresh
}, 60000); // 60 seconds }, 5000); // 5 seconds
return () => clearInterval(interval); return () => clearInterval(interval);
}, [refresh]); }, [refresh]);