diff --git a/app/Http/Controllers/PortalServicesController.php b/app/Http/Controllers/PortalServicesController.php index 7aacb939..3774dd1b 100644 --- a/app/Http/Controllers/PortalServicesController.php +++ b/app/Http/Controllers/PortalServicesController.php @@ -9,6 +9,7 @@ use App\Services\CountryCatalogue; use App\Services\IdempotencyConflict; use App\Services\InterestSubmission; use App\Services\LegacyContent; +use App\Services\PortalProfileAccess; use App\Services\Malware\MalwareScanner; use App\Services\VerifiedDocumentBinary; use App\Services\VirtualDesk; @@ -297,6 +298,7 @@ class PortalServicesController extends Controller public function manage(Request $request, MalwareScanner $scanner): Response { + $managementFilters = $request->validate(['q' => 'nullable|string|max:150', 'state' => 'nullable|in:draft,published,withdrawn', 'intent' => 'nullable|in:document,legislation']); $access = app(\App\Services\DocumentManagementAccess::class); if (! $access->full($request->user())) { $documents = $access->documents($request->user())->where('status', '!=', 'deleted'); @@ -304,7 +306,7 @@ class PortalServicesController extends Controller $health['signatures_at'] = $health['signatures_at']?->format(DATE_ATOM); $editorial = $access->editorial($request->user()); - $resources = $editorial ? $access->editorialResources($request->user())->orderByDesc('r.updated_at') + $resources = $editorial ? $this->filterManagedResources($access->editorialResources($request->user()), $managementFilters)->orderByDesc('r.updated_at') ->get(['r.*', 'd.file_name', 'd.media_type', 'd.size_bytes', 'd.issuer', 'd.status as file_status', 'd.data_classification', 'd.version_no as file_version', 'd.rights_status as file_rights_status', 'd.malware_scan_status', 'd.malware_failure_reason']) : collect(); $links = DB::table('portal_resource_links')->whereIn('resource_id', $resources->pluck('id'))->orderBy('id')->get()->groupBy('resource_id'); $revisions = DB::table('portal_resource_revisions')->whereIn('resource_id', $resources->pluck('id'))->orderByDesc('revision_no')->get(['resource_id', 'revision_no', 'change_type', 'actor_user_id', 'created_at'])->groupBy('resource_id'); @@ -313,6 +315,7 @@ class PortalServicesController extends Controller $resource->revisions = $revisions->get($resource->id, collect())->values(); }); return Inertia::render('portal/manage', [ + 'managementFilters' => $managementFilters, 'associationOptions' => $this->contentAssociations($request), 'documentAccessOnly' => ! $editorial, 'editorialScopeOnly' => $editorial, 'resources' => $resources, 'partnerships' => [], 'territories' => [], 'requirementCatalogue' => [], 'requirementAssociationsEnabled' => false, 'disclosureAuthorization' => null, 'incompleteCount' => $resources->filter(fn ($r) => ! $r->description || ! $r->issuer || ! $r->source_url || $r->source_confirmation === 'unconfirmed' || $r->legal_status === 'unknown' || $r->rights_status === 'unconfirmed')->count(), 'possibleDuplicates' => $resources->whereNotNull('duplicate_of_resource_id')->count(), @@ -331,7 +334,7 @@ class PortalServicesController extends Controller ->where('external_id', 'document-repository-disclosure-authorization-2026-10-06-v1') ->where('action', 'public_disclosure_authorized')->first(['external_id', 'occurred_at', 'after_data']); $disclosureAuthorizationData = $disclosureAuthorization ? json_decode($disclosureAuthorization->after_data, true) : null; - $resources = DB::table('portal_resources as r')->join('documents as d', 'd.id', '=', 'r.document_id')->orderByDesc('r.updated_at')->limit(200) + $resources = $this->filterManagedResources(DB::table('portal_resources as r')->join('documents as d', 'd.id', '=', 'r.document_id')->whereNull('d.deleted_at'), $managementFilters)->orderByDesc('r.updated_at')->limit(200) ->get(['r.*', 'd.file_name', 'd.media_type', 'd.size_bytes', 'd.issuer', 'd.status as file_status', 'd.data_classification', 'd.version_no as file_version', 'd.rights_status as file_rights_status', 'd.malware_scan_status', 'd.malware_failure_reason']); $links = DB::table('portal_resource_links')->whereIn('resource_id', $resources->pluck('id'))->orderBy('id')->get()->groupBy('resource_id'); $resources->each(fn ($resource) => $resource->links = $links->get($resource->id, collect())->values()); @@ -342,6 +345,7 @@ class PortalServicesController extends Controller $malwareHealth['signatures_at'] = $malwareHealth['signatures_at']?->format(DATE_ATOM); return Inertia::render('portal/manage', ['resources' => $resources, + 'managementFilters' => $managementFilters, 'associationOptions' => $this->contentAssociations($request), 'requirementCatalogue' => $this->requirementEvidenceEnabled() ? collect(is_array($catalogue = config('document-requirements.catalogue', [])) ? $catalogue : [])->map(fn ($item, $key) => ['key' => $key, ...$item])->values() : collect(), @@ -367,6 +371,39 @@ class PortalServicesController extends Controller 'possibleDuplicates' => DB::table('portal_resources')->whereNotNull('duplicate_of_resource_id')->count()]); } + private function filterManagedResources(Builder $query, array $filters): Builder + { + if (! empty($filters['q'])) { + $term = '%'.addcslashes($filters['q'], '%_\\').'%'; + $query->where(fn ($q) => $q->where('r.title', 'ilike', $term)->orWhere('r.document_number', 'ilike', $term)->orWhere('d.issuer', 'ilike', $term)->orWhere('d.file_name', 'ilike', $term)); + } + if (! empty($filters['state'])) { + $query->where('r.published', $filters['state'] === 'published'); + if ($filters['state'] !== 'published') { + $method = $filters['state'] === 'withdrawn' ? 'whereExists' : 'whereNotExists'; + $query->$method(fn ($q) => $q->selectRaw('1')->from('portal_resource_revisions as history')->whereColumn('history.resource_id', 'r.id')->whereIn('history.change_type', ['unpublished', 'withdrawn'])); + } + } + return $query; + } + + private function contentAssociations(Request $request): array + { + $access = app(\App\Services\DocumentManagementAccess::class); + $resources = $access->full($request->user()) ? DB::table('portal_resources as r')->join('documents as d', 'd.id', '=', 'r.document_id')->whereNull('d.deleted_at') + : ($access->editorial($request->user()) ? $access->editorialResources($request->user()) : null); + $documents = $resources ? $resources->orderBy('r.title')->get(['r.id as value', 'r.title as label', 'r.document_type']) : collect(); + return [ + 'sector' => \App\Models\Sector::choices()->orderBy('name')->get(['id as value', 'name as label']), + 'territory' => app(\App\Services\InvestmentGeography::class)->publicReferences()->orderBy('name')->get(['id as value', 'name as label']), + 'opportunity' => app(PortalProfileAccess::class)->scope(\App\Models\Opportunity::query(), $request->user())->orderBy('title')->get(['id as value', 'title as label']), + 'document' => $documents, + 'procedure' => $documents->where('document_type', 'procedure')->values(), + 'stage' => collect(config('virtual-desk.steps', []))->map(fn ($step, $key) => ['value' => $key, 'label' => $step['title']])->values(), + 'activity' => app(\App\Services\EconomicReferenceCatalog::class)->activities()->map(fn ($row) => ['value' => $row->id, 'label' => $row->display_label])->values(), + ]; + } + public function upload(Request $request): RedirectResponse { $this->validateManagement($request, ['file' => ['required', 'file', 'mimetypes:application/pdf,text/plain,text/csv,image/jpeg,image/png,image/webp,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.oasis.opendocument.text'.(app(LegacyContent::class)->available() ? ',application/json' : ''), 'max:10240'], @@ -399,7 +436,7 @@ class PortalServicesController extends Controller ScanDocumentForMalware::dispatch($id)->afterCommit(); - return to_route('services.manage')->with('success', 'Documento recebido em armazenamento privado e quarentena. Não aparece na biblioteca pública até validação e publicação explícitas.'); + return to_route('services.manage', array_filter(['intent' => in_array($request->input('intent'), ['document', 'legislation'], true) ? $request->input('intent') : null]))->with('uploaded_document_id', $id)->with('success', 'Documento recebido em armazenamento privado e quarentena. Não aparece na biblioteca pública até validação e publicação explícitas.'); } public function resource(Request $request): RedirectResponse @@ -663,7 +700,7 @@ class PortalServicesController extends Controller 'data_classification' => ['nullable', 'in:internal,public'], 'source_reference' => ['required', 'string', 'min:10', 'max:2000'], 'legislation' => ['required', 'boolean'], 'essential' => ['required', 'boolean'], - 'associations' => ['nullable', 'array', 'max:40'], 'associations.*.target_type' => ['required', 'in:theme,module,activity,stage,procedure,entity,territory,document,investment_type,legal_source'.($this->requirementEvidenceEnabled() ? ',requirement' : '')], + 'associations' => ['nullable', 'array', 'max:40'], 'associations.*.target_type' => ['required', 'in:theme,module,activity,stage,procedure,entity,territory,document,investment_type,legal_source,sector,opportunity'.($this->requirementEvidenceEnabled() ? ',requirement' : '')], 'associations.*.target_key' => ['required', 'string', 'max:180'], 'associations.*.review_status' => ['nullable', 'in:pending,confirmed'], 'associations.*.relationship' => ['required', 'in:applies_to,supports,version_of,translation_of,amends,repeals,implements,cites,related_to'], diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index d9f88f49..194e77f6 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -53,6 +53,7 @@ class HandleInertiaRequests extends Middleware 'name' => config('app.name'), 'homologationReserved' => (bool) config('portal.homologation_only', false), 'profileAccess' => app(PortalProfileAccess::class)->navigation($request->user()), + 'contentManagementHref' => app(\App\Services\ContentManagement::class)->access($request->user()) !== [] ? '/gestao-conteudos' : null, 'portalOperator' => config('portal.operator_name'), 'portalInstitutionalContactEmail' => filter_var(config('portal.institutional_contact_email'), FILTER_VALIDATE_EMAIL) ?: null, 'legacyContentEnabled' => app(LegacyContent::class)->available(), @@ -68,7 +69,7 @@ class HandleInertiaRequests extends Middleware 'create' => ['label' => 'Nova oportunidade', 'href' => '/management/opportunities/create'], 'installationHref' => '/management/opportunity-management-install', ] : null, - 'flash' => ['success' => fn () => $request->session()->get('success'), 'warning' => fn () => $request->session()->get('warning')], + 'flash' => ['success' => fn () => $request->session()->get('success'), 'warning' => fn () => $request->session()->get('warning'), 'uploaded_document_id' => fn () => $request->session()->get('uploaded_document_id')], 'scopedAdministration' => $request->user() && app(\App\Services\ScopedPortalAdministration::class)->administrative($request->user()) ? [ ['label'=>'Zonas e parques','href'=>'/management/investment/zones'], ['label'=>'Referências económicas','href'=>'/management/economic-references/lot-01'], diff --git a/app/Services/ContentManagement.php b/app/Services/ContentManagement.php new file mode 100644 index 00000000..d7507492 --- /dev/null +++ b/app/Services/ContentManagement.php @@ -0,0 +1,49 @@ +hasVerifiedEmail()) return []; + $profiles = app(PortalProfileAccess::class); + $navigation = $profiles->navigation($actor); + $legacy = $profiles->legacyWriteAllowed($actor); + $documents = app(DocumentManagementAccess::class); + $editor = $documents->full($actor) || $documents->editorial($actor); + $upload = $editor || $documents->limited($actor); + $result = []; + if ($legacy || ($navigation['mine'] ?? null) || ($navigation['review'] ?? null)) { + $result['opportunities'] = ['index' => $legacy ? '/management/opportunities' : '/minhas-oportunidades', + 'create' => $legacy ? '/management/opportunities/create' : ($navigation['create'] ?? null), 'profileWorkflow' => ! $legacy]; + } + if ($upload) $result['documents'] = ['index' => '/management/portal-services', 'edit' => $editor]; + return $result; + } + + public function view(User $actor): array + { + $access = $this->access($actor); + abort_unless($access !== [], 403); + $counts = []; + if (isset($access['opportunities'])) { + $counts['opportunities'] = app(PortalProfileAccess::class)->scope(Opportunity::query(), $actor) + ->selectRaw("COALESCE(workflow_context->>'working_state',status) as state, count(*) as total") + ->groupByRaw("COALESCE(workflow_context->>'working_state',status)")->pluck('total', 'state'); + } + if (isset($access['documents'])) { + $documents = app(DocumentManagementAccess::class); + $query = $documents->full($actor) ? DB::table('portal_resources as r')->join('documents as d', 'd.id', '=', 'r.document_id')->whereNull('d.deleted_at') + : ($access['documents']['edit'] ? $documents->editorialResources($actor) : null); + $counts['documents'] = $query ? $query->select('r.published', DB::raw('count(*) as total'))->groupBy('r.published')->get() + ->mapWithKeys(fn ($row) => [$row->published ? 'published' : 'unpublished' => (int) $row->total]) : []; + } + return ['access' => $access, 'counts' => $counts]; + } +} diff --git a/app/Services/PortalProfileAccess.php b/app/Services/PortalProfileAccess.php index 90c21e74..75b656b4 100644 --- a/app/Services/PortalProfileAccess.php +++ b/app/Services/PortalProfileAccess.php @@ -184,7 +184,9 @@ class PortalProfileAccess public function canCreate(User $actor, ?string $organization, string $type): bool { - $included = in_array($type, OpportunityCanonicalContract::BASE_CATEGORIES, true) || app(OpportunityModularProfiles::class)->enabled(); + // Draft preparation is allowed within the assigned modality. Executable + // submission support is checked separately by the canonical contract. + $included = in_array($type, OpportunityManagement::CATEGORIES, true); // This is creation as an authorized Promotor, never an exceptional SU act. return $included && $this->available() && $this->scoped($actor, 'promoter', $organization, $type) && ! $this->has($actor, 'auditor'); @@ -370,7 +372,8 @@ class PortalProfileAccess foreach (json_decode($grant->opportunity_types, true, 512, JSON_THROW_ON_ERROR) as $type) { $key = ($grant->organization_id ?? 'individual').'|'.$type; if (! isset($seen[$key]) && $this->canCreate($actor, $grant->organization_id, $type)) { - $contexts[] = ['organization_id' => $grant->organization_id, 'type' => $type]; + $contexts[] = ['organization_id' => $grant->organization_id, + 'organization_name' => $grant->organization_id ? DB::table('organizations')->where('id', $grant->organization_id)->value('name') : null, 'type' => $type]; $seen[$key] = true; } } diff --git a/docs/investmoz/consolidated-review-matrix-2026-10-03.md b/docs/investmoz/consolidated-review-matrix-2026-10-03.md index 01cd5a03..0dfde85e 100644 --- a/docs/investmoz/consolidated-review-matrix-2026-10-03.md +++ b/docs/investmoz/consolidated-review-matrix-2026-10-03.md @@ -315,3 +315,7 @@ Situação comum observada: `publication_status=draft`, `validation_status=valid - Verificação: `php artisan test tests/Feature/PortalServicesTest.php` — 17 testes/497 asserções aprovados em QA; cobrem publicação autorizada com pesquisa textual e filtros combinados, ficha, download, associação ao módulo e links de requisitos com estados confirmados/pendentes. `npm.cmd run build` concluído. O build alerta sobre pacote opcional `fontaine` e chunk `services` acima de 500 KB; sem erro de compilação. Não houve browser autenticado disponível, portanto não se declara publicação, pesquisa ou download públicos concluídos. - A autorização documental não foi aplicada a zonas, parques, negócios privados ou oportunidades de demonstração. A regra de publicação de parque não exige área confirmada; divergência de área, isoladamente, não é um gate técnico. Se a ficha avançar por decisão própria, deve omitir o total disputado ou mostrar os valores reportados com fonte/escopo, sem alegar disponibilidade. Beluluane, Munhava e Topuito requerem revisão por registo para fonte/localização pública e para retirar ou qualificar a área; Topuito só tem província associada no mestre geográfico, pelo que não se pode anunciar uma associação administrativa a Larde enquanto essa referência não existir. ZEE/ZFI continuam dependentes de acto oficial estruturado e validado, regime e relações territoriais apropriadas; uma área sem confirmação não substitui essas evidências. - Proposta para as seis oportunidades de demonstração já públicas: preservar os registos e IDs, adicionar origem/tipo `demo` e identificá-los visivelmente como **Exemplo de demonstração**, separar a pesquisa de exemplos da pesquisa de oportunidades reais e excluir os exemplos dos resultados reais por defeito. Não os converter nem retirar em massa; cada registo real exigirá origem e proponente documentados. Nenhuma alteração foi feita nesta execução. + +### Referência da entrada única de gestão — QA, 22/10/2026 + +Nos requisitos existentes `MASTER-9f6dd1d9-0001`, `MASTER-9f6dd1d9-0003` e `MASTER-02f44baf-0002`, acrescentar como evidência delimitada a secção «Entrada única de gestão — preparação em QA, 22/10/2026» do manual de rastreabilidade e `tests/Feature/ContentManagementEntryTest.php`: quatro testes/99 asserções, ClamAV real e reversão transaccional em QA. Origem do objectivo e das regras operacionais: decisão expressa da Tempo; navegação, filtros e recuperação do upload são escolhas de implementação. Reutilizados biblioteca, armazenamento, perfis e histórico. Não altera os critérios de aceitação nem fecha os requisitos integrais: integração/contas reais e categorias não demonstradas conservam o seu estado. Geometrias permanecem separadas. \ No newline at end of file diff --git a/docs/investmoz/unification/traceability-and-manual.md b/docs/investmoz/unification/traceability-and-manual.md index 44ebe091..d21f9658 100644 --- a/docs/investmoz/unification/traceability-and-manual.md +++ b/docs/investmoz/unification/traceability-and-manual.md @@ -2227,3 +2227,17 @@ A ferramenta não observou o menu nativo aberto pelo rato; a tentativa de clicar ### Decisão de integração — 22/10/2026 A Tempo encerrou a revisão de `1d921c9d` com as provas existentes e autorizou integrar a candidata e o registo documental `916819eb`. A interacção exclusivamente pelo rato permanece não observada pela ferramenta, **sem carácter bloqueante**; não constitui falha comprovada do portal, não exige substituir o selector nem repetir ensaios. A ausência de geometrias administrativas de Cabo Delgado permanece independente. Executar o processo Git automático existente, preservando alterações concorrentes; não transportar fixtures QA, publicar dados ou activar regras fiscais. + +## Entrada única de gestão — preparação em QA, 22/10/2026 + +Origem: prioridade expressa da Tempo para a equipa cadastrar oportunidades e carregar, editar e publicar documentos e legislação pela interface, sem depender do Codex. A decisão de 22/10 sobre fontes e execução mantém-se. Geometrias administrativas ficam no saldo separado. + +Implementada `/gestao-conteudos`, acessível pelo painel, menu lateral, Balcão e editores. Reutiliza os cadastros, biblioteca, armazenamento, perfis e histórico existentes. Nova oportunidade abre o editor operacional para quem tem contexto de criação; acesso legado é identificado como tal, sem o apresentar como publicação operacional completa. A entrada não atribui papéis. Escolhas técnicas deste delta: navegação agregada, contagens no âmbito do utilizador, pesquisa documental e pré-selecção do ficheiro acabado de carregar. Não são novas regras organizacionais. + +Percurso diário: **Gestão de conteúdos e oportunidades → Nova oportunidade**, escolher promotor/modalidade, preencher e guardar; recuperar em **Gerir oportunidades e histórico**. Para documentos: **Novo documento** ou **Nova legislação → Receber documento em privado → Guardar em quarentena → Registar documento na biblioteca**. O original recém-carregado fica seleccionado; título e emitente conhecidos são recuperados. Completar os metadados, guardar, pesquisar o registo e usar **Editar metadados**, **Publicar** ou **Retirar da publicação**. Associações apresentam nomes dos registos existentes. Campos ainda não conhecidos não são inventados. Publicação para consulta não activa cálculos. + +Verificação nova: quatro testes, 99 asserções, com transacções revertidas exclusivamente em `simulator_fiscal_delta_20261021_test`, loopback 55441, guarda explícita. Demonstrados acesso de equipa sem administração geral, recusa a utilizador sem autorização e ciclo documental completo (upload, ClamAV real, recuperação por pesquisa, edição, publicação/download, retirada, preservação do hash e quatro revisões). Exemplo documental exclusivamente sintético, sem origem oficial ou valor jurídico. Corrigida a classificação de retirados: o histórico existente usa `unpublished`; o filtro e o rótulo reconhecem também `withdrawn` histórico. TypeScript e build QA passaram. + +No navegador, login e MFA normais da conta sintética de equipa; entrada → legislação; entrada → nova oportunidade → guardar preparação incompleta → recarregar, com título recuperado e ficha QA 241. Não foi repetido o workflow integral de revisão/publicação de oportunidades já existente; a prova nova cobre a entrada e a gravação/recuperação. O ensaio documental completo foi HTTP transaccional, não uma prova visual de cada clique. A conta QA tem autorizações explicitamente sintéticas; não comprova as designações das contas reais. Não transportar fixtures, configurações QA ou concessões de acesso. + +Provas locais: `outputs/content-management-20261022/entrada-final.png`, `build-final.log`, e `tests/Feature/ContentManagementEntryTest.php`. Estado: candidata em branch `codex/content-management-entry-QA`, ainda sem integração ou instalação na homologação. Não declarar encerrada a autonomia operacional de todas as contas/categorias apenas por esta entrada. Não houve migration, publicação remota, alteração de papéis activos ou activação fiscal. \ No newline at end of file diff --git a/resources/js/components/app-sidebar.tsx b/resources/js/components/app-sidebar.tsx index 97b2a9b5..0a34292b 100644 --- a/resources/js/components/app-sidebar.tsx +++ b/resources/js/components/app-sidebar.tsx @@ -26,6 +26,7 @@ export function AppSidebar() { educationEnabled, aiEnabled, opportunityManagement, + contentManagementHref, profileAccess, } = usePage<{ auth: Auth; @@ -34,6 +35,7 @@ export function AppSidebar() { aiEnabled?: boolean; profileAccess?: ProfileAccess | null; opportunityManagement?: OpportunityManagementAccess | null; + contentManagementHref?: string | null; }>().props; const groups = portalNavigation({ authenticated: !!auth.user, @@ -48,6 +50,9 @@ export function AppSidebar() { href: group.children?.find((item) => item.href)?.href ?? '/', icon: LayoutGrid, })); + if (contentManagementHref) { + items.unshift({ title: 'Gestão de conteúdos e oportunidades', href: contentManagementHref, icon: LayoutGrid }); + } if (auth.canManagePortal && opportunityManagement) { items.push({ title: opportunityManagement.manage.label, diff --git a/resources/js/components/content-management-entry.tsx b/resources/js/components/content-management-entry.tsx new file mode 100644 index 00000000..c3131602 --- /dev/null +++ b/resources/js/components/content-management-entry.tsx @@ -0,0 +1,6 @@ +import { Link, usePage } from '@inertiajs/react'; + +export default function ContentManagementEntry() { + const { contentManagementHref } = usePage<{ contentManagementHref?: string | null }>().props; + return contentManagementHref ? Gestão de conteúdos e oportunidades : null; +} diff --git a/resources/js/pages/dashboard.tsx b/resources/js/pages/dashboard.tsx index 69959e61..e2677428 100644 --- a/resources/js/pages/dashboard.tsx +++ b/resources/js/pages/dashboard.tsx @@ -1,4 +1,5 @@ import ProfileNavigation from '@/components/profile-navigation'; +import ContentManagementEntry from '@/components/content-management-entry'; import { Head, Link } from '@inertiajs/react'; import { dashboard } from '@/routes'; import type { InvestmentContext } from '@/pages/investor-path/index'; @@ -51,6 +52,7 @@ export default function Dashboard({

Painel do portal

+ {summary ? ( <> {opportunityManagement && ( diff --git a/resources/js/pages/desk/panel.tsx b/resources/js/pages/desk/panel.tsx index 7108dc06..95cab8a2 100644 --- a/resources/js/pages/desk/panel.tsx +++ b/resources/js/pages/desk/panel.tsx @@ -1,4 +1,5 @@ import { createOperationKey } from '@/lib/operation-key'; +import ContentManagementEntry from '@/components/content-management-entry'; import { Link, useForm, usePage } from '@inertiajs/react'; import { useEffect, useRef, useState } from 'react'; import { Frame, input, button, card } from '@/pages/investment/shared'; @@ -84,6 +85,7 @@ export default function Panel({ }, [contextTitle, form.data.title]); return ( +