Files
crewli/api/app/FormBuilder/Defaults/ArtistAdvanceDefault.php
bert.hausmans 895a1690e7 feat(timetable): ArtistAdvanceDefault seeder + bootstrap
Seeds 5 default sections per RFC v0.2 D15 (General Info, Contacts,
Production, Technical Rider, Hospitality) on a per-organisation
artist_advance FormSchema with section_level_submit=true. Each
section ships with 3-4 illustrative form_fields; organisations
customise via the FormBuilder UI later.

Wired into org-creation via the new OrganisationObserver so new
tenants receive the schema automatically. Existing orgs get
coverage via the new artist:seed-advance-default artisan command
(idempotent — orgs that already own a schema are skipped).

Note: introduces a new production-grade default-seeder convention.
Prior FormBuilder defaults were dev-only via FormBuilderDevSeeder
called from DevSeeder::run(). This is the first non-dev path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 22:16:25 +02:00

182 lines
8.1 KiB
PHP

<?php
declare(strict_types=1);
namespace App\FormBuilder\Defaults;
use App\Enums\Artist\AdvanceSectionType;
use App\Enums\FormBuilder\FormFieldType;
use App\Enums\FormBuilder\FormPurpose;
use App\Enums\FormBuilder\FormSchemaSnapshotMode;
use App\Enums\FormBuilder\FormSubmissionMode;
use App\Models\FormBuilder\FormField;
use App\Models\FormBuilder\FormSchema;
use App\Models\FormBuilder\FormSchemaSection;
use App\Models\Organisation;
use App\Models\Scopes\OrganisationScope;
use Illuminate\Support\Facades\DB;
/**
* Default `artist_advance` FormSchema bootstrap per RFC-TIMETABLE
* v0.2 D15. One schema per organisation, with five sections mapped
* to AdvanceSectionType:
*
* - General Info → Custom
* - Contacts → Contacts
* - Production → Production
* - Technical Rider → Production
* - Hospitality → Custom
*
* Each section carries 3-4 illustrative fields. Organisations
* customise via the FormBuilder UI later. The schema is published
* and uses section_level_submit per ARCH-FORM-BUILDER §3.2.5.
*
* Idempotent: if an organisation already owns an artist_advance
* schema (any one), the seeder no-ops and returns the existing row.
*
* Bridge to per-engagement AdvanceSection rows: FormSchemaSection
* slug matches AdvanceSectionType::value (where applicable). Sections
* carrying type=Custom use a stable slug per row name.
*/
final class ArtistAdvanceDefault
{
public static function seedFor(Organisation $organisation): FormSchema
{
$existing = FormSchema::query()
->withoutGlobalScope(OrganisationScope::class)
->where('organisation_id', $organisation->id)
->where('purpose', FormPurpose::ARTIST_ADVANCE->value)
->first();
if ($existing instanceof FormSchema) {
return $existing;
}
return DB::transaction(static function () use ($organisation): FormSchema {
$schema = FormSchema::create([
'organisation_id' => $organisation->id,
'owner_type' => 'organisation',
'owner_id' => $organisation->id,
'name' => 'Artiest advance',
'slug' => 'artiest-advance',
'purpose' => FormPurpose::ARTIST_ADVANCE->value,
'description' => 'Standaard advance-formulier voor artiesten. Pas de secties en velden aan via de FormBuilder.',
'is_published' => true,
'submission_mode' => FormSubmissionMode::DRAFT_SINGLE->value,
'locale' => 'nl',
'snapshot_mode' => FormSchemaSnapshotMode::ON_SUBMIT->value,
'freeze_on_submit' => false,
'section_level_submit' => true,
'auto_save_enabled' => true,
'version' => 1,
]);
foreach (self::sectionDefinitions() as $sortOrder => $def) {
$section = FormSchemaSection::create([
'form_schema_id' => $schema->id,
'slug' => $def['slug'],
'name' => $def['name'],
'sort_order' => $sortOrder + 1,
'submit_independent' => true,
'required_for_schema_submit' => true,
]);
foreach ($def['fields'] as $fieldOrder => $field) {
FormField::create([
'form_schema_id' => $schema->id,
'form_schema_section_id' => $section->id,
'field_type' => $field['type']->value,
'slug' => $field['slug'],
'label' => $field['label'],
'help_text' => $field['help_text'] ?? null,
'is_required' => $field['is_required'] ?? false,
'is_filterable' => false,
'is_portal_visible' => true,
'is_admin_only' => false,
'is_pii' => $field['is_pii'] ?? false,
'display_width' => $field['display_width'] ?? 'full',
'value_storage_hint' => ($field['type']->recommendedValueStorageHint())->value,
'sort_order' => $fieldOrder + 1,
]);
}
}
return $schema->refresh();
});
}
/**
* @return array<int, array{
* slug: string,
* name: string,
* advance_type: AdvanceSectionType,
* fields: array<int, array{
* type: FormFieldType,
* slug: string,
* label: string,
* help_text?: string,
* is_required?: bool,
* is_pii?: bool,
* display_width?: string,
* }>
* }>
*/
private static function sectionDefinitions(): array
{
return [
[
'slug' => 'general-info',
'name' => 'Algemeen',
'advance_type' => AdvanceSectionType::Custom,
'fields' => [
['type' => FormFieldType::DATETIME, 'slug' => 'arrival-datetime', 'label' => 'Aankomsttijd', 'is_required' => true, 'display_width' => 'half'],
['type' => FormFieldType::DATETIME, 'slug' => 'departure-datetime', 'label' => 'Vertrektijd', 'is_required' => true, 'display_width' => 'half'],
['type' => FormFieldType::TEXTAREA, 'slug' => 'general-notes', 'label' => 'Opmerkingen'],
],
],
[
'slug' => 'contacts',
'name' => 'Contactpersonen',
'advance_type' => AdvanceSectionType::Contacts,
'fields' => [
['type' => FormFieldType::TEXT, 'slug' => 'tour-manager-name', 'label' => 'Tour manager', 'is_required' => true, 'is_pii' => true, 'display_width' => 'full'],
['type' => FormFieldType::EMAIL, 'slug' => 'tour-manager-email', 'label' => 'E-mail tour manager', 'is_required' => true, 'is_pii' => true, 'display_width' => 'half'],
['type' => FormFieldType::PHONE, 'slug' => 'tour-manager-phone', 'label' => 'Telefoon tour manager', 'is_pii' => true, 'display_width' => 'half'],
['type' => FormFieldType::TABLE_ROWS, 'slug' => 'additional-contacts', 'label' => 'Aanvullende contactpersonen', 'is_pii' => true],
],
],
[
'slug' => 'production',
'name' => 'Productie',
'advance_type' => AdvanceSectionType::Production,
'fields' => [
['type' => FormFieldType::FILE_UPLOAD, 'slug' => 'stage-plot', 'label' => 'Stage plot'],
['type' => FormFieldType::TEXTAREA, 'slug' => 'monitor-needs', 'label' => 'Monitorwensen'],
['type' => FormFieldType::TEXTAREA, 'slug' => 'special-equipment', 'label' => 'Specifieke apparatuur'],
],
],
[
'slug' => 'technical-rider',
'name' => 'Technische rider',
'advance_type' => AdvanceSectionType::Production,
'fields' => [
['type' => FormFieldType::FILE_UPLOAD, 'slug' => 'input-list', 'label' => 'Input list'],
['type' => FormFieldType::TEXTAREA, 'slug' => 'microphone-preferences', 'label' => 'Microfoonvoorkeuren'],
['type' => FormFieldType::TEXTAREA, 'slug' => 'backline-requirements', 'label' => 'Backline'],
],
],
[
'slug' => 'hospitality',
'name' => 'Hospitality',
'advance_type' => AdvanceSectionType::Custom,
'fields' => [
['type' => FormFieldType::TEXTAREA, 'slug' => 'dressing-room-requirements', 'label' => 'Kleedkamer'],
['type' => FormFieldType::TEXTAREA, 'slug' => 'food-preferences', 'label' => 'Cateringvoorkeuren'],
['type' => FormFieldType::TEXTAREA, 'slug' => 'drinks', 'label' => 'Drankvoorkeuren'],
['type' => FormFieldType::TEXT, 'slug' => 'allergies', 'label' => 'Allergieën', 'is_pii' => true],
],
],
];
}
}