json(['available' => false, 'items' => []]); } app(PartnershipSources::class)->resolve($type, $id, $request->user()); return response()->json(['available' => true, 'items' => DB::table('portal_partnerships as p')->join('partnership_origins as origin', 'origin.partnership_id', '=', 'p.id') ->where('origin.type', $type)->where('origin.target_id', $id)->where('p.status', 'published')->orderByDesc('p.id')->distinct()->get(['p.id', 'p.title'])])->header('Cache-Control', 'private, no-store'); } public function index(Request $request): Response|JsonResponse { $filters = $request->validate(['q' => 'nullable|string|max:120', 'sector_id' => 'nullable|integer', 'partnership_type' => 'nullable|string|max:40', 'kind' => 'nullable|in:offering,seeking', 'territory_id' => 'nullable|uuid', 'origin_type' => 'nullable|string|max:40', 'origin_id' => 'nullable|string|max:100']); $query = DB::table('portal_partnerships as p')->where('p.status', 'published'); foreach (['sector_id', 'partnership_type', 'kind'] as $field) { if (! empty($filters[$field])) { if ($field === 'sector_id') { $sector = \App\Models\Sector::find($filters[$field]); $query->whereIn('p.sector_id', $sector ? \App\Models\Sector::whereIn('slug', \App\Services\SectorOrganization::slugs($sector->slug))->pluck('id') : []); } else { $query->where('p.'.$field, $filters[$field]); } } } if (! empty($filters['q'])) { $term = '%'.$filters['q'].'%'; $query->where(fn ($q) => $q->where('p.title', 'ilike', $term)->orWhere('p.sector', 'ilike', $term) ->orWhereRaw("p.payload->>'summary' ILIKE ?", [$term])->orWhere(fn ($details) => $details->whereRaw("COALESCE(p.payload->>'confidentiality','public')='public'")->where(fn ($body) => $body->where('p.offer', 'ilike', $term)->orWhere('p.seeking', 'ilike', $term)))); } if (! empty($filters['origin_type']) && ! empty($filters['origin_id'])) { // Authenticate access to a private master before exposing a backlink query. app(PartnershipSources::class)->resolve($filters['origin_type'], $filters['origin_id'], $request->user()); $query->whereExists(fn ($q) => $q->selectRaw('1')->from('partnership_origins')->whereColumn('partnership_id', 'p.id')->where('type', $filters['origin_type'])->where('target_id', $filters['origin_id'])); } if (! empty($filters['territory_id'])) { abort_unless(app(InvestmentGeography::class)->publicReferences()->where('id', $filters['territory_id'])->exists(), 422); $ids = app(InvestmentGeography::class)->descendants($filters['territory_id']); $investmentIds = app(InvestmentCatalogue::class)->query(['geo_location_id' => $filters['territory_id']])->pluck('id')->map(fn ($id) => (string) $id)->all(); $query->where(fn ($q) => $q->whereExists(fn ($places) => $places->selectRaw('1')->from('partnership_locations')->whereColumn('partnership_id', 'p.id')->whereIn('geo_location_id', $ids)) ->orWhereExists(fn ($origins) => $origins->selectRaw('1')->from('partnership_origins as origin')->join('project_locations as pl', DB::raw('pl.project_id::text'), '=', 'origin.target_id') ->whereColumn('origin.partnership_id', 'p.id')->where('origin.type', 'project')->whereNull('pl.deleted_at')->where('pl.data_classification', 'public')->whereIn('pl.geo_location_id', $ids)) ->orWhereExists(fn ($origins) => $origins->selectRaw('1')->from('partnership_origins')->whereColumn('partnership_id', 'p.id')->where('type', 'investment')->whereIn('target_id', $investmentIds)) ->orWhereExists(fn ($members) => $members->selectRaw('1')->from('partnership_origins as origin') ->joinSub(app(InvestmentRelations::class)->memberships()->select(['cp.cluster_id', 'cp.project_id']), 'member', DB::raw('member.cluster_id::text'), '=', 'origin.target_id') ->join('project_locations as pl', 'pl.project_id', '=', 'member.project_id')->whereColumn('origin.partnership_id', 'p.id')->where('origin.type', 'cluster') ->whereNull('pl.deleted_at')->where('pl.data_classification', 'public')->whereIn('pl.geo_location_id', $ids)) ->orWhereExists(fn ($zones) => $zones->selectRaw('1')->from('partnership_origins as origin')->join('investment_zone_locations as zl', DB::raw('zl.zone_id::text'), '=', 'origin.target_id') ->join('investment_zones as zone', 'zone.id', '=', 'zl.zone_id')->whereColumn('origin.partnership_id', 'p.id')->where('origin.type', 'zone')->where('zone.publication_status', 'published') ->whereNull('zone.deleted_at')->where('zl.validation_status', 'validated')->whereIn('zl.geo_location_id', $ids))); } $results = $query->orderByDesc('p.id')->select(['p.id', 'p.title', 'p.sector', 'p.province', 'p.kind', 'p.partnership_type', 'p.sector_id', 'p.deadline', 'p.published_at']) ->selectRaw("p.payload->>'summary' as summary")->paginate(12)->withQueryString(); if ($request->expectsJson()) { return response()->json($results); } $mapPoints = []; foreach ($results as $partnership) { $locations = app(InvestmentRelations::class)->locations(array_values(DB::table('partnership_locations')->where('partnership_id', $partnership->id)->get() ->map(fn ($location) => ['id' => $location->geo_location_id, 'role' => $location->role, 'source_reference' => $location->source_reference])->all())); foreach ($locations as $location) { if ($location['location_type'] !== 'site') { continue; } foreach ($location['coordinates'] as $coordinate) { $mapPoints[] = ['geometry' => $coordinate['geometry'], 'properties' => ['id' => count($mapPoints), 'title' => $partnership->title, 'location' => $location['name'].' · '.$location['role'].' · '.$location['source_reference'], 'url' => '/parcerias/'.$partnership->id]]; } } foreach (app(PartnershipSources::class)->forPartnership($partnership->id) as $source) { $context = $source['geography']['locations'] ?? []; foreach ($source['geography']['members'] ?? [] as $member) { $context = [...$context, ...$member['locations']]; } foreach ($context as $location) { foreach ($location['coordinates'] as $coordinate) { $mapPoints[] = ['geometry' => $coordinate['geometry'], 'properties' => ['id' => count($mapPoints), 'title' => $partnership->title, 'location' => 'Contexto da origem: '.$location['name'].'; implantação da parceria não confirmada', 'url' => '/parcerias/'.$partnership->id]]; } } } } return Inertia::render('partnerships/catalogue', ['results' => $results, 'filters' => $filters, 'mapPoints' => $mapPoints, ...$this->catalogues($request)]); } public function dashboard(Request $request): Response { $actor = $request->user(); $organizations = DB::table('memberships')->where('user_id', $actor->id)->whereNull('deleted_at')->pluck('organization_id'); $mine = DB::table('portal_partnerships')->where(fn ($q) => $q->where(fn ($own) => $own->where('user_id', $actor->id)->whereNull('organization_id'))->orWhereIn('organization_id', $organizations))->latest()->limit(100)->get(); $interests = DB::table('partnership_interests as i')->join('portal_partnerships as p', 'p.id', '=', 'i.partnership_id') ->leftJoin('partnership_rooms as room', 'room.interest_id', '=', 'i.id') ->where(fn ($q) => $q->where(fn ($own) => $own->where('i.user_id', $actor->id)->whereNull('i.organization_id'))->orWhereIn('i.organization_id', $organizations)) ->select(['i.id', 'i.partnership_id', 'i.status', 'i.decision_reason', 'p.title', 'room.id as room_id'])->orderByDesc('i.id')->limit(100)->get(); $moderation = Gate::forUser($actor)->allows('manage-portal') ? DB::table('portal_partnerships')->whereIn('status', ['submitted', 'pending'])->orderBy('updated_at')->get(['id', 'title', 'status', 'version']) : []; return Inertia::render('partnerships/dashboard', ['mine' => $mine->map(fn ($p) => ['id' => $p->id, 'title' => $p->title, 'status' => $p->status, 'version' => $p->version]), 'interests' => $interests, 'moderation' => $moderation, 'notifications' => DB::table('partnership_notifications')->where('user_id', $actor->id)->orderByDesc('id')->limit(50)->get()]); } public function edit(Request $request, ?int $id = null): Response { $record = $id ? app(PartnershipExchange::class)->find($id) : null; abort_if($record && ! app(PartnershipExchange::class)->owns($request->user(), $record), 403); $origins = $record ? DB::table('partnership_origins')->where('partnership_id', $id)->get() : []; if (! $record && $request->filled('origin_type') && $request->filled('origin_id')) { $source = app(PartnershipSources::class)->resolve($request->string('origin_type')->toString(), $request->string('origin_id')->toString(), $request->user()); $origins = [['type' => $source['type'], 'target_id' => $source['target_id']]]; } return Inertia::render('partnerships/editor', ['record' => $record ? [...(array) $record, 'payload' => json_decode($record->payload, true)] : null, 'origins' => $origins, 'locations' => $record ? DB::table('partnership_locations')->where('partnership_id', $id)->get() : [], ...$this->catalogues($request)]); } public function save(Request $request, ?int $id = null): RedirectResponse|JsonResponse { $record = app(PartnershipExchange::class)->save($request->user(), $request->all(), $id); return $request->expectsJson() ? response()->json(['id' => $record->id, 'version' => $record->version, 'status' => $record->status]) : redirect('/parcerias/'.$record->id.'/editar')->with('success', 'Rascunho guardado.'); } public function show(Request $request, int $id): Response|JsonResponse { $record = app(PartnershipExchange::class)->find($id); $owner = $request->user() && app(PartnershipExchange::class)->owns($request->user(), $record, false); $moderator = $request->user() && Gate::forUser($request->user())->allows('manage-portal'); abort_unless($record->status === 'published' || $owner || $moderator, 404); $payload = json_decode($record->payload, true); unset($payload['_creation_hash']); $privateDetails = ($payload['confidentiality'] ?? 'public') === 'private' && ! $owner && ! $moderator; $mine = $request->user() ? DB::table('partnership_interests')->where('partnership_id', $id)->get()->filter(fn ($i) => app(PartnershipExchange::class)->owns($request->user(), $i, false))->values() : collect(); $interests = $owner ? DB::table('partnership_interests')->where('partnership_id', $id)->orderBy('id')->get() : $mine; $interestData = $interests->map(function ($interest) use ($record) { return [...(array) $interest, 'payload' => json_decode($interest->payload, true), 'assessment' => json_decode($interest->assessment, true), 'match' => app(PartnershipMatching::class)->explain($record, $interest), 'room_id' => DB::table('partnership_rooms')->where('interest_id', $interest->id)->value('id')]; }); $props = ['record' => ['id' => $record->id, 'title' => $record->title, 'status' => $record->status, 'version' => $record->version, 'kind' => $record->kind, 'partnership_type' => $record->partnership_type, 'sector' => $record->sector, 'province' => $record->province, 'sector_id' => $record->sector_id, 'deadline' => $record->deadline, 'offer' => $privateDetails ? null : $record->offer, 'seeking' => $privateDetails ? null : $record->seeking, 'payload' => $privateDetails ? ['summary' => $payload['summary'] ?? '', 'confidentiality' => 'private'] : $payload], 'owner' => (bool) $owner, 'canEdit' => $request->user() && app(PartnershipExchange::class)->owns($request->user(), $record), 'moderator' => (bool) $moderator, 'origins' => app(PartnershipSources::class)->forPartnership($id, $owner ? $request->user() : null), 'interests' => $interestData, 'locations' => app(InvestmentRelations::class)->locations(array_values(DB::table('partnership_locations')->where('partnership_id', $id)->get()->map(fn ($l) => ['id' => $l->geo_location_id, 'role' => $l->role, 'source_reference' => $l->source_reference])->all())), 'history' => ($owner || $moderator) ? DB::table('partnership_events')->where('partnership_id', $id)->where('event_type', 'like', 'partnership_opportunity.%')->orderBy('occurred_at')->get(['event_type', 'actor_id', 'occurred_at', 'payload']) : [], ...$this->catalogues($request)]; if ($request->user()) { Inertia::encryptHistory(); } return $request->expectsJson() ? response()->json($props)->header('Cache-Control', 'private, no-store') : Inertia::render('partnerships/detail', $props); } public function transition(Request $request, int $id): RedirectResponse|JsonResponse { $data = $request->validate(['version' => 'required|integer|min:1', 'action' => 'required|string', 'reason' => 'required|string|max:2000']); $record = app(PartnershipExchange::class)->transition($request->user(), $id, $data['version'], $data['action'], $data['reason']); return $request->expectsJson() ? response()->json(['status' => $record->status, 'version' => $record->version]) : back()->with('success', 'Decisão registada.'); } public function interest(Request $request, int $id): RedirectResponse|JsonResponse { $interest = app(PartnershipExchange::class)->interest($request->user(), $id, $request->all()); return $request->expectsJson() ? response()->json(['id' => $interest->id, 'version' => $interest->version]) : back()->with('success', 'Interesse guardado.'); } public function decide(Request $request, int $id): RedirectResponse|JsonResponse { $data = $request->validate(['version' => 'required|integer|min:1', 'action' => 'required|string', 'reason' => 'required|string|max:2000', 'assessment' => 'array|max:7']); $interest = app(PartnershipExchange::class)->decideInterest($request->user(), $id, $data['version'], $data['action'], $data['reason'], $data['assessment'] ?? []); return $request->expectsJson() ? response()->json(['status' => $interest->status, 'version' => $interest->version]) : back()->with('success', 'Candidatura atualizada.'); } public function room(Request $request, int $id): Response|JsonResponse { $context = app(PartnershipExchange::class)->room($request->user(), $id); $props = [...$context, 'room' => [...(array) $context['room'], 'agreement' => $context['room']->agreement ? json_decode($context['room']->agreement, true) : null], 'messages' => DB::table('partnership_messages as m')->join('users as u', 'u.id', '=', 'm.author_id')->where('room_id', $id)->orderBy('m.id')->get(['m.id', 'm.body', 'm.created_at', 'm.author_id', 'u.name']), 'documents' => DB::table('partnership_documents as link')->join('documents as d', 'd.id', '=', 'link.document_id')->where('room_id', $id) ->where(fn ($q) => $q->where('link.shared', true)->orWhere('link.owner_id', $request->user()->id)) ->get(['link.id', 'link.shared', 'link.owner_id', 'd.file_name', 'd.status', 'd.malware_scan_status', 'd.id as document_id']), 'milestones' => DB::table('partnership_milestones')->where('room_id', $id)->orderBy('id')->get(), 'canWrite' => app(PartnershipExchange::class)->owns($request->user(), $context['partnership']) || app(PartnershipExchange::class)->owns($request->user(), $context['interest'])]; return $request->expectsJson() ? response()->json($props) : Inertia::render('partnerships/room', $props); } public function message(Request $request, int $id): RedirectResponse|JsonResponse { $data = $request->validate(['client_key' => 'required|uuid', 'body' => 'required|string|max:15000']); DB::transaction(function () use ($request, $id, $data) { $context = app(PartnershipExchange::class)->room($request->user(), $id, true); $locked = DB::table('partnership_rooms')->where('id', $id)->lockForUpdate()->first(); abort_unless($locked !== null && ! in_array($locked->status, ['closed', 'cancelled'], true), 409, 'Sala encerrada.'); $key = ['room_id' => $id, 'author_id' => $request->user()->id, 'client_key' => $data['client_key']]; $existing = DB::table('partnership_messages')->where($key)->first(); if ($existing) { abort_unless($existing->body === $data['body'], 409, 'Chave já utilizada com outro conteúdo.'); return; } DB::table('partnership_messages')->insert([...$key, 'body' => $data['body']]); app(PartnershipExchange::class)->event($context['partnership'], $request->user(), 'partnership.message_created.v1', ['room_id' => $id], 'Nova mensagem privada', [$context['partnership']->user_id, $context['interest']->user_id]); }); return $request->expectsJson() ? response()->json(['sent' => true]) : back()->with('success', 'Mensagem registada.'); } public function agreement(Request $request, int $id): RedirectResponse|JsonResponse { $data = $request->validate(['version' => 'required|integer|min:1', 'action' => 'required|in:record,confirm,start,complete,cancel,renew', 'title' => 'required_if:action,record|nullable|string|max:200', 'terms' => 'required_if:action,record|nullable|string|max:20000', 'reason' => 'required|string|max:2000']); DB::transaction(function () use ($request, $id, $data) { $context = app(PartnershipExchange::class)->room($request->user(), $id, true); $room = DB::table('partnership_rooms')->where('id', $id)->lockForUpdate()->first(); abort_unless((int) $room->version === (int) $data['version'], 409, 'Acordo alterado.'); $agreement = $room->agreement ? json_decode($room->agreement, true) : null; $status = $room->status; if ($data['action'] === 'record') { abort_unless(in_array($status, ['negotiation', 'agreement_recorded'], true), 409); $previous = $agreement['previous_versions'] ?? []; if ($agreement) { $previous[] = ['title' => $agreement['title'], 'terms' => $agreement['terms'], 'confirmations' => $agreement['confirmations'], 'recorded_at' => $agreement['recorded_at']]; } $agreement = ['title' => $data['title'], 'terms' => $data['terms'], 'recorded_by' => $request->user()->id, 'recorded_at' => now()->toIso8601String(), 'confirmations' => [], 'parties' => [$context['partnership']->user_id, $context['interest']->user_id], 'legal_signature_status' => 'not_verified', 'previous_versions' => $previous]; $status = 'agreement_recorded'; } elseif ($data['action'] === 'confirm') { abort_unless($status === 'agreement_recorded' && $agreement, 409); $party = app(PartnershipExchange::class)->owns($request->user(), $context['partnership']) ? 'proponent' : 'candidate'; $agreement['confirmations'][$party] = ['actor_id' => $request->user()->id, 'date' => now()->toIso8601String(), 'reason' => $data['reason']]; if (count($agreement['confirmations']) === 2) { $status = 'agreed'; } } else { $allowed = ['start' => ['agreed'], 'complete' => ['execution'], 'cancel' => ['negotiation', 'agreement_recorded', 'agreed', 'execution'], 'renew' => ['agreed', 'execution']]; abort_unless(in_array($status, $allowed[$data['action']] ?? [], true), 409); if ($data['action'] === 'complete') { abort_if(DB::table('partnership_milestones')->where('room_id', $id)->where('status', '!=', 'completed')->exists(), 409, 'Existem marcos pendentes.'); } $status = ['start' => 'execution', 'complete' => 'closed', 'cancel' => 'cancelled', 'renew' => 'negotiation'][$data['action']]; if ($data['action'] === 'renew') { $agreement['previous_versions'][] = ['title' => $agreement['title'], 'terms' => $agreement['terms'], 'confirmations' => $agreement['confirmations']]; $agreement['confirmations'] = []; } } DB::table('partnership_rooms')->where('id', $id)->update(['agreement' => $agreement ? json_encode($agreement) : null, 'status' => $status, 'version' => $room->version + 1, 'updated_at' => now()]); app(PartnershipExchange::class)->event($context['partnership'], $request->user(), 'partnership_agreement.changed.v1', ['room_id' => $id, 'status' => $status, 'version' => $room->version + 1, 'reason' => $data['reason']], 'Acordo: '.$status, [$context['partnership']->user_id, $context['interest']->user_id]); }); return $request->expectsJson() ? response()->json(['recorded' => true]) : back()->with('success', 'Acordo atualizado; assinatura jurídica não verificada.'); } public function milestone(Request $request, int $id, ?int $milestone = null): RedirectResponse|JsonResponse { $data = $request->validate(['title' => 'required|string|max:200', 'responsible_id' => 'required|integer', 'due_date' => 'nullable|date_format:Y-m-d', 'status' => 'required|in:pending,progress,completed', 'evidence' => 'required_if:status,completed|nullable|string|max:10000', 'version' => ($milestone ? 'required' : 'nullable').'|integer|min:1']); DB::transaction(function () use ($request, $id, $milestone, $data) { $context = app(PartnershipExchange::class)->room($request->user(), $id, true); $locked = DB::table('partnership_rooms')->where('id', $id)->lockForUpdate()->first(); abort_unless($locked !== null && in_array($locked->status, ['agreement_recorded', 'agreed', 'execution'], true), 409); $responsible = User::query()->whereKey($data['responsible_id'])->first(); abort_unless($responsible !== null && (app(PartnershipExchange::class)->owns($responsible, $context['partnership']) || app(PartnershipExchange::class)->owns($responsible, $context['interest'])), 422, 'Responsável deve ser participante identificado.'); if ($milestone) { $existing = DB::table('partnership_milestones')->where('room_id', $id)->where('id', $milestone)->lockForUpdate()->first(); abort_unless($existing !== null, 404); abort_unless((int) $existing->version === (int) $data['version'], 409); DB::table('partnership_milestones')->where('id', $milestone)->update([...$data, 'version' => $existing->version + 1, 'updated_at' => now()]); } else { unset($data['version']); $milestone = DB::table('partnership_milestones')->insertGetId([...$data, 'room_id' => $id, 'created_at' => now(), 'updated_at' => now()]); } app(PartnershipExchange::class)->event($context['partnership'], $request->user(), 'partnership_milestone.changed.v1', ['milestone_id' => $milestone, 'status' => $data['status']], 'Marco atualizado', [$context['partnership']->user_id, $context['interest']->user_id]); }); return $request->expectsJson() ? response()->json(['saved' => true]) : back()->with('success', 'Marco guardado.'); } public function upload(Request $request, int $id): RedirectResponse|JsonResponse { $data = $request->validate(['file' => 'required|file|mimes:pdf,jpg,jpeg,png|max:10240', 'shared' => 'required|boolean']); $context = app(PartnershipExchange::class)->room($request->user(), $id, true); $file = $request->file('file'); $documentId = (string) Str::uuid(); $stored = $file->storeAs('partnerships/'.$id, $documentId, 'local'); abort_unless(is_string($stored) && $stored !== '', 503); try { DB::transaction(function () use ($request, $id, $data, $file, $documentId, $stored, $context) { app(PartnershipExchange::class)->room($request->user(), $id, true); $locked = DB::table('partnership_rooms')->where('id', $id)->lockForUpdate()->first(); abort_unless($locked !== null && ! in_array($locked->status, ['closed', 'cancelled'], true), 409); DB::table('documents')->insert(['id' => $documentId, 'source_system' => 'partnership-exchange', 'data_classification' => 'restricted', 'file_name' => basename($file->getClientOriginalName()), 'media_type' => $file->getMimeType(), 'storage_key' => $stored, 'sha256' => hash_file('sha256', Storage::disk('local')->path($stored)), 'status' => 'quarantined', 'size_bytes' => $file->getSize(), 'uploaded_by' => $request->user()->id]); DB::table('partnership_documents')->insert(['room_id' => $id, 'document_id' => $documentId, 'owner_id' => $request->user()->id, 'shared' => $data['shared']]); app(PartnershipExchange::class)->event($context['partnership'], $request->user(), 'partnership.document_added.v1', ['room_id' => $id, 'document_id' => $documentId, 'shared' => $data['shared']], 'Documento privado anexado'); }); } catch (\Throwable $e) { Storage::disk('local')->delete($stored); throw $e; } ScanDocumentForMalware::dispatch($documentId)->afterCommit(); return $request->expectsJson() ? response()->json(['uploaded' => true]) : back()->with('success', 'Documento privado guardado; análise antimalware pendente.'); } public function download(Request $request, int $id, int $attachment): \Illuminate\Http\Response|BinaryFileResponse { $context = app(PartnershipExchange::class)->room($request->user(), $id); $document = DB::table('partnership_documents as link')->join('documents as d', 'd.id', '=', 'link.document_id')->where('link.room_id', $id)->where('link.id', $attachment) ->where(fn ($q) => $q->where('link.shared', true)->orWhere('link.owner_id', $request->user()->id))->where('d.status', 'active')->whereNull('d.deleted_at') ->where('d.malware_scan_status', 'clean')->whereColumn('d.malware_scanned_sha256', 'd.sha256')->first(['d.*']); abort_unless($document !== null, 404); $response = app(VerifiedDocumentBinary::class)->respond($document); app(PartnershipExchange::class)->event($context['partnership'], $request->user(), 'partnership.document_accessed.v1', ['room_id' => $id, 'attachment_id' => $attachment], 'Documento consultado'); return $response; } public function share(Request $request, int $id, int $attachment): RedirectResponse|JsonResponse { $data = $request->validate(['shared' => 'required|boolean']); DB::transaction(function () use ($request, $id, $attachment, $data) { $context = app(PartnershipExchange::class)->room($request->user(), $id, (bool) $data['shared']); $link = DB::table('partnership_documents')->where('room_id', $id)->where('id', $attachment)->lockForUpdate()->first(); abort_unless($link !== null && (int) $link->owner_id === $request->user()->id, 403, 'Só o autor pode autorizar ou retirar a partilha.'); DB::table('partnership_documents')->where('id', $attachment)->update(['shared' => $data['shared']]); app(PartnershipExchange::class)->event($context['partnership'], $request->user(), 'partnership.document_sharing_changed.v1', ['room_id' => $id, 'attachment_id' => $attachment, 'shared' => $data['shared']], 'Autorização documental atualizada'); }); return $request->expectsJson() ? response()->json(['saved' => true]) : back()->with('success', 'Autorização de partilha atualizada.'); } public function notification(Request $request, int $id): RedirectResponse { abort_unless(DB::table('partnership_notifications')->where('id', $id)->where('user_id', $request->user()->id)->update(['read_at' => now()]) > 0, 404); return back(); } /** @return array */ private function catalogues(Request $request): array { $sources = []; foreach (InvestmentPortfolioController::visible()->where(fn ($q) => $q->whereNotNull('project_id')->orWhereNotNull('cluster_id'))->get(['id', 'title', 'project_id', 'cluster_id']) as $dossier) { $sources[] = ['type' => $dossier->project_id ? 'project' : 'cluster', 'target_id' => $dossier->project_id ?? $dossier->cluster_id, 'title' => $dossier->title, 'href' => '/carteira/'.$dossier->id]; } foreach (DB::table('opportunities')->where('status', 'published')->whereNotNull('published_at')->get(['id', 'title', 'slug']) as $investment) { $sources[] = ['type' => 'investment', 'target_id' => (string) $investment->id, 'title' => $investment->title, 'href' => '/oportunidades/'.$investment->slug]; } foreach (DB::table('investment_zones')->where('publication_status', 'published')->whereNull('deleted_at')->get(['id', 'name']) as $zone) { $sources[] = ['type' => 'zone', 'target_id' => $zone->id, 'title' => $zone->name, 'href' => '/zonas/'.$zone->id]; } foreach (ExistingBusiness::query()->published()->get(['id', 'public_title', 'public_slug']) as $business) { $sources[] = ['type' => 'business', 'target_id' => $business->id, 'title' => $business->public_title, 'href' => '/negocios/'.$business->public_slug]; } if ($request->user()) { foreach (DB::table('investment_company_declarations')->where('user_id', $request->user()->id)->get(['id']) as $company) { $sources[] = ['type' => 'company', 'target_id' => $company->id, 'title' => 'Declaração empresarial existente', 'href' => '/investidor/profile']; } foreach (DB::table('simulation_form_drafts')->where('owner_user_id', $request->user()->id)->whereNull('deleted_at')->get(['id', 'created_at']) as $draft) { $sources[] = ['type' => 'simulation', 'target_id' => $draft->id, 'title' => 'Ficha de projeto · '.$draft->created_at, 'href' => '/simulador-fiscal?draft='.$draft->id]; } } return ['types' => config('partnerships.types'), 'criteria' => config('partnerships.criteria'), 'sources' => $sources, 'sectors' => \App\Models\Sector::choices()->orderBy('name')->get(['id', 'name', 'slug']), 'territories' => app(InvestmentGeography::class)->publicReferences()->orderBy('name')->get(['id', 'name', 'location_type', 'parent_id']), 'organizations' => $request->user() ? DB::table('memberships as m')->join('organizations as o', 'o.id', '=', 'm.organization_id')->where('m.user_id', $request->user()->id) ->whereNull('m.deleted_at')->whereNull('o.deleted_at')->whereIn('m.role', ['admin', 'manager', 'officer'])->get(['o.id', 'o.name']) : []]; } }