feat: add customizable event types with admin management

Implements GitHub issue #139 - allows users to create and manage custom
event types beyond the default presets (wedding, birthday, corporate, other).

Backend:
- Add event_types table migration with default system types
- Create eventTypeService for CRUD operations with legacy fallback
- Add adminEventTypes routes with full REST API
- Update event validation to use dynamic event types
- Update slug generation to use custom slug_prefix

Frontend:
- Add EventTypesPage with full CRUD admin interface
- Add eventTypes.service.ts API client
- Update CreateEventPage to fetch types dynamically
- Add Event Types navigation in admin sidebar
- Add i18n translations (EN/DE)

Backward compatible: existing galleries continue to work, legacy types
accepted even if database is empty via fallback mechanisms.
This commit is contained in:
Paul Nothaft
2026-01-22 13:54:23 +01:00
parent 6b3ead747b
commit f8881d5bd6
16 changed files with 1502 additions and 24 deletions
@@ -0,0 +1,107 @@
/**
* Migration: Add event_types table
*
* Creates a customizable event types system to replace hardcoded event types.
* This allows users to:
* - Rename existing event types (wedding, birthday, corporate, other)
* - Create custom event types with custom slug prefixes
* - Associate default theme presets with event types
*
* Backward compatible: Existing events keep their event_type strings.
* New events can use either legacy strings or custom event type slug_prefix.
*/
const { createTableIfNotExists, insertIfNotExists } = require('../helpers');
exports.up = async function(knex) {
console.log('Creating event_types table...');
// Create event_types table
const hasEventTypesTable = await knex.schema.hasTable('event_types');
if (!hasEventTypesTable) {
await knex.schema.createTable('event_types', (table) => {
table.increments('id').primary();
table.string('name', 100).notNullable(); // Display name: "Family Shoot"
table.string('slug_prefix', 50).unique().notNullable(); // URL prefix: "family"
table.string('emoji', 10); // Icon emoji: "👨‍👩‍👧"
table.string('theme_preset', 50); // Default theme: "elegantWedding"
table.text('theme_config'); // Custom theme JSON overrides (optional)
table.integer('display_order').defaultTo(0); // Sorting in dropdowns
table.boolean('is_system').defaultTo(false); // Protect default types
table.boolean('is_active').defaultTo(true); // Allow hiding types
table.timestamp('created_at').defaultTo(knex.fn.now());
table.timestamp('updated_at').defaultTo(knex.fn.now());
// Indexes for performance
table.index('slug_prefix');
table.index('display_order');
table.index('is_active');
});
console.log('event_types table created');
} else {
console.log('event_types table already exists, skipping creation');
}
// Seed default event types (matching current hardcoded values)
const existingTypes = await knex('event_types').select('slug_prefix');
const existingSlugs = existingTypes.map(t => t.slug_prefix);
const defaultTypes = [
{
name: 'Wedding',
slug_prefix: 'wedding',
emoji: '💒',
theme_preset: 'elegantWedding',
display_order: 1,
is_system: true,
is_active: true
},
{
name: 'Birthday',
slug_prefix: 'birthday',
emoji: '🎂',
theme_preset: 'birthdayFun',
display_order: 2,
is_system: true,
is_active: true
},
{
name: 'Corporate',
slug_prefix: 'corporate',
emoji: '🏢',
theme_preset: 'corporateTimeline',
display_order: 3,
is_system: true,
is_active: true
},
{
name: 'Other',
slug_prefix: 'other',
emoji: '📸',
theme_preset: 'default',
display_order: 4,
is_system: true,
is_active: true
}
];
const typesToInsert = defaultTypes.filter(type => !existingSlugs.includes(type.slug_prefix));
if (typesToInsert.length > 0) {
await knex('event_types').insert(typesToInsert);
console.log(`Inserted ${typesToInsert.length} default event types`);
} else {
console.log('Default event types already exist, skipping seed');
}
console.log('Migration 061_add_event_types_table completed successfully');
};
exports.down = async function(knex) {
console.log('Rolling back event_types table...');
// Drop the table (data will be lost)
await knex.schema.dropTableIfExists('event_types');
console.log('event_types table dropped');
};