validate(['q' => ['nullable', 'string', 'max:120'], 'page' => ['nullable', 'integer', 'min:1']]); $term = trim($input['q'] ?? ''); $tokens = $search->tokens($term); $results = ['data' => [], 'total' => 0, 'current_page' => 1, 'last_page' => 1, 'per_page' => 5, 'unavailable' => []]; if ($tokens !== []) { $factories = [ 'Páginas e serviços' => function () use ($search, $tokens, $term) { $pages = array_values(array_filter($this->pages(), fn ($page) => $search->matches(implode(' ', $page), $tokens))); usort($pages, fn ($a, $b) => $search->score($b['title'], $term, $tokens) <=> $search->score($a['title'], $term, $tokens) ?: strcmp($a['title'], $b['title']) ?: strcmp($a['url'], $b['url'])); return ['count' => count($pages), 'fetch' => fn ($offset, $limit) => array_slice($pages, $offset, $limit)]; }, 'Oportunidades' => function () use ($catalogue, $search, $tokens, $term) { $query = $catalogue->query(['include_national' => true]); foreach ($tokens as $token) { $query->where(function ($match) use ($catalogue, $search, $token) { $search->orFields($match, ['opportunities.title', 'opportunities.summary', 'opportunities.description', 'opportunities.reference', 'opportunities.province'], $token); $match->orWhereHas('sector', fn ($sector) => $search->constrain($sector, ['name'], [$token])); $match->orWhereExists(fn ($location) => $location->selectRaw('1')->from('geo_locations as search_location') ->whereIn('search_location.id', app(InvestmentGeography::class)->publicReferences()->select('id')) ->where(fn ($name) => $search->orFields($name, ['search_location.name'], $token)) ->where(fn ($direct) => $direct->whereColumn('search_location.id', 'opportunities.primary_territory_id') ->orWhereExists(fn ($link) => $link->selectRaw('1')->from('opportunity_locations as search_link') ->whereColumn('search_link.opportunity_id', 'opportunities.id')->whereColumn('search_link.geo_location_id', 'search_location.id')))); if (preg_match('/^[a-z0-9]+$/', $token)) { $match->orWhereIn('opportunities.id', $catalogue->query(['q' => $token, 'include_national' => true])->reorder()->select('opportunities.id')); } }); } return $this->source($query, $search, $term, $tokens, 'opportunities.title', 'opportunities.id', fn ($r) => ['title' => $r->title, 'description' => $r->summary, 'url' => '/oportunidades/'.rawurlencode($r->slug)]); }, 'Negócios' => function () use ($search, $tokens, $term) { $query = ExistingBusiness::query()->published(); $search->constrain($query, ['public_title', 'activity_code', 'objective'], $tokens); return $this->source($query, $search, $term, $tokens, 'public_title', 'existing_businesses.id', fn ($r) => ['title' => $r->public_title, 'description' => $r->objective, 'url' => '/negocios/'.rawurlencode($r->public_slug)]); }, 'Carteira: projectos, clusters e corredores' => function () use ($search, $tokens, $term) { $query = InvestmentPortfolioController::visible(); $search->constrain($query, ['investment_dossiers.title', 'investment_dossiers.content'], $tokens); return $this->source($query, $search, $term, $tokens, 'investment_dossiers.title', 'investment_dossiers.id', fn ($r) => ['title' => $r->title, 'description' => $r->corridor_id ? 'Corredor' : ($r->cluster_id ? 'Cluster' : 'Projecto âncora'), 'url' => '/carteira/'.$r->id]); }, 'Documentos e legislação' => function () use ($search, $tokens, $term) { $query = DB::table('portal_resources as r')->join('documents as d', 'd.id', '=', 'r.document_id') ->where('r.published', true)->where('r.rights_status', '!=', 'restricted')->where('d.rights_status', '!=', 'restricted') ->where('d.status', 'active')->where('d.data_classification', 'public')->whereNull('d.deleted_at') ->where('d.malware_scan_status', 'clean')->whereColumn('d.malware_scanned_sha256', 'd.sha256'); $search->constrain($query, ['r.title', 'r.description', 'r.document_number', 'd.issuer', 'r.source_reference', 'r.category', 'r.topic'], $tokens); $query->select('r.id', 'r.title', 'r.description'); return $this->source($query, $search, $term, $tokens, 'r.title', 'r.id', fn ($r) => ['title' => $r->title, 'description' => $r->description, 'url' => '/documentos/'.$r->id]); }, 'Artigos' => function () use ($search, $tokens, $term) { if (! Schema::hasColumn('portal_resources', 'editorial_content')) { return ['count' => 0, 'fetch' => fn ($offset, $limit) => []]; } $query = app(EditorialReading::class)->publicResources() ->whereRaw("nullif(btrim(r.editorial_content->>'body'), '') is not null"); $search->constrain($query, ['r.title', "(r.editorial_content->>'body')"], $tokens); $query->select('r.id', 'r.title')->selectRaw("r.editorial_content->>'body' as body"); return $this->source($query, $search, $term, $tokens, 'r.title', 'r.id', fn ($r) => ['title' => $r->title, 'description' => Str::limit(trim(strip_tags($r->body)), 220), 'url' => '/artigos/'.$r->id]); }, 'Sectores' => function () use ($search, $tokens, $term) { $query = Sector::query()->where('is_active', true)->whereNotNull('published_at'); $search->constrain($query, ['name', 'description', 'short_description'], $tokens); return $this->source($query, $search, $term, $tokens, 'name', 'sectors.id', fn ($r) => ['title' => $r->name, 'description' => $r->short_description, 'url' => SectorOrganization::href($r->slug)]); }, 'Localizações' => function () use ($search, $tokens, $term) { $query = app(InvestmentGeography::class)->publicReferences() ->whereIn('location_type', ['province', 'district', 'admin_post', 'locality']); $search->constrain($query, ['geo_locations.name', 'geo_locations.source_code'], $tokens); $query->select('geo_locations.id', 'geo_locations.name', 'geo_locations.location_type'); return $this->source($query, $search, $term, $tokens, 'geo_locations.name', 'geo_locations.id', fn ($r) => ['title' => $r->name, 'description' => ['province' => 'Província', 'district' => 'Distrito', 'admin_post' => 'Posto administrativo', 'locality' => 'Localidade'][$r->location_type], 'url' => '/oportunidades?'.http_build_query([$r->location_type.'_id' => $r->id])]); }, 'Zonas de investimento' => function () use ($search, $tokens, $term) { $query = app(InvestmentZones::class)->visible(); $search->constrain($query, ['name', 'code'], $tokens); return $this->source($query, $search, $term, $tokens, 'name', 'investment_zones.id', fn ($r) => ['title' => $r->name, 'description' => $r->code, 'url' => '/zonas/'.$r->id]); }, 'Parcerias' => function () use ($search, $tokens, $term) { $query = DB::table('portal_partnerships as p')->where('p.status', 'published'); $search->constrain($query, ['p.title', 'p.sector', 'p.province'], $tokens); $query->select('p.id', 'p.title', 'p.sector', 'p.province'); return $this->source($query, $search, $term, $tokens, 'p.title', 'p.id', fn ($r) => ['title' => $r->title, 'description' => implode(' · ', array_filter([$r->sector, $r->province])), 'url' => '/parcerias/'.$r->id]); }, ]; $results = $federated->page($factories, (int) ($input['page'] ?? 1), 5, fn ($item) => $search->score($item['title'], $term, $tokens)); } return Inertia::render('portal/search', ['query' => $term, 'results' => $results]); } private function source($query, SearchTerms $search, string $term, array $tokens, string $title, string $id, callable $present): array { $count = (clone $query)->reorder()->count(); $query->reorder(); $search->orderByRelevance($query, $title, $term, $tokens, $id); return ['count' => $count, 'fetch' => fn ($offset, $limit) => (clone $query)->offset($offset)->limit($limit)->get()->map($present)->all()]; } /** Public service entry points and terms used to find them. */ private function pages(): array { return [ ['title' => 'Como investir em Moçambique', 'description' => 'Procedimentos e etapas para investir', 'url' => '/como-investir'], ['title' => 'Iniciar ou continuar investimento', 'description' => 'Preparar uma ideia e dossier de investimento', 'url' => '/investimentos/iniciar'], ['title' => 'Oportunidades de investimento', 'description' => 'Pesquisar por sector, localização e maturidade', 'url' => '/oportunidades'], ['title' => 'Carteira de investimento', 'description' => 'Projectos âncora, clusters e corredores', 'url' => '/carteira'], ['title' => 'Negócios existentes', 'description' => 'Compra, venda e participação em negócios', 'url' => '/negocios'], ['title' => 'Sectores de investimento', 'description' => 'Actividades económicas e sectores prioritários', 'url' => '/sectores'], ['title' => 'Zonas de investimento', 'description' => 'Zonas económicas especiais, ZEE, ZFI e parques industriais', 'url' => '/zonas'], ['title' => 'Contextos de investimento', 'description' => 'Formas de iniciar e preparar um investimento', 'url' => '/contextos-de-investimento'], ['title' => 'Documentos e formulários', 'description' => 'Acervo documental e formulários para investidores e empresas', 'url' => '/documentos'], ['title' => 'Formulário do investidor', 'description' => 'Dados e contacto para investir', 'url' => '/formularios/investidor'], ['title' => 'Formulário de empresa', 'description' => 'Dados e contacto para empresas', 'url' => '/formularios/empresa'], ['title' => 'Legislação e incentivos', 'description' => 'Leis, decretos, benefícios e enquadramento legal', 'url' => '/legislacao'], ['title' => 'Simulador fiscal', 'description' => 'Simular impostos e cenários fiscais de investimento', 'url' => '/simulador-fiscal'], ['title' => 'Formulários fiscais', 'description' => 'Preparação dos formulários IVA e IRPC', 'url' => '/formularios/fiscais'], ['title' => 'Terra e DUAT', 'description' => 'Apoio ao uso e aproveitamento da terra', 'url' => '/terra-duat'], ['title' => 'Bolsa de parcerias', 'description' => 'Encontrar parceiros e propostas', 'url' => '/parcerias'], ['title' => 'Apoio ao investidor', 'description' => 'Contactos e pedido de apoio', 'url' => '/apoio'], ['title' => 'Educação para investimento', 'description' => 'Conteúdos educativos e orientação', 'url' => '/educacao'], ['title' => 'Sobre o InvesteMoz', 'description' => 'Informação institucional do portal', 'url' => '/sobre'], ['title' => 'Contacto', 'description' => 'Contactar o portal InvesteMoz', 'url' => '/contacto'], ['title' => 'Política de privacidade', 'description' => 'Privacidade e tratamento de dados pessoais', 'url' => '/politica-de-privacidade'], ['title' => 'Termos de uso', 'description' => 'Condições de utilização do portal', 'url' => '/termos-de-uso'], ]; } }