feat(setup): event-types step in first-run wizard + un-hardcode event type deps (#800)

Fresh installs can now shape the event-type catalog during the setup
wizard — rename, delete or replace the seeded defaults while nothing
references them. On existing installs system types stay protected.

- New wizard step between features and config: edit name/URL prefix,
  remove, or add types; defaults shown as recommendations
- setup_wizard_completed app setting (migration 161): seeded true when
  an admin already exists, false on fresh installs; POST /api/setup/
  complete (adminAuth) flips it when the wizard finishes
- deleteEventType: system types deletable only while the flag is unset;
  in-use check extended to quotes; per-type reminder template
  (event_reminder_<slug>) is deleted with the type
- reminder-template self-heal no longer resurrects templates for slugs
  removed from the catalog
- v1 API event creation validates event_type against the live catalog
  instead of a hardcoded whitelist (custom types were rejected; the
  never-seeded 'family' slug is no longer silently accepted)
- contract→event conversion resolves the event type via
  crm_default_event_type / resolveDefaultEventType instead of
  hardcoding 'wedding' (resolveDefaultEventType moved from quoteService
  to eventTypeService for reuse)
This commit is contained in:
Paul Nothaft
2026-07-15 21:30:23 +02:00
parent aab9e1a937
commit 7eb6357b4a
14 changed files with 573 additions and 47 deletions
@@ -0,0 +1,43 @@
/**
* Migration 161: `setup_wizard_completed` app setting (#800).
*
* The setup wizard gains an event-types step that may rename or DELETE the
* seeded system event types. That is only safe on a pristine install, so the
* backend gates system-type deletion on this flag being unset (plus zero
* usage — see eventTypeService.deleteEventType).
*
* Backfill rule: any install that already has an admin account predates the
* wizard step (or already finished the wizard), so it is marked completed
* here — the deletion window never opens on existing setups. A genuinely
* fresh install runs this migration BEFORE its first admin is created, so
* the flag starts false and the wizard's finish call flips it to true.
*
* Idempotent: skips when the key already exists. Values are JSON-stringified
* to match getAppSetting's JSON.parse on read.
*/
exports.up = async function up(knex) {
if (!(await knex.schema.hasTable('app_settings'))) return;
const existing = await knex('app_settings')
.where({ setting_key: 'setup_wizard_completed' })
.first();
if (existing) return;
let hasAdmin = false;
if (await knex.schema.hasTable('admin_users')) {
const row = await knex('admin_users').count({ c: '*' }).first();
hasAdmin = Number(row?.c || 0) > 0;
}
await knex('app_settings').insert({
setting_key: 'setup_wizard_completed',
setting_value: JSON.stringify(hasAdmin),
setting_type: 'boolean',
updated_at: new Date(),
});
};
exports.down = async function down(knex) {
if (!(await knex.schema.hasTable('app_settings'))) return;
await knex('app_settings').where({ setting_key: 'setup_wizard_completed' }).del();
};