Add ticket system with QR scanner and PDF generation
- Add ticket validation and check-in API endpoints - Add PDF ticket generation with QR codes (pdfkit) - Add admin QR scanner page with camera support - Add admin site settings page - Update email templates with PDF ticket download link - Add checked_in_by_admin_id field for audit tracking - Update booking success page with ticket download - Various UI improvements to events and booking pages
This commit is contained in:
@@ -74,6 +74,8 @@ async function migrate() {
|
||||
title_es TEXT,
|
||||
description TEXT NOT NULL,
|
||||
description_es TEXT,
|
||||
short_description TEXT,
|
||||
short_description_es TEXT,
|
||||
start_datetime TEXT NOT NULL,
|
||||
end_datetime TEXT,
|
||||
location TEXT NOT NULL,
|
||||
@@ -98,6 +100,14 @@ async function migrate() {
|
||||
await (db as any).run(sql`ALTER TABLE events ADD COLUMN external_booking_url TEXT`);
|
||||
} catch (e) { /* column may already exist */ }
|
||||
|
||||
// Add short description columns to events
|
||||
try {
|
||||
await (db as any).run(sql`ALTER TABLE events ADD COLUMN short_description TEXT`);
|
||||
} catch (e) { /* column may already exist */ }
|
||||
try {
|
||||
await (db as any).run(sql`ALTER TABLE events ADD COLUMN short_description_es TEXT`);
|
||||
} catch (e) { /* column may already exist */ }
|
||||
|
||||
await (db as any).run(sql`
|
||||
CREATE TABLE IF NOT EXISTS tickets (
|
||||
id TEXT PRIMARY KEY,
|
||||
@@ -151,6 +161,11 @@ async function migrate() {
|
||||
`);
|
||||
} catch (e) { /* migration may have already run */ }
|
||||
|
||||
// Migration: Add checked_in_by_admin_id column to tickets
|
||||
try {
|
||||
await (db as any).run(sql`ALTER TABLE tickets ADD COLUMN checked_in_by_admin_id TEXT REFERENCES users(id)`);
|
||||
} catch (e) { /* column may already exist */ }
|
||||
|
||||
// Make attendee_email and attendee_phone nullable (recreate table if needed or just allow nulls for new entries)
|
||||
// SQLite doesn't support altering column constraints, so we'll just ensure new entries work
|
||||
|
||||
@@ -347,6 +362,28 @@ async function migrate() {
|
||||
updated_at TEXT NOT NULL
|
||||
)
|
||||
`);
|
||||
|
||||
// Site settings table
|
||||
await (db as any).run(sql`
|
||||
CREATE TABLE IF NOT EXISTS site_settings (
|
||||
id TEXT PRIMARY KEY,
|
||||
timezone TEXT NOT NULL DEFAULT 'America/Asuncion',
|
||||
site_name TEXT NOT NULL DEFAULT 'Spanglish',
|
||||
site_description TEXT,
|
||||
site_description_es TEXT,
|
||||
contact_email TEXT,
|
||||
contact_phone TEXT,
|
||||
facebook_url TEXT,
|
||||
instagram_url TEXT,
|
||||
twitter_url TEXT,
|
||||
linkedin_url TEXT,
|
||||
maintenance_mode INTEGER NOT NULL DEFAULT 0,
|
||||
maintenance_message TEXT,
|
||||
maintenance_message_es TEXT,
|
||||
updated_at TEXT NOT NULL,
|
||||
updated_by TEXT REFERENCES users(id)
|
||||
)
|
||||
`);
|
||||
} else {
|
||||
// PostgreSQL migrations
|
||||
await (db as any).execute(sql`
|
||||
@@ -415,6 +452,8 @@ async function migrate() {
|
||||
title_es VARCHAR(255),
|
||||
description TEXT NOT NULL,
|
||||
description_es TEXT,
|
||||
short_description VARCHAR(300),
|
||||
short_description_es VARCHAR(300),
|
||||
start_datetime TIMESTAMP NOT NULL,
|
||||
end_datetime TIMESTAMP,
|
||||
location VARCHAR(500) NOT NULL,
|
||||
@@ -439,6 +478,14 @@ async function migrate() {
|
||||
await (db as any).execute(sql`ALTER TABLE events ADD COLUMN external_booking_url VARCHAR(500)`);
|
||||
} catch (e) { /* column may already exist */ }
|
||||
|
||||
// Add short description columns to events
|
||||
try {
|
||||
await (db as any).execute(sql`ALTER TABLE events ADD COLUMN short_description VARCHAR(300)`);
|
||||
} catch (e) { /* column may already exist */ }
|
||||
try {
|
||||
await (db as any).execute(sql`ALTER TABLE events ADD COLUMN short_description_es VARCHAR(300)`);
|
||||
} catch (e) { /* column may already exist */ }
|
||||
|
||||
await (db as any).execute(sql`
|
||||
CREATE TABLE IF NOT EXISTS tickets (
|
||||
id UUID PRIMARY KEY,
|
||||
@@ -462,6 +509,11 @@ async function migrate() {
|
||||
try {
|
||||
await (db as any).execute(sql`ALTER TABLE tickets ADD COLUMN attendee_ruc VARCHAR(15)`);
|
||||
} catch (e) { /* column may already exist */ }
|
||||
|
||||
// Add checked_in_by_admin_id column to tickets
|
||||
try {
|
||||
await (db as any).execute(sql`ALTER TABLE tickets ADD COLUMN checked_in_by_admin_id UUID REFERENCES users(id)`);
|
||||
} catch (e) { /* column may already exist */ }
|
||||
|
||||
await (db as any).execute(sql`
|
||||
CREATE TABLE IF NOT EXISTS payments (
|
||||
@@ -642,6 +694,28 @@ async function migrate() {
|
||||
updated_at TIMESTAMP NOT NULL
|
||||
)
|
||||
`);
|
||||
|
||||
// Site settings table
|
||||
await (db as any).execute(sql`
|
||||
CREATE TABLE IF NOT EXISTS site_settings (
|
||||
id UUID PRIMARY KEY,
|
||||
timezone VARCHAR(100) NOT NULL DEFAULT 'America/Asuncion',
|
||||
site_name VARCHAR(255) NOT NULL DEFAULT 'Spanglish',
|
||||
site_description TEXT,
|
||||
site_description_es TEXT,
|
||||
contact_email VARCHAR(255),
|
||||
contact_phone VARCHAR(50),
|
||||
facebook_url VARCHAR(500),
|
||||
instagram_url VARCHAR(500),
|
||||
twitter_url VARCHAR(500),
|
||||
linkedin_url VARCHAR(500),
|
||||
maintenance_mode INTEGER NOT NULL DEFAULT 0,
|
||||
maintenance_message TEXT,
|
||||
maintenance_message_es TEXT,
|
||||
updated_at TIMESTAMP NOT NULL,
|
||||
updated_by UUID REFERENCES users(id)
|
||||
)
|
||||
`);
|
||||
}
|
||||
|
||||
console.log('Migrations completed successfully!');
|
||||
|
||||
@@ -66,6 +66,8 @@ export const sqliteEvents = sqliteTable('events', {
|
||||
titleEs: text('title_es'),
|
||||
description: text('description').notNull(),
|
||||
descriptionEs: text('description_es'),
|
||||
shortDescription: text('short_description'),
|
||||
shortDescriptionEs: text('short_description_es'),
|
||||
startDatetime: text('start_datetime').notNull(),
|
||||
endDatetime: text('end_datetime'),
|
||||
location: text('location').notNull(),
|
||||
@@ -93,6 +95,7 @@ export const sqliteTickets = sqliteTable('tickets', {
|
||||
preferredLanguage: text('preferred_language'),
|
||||
status: text('status', { enum: ['pending', 'confirmed', 'cancelled', 'checked_in'] }).notNull().default('pending'),
|
||||
checkinAt: text('checkin_at'),
|
||||
checkedInByAdminId: text('checked_in_by_admin_id').references(() => sqliteUsers.id), // Who performed the check-in
|
||||
qrCode: text('qr_code'),
|
||||
adminNote: text('admin_note'),
|
||||
createdAt: text('created_at').notNull(),
|
||||
@@ -246,6 +249,32 @@ export const sqliteEmailSettings = sqliteTable('email_settings', {
|
||||
updatedAt: text('updated_at').notNull(),
|
||||
});
|
||||
|
||||
// Site Settings table for global website configuration
|
||||
export const sqliteSiteSettings = sqliteTable('site_settings', {
|
||||
id: text('id').primaryKey(),
|
||||
// Timezone configuration
|
||||
timezone: text('timezone').notNull().default('America/Asuncion'),
|
||||
// Site info
|
||||
siteName: text('site_name').notNull().default('Spanglish'),
|
||||
siteDescription: text('site_description'),
|
||||
siteDescriptionEs: text('site_description_es'),
|
||||
// Contact info
|
||||
contactEmail: text('contact_email'),
|
||||
contactPhone: text('contact_phone'),
|
||||
// Social links (can also be stored here as fallback)
|
||||
facebookUrl: text('facebook_url'),
|
||||
instagramUrl: text('instagram_url'),
|
||||
twitterUrl: text('twitter_url'),
|
||||
linkedinUrl: text('linkedin_url'),
|
||||
// Other settings
|
||||
maintenanceMode: integer('maintenance_mode', { mode: 'boolean' }).notNull().default(false),
|
||||
maintenanceMessage: text('maintenance_message'),
|
||||
maintenanceMessageEs: text('maintenance_message_es'),
|
||||
// Metadata
|
||||
updatedAt: text('updated_at').notNull(),
|
||||
updatedBy: text('updated_by').references(() => sqliteUsers.id),
|
||||
});
|
||||
|
||||
// ==================== PostgreSQL Schema ====================
|
||||
export const pgUsers = pgTable('users', {
|
||||
id: uuid('id').primaryKey(),
|
||||
@@ -308,6 +337,8 @@ export const pgEvents = pgTable('events', {
|
||||
titleEs: varchar('title_es', { length: 255 }),
|
||||
description: pgText('description').notNull(),
|
||||
descriptionEs: pgText('description_es'),
|
||||
shortDescription: varchar('short_description', { length: 300 }),
|
||||
shortDescriptionEs: varchar('short_description_es', { length: 300 }),
|
||||
startDatetime: timestamp('start_datetime').notNull(),
|
||||
endDatetime: timestamp('end_datetime'),
|
||||
location: varchar('location', { length: 500 }).notNull(),
|
||||
@@ -335,6 +366,7 @@ export const pgTickets = pgTable('tickets', {
|
||||
preferredLanguage: varchar('preferred_language', { length: 10 }),
|
||||
status: varchar('status', { length: 20 }).notNull().default('pending'),
|
||||
checkinAt: timestamp('checkin_at'),
|
||||
checkedInByAdminId: uuid('checked_in_by_admin_id').references(() => pgUsers.id), // Who performed the check-in
|
||||
qrCode: varchar('qr_code', { length: 255 }),
|
||||
adminNote: pgText('admin_note'),
|
||||
createdAt: timestamp('created_at').notNull(),
|
||||
@@ -480,6 +512,32 @@ export const pgEmailSettings = pgTable('email_settings', {
|
||||
updatedAt: timestamp('updated_at').notNull(),
|
||||
});
|
||||
|
||||
// Site Settings table for global website configuration
|
||||
export const pgSiteSettings = pgTable('site_settings', {
|
||||
id: uuid('id').primaryKey(),
|
||||
// Timezone configuration
|
||||
timezone: varchar('timezone', { length: 100 }).notNull().default('America/Asuncion'),
|
||||
// Site info
|
||||
siteName: varchar('site_name', { length: 255 }).notNull().default('Spanglish'),
|
||||
siteDescription: pgText('site_description'),
|
||||
siteDescriptionEs: pgText('site_description_es'),
|
||||
// Contact info
|
||||
contactEmail: varchar('contact_email', { length: 255 }),
|
||||
contactPhone: varchar('contact_phone', { length: 50 }),
|
||||
// Social links
|
||||
facebookUrl: varchar('facebook_url', { length: 500 }),
|
||||
instagramUrl: varchar('instagram_url', { length: 500 }),
|
||||
twitterUrl: varchar('twitter_url', { length: 500 }),
|
||||
linkedinUrl: varchar('linkedin_url', { length: 500 }),
|
||||
// Other settings
|
||||
maintenanceMode: pgInteger('maintenance_mode').notNull().default(0),
|
||||
maintenanceMessage: pgText('maintenance_message'),
|
||||
maintenanceMessageEs: pgText('maintenance_message_es'),
|
||||
// Metadata
|
||||
updatedAt: timestamp('updated_at').notNull(),
|
||||
updatedBy: uuid('updated_by').references(() => pgUsers.id),
|
||||
});
|
||||
|
||||
// Export the appropriate schema based on DB_TYPE
|
||||
export const users = dbType === 'postgres' ? pgUsers : sqliteUsers;
|
||||
export const events = dbType === 'postgres' ? pgEvents : sqliteEvents;
|
||||
@@ -497,6 +555,7 @@ export const eventPaymentOverrides = dbType === 'postgres' ? pgEventPaymentOverr
|
||||
export const magicLinkTokens = dbType === 'postgres' ? pgMagicLinkTokens : sqliteMagicLinkTokens;
|
||||
export const userSessions = dbType === 'postgres' ? pgUserSessions : sqliteUserSessions;
|
||||
export const invoices = dbType === 'postgres' ? pgInvoices : sqliteInvoices;
|
||||
export const siteSettings = dbType === 'postgres' ? pgSiteSettings : sqliteSiteSettings;
|
||||
|
||||
// Type exports
|
||||
export type User = typeof sqliteUsers.$inferSelect;
|
||||
@@ -523,3 +582,5 @@ export type UserSession = typeof sqliteUserSessions.$inferSelect;
|
||||
export type NewUserSession = typeof sqliteUserSessions.$inferInsert;
|
||||
export type Invoice = typeof sqliteInvoices.$inferSelect;
|
||||
export type NewInvoice = typeof sqliteInvoices.$inferInsert;
|
||||
export type SiteSettings = typeof sqliteSiteSettings.$inferSelect;
|
||||
export type NewSiteSettings = typeof sqliteSiteSettings.$inferInsert;
|
||||
|
||||
Reference in New Issue
Block a user