400 lines
31 KiB
PHP
400 lines
31 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Models\ExistingBusiness;
|
|
use App\Models\Sector;
|
|
use App\Services\InvestmentCatalogue;
|
|
use App\Services\InvestmentGeography;
|
|
use App\Services\InvestmentRelations;
|
|
use App\Services\PartnershipAgreementHistory;
|
|
use App\Services\PartnershipAgreementProjection;
|
|
use App\Services\PartnershipAgreements;
|
|
use App\Services\PartnershipCandidacies;
|
|
use App\Services\PartnershipCandidacyHistory;
|
|
use App\Services\PartnershipDraftSaves;
|
|
use App\Services\PartnershipExchange;
|
|
use App\Services\PartnershipMatching;
|
|
use App\Services\PartnershipMilestones;
|
|
use App\Services\PartnershipNotifications;
|
|
use App\Services\PartnershipPublication;
|
|
use App\Services\PartnershipPublicationHistory;
|
|
use App\Services\PartnershipRoomUploads;
|
|
use App\Services\PartnershipSources;
|
|
use App\Services\PublicSearchMetrics;
|
|
use App\Services\SectorOrganization;
|
|
use App\Services\VerifiedDocumentBinary;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Gate;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Inertia\Inertia;
|
|
use Inertia\Response;
|
|
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
|
|
|
class PartnershipExchangeController extends Controller
|
|
{
|
|
public function source(Request $request, string $type, string $id): JsonResponse
|
|
{
|
|
if (! Schema::hasTable('partnership_origins')) {
|
|
return response()->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 = Sector::whereKey($filters[$field])->first();
|
|
$query->whereIn('p.sector_id', $sector ? Sector::whereIn('slug', 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();
|
|
app(PublicSearchMetrics::class)->candidate($request, 'partnerships', $results->total(), $filters);
|
|
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();
|
|
$exchange = app(PartnershipExchange::class);
|
|
$organizations = $exchange->readableOrganizations($actor);
|
|
$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', 'p.status as partnership_status', 'p.user_id', 'p.organization_id', 'room.id as room_id'])->orderByDesc('i.id')->limit(100)->get()
|
|
->map(function ($interest) use ($actor, $exchange) {
|
|
$canRead = $interest->partnership_status === 'published' || $exchange->owns($actor, $interest, false) || Gate::forUser($actor)->allows('manage-portal');
|
|
|
|
return ['id' => $interest->id, 'partnership_id' => $interest->partnership_id, 'status' => $interest->status, 'decision_reason' => $interest->decision_reason,
|
|
'title' => $canRead ? $interest->title : null, 'partnership_href' => $canRead ? '/parcerias/'.$interest->partnership_id : null, 'room_id' => $interest->room_id];
|
|
});
|
|
$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' => app(PartnershipNotifications::class)->forUser($actor)]);
|
|
}
|
|
|
|
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']]];
|
|
}
|
|
|
|
$payload = $record ? json_decode($record->payload, true) : [];
|
|
unset($payload['_creation_hash']);
|
|
|
|
return Inertia::render('partnerships/editor', ['editorActorId' => $request->user()->id, 'saveCommands' => $record ? app(PartnershipDraftSaves::class)->commands($request->user(), $record) : [], 'record' => $record ? [...(array) $record, 'payload' => $payload] : 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, 'replayed' => (bool) ($record->command_replayed ?? false), 'saved_version' => $record->command_saved_version ?? null]) : 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;
|
|
$commands = $request->user() ? app(PartnershipCandidacies::class)->commands($request->user(), $id) : [];
|
|
$interestData = $interests->map(function ($interest) use ($record, $request, $commands) {
|
|
return [...(array) $interest, 'payload' => json_decode($interest->payload, true), 'assessment' => json_decode($interest->assessment, true),
|
|
'can_edit' => $request->user() && app(PartnershipExchange::class)->owns($request->user(), $interest), 'commands' => $commands[(int) $interest->id] ?? [],
|
|
'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,
|
|
'acceptingInterests' => app(PartnershipCandidacies::class)->accepting($record),
|
|
'canStartInterest' => $request->user() && ! $owner && ! DB::table('partnership_interests')->where('partnership_id', $id)->where('user_id', $request->user()->id)->exists(),
|
|
'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())),
|
|
'historySummary' => ($owner || $moderator) ? app(PartnershipPublicationHistory::class)->summary($request->user(), $id) : null,
|
|
'publicationActions' => app(PartnershipPublication::class)->actions($request->user(), $record),
|
|
'publicationCommands' => app(PartnershipPublication::class)->commands($request->user(), $record),
|
|
...$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 publicationHistory(Request $request, int $id): Response|JsonResponse
|
|
{
|
|
$data = $request->validate(['before' => 'nullable|string|max:4000']);
|
|
$history = app(PartnershipPublicationHistory::class)->page($request->user(), $id, $data['before'] ?? null);
|
|
|
|
return $request->expectsJson() ? response()->json($history) : Inertia::render('partnerships/publication-history', $history);
|
|
}
|
|
|
|
public function transition(Request $request, int $id): RedirectResponse|JsonResponse
|
|
{
|
|
$data = $request->validate(['client_key' => 'nullable|uuid', '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'], $data['client_key'] ?? null);
|
|
|
|
return $request->expectsJson() ? response()->json(['id' => $record->id, 'status' => $record->status, 'version' => $record->version, 'replayed' => (bool) ($record->command_replayed ?? false)]) : 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, 'replayed' => (bool) ($interest->command_replayed ?? false)]) : back()->with('success', 'Interesse guardado.');
|
|
}
|
|
|
|
public function decide(Request $request, int $id): RedirectResponse|JsonResponse
|
|
{
|
|
$data = $request->validate(['client_key' => 'nullable|uuid', 'partnership_version' => 'nullable|integer|min:1', 'weights_hash' => 'nullable|string|regex:/^[a-f0-9]{64}$/', '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'] ?? [], $data['partnership_version'] ?? null, $data['weights_hash'] ?? null, $data['client_key'] ?? null);
|
|
|
|
return $request->expectsJson() ? response()->json(['status' => $interest->status, 'version' => $interest->version, 'replayed' => (bool) ($interest->command_replayed ?? false)]) : back()->with('success', 'Candidatura atualizada.');
|
|
}
|
|
|
|
public function candidacyHistory(Request $request, int $id): Response|JsonResponse
|
|
{
|
|
$data = $request->validate(['before' => 'nullable|string|max:4000']);
|
|
$history = app(PartnershipCandidacyHistory::class)->page($request->user(), $id, $data['before'] ?? null);
|
|
|
|
return $request->expectsJson() ? response()->json($history) : Inertia::render('partnerships/interest-history', $history);
|
|
}
|
|
|
|
public function agreementHistory(Request $request, int $id): Response|JsonResponse
|
|
{
|
|
$data = $request->validate(['before' => 'nullable|string|max:4000']);
|
|
$history = app(PartnershipAgreementHistory::class)->page($request->user(), $id, $data['before'] ?? null);
|
|
|
|
return $request->expectsJson() ? response()->json($history) : Inertia::render('partnerships/agreement-history', $history);
|
|
}
|
|
|
|
public function room(Request $request, int $id): Response|JsonResponse
|
|
{
|
|
$context = app(PartnershipExchange::class)->room($request->user(), $id);
|
|
$participants = app(PartnershipExchange::class)->participants($context['partnership'], $context['interest']);
|
|
$participantIds = array_column($participants, 'id');
|
|
$canWrite = app(PartnershipExchange::class)->owns($request->user(), $context['partnership']) || app(PartnershipExchange::class)->owns($request->user(), $context['interest']);
|
|
$canAssign = $canWrite && in_array($context['room']->status, ['agreement_recorded', 'agreed', 'execution'], true);
|
|
$creationKeys = app(PartnershipMilestones::class)->creationKeys($request->user(), (int) $context['partnership']->id, $id);
|
|
$partnership = (array) $context['partnership'];
|
|
$payload = json_decode($partnership['payload'], true, 512, JSON_THROW_ON_ERROR);
|
|
unset($payload['_creation_hash']);
|
|
$partnership['payload'] = json_encode($payload, JSON_THROW_ON_ERROR);
|
|
$props = [...$context, 'partnership' => $partnership, 'room' => [...(array) $context['room'], 'agreement' => app(PartnershipAgreementProjection::class)->current($context['room']->agreement)],
|
|
'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', 'm.client_key', 'u.name'])
|
|
->map(fn ($message) => [...(array) $message, 'client_key' => (int) $message->author_id === $request->user()->id ? $message->client_key : null]),
|
|
'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 as m')->leftJoin('users as responsible', 'responsible.id', '=', 'm.responsible_id')->where('m.room_id', $id)->orderBy('m.id')
|
|
->get(['m.*', 'responsible.name as responsible_name'])->map(fn ($milestone) => [...(array) $milestone, 'responsible_is_participant' => in_array((int) $milestone->responsible_id, $participantIds, true), 'client_key' => $creationKeys[(int) $milestone->id] ?? null]),
|
|
'agreementActorId' => $request->user()->id, 'agreementParty' => app(PartnershipAgreements::class)->party($request->user(), $context), 'agreementCommands' => app(PartnershipAgreements::class)->commands($request->user(), $context), 'participants' => $canAssign ? $participants : [], 'canWrite' => $canWrite];
|
|
|
|
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);
|
|
abort_unless(app(PartnershipExchange::class)->owns($request->user(), $context['partnership']) || app(PartnershipExchange::class)->owns($request->user(), $context['interest']), 403);
|
|
$locked = DB::table('partnership_rooms')->where('id', $id)->lockForUpdate()->first();
|
|
abort_unless($locked !== null, 404);
|
|
$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;
|
|
}
|
|
abort_if(in_array($locked->status, ['closed', 'cancelled'], true), 409, 'Sala encerrada.');
|
|
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
|
|
{
|
|
$result = app(PartnershipAgreements::class)->act($request->user(), $id, $request->all());
|
|
|
|
return $request->expectsJson() ? response()->json($result) : 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(['client_key' => 'nullable|uuid', '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']);
|
|
if (isset($data['client_key'])) {
|
|
$data['client_key'] = strtolower($data['client_key']);
|
|
}
|
|
$result = app(PartnershipMilestones::class)->save($request->user(), $id, $data, $milestone);
|
|
|
|
return $request->expectsJson() ? response()->json($result) : back()->with('success', 'Marco guardado.');
|
|
}
|
|
|
|
public function upload(Request $request, int $id): RedirectResponse|JsonResponse
|
|
{
|
|
$data = $request->validate(['client_key' => 'nullable|uuid', 'file' => 'required|file|mimes:pdf,jpg,jpeg,png|max:10240', 'shared' => 'required|boolean']);
|
|
$file = $request->file('file');
|
|
abort_unless($file instanceof UploadedFile, 422);
|
|
$result = app(PartnershipRoomUploads::class)->save($request->user(), $id, $file, (bool) $data['shared'], isset($data['client_key']) ? strtolower($data['client_key']) : null);
|
|
|
|
return $request->expectsJson() ? response()->json($result) : back()->with('success', 'Envio reconciliado com o documento privado. Consulte o estado da análise na lista.')->with('uploaded_document_id', $result['document_id']);
|
|
}
|
|
|
|
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
|
|
{
|
|
app(PartnershipNotifications::class)->markRead($request->user(), $id);
|
|
|
|
return back();
|
|
}
|
|
|
|
/** @return array<string,mixed> */
|
|
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' => 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']) : []];
|
|
}
|
|
}
|