feat: implement admin blocks management feature

- Added AdminBlocksManagement component for managing court blocks.
- Implemented functionality to create, edit, and delete blocks.
- Integrated fetching of courts and blocks from the API.
- Added validation for block creation and editing forms.
- Enhanced UI with responsive design for mobile and desktop views.
- Created database migration for court_blocks table and updated users table with theme_preference.
This commit is contained in:
mikicvi
2025-12-29 17:04:16 +00:00
parent 54240a2cfd
commit 40c56770a2
13 changed files with 2164 additions and 215 deletions
+176
View File
@@ -0,0 +1,176 @@
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { courtBlocks, courts, users, announcements } from '@/lib/db/schema';
import { eq, gte, asc } from 'drizzle-orm';
import { getSession } from '@/lib/session';
import { logActivity, ACTIONS, ENTITY_TYPES } from '@/lib/activity-logger';
export async function GET(request: NextRequest) {
try {
const session = await getSession();
if (!session || session.role !== 'admin') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { searchParams } = new URL(request.url);
const includeExpired = searchParams.get('includeExpired') === 'true';
// Get today's date
const today = new Date().toISOString().split('T')[0];
// Fetch blocks with court and creator info
let query = db
.select({
id: courtBlocks.id,
courtId: courtBlocks.courtId,
date: courtBlocks.date,
startTime: courtBlocks.startTime,
endTime: courtBlocks.endTime,
reason: courtBlocks.reason,
createdBy: courtBlocks.createdBy,
createdAt: courtBlocks.createdAt,
court: {
id: courts.id,
name: courts.name,
},
creator: {
id: users.id,
name: users.name,
surname: users.surname,
},
})
.from(courtBlocks)
.leftJoin(courts, eq(courtBlocks.courtId, courts.id))
.innerJoin(users, eq(courtBlocks.createdBy, users.id));
const rawBlocks = includeExpired
? await query.orderBy(asc(courtBlocks.date), asc(courtBlocks.startTime))
: await query
.where(gte(courtBlocks.date, today))
.orderBy(asc(courtBlocks.date), asc(courtBlocks.startTime));
// Transform to flat structure for frontend
const blocks = rawBlocks.map((block) => ({
id: block.id,
courtId: block.courtId,
courtName: block.court?.name || null,
date: block.date,
startTime: block.startTime,
endTime: block.endTime,
reason: block.reason,
createdBy: block.createdBy,
creatorName: block.creator ? `${block.creator.name} ${block.creator.surname}` : 'Unknown',
createdAt: block.createdAt,
}));
return NextResponse.json({ blocks });
} catch (error) {
console.error('Error fetching blocks:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
export async function POST(request: NextRequest) {
try {
const session = await getSession();
if (!session || session.role !== 'admin') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { courtId, date, startTime, endTime, reason, createAnnouncement } = await request.json();
if (!date || !startTime || !endTime || !reason) {
return NextResponse.json(
{ error: 'Missing required fields: date, startTime, endTime, reason' },
{ status: 400 }
);
}
// Validate date is not in the past
const blockDate = new Date(date);
const today = new Date();
today.setHours(0, 0, 0, 0);
if (blockDate < today) {
return NextResponse.json({ error: 'Cannot create blocks for past dates' }, { status: 400 });
}
// Get court name if courtId is provided
let courtName = 'All Courts';
if (courtId) {
const court = await db.select().from(courts).where(eq(courts.id, courtId)).limit(1);
if (court.length === 0) {
return NextResponse.json({ error: 'Court not found' }, { status: 400 });
}
courtName = court[0].name;
}
// Create the block
const blockId = crypto.randomUUID();
const [newBlock] = await db
.insert(courtBlocks)
.values({
id: blockId,
courtId: courtId || null, // null means all courts
date,
startTime,
endTime,
reason,
createdBy: session.userId,
createdAt: new Date(),
})
.returning();
// Log activity
await logActivity({
userId: session.userId,
action: ACTIONS.BLOCK_CREATE,
entityType: ENTITY_TYPES.COURT_BLOCK,
entityId: blockId,
details: {
courtId: courtId || 'all',
date,
startTime,
endTime,
reason,
},
});
// Optionally create an announcement that expires when the block ends
let announcementCreated = false;
if (createAnnouncement) {
const formattedDate = new Date(date).toLocaleDateString('en-IE', {
weekday: 'long',
day: 'numeric',
month: 'long',
year: 'numeric',
});
// Set expiry to end of block day at the end time
const expiryDate = new Date(date);
const [endHour] = endTime.split(':').map(Number);
expiryDate.setHours(endHour, 0, 0, 0);
await db.insert(announcements).values({
id: crypto.randomUUID(),
title: `${reason} - ${formattedDate}`,
content: `${courtName} will be unavailable on ${formattedDate} from ${startTime} to ${endTime} due to: ${reason}`,
priority: 'high',
expiresAt: expiryDate,
isActive: true,
createdAt: new Date(),
updatedAt: new Date(),
});
announcementCreated = true;
}
return NextResponse.json({
block: newBlock,
announcementCreated,
message: 'Block created successfully',
});
} catch (error) {
console.error('Error creating block:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}