first commit

This commit is contained in:
Michaël
2026-01-29 14:13:11 -03:00
commit 2302748c87
105 changed files with 93301 additions and 0 deletions

View File

@@ -0,0 +1,269 @@
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
import { db, events, tickets } from '../db/index.js';
import { eq, desc, and, gte, sql } from 'drizzle-orm';
import { requireAuth, getAuthUser } from '../lib/auth.js';
import { generateId, getNow } from '../lib/utils.js';
interface UserContext {
id: string;
email: string;
name: string;
role: string;
}
const eventsRouter = new Hono<{ Variables: { user: UserContext } }>();
// Custom validation error handler
const validationHook = (result: any, c: any) => {
if (!result.success) {
const errors = result.error.issues.map((i: any) => `${i.path.join('.')}: ${i.message}`).join(', ');
return c.json({ error: errors }, 400);
}
};
const createEventSchema = z.object({
title: z.string().min(1),
titleEs: z.string().optional().nullable(),
description: z.string().min(1),
descriptionEs: z.string().optional().nullable(),
startDatetime: z.string(),
endDatetime: z.string().optional().nullable(),
location: z.string().min(1),
locationUrl: z.string().url().optional().nullable().or(z.literal('')),
price: z.number().min(0).default(0),
currency: z.string().default('PYG'),
capacity: z.number().min(1).default(50),
status: z.enum(['draft', 'published', 'cancelled', 'completed', 'archived']).default('draft'),
// Accept relative paths (/uploads/...) or full URLs
bannerUrl: z.string().optional().nullable().or(z.literal('')),
});
const updateEventSchema = createEventSchema.partial();
// Get all events (public)
eventsRouter.get('/', async (c) => {
const status = c.req.query('status');
const upcoming = c.req.query('upcoming');
let query = (db as any).select().from(events);
if (status) {
query = query.where(eq((events as any).status, status));
}
if (upcoming === 'true') {
const now = getNow();
query = query.where(
and(
eq((events as any).status, 'published'),
gte((events as any).startDatetime, now)
)
);
}
const result = await query.orderBy(desc((events as any).startDatetime)).all();
// Get ticket counts for each event
const eventsWithCounts = await Promise.all(
result.map(async (event: any) => {
const ticketCount = await (db as any)
.select({ count: sql<number>`count(*)` })
.from(tickets)
.where(
and(
eq((tickets as any).eventId, event.id),
eq((tickets as any).status, 'confirmed')
)
)
.get();
return {
...event,
bookedCount: ticketCount?.count || 0,
availableSeats: event.capacity - (ticketCount?.count || 0),
};
})
);
return c.json({ events: eventsWithCounts });
});
// Get single event (public)
eventsRouter.get('/:id', async (c) => {
const id = c.req.param('id');
const event = await (db as any).select().from(events).where(eq((events as any).id, id)).get();
if (!event) {
return c.json({ error: 'Event not found' }, 404);
}
// Get ticket count
const ticketCount = await (db as any)
.select({ count: sql<number>`count(*)` })
.from(tickets)
.where(
and(
eq((tickets as any).eventId, id),
eq((tickets as any).status, 'confirmed')
)
)
.get();
return c.json({
event: {
...event,
bookedCount: ticketCount?.count || 0,
availableSeats: event.capacity - (ticketCount?.count || 0),
},
});
});
// Get next upcoming event (public)
eventsRouter.get('/next/upcoming', async (c) => {
const now = getNow();
const event = await (db as any)
.select()
.from(events)
.where(
and(
eq((events as any).status, 'published'),
gte((events as any).startDatetime, now)
)
)
.orderBy((events as any).startDatetime)
.limit(1)
.get();
if (!event) {
return c.json({ event: null });
}
const ticketCount = await (db as any)
.select({ count: sql<number>`count(*)` })
.from(tickets)
.where(
and(
eq((tickets as any).eventId, event.id),
eq((tickets as any).status, 'confirmed')
)
)
.get();
return c.json({
event: {
...event,
bookedCount: ticketCount?.count || 0,
availableSeats: event.capacity - (ticketCount?.count || 0),
},
});
});
// Create event (admin/organizer only)
eventsRouter.post('/', requireAuth(['admin', 'organizer']), zValidator('json', createEventSchema, validationHook), async (c) => {
const data = c.req.valid('json');
const user = c.get('user');
const now = getNow();
const id = generateId();
const newEvent = {
id,
...data,
createdAt: now,
updatedAt: now,
};
await (db as any).insert(events).values(newEvent);
return c.json({ event: newEvent }, 201);
});
// Update event (admin/organizer only)
eventsRouter.put('/:id', requireAuth(['admin', 'organizer']), zValidator('json', updateEventSchema, validationHook), async (c) => {
const id = c.req.param('id');
const data = c.req.valid('json');
const existing = await (db as any).select().from(events).where(eq((events as any).id, id)).get();
if (!existing) {
return c.json({ error: 'Event not found' }, 404);
}
const now = getNow();
await (db as any)
.update(events)
.set({ ...data, updatedAt: now })
.where(eq((events as any).id, id));
const updated = await (db as any).select().from(events).where(eq((events as any).id, id)).get();
return c.json({ event: updated });
});
// Delete event (admin only)
eventsRouter.delete('/:id', requireAuth(['admin']), async (c) => {
const id = c.req.param('id');
const existing = await (db as any).select().from(events).where(eq((events as any).id, id)).get();
if (!existing) {
return c.json({ error: 'Event not found' }, 404);
}
await (db as any).delete(events).where(eq((events as any).id, id));
return c.json({ message: 'Event deleted successfully' });
});
// Get event attendees (admin/organizer only)
eventsRouter.get('/:id/attendees', requireAuth(['admin', 'organizer', 'staff']), async (c) => {
const id = c.req.param('id');
const attendees = await (db as any)
.select()
.from(tickets)
.where(eq((tickets as any).eventId, id))
.all();
return c.json({ attendees });
});
// Duplicate event (admin/organizer only)
eventsRouter.post('/:id/duplicate', requireAuth(['admin', 'organizer']), async (c) => {
const id = c.req.param('id');
const existing = await (db as any).select().from(events).where(eq((events as any).id, id)).get();
if (!existing) {
return c.json({ error: 'Event not found' }, 404);
}
const now = getNow();
const newId = generateId();
// Create a copy with modified title and draft status
const duplicatedEvent = {
id: newId,
title: `${existing.title} (Copy)`,
titleEs: existing.titleEs ? `${existing.titleEs} (Copia)` : null,
description: existing.description,
descriptionEs: existing.descriptionEs,
startDatetime: existing.startDatetime,
endDatetime: existing.endDatetime,
location: existing.location,
locationUrl: existing.locationUrl,
price: existing.price,
currency: existing.currency,
capacity: existing.capacity,
status: 'draft',
bannerUrl: existing.bannerUrl,
createdAt: now,
updatedAt: now,
};
await (db as any).insert(events).values(duplicatedEvent);
return c.json({ event: duplicatedEvent, message: 'Event duplicated successfully' }, 201);
});
export default eventsRouter;