*/ public function toArray(Request $request): array { // Public endpoints must resolve cross-org โ€” skip OrganisationScope // on the FormField / FormSchemaSection relations (both registered // via addendum Q2 / WS-4 Commit 3). The schema-level tenant check // already happens at PublicFormTokenResolver::resolve(). $this->resource->loadMissing([ 'fields' => fn ($q) => $q->withoutGlobalScope(OrganisationScope::class), 'fields.validationRules', 'fields.configs', 'fields.options', 'sections' => fn ($q) => $q->withoutGlobalScope(OrganisationScope::class), ]); $visibleFields = $this->fields ->filter(fn ($f) => (bool) $f->is_portal_visible && ! (bool) $f->is_admin_only) ->values(); $organisationId = $this->organisation_id; $availableTagsByCategory = $this->availableTagsByCategory($organisationId, $visibleFields); return [ 'id' => $this->id, 'name' => $this->name, 'slug' => $this->slug, 'purpose' => $this->purpose instanceof \BackedEnum ? $this->purpose->value : $this->purpose, 'description' => $this->description, 'locale' => $this->locale, 'version' => (int) $this->version, 'opened_at' => now()->toIso8601String(), 'consent_version' => $this->consent_version, 'submission_deadline' => optional($this->submission_deadline)->toIso8601String(), 'section_level_submit' => (bool) $this->section_level_submit, 'sections' => $this->sections->map(fn ($s) => [ 'id' => $s->id, 'slug' => $s->slug, 'name' => $s->name, 'description' => $s->description, 'sort_order' => (int) $s->sort_order, ])->values()->all(), 'fields' => $visibleFields->map(function (FormField $f) use ($availableTagsByCategory): array { $isTagPicker = $f->field_type === FormFieldType::TAG_PICKER->value; return [ 'id' => $f->id, 'slug' => $f->slug, 'field_type' => $f->field_type, 'label' => $f->label, 'help_text' => $f->help_text, 'options' => $f->options->isNotEmpty() ? app(FormFieldOptionService::class)->toJsonShape($f->options) : null, 'available_tags' => $isTagPicker ? $this->tagsForField($f, $availableTagsByCategory) : null, 'validation_rules' => app(FormFieldValidationRuleService::class)->toJsonShape( $f->validationRules, ), 'configs' => app(FormFieldConfigService::class)->toJsonShape($f->configs), 'is_required' => (bool) $f->is_required, 'display_width' => $f->display_width instanceof \BackedEnum ? $f->display_width->value : $f->display_width, 'conditional_logic' => app(FormFieldConditionalLogicService::class)->toJsonShape( $f->rootConditionalLogicGroup(), ), 'sort_order' => (int) $f->sort_order, 'form_schema_section_id' => $f->form_schema_section_id, ]; })->values()->all(), ]; } /** * Prefetch every active person_tag for the org once and bucket by * category so TAG_PICKER fields can apply their own category filter * without N+1 queries. * * @param \Illuminate\Support\Collection $visibleFields * @return array>> categoryKey โ†’ rows */ private function availableTagsByCategory(?string $organisationId, $visibleFields): array { if ($organisationId === null) { return []; } $hasTagPicker = $visibleFields->contains(fn ($f) => $f->field_type === FormFieldType::TAG_PICKER->value); if (! $hasTagPicker) { return []; } // Named-scope bypass only โ€” don't unintentionally strip future // soft-delete or is_active scopes if any land later. $rows = PersonTag::query() ->withoutGlobalScope(OrganisationScope::class) ->where('organisation_id', $organisationId) ->where('is_active', true) ->orderBy('sort_order') ->orderBy('name') ->get(['id', 'name', 'category']); $grouped = []; foreach ($rows as $tag) { $category = (string) ($tag->category ?? ''); $grouped[$category][] = [ 'id' => (string) $tag->id, 'name' => (string) $tag->name, 'category' => $category, ]; } return $grouped; } /** * @param array>> $byCategory * @return array> */ private function tagsForField(FormField $field, array $byCategory): array { $configs = app(FormFieldConfigService::class)->toJsonShape($field->configs); $filter = (array) ($configs['tag_categories']['categories'] ?? []); if ($filter === []) { $out = []; foreach ($byCategory as $rows) { foreach ($rows as $row) { $out[] = $row; } } return $out; } $out = []; foreach ($filter as $category) { foreach ($byCategory[(string) $category] ?? [] as $row) { $out[] = $row; } } return $out; } }