fixes, theming, branding
This commit is contained in:
@@ -4,7 +4,7 @@ import { announcements } from '@/lib/db/schema';
|
|||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
import { getSession } from '@/lib/session';
|
import { getSession } from '@/lib/session';
|
||||||
|
|
||||||
export async function PUT(request: NextRequest, { params }: { params: { id: string } }) {
|
export async function PUT(request: NextRequest, context: { params: Promise<{ id: string }> }) {
|
||||||
try {
|
try {
|
||||||
const session = await getSession();
|
const session = await getSession();
|
||||||
if (!session || session.role !== 'admin') {
|
if (!session || session.role !== 'admin') {
|
||||||
@@ -12,7 +12,8 @@ export async function PUT(request: NextRequest, { params }: { params: { id: stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { title, content, priority, expiresAt, isActive } = await request.json();
|
const { title, content, priority, expiresAt, isActive } = await request.json();
|
||||||
const announcementId = params.id;
|
const { id } = await context.params;
|
||||||
|
const announcementId = id;
|
||||||
|
|
||||||
if (!title || !content) {
|
if (!title || !content) {
|
||||||
return NextResponse.json({ error: 'Title and content are required' }, { status: 400 });
|
return NextResponse.json({ error: 'Title and content are required' }, { status: 400 });
|
||||||
@@ -49,14 +50,15 @@ export async function PUT(request: NextRequest, { params }: { params: { id: stri
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) {
|
export async function DELETE(request: NextRequest, context: { params: Promise<{ id: string }> }) {
|
||||||
try {
|
try {
|
||||||
const session = await getSession();
|
const session = await getSession();
|
||||||
if (!session || session.role !== 'admin') {
|
if (!session || session.role !== 'admin') {
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const announcementId = params.id;
|
const { id } = await context.params;
|
||||||
|
const announcementId = id;
|
||||||
|
|
||||||
// Check if announcement exists
|
// Check if announcement exists
|
||||||
const existing = await db.select().from(announcements).where(eq(announcements.id, announcementId)).limit(1);
|
const existing = await db.select().from(announcements).where(eq(announcements.id, announcementId)).limit(1);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { courts } from '@/lib/db/schema';
|
|||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
import { getSession } from '@/lib/session';
|
import { getSession } from '@/lib/session';
|
||||||
|
|
||||||
export async function PUT(request: NextRequest, { params }: { params: { id: string } }) {
|
export async function PUT(request: NextRequest, context: { params: Promise<{ id: string }> }) {
|
||||||
try {
|
try {
|
||||||
const session = await getSession();
|
const session = await getSession();
|
||||||
if (!session || session.role !== 'admin') {
|
if (!session || session.role !== 'admin') {
|
||||||
@@ -12,7 +12,8 @@ export async function PUT(request: NextRequest, { params }: { params: { id: stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { name, isActive } = await request.json();
|
const { name, isActive } = await request.json();
|
||||||
const courtId = params.id;
|
const { id } = await context.params;
|
||||||
|
const courtId = id;
|
||||||
|
|
||||||
if (!name) {
|
if (!name) {
|
||||||
return NextResponse.json({ error: 'Court name is required' }, { status: 400 });
|
return NextResponse.json({ error: 'Court name is required' }, { status: 400 });
|
||||||
@@ -46,14 +47,15 @@ export async function PUT(request: NextRequest, { params }: { params: { id: stri
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) {
|
export async function DELETE(request: NextRequest, context: { params: Promise<{ id: string }> }) {
|
||||||
try {
|
try {
|
||||||
const session = await getSession();
|
const session = await getSession();
|
||||||
if (!session || session.role !== 'admin') {
|
if (!session || session.role !== 'admin') {
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const courtId = params.id;
|
const { id } = await context.params;
|
||||||
|
const courtId = id;
|
||||||
|
|
||||||
// Check if court exists
|
// Check if court exists
|
||||||
const existing = await db.select().from(courts).where(eq(courts.id, courtId)).limit(1);
|
const existing = await db.select().from(courts).where(eq(courts.id, courtId)).limit(1);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { db } from '@/lib/db';
|
|||||||
import { settings } from '@/lib/db/schema';
|
import { settings } from '@/lib/db/schema';
|
||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
import { getSession } from '@/lib/session';
|
import { getSession } from '@/lib/session';
|
||||||
|
import { invalidateAppConfigCache } from '@/lib/app-config';
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
@@ -70,6 +71,9 @@ export async function PUT(request: NextRequest) {
|
|||||||
|
|
||||||
await Promise.all(updatePromises);
|
await Promise.all(updatePromises);
|
||||||
|
|
||||||
|
// Invalidate app config cache since settings changed
|
||||||
|
invalidateAppConfigCache();
|
||||||
|
|
||||||
return NextResponse.json({ message: 'Settings updated successfully' });
|
return NextResponse.json({ message: 'Settings updated successfully' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating settings:', error);
|
console.error('Error updating settings:', error);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { eq } from 'drizzle-orm';
|
|||||||
import { getSession } from '@/lib/session';
|
import { getSession } from '@/lib/session';
|
||||||
import bcrypt from 'bcryptjs';
|
import bcrypt from 'bcryptjs';
|
||||||
|
|
||||||
export async function PUT(request: NextRequest, { params }: { params: { id: string } }) {
|
export async function PUT(request: NextRequest, context: { params: Promise<{ id: string }> }) {
|
||||||
try {
|
try {
|
||||||
const session = await getSession();
|
const session = await getSession();
|
||||||
if (!session || session.role !== 'admin') {
|
if (!session || session.role !== 'admin') {
|
||||||
@@ -13,7 +13,8 @@ export async function PUT(request: NextRequest, { params }: { params: { id: stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { name, surname, email, role, password } = await request.json();
|
const { name, surname, email, role, password } = await request.json();
|
||||||
const userId = params.id;
|
const { id } = await context.params;
|
||||||
|
const userId = id;
|
||||||
|
|
||||||
if (!name || !surname || !email) {
|
if (!name || !surname || !email) {
|
||||||
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
|
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
|
||||||
@@ -58,14 +59,15 @@ export async function PUT(request: NextRequest, { params }: { params: { id: stri
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) {
|
export async function DELETE(request: NextRequest, context: { params: Promise<{ id: string }> }) {
|
||||||
try {
|
try {
|
||||||
const session = await getSession();
|
const session = await getSession();
|
||||||
if (!session || session.role !== 'admin') {
|
if (!session || session.role !== 'admin') {
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const userId = params.id;
|
const { id } = await context.params;
|
||||||
|
const userId = id;
|
||||||
|
|
||||||
// Prevent admin from deleting themselves
|
// Prevent admin from deleting themselves
|
||||||
if (session.userId === userId) {
|
if (session.userId === userId) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { bookings, settings } from '@/lib/db/schema';
|
|||||||
import { eq, and } from 'drizzle-orm';
|
import { eq, and } from 'drizzle-orm';
|
||||||
import { logActivity, ACTIONS, ENTITY_TYPES } from '@/lib/activity-logger';
|
import { logActivity, ACTIONS, ENTITY_TYPES } from '@/lib/activity-logger';
|
||||||
|
|
||||||
export async function PATCH(request: NextRequest, { params }: { params: { id: string } }) {
|
export async function PATCH(request: NextRequest, context: { params: Promise<{ id: string }> }) {
|
||||||
try {
|
try {
|
||||||
const session = await getSession();
|
const session = await getSession();
|
||||||
if (!session) {
|
if (!session) {
|
||||||
@@ -13,7 +13,8 @@ export async function PATCH(request: NextRequest, { params }: { params: { id: st
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { notes } = await request.json();
|
const { notes } = await request.json();
|
||||||
const bookingId = params.id;
|
const { id } = await context.params;
|
||||||
|
const bookingId = id;
|
||||||
|
|
||||||
// Check if booking exists and belongs to user
|
// Check if booking exists and belongs to user
|
||||||
const existingBooking = await db
|
const existingBooking = await db
|
||||||
@@ -98,14 +99,15 @@ export async function PATCH(request: NextRequest, { params }: { params: { id: st
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) {
|
export async function DELETE(request: NextRequest, context: { params: Promise<{ id: string }> }) {
|
||||||
try {
|
try {
|
||||||
const session = await getSession();
|
const session = await getSession();
|
||||||
if (!session) {
|
if (!session) {
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const bookingId = params.id;
|
const { id } = await context.params;
|
||||||
|
const bookingId = id;
|
||||||
|
|
||||||
// Check if booking exists and belongs to user
|
// Check if booking exists and belongs to user
|
||||||
const existingBooking = await db
|
const existingBooking = await db
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { bookings, courts, timeSlots, settings, metrics } from '@/lib/db/schema';
|
import { bookings, courts, timeSlots, settings, metrics } from '@/lib/db/schema';
|
||||||
import { eq, and } from 'drizzle-orm';
|
import { eq, and, gte, asc } from 'drizzle-orm';
|
||||||
import { getSession } from '@/lib/session';
|
import { getSession } from '@/lib/session';
|
||||||
import { logActivity, ACTIONS, ENTITY_TYPES } from '@/lib/activity-logger';
|
import { logActivity, ACTIONS, ENTITY_TYPES } from '@/lib/activity-logger';
|
||||||
|
|
||||||
@@ -12,6 +12,9 @@ export async function GET(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get today's date to filter out past bookings
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
|
||||||
const userBookings = await db
|
const userBookings = await db
|
||||||
.select({
|
.select({
|
||||||
id: bookings.id,
|
id: bookings.id,
|
||||||
@@ -29,7 +32,8 @@ export async function GET(request: NextRequest) {
|
|||||||
})
|
})
|
||||||
.from(bookings)
|
.from(bookings)
|
||||||
.innerJoin(courts, eq(bookings.courtId, courts.id))
|
.innerJoin(courts, eq(bookings.courtId, courts.id))
|
||||||
.where(eq(bookings.userId, session.userId));
|
.where(and(eq(bookings.userId, session.userId), eq(bookings.status, 'active'), gte(bookings.date, today)))
|
||||||
|
.orderBy(asc(bookings.date), asc(bookings.startTime));
|
||||||
|
|
||||||
return NextResponse.json({ bookings: userBookings });
|
return NextResponse.json({ bookings: userBookings });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { getAppConfig } from '@/lib/app-config';
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const config = await getAppConfig();
|
||||||
|
return NextResponse.json(config);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching app config:', error);
|
||||||
|
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,8 +6,10 @@ import { eq } from 'drizzle-orm';
|
|||||||
import { DashboardHeader } from '@/components/dashboard/dashboard-header';
|
import { DashboardHeader } from '@/components/dashboard/dashboard-header';
|
||||||
import { EnhancedBookingCalendar } from '@/components/booking/enhanced-booking-calendar';
|
import { EnhancedBookingCalendar } from '@/components/booking/enhanced-booking-calendar';
|
||||||
import { UserBookingManagement } from '@/components/booking/user-booking-management';
|
import { UserBookingManagement } from '@/components/booking/user-booking-management';
|
||||||
|
import { getAppConfig } from '@/lib/app-config';
|
||||||
|
|
||||||
export default async function DashboardPage() {
|
export default async function DashboardPage() {
|
||||||
|
const config = await getAppConfig();
|
||||||
const session = await getSession();
|
const session = await getSession();
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
@@ -38,7 +40,7 @@ export default async function DashboardPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-gray-900 dark:to-gray-800'>
|
<div className='min-h-screen bg-background'>
|
||||||
<DashboardHeader user={userWithSession} />
|
<DashboardHeader user={userWithSession} />
|
||||||
|
|
||||||
<main className='container mx-auto px-4 py-8'>
|
<main className='container mx-auto px-4 py-8'>
|
||||||
@@ -51,7 +53,9 @@ export default async function DashboardPage() {
|
|||||||
{user.name && user.surname ? `${user.name} ${user.surname}` : user.email.split('@')[0]}!
|
{user.name && user.surname ? `${user.name} ${user.surname}` : user.email.split('@')[0]}!
|
||||||
🏓
|
🏓
|
||||||
</h1>
|
</h1>
|
||||||
<p className='text-muted-foreground'>Book your table tennis court and enjoy your game</p>
|
<p className='text-muted-foreground'>
|
||||||
|
Book your {config.sportName.toLowerCase()} court and enjoy your game
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<EnhancedBookingCalendar />
|
<EnhancedBookingCalendar />
|
||||||
|
|||||||
+90
-55
@@ -4,65 +4,99 @@
|
|||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--background: 0 0% 100%;
|
--background: 0 0% 98.8235%;
|
||||||
--foreground: 222.2 84% 4.9%;
|
--foreground: 0 0% 9.0196%;
|
||||||
|
--card: 0 0% 98.8235%;
|
||||||
--card: 0 0% 100%;
|
--card-foreground: 0 0% 9.0196%;
|
||||||
--card-foreground: 222.2 84% 4.9%;
|
--popover: 0 0% 98.8235%;
|
||||||
|
--popover-foreground: 0 0% 32.1569%;
|
||||||
--popover: 0 0% 100%;
|
--primary: 151.3274 66.8639% 66.8627%;
|
||||||
--popover-foreground: 222.2 84% 4.9%;
|
--primary-foreground: 153.3333 13.0435% 13.5294%;
|
||||||
|
--secondary: 0 0% 99.2157%;
|
||||||
--primary: 222.2 47.4% 11.2%;
|
--secondary-foreground: 0 0% 9.0196%;
|
||||||
--primary-foreground: 210 40% 98%;
|
--muted: 0 0% 92.9412%;
|
||||||
|
--muted-foreground: 0 0% 12.549%;
|
||||||
--secondary: 210 40% 96%;
|
--accent: 0 0% 92.9412%;
|
||||||
--secondary-foreground: 222.2 84% 4.9%;
|
--accent-foreground: 0 0% 12.549%;
|
||||||
|
--destructive: 9.8901 81.982% 43.5294%;
|
||||||
--muted: 210 40% 96%;
|
--destructive-foreground: 0 100% 99.4118%;
|
||||||
--muted-foreground: 215.4 16.3% 46.9%;
|
--border: 0 0% 87.451%;
|
||||||
|
--input: 0 0% 96.4706%;
|
||||||
--accent: 210 40% 96%;
|
--ring: 151.3274 66.8639% 66.8627%;
|
||||||
--accent-foreground: 222.2 84% 4.9%;
|
--chart-1: 151.3274 66.8639% 66.8627%;
|
||||||
|
--chart-2: 217.2193 91.2195% 59.8039%;
|
||||||
--destructive: 0 84.2% 60.2%;
|
--chart-3: 258.3117 89.5349% 66.2745%;
|
||||||
--destructive-foreground: 210 40% 98%;
|
--chart-4: 37.6923 92.126% 50.1961%;
|
||||||
|
--chart-5: 160.1183 84.0796% 39.4118%;
|
||||||
--border: 214.3 31.8% 91.4%;
|
--sidebar: 0 0% 98.8235%;
|
||||||
--input: 214.3 31.8% 91.4%;
|
--sidebar-foreground: 0 0% 43.9216%;
|
||||||
--ring: 222.2 84% 4.9%;
|
--sidebar-primary: 151.3274 66.8639% 66.8627%;
|
||||||
|
--sidebar-primary-foreground: 153.3333 13.0435% 13.5294%;
|
||||||
|
--sidebar-accent: 0 0% 92.9412%;
|
||||||
|
--sidebar-accent-foreground: 0 0% 12.549%;
|
||||||
|
--sidebar-border: 0 0% 87.451%;
|
||||||
|
--sidebar-ring: 151.3274 66.8639% 66.8627%;
|
||||||
|
--font-sans: Outfit, sans-serif;
|
||||||
|
--font-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;
|
||||||
|
--font-mono: monospace;
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
|
--shadow-2xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.09);
|
||||||
|
--shadow-xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.09);
|
||||||
|
--shadow-sm: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 1px 2px -1px hsl(0 0% 0% / 0.17);
|
||||||
|
--shadow: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 1px 2px -1px hsl(0 0% 0% / 0.17);
|
||||||
|
--shadow-md: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 2px 4px -1px hsl(0 0% 0% / 0.17);
|
||||||
|
--shadow-lg: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 4px 6px -1px hsl(0 0% 0% / 0.17);
|
||||||
|
--shadow-xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 8px 10px -1px hsl(0 0% 0% / 0.17);
|
||||||
|
--shadow-2xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.43);
|
||||||
|
--tracking-normal: 0.025em;
|
||||||
|
--spacing: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: 222.2 84% 4.9%;
|
--background: 0 0% 7.0588%;
|
||||||
--foreground: 210 40% 98%;
|
--foreground: 214.2857 31.8182% 91.3725%;
|
||||||
|
--card: 0 0% 9.0196%;
|
||||||
--card: 222.2 84% 4.9%;
|
--card-foreground: 214.2857 31.8182% 91.3725%;
|
||||||
--card-foreground: 210 40% 98%;
|
--popover: 0 0% 14.1176%;
|
||||||
|
--popover-foreground: 0 0% 66.2745%;
|
||||||
--popover: 222.2 84% 4.9%;
|
--primary: 154.898 100% 19.2157%;
|
||||||
--popover-foreground: 210 40% 98%;
|
--primary-foreground: 152.7273 19.2982% 88.8235%;
|
||||||
|
--secondary: 0 0% 14.1176%;
|
||||||
--primary: 210 40% 98%;
|
--secondary-foreground: 0 0% 98.0392%;
|
||||||
--primary-foreground: 222.2 47.4% 11.2%;
|
--muted: 0 0% 12.1569%;
|
||||||
|
--muted-foreground: 0 0% 63.5294%;
|
||||||
--secondary: 217.2 32.6% 17.5%;
|
--accent: 0 0% 19.2157%;
|
||||||
--secondary-foreground: 210 40% 98%;
|
--accent-foreground: 0 0% 98.0392%;
|
||||||
|
--destructive: 6.6667 60% 20.5882%;
|
||||||
--muted: 217.2 32.6% 17.5%;
|
--destructive-foreground: 12 12.1951% 91.9608%;
|
||||||
--muted-foreground: 215 20.2% 65.1%;
|
--border: 0 0% 16.0784%;
|
||||||
|
--input: 0 0% 14.1176%;
|
||||||
--accent: 217.2 32.6% 17.5%;
|
--ring: 141.8919 69.1589% 58.0392%;
|
||||||
--accent-foreground: 210 40% 98%;
|
--chart-1: 141.8919 69.1589% 58.0392%;
|
||||||
|
--chart-2: 213.1169 93.9024% 67.8431%;
|
||||||
--destructive: 0 62.8% 30.6%;
|
--chart-3: 255.1351 91.7355% 76.2745%;
|
||||||
--destructive-foreground: 210 40% 98%;
|
--chart-4: 43.2558 96.4126% 56.2745%;
|
||||||
|
--chart-5: 172.4551 66.0079% 50.3922%;
|
||||||
--border: 217.2 32.6% 17.5%;
|
--sidebar: 0 0% 7.0588%;
|
||||||
--input: 217.2 32.6% 17.5%;
|
--sidebar-foreground: 0 0% 53.7255%;
|
||||||
--ring: 212.7 26.8% 83.9%;
|
--sidebar-primary: 154.898 100% 19.2157%;
|
||||||
|
--sidebar-primary-foreground: 152.7273 19.2982% 88.8235%;
|
||||||
|
--sidebar-accent: 0 0% 19.2157%;
|
||||||
|
--sidebar-accent-foreground: 0 0% 98.0392%;
|
||||||
|
--sidebar-border: 0 0% 16.0784%;
|
||||||
|
--sidebar-ring: 141.8919 69.1589% 58.0392%;
|
||||||
|
--font-sans: Outfit, sans-serif;
|
||||||
|
--font-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;
|
||||||
|
--font-mono: monospace;
|
||||||
|
--radius: 0.5rem;
|
||||||
|
--shadow-2xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.09);
|
||||||
|
--shadow-xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.09);
|
||||||
|
--shadow-sm: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 1px 2px -1px hsl(0 0% 0% / 0.17);
|
||||||
|
--shadow: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 1px 2px -1px hsl(0 0% 0% / 0.17);
|
||||||
|
--shadow-md: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 2px 4px -1px hsl(0 0% 0% / 0.17);
|
||||||
|
--shadow-lg: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 4px 6px -1px hsl(0 0% 0% / 0.17);
|
||||||
|
--shadow-xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 8px 10px -1px hsl(0 0% 0% / 0.17);
|
||||||
|
--shadow-2xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.43);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,5 +106,6 @@
|
|||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
|
letter-spacing: var(--tracking-normal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-3
@@ -3,13 +3,18 @@ import type { Metadata } from 'next';
|
|||||||
import { Inter } from 'next/font/google';
|
import { Inter } from 'next/font/google';
|
||||||
import { ThemeProvider } from '@/components/theme-provider';
|
import { ThemeProvider } from '@/components/theme-provider';
|
||||||
import { Toaster } from '@/components/ui/toaster';
|
import { Toaster } from '@/components/ui/toaster';
|
||||||
|
import { getAppConfig } from '@/lib/app-config';
|
||||||
|
|
||||||
const inter = Inter({ subsets: ['latin'] });
|
const inter = Inter({ subsets: ['latin'] });
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export async function generateMetadata(): Promise<Metadata> {
|
||||||
title: 'Table Tennis Booking System',
|
const config = await getAppConfig();
|
||||||
description: 'Book your table tennis court slots with ease',
|
|
||||||
|
return {
|
||||||
|
title: config.appTitle,
|
||||||
|
description: config.appDescription,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
+6
-3
@@ -1,13 +1,16 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { LoginForm } from '@/components/auth/LoginForm';
|
import { LoginForm } from '@/components/auth/LoginForm';
|
||||||
|
import { getAppConfig } from '@/lib/app-config';
|
||||||
|
|
||||||
|
export default async function LoginPage() {
|
||||||
|
const config = await getAppConfig();
|
||||||
|
|
||||||
export default function LoginPage() {
|
|
||||||
return (
|
return (
|
||||||
<div className='min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-gray-900 dark:to-gray-800 flex items-center justify-center px-4'>
|
<div className='min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-gray-900 dark:to-gray-800 flex items-center justify-center px-4'>
|
||||||
<div className='w-full max-w-md space-y-6'>
|
<div className='w-full max-w-md space-y-6'>
|
||||||
<div className='text-center'>
|
<div className='text-center'>
|
||||||
<h1 className='text-3xl font-bold text-foreground mb-2'>🏓 TT Booking</h1>
|
<h1 className='text-3xl font-bold text-foreground mb-2'>🏓 {config.clubName}</h1>
|
||||||
<p className='text-muted-foreground'>Professional table tennis court booking system</p>
|
<p className='text-muted-foreground'>{config.appDescription}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<LoginForm />
|
<LoginForm />
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { RegisterForm } from '@/components/auth/RegisterForm';
|
import { RegisterForm } from '@/components/auth/RegisterForm';
|
||||||
|
import { getAppConfig } from '@/lib/app-config';
|
||||||
|
|
||||||
|
export default async function RegisterPage() {
|
||||||
|
const config = await getAppConfig();
|
||||||
|
|
||||||
export default function RegisterPage() {
|
|
||||||
return (
|
return (
|
||||||
<div className='min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-gray-900 dark:to-gray-800 flex items-center justify-center px-4'>
|
<div className='min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-gray-900 dark:to-gray-800 flex items-center justify-center px-4'>
|
||||||
<div className='w-full max-w-md space-y-6'>
|
<div className='w-full max-w-md space-y-6'>
|
||||||
<div className='text-center'>
|
<div className='text-center'>
|
||||||
<h1 className='text-3xl font-bold text-foreground mb-2'>🏓 TT Booking</h1>
|
<h1 className='text-3xl font-bold text-foreground mb-2'>🏓 {config.clubName}</h1>
|
||||||
<p className='text-muted-foreground'>Join our table tennis community</p>
|
<p className='text-muted-foreground'>Join our {config.sportName.toLowerCase()} community</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<RegisterForm />
|
<RegisterForm />
|
||||||
|
|||||||
@@ -8,6 +8,17 @@ import { Label } from '@/components/ui/label';
|
|||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from '@/components/ui/alert-dialog';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
@@ -37,7 +48,9 @@ export function AdminAnnouncementManagement() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
|
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
|
||||||
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
|
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
|
||||||
|
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||||
const [editingAnnouncement, setEditingAnnouncement] = useState<Announcement | null>(null);
|
const [editingAnnouncement, setEditingAnnouncement] = useState<Announcement | null>(null);
|
||||||
|
const [announcementToDelete, setAnnouncementToDelete] = useState<Announcement | null>(null);
|
||||||
const [formData, setFormData] = useState<AnnouncementFormData>({
|
const [formData, setFormData] = useState<AnnouncementFormData>({
|
||||||
title: '',
|
title: '',
|
||||||
content: '',
|
content: '',
|
||||||
@@ -176,12 +189,21 @@ export function AdminAnnouncementManagement() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openDeleteDialog = (announcement: Announcement) => {
|
||||||
|
setAnnouncementToDelete(announcement);
|
||||||
|
setIsDeleteDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmDeleteAnnouncement = async () => {
|
||||||
|
if (announcementToDelete) {
|
||||||
|
await handleDeleteAnnouncement(announcementToDelete.id);
|
||||||
|
setIsDeleteDialogOpen(false);
|
||||||
|
setAnnouncementToDelete(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleDeleteAnnouncement = async (announcementId: string) => {
|
const handleDeleteAnnouncement = async (announcementId: string) => {
|
||||||
try {
|
try {
|
||||||
if (!confirm('Are you sure you want to delete this announcement?')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(`/api/admin/announcements/${announcementId}`, {
|
const response = await fetch(`/api/admin/announcements/${announcementId}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
});
|
});
|
||||||
@@ -444,7 +466,7 @@ export function AdminAnnouncementManagement() {
|
|||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
size='sm'
|
size='sm'
|
||||||
onClick={() => handleDeleteAnnouncement(announcement.id)}
|
onClick={() => openDeleteDialog(announcement)}
|
||||||
className='text-destructive hover:text-destructive/90'
|
className='text-destructive hover:text-destructive/90'
|
||||||
>
|
>
|
||||||
<Trash2 className='h-4 w-4' />
|
<Trash2 className='h-4 w-4' />
|
||||||
@@ -539,6 +561,29 @@ export function AdminAnnouncementManagement() {
|
|||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Delete Confirmation Dialog */}
|
||||||
|
<AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
Are you sure you want to delete{' '}
|
||||||
|
{announcementToDelete ? `"${announcementToDelete.title}"` : 'this announcement'}? This
|
||||||
|
action cannot be undone.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={confirmDeleteAnnouncement}
|
||||||
|
className='bg-destructive hover:bg-destructive/90'
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,17 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from '@/components/ui/alert-dialog';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
@@ -32,7 +43,9 @@ export function AdminCourtManagement() {
|
|||||||
const [editing, setEditing] = useState<string | null>(null);
|
const [editing, setEditing] = useState<string | null>(null);
|
||||||
const [deleting, setDeleting] = useState<string | null>(null);
|
const [deleting, setDeleting] = useState<string | null>(null);
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
|
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||||
const [editingCourt, setEditingCourt] = useState<Court | null>(null);
|
const [editingCourt, setEditingCourt] = useState<Court | null>(null);
|
||||||
|
const [courtToDelete, setCourtToDelete] = useState<Court | null>(null);
|
||||||
const [formData, setFormData] = useState<CourtFormData>({
|
const [formData, setFormData] = useState<CourtFormData>({
|
||||||
name: '',
|
name: '',
|
||||||
isActive: true,
|
isActive: true,
|
||||||
@@ -150,11 +163,20 @@ export function AdminCourtManagement() {
|
|||||||
setIsDialogOpen(true);
|
setIsDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (courtId: string) => {
|
const openDeleteDialog = (court: Court) => {
|
||||||
if (!confirm('Are you sure you want to delete this court? This action cannot be undone.')) {
|
setCourtToDelete(court);
|
||||||
return;
|
setIsDeleteDialogOpen(true);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const confirmDeleteCourt = async () => {
|
||||||
|
if (courtToDelete) {
|
||||||
|
await handleDelete(courtToDelete.id);
|
||||||
|
setIsDeleteDialogOpen(false);
|
||||||
|
setCourtToDelete(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (courtId: string) => {
|
||||||
try {
|
try {
|
||||||
setDeleting(courtId);
|
setDeleting(courtId);
|
||||||
const response = await fetch(`/api/admin/courts/${courtId}`, {
|
const response = await fetch(`/api/admin/courts/${courtId}`, {
|
||||||
@@ -319,7 +341,7 @@ export function AdminCourtManagement() {
|
|||||||
<Button
|
<Button
|
||||||
size='sm'
|
size='sm'
|
||||||
variant='outline'
|
variant='outline'
|
||||||
onClick={() => handleDelete(court.id)}
|
onClick={() => openDeleteDialog(court)}
|
||||||
disabled={deleting === court.id}
|
disabled={deleting === court.id}
|
||||||
className='text-red-600 hover:text-red-700'
|
className='text-red-600 hover:text-red-700'
|
||||||
>
|
>
|
||||||
@@ -337,6 +359,28 @@ export function AdminCourtManagement() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
|
{/* Delete Confirmation Dialog */}
|
||||||
|
<AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
Are you sure you want to delete {courtToDelete ? `"${courtToDelete.name}"` : 'this court'}?
|
||||||
|
This action cannot be undone.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={confirmDeleteCourt}
|
||||||
|
className='bg-destructive hover:bg-destructive/90'
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,12 @@ interface Setting {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface SettingsData {
|
interface SettingsData {
|
||||||
|
// Club/Brand Settings
|
||||||
|
club_name: string;
|
||||||
|
sport_name: string;
|
||||||
|
app_title: string;
|
||||||
|
app_description: string;
|
||||||
|
// Booking Settings
|
||||||
booking_window_days: string;
|
booking_window_days: string;
|
||||||
max_booking_duration_hours: string;
|
max_booking_duration_hours: string;
|
||||||
min_booking_duration_minutes: string;
|
min_booking_duration_minutes: string;
|
||||||
@@ -30,6 +36,12 @@ interface SettingsData {
|
|||||||
|
|
||||||
export function AdminSettingsManagement() {
|
export function AdminSettingsManagement() {
|
||||||
const [settings, setSettings] = useState<SettingsData>({
|
const [settings, setSettings] = useState<SettingsData>({
|
||||||
|
// Club/Brand Settings
|
||||||
|
club_name: 'TT Club',
|
||||||
|
sport_name: 'Table Tennis',
|
||||||
|
app_title: 'Table Tennis Booking System',
|
||||||
|
app_description: 'Book your table tennis court slots with ease',
|
||||||
|
// Booking Settings
|
||||||
booking_window_days: '7',
|
booking_window_days: '7',
|
||||||
max_booking_duration_hours: '2',
|
max_booking_duration_hours: '2',
|
||||||
min_booking_duration_minutes: '30',
|
min_booking_duration_minutes: '30',
|
||||||
@@ -38,7 +50,7 @@ export function AdminSettingsManagement() {
|
|||||||
allow_weekend_bookings: 'true',
|
allow_weekend_bookings: 'true',
|
||||||
max_bookings_per_user_per_hour_per_day: '1',
|
max_bookings_per_user_per_hour_per_day: '1',
|
||||||
allow_booking_modifications: 'true',
|
allow_booking_modifications: 'true',
|
||||||
booking_modification_hours_before: '2',
|
booking_modification_hours_before: '1',
|
||||||
});
|
});
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
@@ -54,6 +66,12 @@ export function AdminSettingsManagement() {
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const settingsMap: SettingsData = {
|
const settingsMap: SettingsData = {
|
||||||
|
// Club/Brand Settings
|
||||||
|
club_name: 'TT Club',
|
||||||
|
sport_name: 'Table Tennis',
|
||||||
|
app_title: 'Table Tennis Booking System',
|
||||||
|
app_description: 'Book your table tennis court slots with ease',
|
||||||
|
// Booking Settings
|
||||||
booking_window_days: '7',
|
booking_window_days: '7',
|
||||||
max_booking_duration_hours: '2',
|
max_booking_duration_hours: '2',
|
||||||
min_booking_duration_minutes: '30',
|
min_booking_duration_minutes: '30',
|
||||||
@@ -62,10 +80,8 @@ export function AdminSettingsManagement() {
|
|||||||
allow_weekend_bookings: 'true',
|
allow_weekend_bookings: 'true',
|
||||||
max_bookings_per_user_per_hour_per_day: '1',
|
max_bookings_per_user_per_hour_per_day: '1',
|
||||||
allow_booking_modifications: 'true',
|
allow_booking_modifications: 'true',
|
||||||
booking_modification_hours_before: '2',
|
booking_modification_hours_before: '1',
|
||||||
};
|
}; // Map the settings array to our object
|
||||||
|
|
||||||
// Map the settings array to our object
|
|
||||||
data.settings?.forEach((setting: Setting) => {
|
data.settings?.forEach((setting: Setting) => {
|
||||||
if (setting.key in settingsMap) {
|
if (setting.key in settingsMap) {
|
||||||
settingsMap[setting.key as keyof SettingsData] = setting.value;
|
settingsMap[setting.key as keyof SettingsData] = setting.value;
|
||||||
@@ -189,6 +205,67 @@ export function AdminSettingsManagement() {
|
|||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className='space-y-6'>
|
<CardContent className='space-y-6'>
|
||||||
|
{/* Club/Brand Configuration Section */}
|
||||||
|
<div>
|
||||||
|
<h3 className='text-lg font-medium mb-4'>Club & Branding</h3>
|
||||||
|
<div className='grid grid-cols-1 md:grid-cols-2 gap-6'>
|
||||||
|
{/* Club Name */}
|
||||||
|
<div className='space-y-2'>
|
||||||
|
<Label htmlFor='club_name'>Club Name</Label>
|
||||||
|
<Input
|
||||||
|
id='club_name'
|
||||||
|
type='text'
|
||||||
|
placeholder='e.g., Downtown TT Club'
|
||||||
|
value={settings.club_name}
|
||||||
|
onChange={(e) => updateSetting('club_name', e.target.value)}
|
||||||
|
/>
|
||||||
|
<p className='text-sm text-gray-500'>The name of your club or organization</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sport Name */}
|
||||||
|
<div className='space-y-2'>
|
||||||
|
<Label htmlFor='sport_name'>Sport Name</Label>
|
||||||
|
<Input
|
||||||
|
id='sport_name'
|
||||||
|
type='text'
|
||||||
|
placeholder='e.g., Table Tennis, Ping Pong, Badminton'
|
||||||
|
value={settings.sport_name}
|
||||||
|
onChange={(e) => updateSetting('sport_name', e.target.value)}
|
||||||
|
/>
|
||||||
|
<p className='text-sm text-gray-500'>The sport played at your facility</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* App Title */}
|
||||||
|
<div className='space-y-2'>
|
||||||
|
<Label htmlFor='app_title'>Application Title</Label>
|
||||||
|
<Input
|
||||||
|
id='app_title'
|
||||||
|
type='text'
|
||||||
|
placeholder='e.g., Downtown TT Booking'
|
||||||
|
value={settings.app_title}
|
||||||
|
onChange={(e) => updateSetting('app_title', e.target.value)}
|
||||||
|
/>
|
||||||
|
<p className='text-sm text-gray-500'>Main title shown in browser and app header</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* App Description */}
|
||||||
|
<div className='space-y-2'>
|
||||||
|
<Label htmlFor='app_description'>Application Description</Label>
|
||||||
|
<Input
|
||||||
|
id='app_description'
|
||||||
|
type='text'
|
||||||
|
placeholder='e.g., Book your court slots with ease'
|
||||||
|
value={settings.app_description}
|
||||||
|
onChange={(e) => updateSetting('app_description', e.target.value)}
|
||||||
|
/>
|
||||||
|
<p className='text-sm text-gray-500'>Short description for login/register pages</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Booking Configuration Section */}
|
||||||
|
<div>
|
||||||
|
<h3 className='text-lg font-medium mb-4'>Booking Configuration</h3>
|
||||||
<div className='grid grid-cols-1 md:grid-cols-2 gap-6'>
|
<div className='grid grid-cols-1 md:grid-cols-2 gap-6'>
|
||||||
{/* Booking Window */}
|
{/* Booking Window */}
|
||||||
<div className='space-y-2'>
|
<div className='space-y-2'>
|
||||||
@@ -260,14 +337,18 @@ export function AdminSettingsManagement() {
|
|||||||
|
|
||||||
{/* Booking Restrictions */}
|
{/* Booking Restrictions */}
|
||||||
<div className='space-y-2'>
|
<div className='space-y-2'>
|
||||||
<Label htmlFor='max_bookings_per_user_per_hour_per_day'>Max Bookings per User per Hour</Label>
|
<Label htmlFor='max_bookings_per_user_per_hour_per_day'>
|
||||||
|
Max Bookings per User per Hour
|
||||||
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id='max_bookings_per_user_per_hour_per_day'
|
id='max_bookings_per_user_per_hour_per_day'
|
||||||
type='number'
|
type='number'
|
||||||
min='1'
|
min='1'
|
||||||
max='5'
|
max='5'
|
||||||
value={settings.max_bookings_per_user_per_hour_per_day}
|
value={settings.max_bookings_per_user_per_hour_per_day}
|
||||||
onChange={(e) => updateSetting('max_bookings_per_user_per_hour_per_day', e.target.value)}
|
onChange={(e) =>
|
||||||
|
updateSetting('max_bookings_per_user_per_hour_per_day', e.target.value)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<p className='text-sm text-gray-500'>Maximum bookings per user per hour on the same day</p>
|
<p className='text-sm text-gray-500'>Maximum bookings per user per hour on the same day</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -324,6 +405,7 @@ export function AdminSettingsManagement() {
|
|||||||
<p className='text-sm text-gray-500'>Whether users can book courts on weekends</p>
|
<p className='text-sm text-gray-500'>Whether users can book courts on weekends</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className='border-t pt-6'>
|
<div className='border-t pt-6'>
|
||||||
<h3 className='text-lg font-medium mb-4'>Current Configuration Summary</h3>
|
<h3 className='text-lg font-medium mb-4'>Current Configuration Summary</h3>
|
||||||
|
|||||||
@@ -7,6 +7,17 @@ import { Input } from '@/components/ui/input';
|
|||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from '@/components/ui/alert-dialog';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { Plus, Edit, Trash2, Clock } from 'lucide-react';
|
import { Plus, Edit, Trash2, Clock } from 'lucide-react';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
@@ -30,7 +41,11 @@ export function AdminTimeSlotManagement() {
|
|||||||
const [timeSlots, setTimeSlots] = useState<TimeSlot[]>([]);
|
const [timeSlots, setTimeSlots] = useState<TimeSlot[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [showDialog, setShowDialog] = useState(false);
|
const [showDialog, setShowDialog] = useState(false);
|
||||||
|
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||||
|
const [showWipeDayDialog, setShowWipeDayDialog] = useState(false);
|
||||||
const [editingSlot, setEditingSlot] = useState<TimeSlot | null>(null);
|
const [editingSlot, setEditingSlot] = useState<TimeSlot | null>(null);
|
||||||
|
const [slotToDelete, setSlotToDelete] = useState<TimeSlot | null>(null);
|
||||||
|
const [dayToWipe, setDayToWipe] = useState<number | null>(null);
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
dayOfWeek: 1, // Default to Monday (Irish standard)
|
dayOfWeek: 1, // Default to Monday (Irish standard)
|
||||||
startTime: '',
|
startTime: '',
|
||||||
@@ -115,11 +130,20 @@ export function AdminTimeSlotManagement() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (id: string) => {
|
const openDeleteDialog = (slot: TimeSlot) => {
|
||||||
if (!confirm('Are you sure you want to delete this time slot?')) {
|
setSlotToDelete(slot);
|
||||||
return;
|
setShowDeleteDialog(true);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const confirmDeleteSlot = async () => {
|
||||||
|
if (slotToDelete) {
|
||||||
|
await handleDelete(slotToDelete.id);
|
||||||
|
setShowDeleteDialog(false);
|
||||||
|
setSlotToDelete(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (id: string) => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const response = await fetch(`/api/admin/time-slots/${id}`, {
|
const response = await fetch(`/api/admin/time-slots/${id}`, {
|
||||||
@@ -152,14 +176,23 @@ export function AdminTimeSlotManagement() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleWipeDay = async (dayOfWeek: number) => {
|
const openWipeDayDialog = (dayOfWeek: number) => {
|
||||||
const dayName = DAYS[dayOfWeek];
|
setDayToWipe(dayOfWeek);
|
||||||
if (!confirm(`Are you sure you want to delete ALL time slots for ${dayName}? This action cannot be undone.`)) {
|
setShowWipeDayDialog(true);
|
||||||
return;
|
};
|
||||||
}
|
|
||||||
|
|
||||||
|
const confirmWipeDay = async () => {
|
||||||
|
if (dayToWipe !== null) {
|
||||||
|
await handleWipeDay(dayToWipe);
|
||||||
|
setShowWipeDayDialog(false);
|
||||||
|
setDayToWipe(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleWipeDay = async (dayOfWeek: number) => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
const dayName = DAYS[dayOfWeek];
|
||||||
const slotsToDelete = timeSlots.filter((slot) => slot.dayOfWeek === dayOfWeek);
|
const slotsToDelete = timeSlots.filter((slot) => slot.dayOfWeek === dayOfWeek);
|
||||||
|
|
||||||
// Delete all slots for this day
|
// Delete all slots for this day
|
||||||
@@ -322,7 +355,7 @@ export function AdminTimeSlotManagement() {
|
|||||||
<Button
|
<Button
|
||||||
size='sm'
|
size='sm'
|
||||||
variant='outline'
|
variant='outline'
|
||||||
onClick={() => handleWipeDay(jsDayOfWeek)}
|
onClick={() => openWipeDayDialog(jsDayOfWeek)}
|
||||||
className='text-destructive hover:text-destructive/80 hover:bg-destructive/10'
|
className='text-destructive hover:text-destructive/80 hover:bg-destructive/10'
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
@@ -369,7 +402,7 @@ export function AdminTimeSlotManagement() {
|
|||||||
<Button
|
<Button
|
||||||
size='sm'
|
size='sm'
|
||||||
variant='outline'
|
variant='outline'
|
||||||
onClick={() => handleDelete(slot.id)}
|
onClick={() => openDeleteDialog(slot)}
|
||||||
className='text-destructive hover:text-destructive/80'
|
className='text-destructive hover:text-destructive/80'
|
||||||
>
|
>
|
||||||
<Trash2 className='h-4 w-4' />
|
<Trash2 className='h-4 w-4' />
|
||||||
@@ -389,6 +422,46 @@ export function AdminTimeSlotManagement() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
|
{/* Delete Time Slot Dialog */}
|
||||||
|
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
Are you sure you want to delete this time slot? This action cannot be undone.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={confirmDeleteSlot}
|
||||||
|
className='bg-destructive hover:bg-destructive/90'
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
|
||||||
|
{/* Wipe Day Dialog */}
|
||||||
|
<AlertDialog open={showWipeDayDialog} onOpenChange={setShowWipeDayDialog}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
Are you sure you want to delete ALL time slots for{' '}
|
||||||
|
{dayToWipe !== null ? DAYS[dayToWipe] : 'this day'}? This action cannot be undone.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction onClick={confirmWipeDay} className='bg-destructive hover:bg-destructive/90'>
|
||||||
|
Delete All
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,17 @@ import { Input } from '@/components/ui/input';
|
|||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from '@/components/ui/alert-dialog';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
@@ -35,7 +46,9 @@ export function AdminUserManagement() {
|
|||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
|
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
|
||||||
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
|
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
|
||||||
|
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||||
const [editingUser, setEditingUser] = useState<User | null>(null);
|
const [editingUser, setEditingUser] = useState<User | null>(null);
|
||||||
|
const [userToDelete, setUserToDelete] = useState<User | null>(null);
|
||||||
const [formData, setFormData] = useState<UserFormData>({
|
const [formData, setFormData] = useState<UserFormData>({
|
||||||
name: '',
|
name: '',
|
||||||
surname: '',
|
surname: '',
|
||||||
@@ -184,12 +197,21 @@ export function AdminUserManagement() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openDeleteDialog = (user: User) => {
|
||||||
|
setUserToDelete(user);
|
||||||
|
setIsDeleteDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmDeleteUser = async () => {
|
||||||
|
if (userToDelete) {
|
||||||
|
await handleDeleteUser(userToDelete.id);
|
||||||
|
setIsDeleteDialogOpen(false);
|
||||||
|
setUserToDelete(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleDeleteUser = async (userId: string) => {
|
const handleDeleteUser = async (userId: string) => {
|
||||||
try {
|
try {
|
||||||
if (!confirm('Are you sure you want to delete this user? This action cannot be undone.')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(`/api/admin/users/${userId}`, {
|
const response = await fetch(`/api/admin/users/${userId}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
});
|
});
|
||||||
@@ -406,7 +428,7 @@ export function AdminUserManagement() {
|
|||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
size='sm'
|
size='sm'
|
||||||
onClick={() => handleDeleteUser(user.id)}
|
onClick={() => openDeleteDialog(user)}
|
||||||
className='text-red-600 hover:text-red-700'
|
className='text-red-600 hover:text-red-700'
|
||||||
>
|
>
|
||||||
<Trash2 className='h-4 w-4' />
|
<Trash2 className='h-4 w-4' />
|
||||||
@@ -498,6 +520,29 @@ export function AdminUserManagement() {
|
|||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Delete Confirmation Dialog */}
|
||||||
|
<AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
Are you sure you want to delete{' '}
|
||||||
|
{userToDelete ? `${userToDelete.name} ${userToDelete.surname}` : 'this user'}? This action
|
||||||
|
cannot be undone.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={confirmDeleteUser}
|
||||||
|
className='bg-destructive hover:bg-destructive/90'
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -473,11 +473,11 @@ export function EnhancedBookingCalendar() {
|
|||||||
: ''
|
: ''
|
||||||
} ${
|
} ${
|
||||||
isTodayDate && !isSelectedDate
|
isTodayDate && !isSelectedDate
|
||||||
? 'ring-2 ring-blue-400 ring-opacity-50 bg-blue-50 dark:bg-blue-950 border-blue-200 dark:border-blue-800 hover:bg-blue-100 dark:hover:bg-blue-900 text-foreground'
|
? 'ring-2 ring-primary/20 bg-accent border-primary/20 hover:bg-accent/80 text-foreground'
|
||||||
: ''
|
: ''
|
||||||
} ${
|
} ${
|
||||||
isSelectedDate && isTodayDate
|
isSelectedDate && isTodayDate
|
||||||
? 'bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 dark:from-blue-700 dark:to-blue-800 dark:hover:from-blue-800 dark:hover:to-blue-900 text-white'
|
? 'bg-gradient-to-r from-primary to-primary/80 hover:from-primary/90 hover:to-primary/70 text-primary-foreground'
|
||||||
: ''
|
: ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -501,8 +501,8 @@ export function EnhancedBookingCalendar() {
|
|||||||
<div
|
<div
|
||||||
className={`text-center p-4 rounded-lg ${
|
className={`text-center p-4 rounded-lg ${
|
||||||
isToday(selectedDate)
|
isToday(selectedDate)
|
||||||
? 'bg-gradient-to-r from-blue-500 to-indigo-600 text-white dark:from-blue-600 dark:to-indigo-700'
|
? 'bg-gradient-to-r from-primary to-primary/80 text-primary-foreground'
|
||||||
: 'bg-blue-50 dark:bg-blue-950'
|
: 'bg-accent/50'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<h3
|
<h3
|
||||||
@@ -520,7 +520,7 @@ export function EnhancedBookingCalendar() {
|
|||||||
{isToday(selectedDate) && (
|
{isToday(selectedDate) && (
|
||||||
<div className='flex items-center justify-center gap-2 mt-2'>
|
<div className='flex items-center justify-center gap-2 mt-2'>
|
||||||
<div className='w-2 h-2 bg-orange-300 dark:bg-orange-400 rounded-full animate-pulse' />
|
<div className='w-2 h-2 bg-orange-300 dark:bg-orange-400 rounded-full animate-pulse' />
|
||||||
<span className='text-sm font-medium text-blue-100 dark:text-blue-200'>Today</span>
|
<span className='text-sm font-medium text-primary-foreground/90'>Today</span>
|
||||||
<div className='w-2 h-2 bg-orange-300 dark:bg-orange-400 rounded-full animate-pulse' />
|
<div className='w-2 h-2 bg-orange-300 dark:bg-orange-400 rounded-full animate-pulse' />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -61,16 +61,8 @@ export function UserBookingManagement() {
|
|||||||
const response = await fetch('/api/bookings');
|
const response = await fetch('/api/bookings');
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
// Filter to show only future and today's bookings
|
// API already filters to show only active future bookings (today onwards)
|
||||||
const now = new Date();
|
setBookings(data.bookings || []);
|
||||||
const today = now.toISOString().split('T')[0];
|
|
||||||
|
|
||||||
const relevantBookings = data.bookings.filter((booking: Booking) => {
|
|
||||||
if (booking.status !== 'active') return false;
|
|
||||||
return booking.date >= today;
|
|
||||||
});
|
|
||||||
|
|
||||||
setBookings(relevantBookings);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching bookings:', error);
|
console.error('Error fetching bookings:', error);
|
||||||
@@ -282,7 +274,7 @@ export function UserBookingManagement() {
|
|||||||
<CardHeader className='pb-3 flex flex-row items-center justify-between'>
|
<CardHeader className='pb-3 flex flex-row items-center justify-between'>
|
||||||
<CardTitle className='text-base flex items-center gap-2'>
|
<CardTitle className='text-base flex items-center gap-2'>
|
||||||
<Calendar className='h-4 w-4' />
|
<Calendar className='h-4 w-4' />
|
||||||
Your Bookings
|
Your Upcoming Bookings
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<Button size='sm' variant='outline' onClick={fetchBookings}>
|
<Button size='sm' variant='outline' onClick={fetchBookings}>
|
||||||
<RefreshCw className='h-4 w-4' />
|
<RefreshCw className='h-4 w-4' />
|
||||||
@@ -390,16 +382,16 @@ export function UserBookingManagement() {
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className='space-y-4'>
|
<div className='space-y-4'>
|
||||||
{selectedBooking && (
|
{selectedBooking && (
|
||||||
<div className='bg-blue-50 p-4 rounded-lg space-y-2 dark:bg-blue-950'>
|
<div className='bg-accent/50 p-4 rounded-lg space-y-2'>
|
||||||
<div className='flex items-center gap-2 text-sm'>
|
<div className='flex items-center gap-2 text-sm text-foreground'>
|
||||||
<Calendar className='h-4 w-4' />
|
<Calendar className='h-4 w-4' />
|
||||||
{formatDate(selectedBooking.date)}
|
{formatDate(selectedBooking.date)}
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center gap-2 text-sm'>
|
<div className='flex items-center gap-2 text-sm text-foreground'>
|
||||||
<Clock className='h-4 w-4' />
|
<Clock className='h-4 w-4' />
|
||||||
{selectedBooking.startTime} - {selectedBooking.endTime}
|
{selectedBooking.startTime} - {selectedBooking.endTime}
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center gap-2 text-sm'>
|
<div className='flex items-center gap-2 text-sm text-foreground'>
|
||||||
<MapPin className='h-4 w-4' />
|
<MapPin className='h-4 w-4' />
|
||||||
{selectedBooking.court.name}
|
{selectedBooking.court.name}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { CalendarIcon, Clock, MapPin } from 'lucide-react';
|
import { CalendarIcon, Clock, MapPin } from 'lucide-react';
|
||||||
|
import { useToast } from '@/hooks/use-toast';
|
||||||
|
|
||||||
const timeSlots = [
|
const timeSlots = [
|
||||||
'09:00',
|
'09:00',
|
||||||
@@ -31,6 +32,7 @@ export function BookingCalendar() {
|
|||||||
const [selectedDate, setSelectedDate] = useState<Date>(new Date());
|
const [selectedDate, setSelectedDate] = useState<Date>(new Date());
|
||||||
const [selectedSlot, setSelectedSlot] = useState<string | null>(null);
|
const [selectedSlot, setSelectedSlot] = useState<string | null>(null);
|
||||||
const [selectedCourt, setSelectedCourt] = useState<string | null>(null);
|
const [selectedCourt, setSelectedCourt] = useState<string | null>(null);
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
const formatDate = (date: Date) => {
|
const formatDate = (date: Date) => {
|
||||||
return date.toLocaleDateString('en-IE', {
|
return date.toLocaleDateString('en-IE', {
|
||||||
@@ -66,13 +68,24 @@ export function BookingCalendar() {
|
|||||||
setSelectedSlot(null);
|
setSelectedSlot(null);
|
||||||
setSelectedCourt(null);
|
setSelectedCourt(null);
|
||||||
// Show success message
|
// Show success message
|
||||||
alert('Booking created successfully!');
|
toast({
|
||||||
|
title: 'Success',
|
||||||
|
description: 'Booking created successfully!',
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
alert(result.error || 'Booking failed');
|
toast({
|
||||||
|
title: 'Error',
|
||||||
|
description: result.error || 'Booking failed',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Booking error:', error);
|
console.error('Booking error:', error);
|
||||||
alert('An error occurred while creating the booking');
|
toast({
|
||||||
|
title: 'Error',
|
||||||
|
description: 'An error occurred while creating the booking',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { useToast } from '@/hooks/use-toast';
|
|||||||
import { NotificationBell, AnnouncementsModal } from '@/components/notifications/announcements';
|
import { NotificationBell, AnnouncementsModal } from '@/components/notifications/announcements';
|
||||||
import { UserProfile } from '@/components/user/user-profile';
|
import { UserProfile } from '@/components/user/user-profile';
|
||||||
import { ModeToggle } from '@/components/ui/mode-toggle';
|
import { ModeToggle } from '@/components/ui/mode-toggle';
|
||||||
|
import type { AppConfig } from '@/lib/app-config';
|
||||||
|
|
||||||
interface DashboardHeaderProps {
|
interface DashboardHeaderProps {
|
||||||
user: {
|
user: {
|
||||||
@@ -28,6 +29,23 @@ export function DashboardHeader({ user }: DashboardHeaderProps) {
|
|||||||
const [showAnnouncements, setShowAnnouncements] = useState(false);
|
const [showAnnouncements, setShowAnnouncements] = useState(false);
|
||||||
const [showUserProfile, setShowUserProfile] = useState(false);
|
const [showUserProfile, setShowUserProfile] = useState(false);
|
||||||
const [unreadCount, setUnreadCount] = useState(0);
|
const [unreadCount, setUnreadCount] = useState(0);
|
||||||
|
const [appConfig, setAppConfig] = useState<AppConfig | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchAppConfig = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/config');
|
||||||
|
if (response.ok) {
|
||||||
|
const config = await response.json();
|
||||||
|
setAppConfig(config);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching app config:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchAppConfig();
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Fetch unread announcements count on component mount
|
// Fetch unread announcements count on component mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -78,7 +96,7 @@ export function DashboardHeader({ user }: DashboardHeaderProps) {
|
|||||||
<div className='flex items-center space-x-4'>
|
<div className='flex items-center space-x-4'>
|
||||||
<div className='flex items-center space-x-2'>
|
<div className='flex items-center space-x-2'>
|
||||||
<Calendar className='h-6 w-6 text-primary' />
|
<Calendar className='h-6 w-6 text-primary' />
|
||||||
<h1 className='text-xl font-bold text-foreground'>TT Booking</h1>
|
<h1 className='text-xl font-bold text-foreground'>{appConfig?.clubName || 'TT Booking'}</h1>
|
||||||
</div>
|
</div>
|
||||||
{user.role === 'admin' && <Badge variant='secondary'>Admin</Badge>}
|
{user.role === 'admin' && <Badge variant='secondary'>Admin</Badge>}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { db } from '@/lib/db';
|
||||||
|
import { settings } from '@/lib/db/schema';
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
|
||||||
|
export interface AppConfig {
|
||||||
|
clubName: string;
|
||||||
|
sportName: string;
|
||||||
|
appTitle: string;
|
||||||
|
appDescription: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultConfig: AppConfig = {
|
||||||
|
clubName: 'TT Club',
|
||||||
|
sportName: 'Table Tennis',
|
||||||
|
appTitle: 'Table Tennis Booking System',
|
||||||
|
appDescription: 'Book your table tennis court slots with ease',
|
||||||
|
};
|
||||||
|
|
||||||
|
let cachedConfig: AppConfig | null = null;
|
||||||
|
let cacheTime: number = 0;
|
||||||
|
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get app configuration with caching
|
||||||
|
* This function fetches the club/brand settings from the database
|
||||||
|
*/
|
||||||
|
export async function getAppConfig(): Promise<AppConfig> {
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// Return cached config if still valid
|
||||||
|
if (cachedConfig && now - cacheTime < CACHE_DURATION) {
|
||||||
|
return cachedConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch all brand/club settings
|
||||||
|
const configSettings = await db
|
||||||
|
.select()
|
||||||
|
.from(settings)
|
||||||
|
.where(eq(settings.key, 'club_name'))
|
||||||
|
.union(db.select().from(settings).where(eq(settings.key, 'sport_name')))
|
||||||
|
.union(db.select().from(settings).where(eq(settings.key, 'app_title')))
|
||||||
|
.union(db.select().from(settings).where(eq(settings.key, 'app_description')));
|
||||||
|
|
||||||
|
// Build config object
|
||||||
|
const config: AppConfig = { ...defaultConfig };
|
||||||
|
|
||||||
|
configSettings.forEach((setting) => {
|
||||||
|
switch (setting.key) {
|
||||||
|
case 'club_name':
|
||||||
|
config.clubName = setting.value;
|
||||||
|
break;
|
||||||
|
case 'sport_name':
|
||||||
|
config.sportName = setting.value;
|
||||||
|
break;
|
||||||
|
case 'app_title':
|
||||||
|
config.appTitle = setting.value;
|
||||||
|
break;
|
||||||
|
case 'app_description':
|
||||||
|
config.appDescription = setting.value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cache the result
|
||||||
|
cachedConfig = config;
|
||||||
|
cacheTime = now;
|
||||||
|
|
||||||
|
return config;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching app config:', error);
|
||||||
|
// Return default config on error
|
||||||
|
return defaultConfig;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate the cache (call when settings are updated)
|
||||||
|
*/
|
||||||
|
export function invalidateAppConfigCache() {
|
||||||
|
cachedConfig = null;
|
||||||
|
cacheTime = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get app config for client components (no database access)
|
||||||
|
* This should be used with server-side rendered props
|
||||||
|
*/
|
||||||
|
export function getDefaultAppConfig(): AppConfig {
|
||||||
|
return { ...defaultConfig };
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
#!/usr/bin/env tsx
|
||||||
|
|
||||||
|
import { db } from '../lib/db';
|
||||||
|
import { settings } from '../lib/db/schema';
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
|
||||||
|
async function seedBrandSettings() {
|
||||||
|
console.log('🌱 Seeding brand settings...');
|
||||||
|
|
||||||
|
const brandSettings = [
|
||||||
|
{ key: 'club_name', value: 'TT Club' },
|
||||||
|
{ key: 'sport_name', value: 'Table Tennis' },
|
||||||
|
{ key: 'app_title', value: 'Table Tennis Booking System' },
|
||||||
|
{ key: 'app_description', value: 'Book your table tennis court slots with ease' },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const setting of brandSettings) {
|
||||||
|
try {
|
||||||
|
// Check if setting exists
|
||||||
|
const existing = await db.select().from(settings).where(eq(settings.key, setting.key)).limit(1);
|
||||||
|
|
||||||
|
if (existing.length === 0) {
|
||||||
|
// Insert new setting
|
||||||
|
await db.insert(settings).values({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
key: setting.key,
|
||||||
|
value: setting.value,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
});
|
||||||
|
console.log(`✅ Added setting: ${setting.key} = ${setting.value}`);
|
||||||
|
} else {
|
||||||
|
console.log(`⚠️ Setting already exists: ${setting.key}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ Error adding setting ${setting.key}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Brand settings seeded successfully!');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
seedBrandSettings().catch((error) => {
|
||||||
|
console.error('❌ Error seeding brand settings:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
+1
-1
@@ -27,5 +27,5 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules", "scripts/old-seeds/**/*"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user