Reconciliar fontes instaladas 423bd512d8c5 com main

This commit is contained in:
2026-10-20 14:45:19 +02:00
parent 507387502c
commit 9ab721c97f
108 changed files with 2766 additions and 494 deletions
@@ -53,6 +53,36 @@ class PortalManagementController extends Controller
return Inertia::render('management/history', ['events' => $events]);
}
private function interestAssignmentEtag(InterestRequest $interest): string
{
return hash('sha256',json_encode([$interest->id,$interest->status,$interest->assigned_to,$interest->updated_at?->format('Y-m-d H:i:s.u')],JSON_THROW_ON_ERROR));
}
public function interestAssignments(Request $request): Response|JsonResponse
{
abort_unless(app(\App\Services\ScopedPortalAdministration::class)->administrative($request->user()),403);
$rows=InterestRequest::whereNotIn('status',['cancelled','closed','refused','converted'])->orderBy('id')->get(['id','reference','status','assigned_to','updated_at'])
->map(fn($row)=>['id'=>$row->id,'protocol'=>$row->reference,'status'=>$row->status,'analyst_id'=>$row->assigned_to,'version'=>0,'etag'=>$this->interestAssignmentEtag($row),'due_at'=>null]);
$data=['kind'=>'interest','requests'=>$rows,'analysts'=>$this->interestAssignees()->get(['id','name'])];
return $request->expectsJson()?response()->json($data):Inertia::render('management/desk-assignments',$data);
}
public function assignInterest(Request $request, InterestRequest $interest): RedirectResponse|JsonResponse
{
abort_unless(app(\App\Services\ScopedPortalAdministration::class)->administrative($request->user()),403);
$data=$request->validate(['etag'=>'required|string|size:64','analyst_id'=>['required','integer',Rule::in($this->interestAssignees()->pluck('id'))],'reason'=>'required|string|min:10|max:2000'],['reason.required'=>'Explique o motivo da atribuição.','reason.min'=>'Explique o motivo da atribuição com pelo menos 10 caracteres.','analyst_id.required'=>'Seleccione o responsável.','analyst_id.in'=>'Seleccione um responsável autorizado.']);
DB::transaction(function()use($request,$interest,$data){
$row=InterestRequest::whereKey($interest->id)->lockForUpdate()->firstOrFail();
if (!hash_equals($this->interestAssignmentEtag($row),$data['etag'])) throw ValidationException::withMessages(['etag'=>'A atribuição mudou. Recupere a lista actual e tente novamente.']);
if (in_array($row->status,['cancelled','closed','refused','converted'],true)) throw ValidationException::withMessages(['status'=>'Este pedido já não permite uma nova atribuição. Recupere a lista actual.']);
$owner=(int)($row->metadata['portal_user_id']??0);
abort_if($owner===(int)$data['analyst_id'] || InvestmentSubmission::where('interest_request_id',$row->id)->where('user_id',$data['analyst_id'])->exists(),422,'O interessado não pode acompanhar o seu próprio pedido como equipa.');
PortalAudit::save($row,['assigned_to'=>(int)$data['analyst_id']],$request->user()->id);
app(\App\Services\PortalProfileAccess::class)->audit($request->user(),'interest_assigned',['interest_id'=>$row->id,'assigned_to'=>(int)$data['analyst_id'],'reason'=>$data['reason']]);
});
return $request->expectsJson()?response()->json(['ok'=>true]):back()->with('success','Responsável atribuído.');
}
public function interests(Request $request): Response
{
$filters = $request->validate([
@@ -65,6 +95,7 @@ class PortalManagementController extends Controller
$query = InterestRequest::query()->with('assignee:id,name')->select([
'id', 'reference', 'full_name', 'email', 'country', 'metadata', 'sector_interest', 'existing_business_id', 'modality', 'status', 'assigned_to', 'next_action', 'next_action_due_at', 'created_at',
]);
app(\App\Services\ScopedPortalAdministration::class)->interestQuery($query, $request->user());
if ($country = $filters['country'] ?? null) {
$values = app(CountryCatalogue::class)->filterValues($country);
$query->whereIn(DB::raw('LOWER(BTRIM(country))'), $values);
@@ -111,6 +142,7 @@ class PortalManagementController extends Controller
public function interest(InterestRequest $interest): Response
{
abort_unless(app(\App\Services\ScopedPortalAdministration::class)->interest(request()->user(), $interest),403);
$history = DB::table('business_workflow_transitions')->where('interest_request_id', $interest->id)
->orderByDesc('created_at')->limit(50)->get(['from_status', 'to_status', 'actor_id', 'reason', 'created_at'])
->map(function ($row) {
@@ -138,12 +170,18 @@ class PortalManagementController extends Controller
public function review(Request $request, InterestRequest $interest, InterestWorkflow $workflow): RedirectResponse|JsonResponse
{
abort_unless(app(\App\Services\ScopedPortalAdministration::class)->interest(request()->user(), $interest),403);
$validated = $request->validate([
'status' => ['required', Rule::in(self::STATES)],
'note' => ['nullable', 'string', 'min:10', 'max:2000'],
'next_action' => ['nullable', 'required_with:next_action_due_at', 'string', 'max:180'],
'next_action_due_at' => ['nullable', 'required_with:next_action', 'date', 'after_or_equal:today'],
'assigned_to' => ['nullable', 'integer', Rule::in($this->interestAssignees()->pluck('id'))],
], [
'next_action_due_at.required_with' => 'Indique o prazo da próxima acção.',
'next_action_due_at.date' => 'Indique uma data válida para a próxima acção.',
'next_action_due_at.after_or_equal' => 'O prazo da próxima acção deve ser hoje ou uma data futura.',
'next_action.required_with' => 'Descreva a próxima acção correspondente ao prazo indicado.',
]);
$before = $interest->status;
$updated = $workflow->transition($request->user(), $interest, $validated['status'], $validated['note'] ?? null,
@@ -160,6 +198,7 @@ class PortalManagementController extends Controller
public function withdrawProponentSharingConsent(Request $request, InterestRequest $interest): RedirectResponse|JsonResponse
{
abort_unless(app(\App\Services\ScopedPortalAdministration::class)->interest(request()->user(), $interest),403);
$data = $request->validate(['reason' => ['required', 'string', 'min:10', 'max:2000']]);
$this->withdrawConsent($request, $interest, $data['reason'], 'share_contact_with_proponent');
@@ -169,6 +208,7 @@ class PortalManagementController extends Controller
public function withdrawContactConsent(Request $request, InterestRequest $interest): RedirectResponse|JsonResponse
{
abort_unless(app(\App\Services\ScopedPortalAdministration::class)->interest(request()->user(), $interest),403);
$data = $request->validate(['reason' => ['required', 'string', 'min:10', 'max:2000']]);
$this->withdrawConsent($request, $interest, $data['reason'], 'respond_to_interest');
@@ -217,7 +257,9 @@ class PortalManagementController extends Controller
['triagers', 'reviewers', 'approvers_l1', 'approvers_l2', 'publishers'],
)));
return User::query()->whereNotNull('email_verified_at')->whereIn(DB::raw('lower(email)'), $emails)->orderBy('name');
return User::query()->whereNotNull('email_verified_at')->where(function ($q) use ($emails) {
$q->whereIn(DB::raw('lower(email)'), $emails)->orWhereIn('id', DB::table('portal_superuser_pair')->select('first_user_id'))->orWhereIn('id', DB::table('portal_superuser_pair')->select('second_user_id'));
})->orderBy('name');
}
public function catalog(Request $request, string $resource): Response