> */ public function build(User $actor, string $area): array { abort_unless(auth()->id() === $actor->id && in_array($area, ['procurement', 'people'], true), 403); abort_unless($area !== 'people' || app(PeopleHiring::class)->kinds($actor) !== [], 403); if (app(OrganizationAccess::class)->maintenanceOnly($actor)) { return []; } $rows = $area === 'people' ? $this->people($actor) : [...$this->purchases($actor), ...$this->suppliers($actor), ...$this->quotations($actor), ...$this->documentary($actor), ...$this->formalReviews($actor), ...$this->supply($actor), ...$this->suggestions($actor), ...$this->awards($actor), ...$this->warehouse($actor), ...$this->inventoryCorrections($actor), ...$this->lotSetups($actor), ...$this->finance($actor)]; if ($area === 'procurement') { $rows = [...$rows, ...$this->supplierImprovements($actor), ...$this->supplierQuality($actor), ...$this->supplyFollowUp($actor), ...$this->lotQuality($actor)]; } usort($rows, fn ($a, $b) => strcmp($a['created_at'] ?? '', $b['created_at'] ?? '') ?: strnatcmp($a['id'], $b['id'])); return $rows; } /** @return list> */ private function lotQuality(User $actor): array { $lots = app(InventoryLots::class); if (! Schema::hasTable('inventory_lot_allocations') || (! $lots->allows($actor, 'inventory_lot_prepare') && ! $lots->allows($actor, 'inventory_lot_release') && ! $lots->allows($actor, 'inventory_lot_hold'))) { return []; } $rows = []; foreach ($lots->activeSetups()->lazyById(100) as $setup) { $context = $lots->context($setup->stock_item_id, $setup->warehouse_id); foreach ($context['lots'] ?? [] as $entry) { if ($entry['quantity_milli'] <= 0) { continue; } $metadata = SupplyRecord::query()->whereKey($entry['metadata_id'])->firstOrFail(); $action = $entry['state'] === 'unidentified' && $lots->canIdentify($actor, $metadata) ? 'identify' : ($entry['state'] === 'held' && $lots->canDecideLot($actor, $metadata, $context, $entry, 'release') ? 'release' : ($entry['quality_released'] && ! $entry['eligible'] && $lots->canDecideLot($actor, $metadata, $context, $entry, 'hold') ? 'hold' : null)); if ($action === null) { continue; } $rows[] = ['id' => 'lot-quality:'.$setup->warehouse_id.':'.$entry['id'].':'.$action.':'.$entry['version'].':'.$entry['state_version'], 'module' => 'warehouse', 'reference' => $setup->snapshot['item']['code'].' / LOTE-'.$entry['id'].' / '.$setup->snapshot['warehouse']['code'], 'label' => match ($action) { 'identify' => 'Completar identificação de lote retido', 'release' => 'Conferir qualidade para libertação independente do lote', default => 'Conferir retenção de lote anteriormente libertado com validade insuficiente' }, 'task' => 'lot_'.$action, 'status' => $entry['state'], 'blocked' => false, 'created_at' => $this->date($metadata->created_at), 'due_on' => null, 'href' => '/stock/lotes?lot_id='.$entry['id'].'&warehouse_id='.$setup->warehouse_id.'#lot-'.$setup->warehouse_id.'-'.$entry['id']]; } } return $rows; } /** @return list> */ private function supplyFollowUp(User $actor): array { if (! Schema::hasTable('supply_records')) { return []; } $planning = app(SupplyPlanning::class); $coordination = app(SupplyCoordination::class); $rows = []; foreach (['reservation' => 'supply_reserve', 'delivery' => 'supply_delivery'] as $kind => $permission) { if (! $planning->allows($actor, $permission)) { continue; } foreach ($planning->latest($kind)->whereIn('state', $kind === 'reservation' ? ['active', 'fulfilled'] : ['active'])->lazyById(100) as $record) { if (! $coordination->canManage($actor, $record, 'edit')) { continue; } $remaining = $kind === 'reservation' ? $coordination->remainingReservation($record) : $coordination->remainingDelivery($record); if ($remaining <= 0) { continue; } $rows[] = ['id' => 'coordination:'.$record->id.':followup', 'module' => 'supply', 'reference' => trim(($record->snapshot['item']['code'] ?? '').' '.($record->data['reference'] ?? $record->snapshot['order']['number'] ?? 'FOR-'.$record->id)), 'label' => $kind === 'reservation' ? 'Acompanhar necessidade planeada e associar saída efectiva' : 'Acompanhar entrega confirmada ainda por receber', 'task' => $kind.'_followup', 'status' => $record->state, 'blocked' => false, 'created_at' => $this->date($record->created_at), 'due_on' => $record->data[$kind === 'reservation' ? 'needed_on' : 'expected_on'], 'href' => '/compras/fornecimento?view='.($kind === 'reservation' ? 'reservations' : 'deliveries').'&record_id='.$record->id.'#supply-record-'.$record->id]; } } return $rows; } /** @return list> */ private function supplierQuality(User $actor): array { if (! Schema::hasTable('supplier_evaluations') || ! $actor->hasFunctions(SupplierEvaluationWorkflow::ROLES)) { return []; } $workflow = app(SupplierEvaluationWorkflow::class); $responses = app(SupplierResponseWorkflow::class); $rows = []; foreach (SupplierEvaluation::query()->visibleTo($actor)->whereIn('status', ['draft', 'returned', 'review'])->lazyById(100) as $evaluation) { $action = $evaluation->status === 'review' ? 'review' : 'edit'; if (! $workflow->canAct($actor, $evaluation, $action) || ($action === 'edit' && ! in_array($actor->id, $evaluation->contributors, true))) { continue; } $rows[] = ['id' => 'supplier-evaluation:'.$evaluation->id.':'.$action.':'.$evaluation->version, 'module' => 'purchases', 'reference' => 'AVA-'.$evaluation->id.' / R'.$evaluation->revision, 'label' => $action === 'review' ? 'Conferir avaliação de qualidade do fornecedor' : 'Completar e submeter avaliação de qualidade do fornecedor', 'task' => 'supplier_evaluation_'.$action, 'status' => $evaluation->status, 'blocked' => false, 'created_at' => $this->date($evaluation->created_at), 'due_on' => null, 'href' => '/fornecedores/avaliacoes/'.$evaluation->id]; } if (! Schema::hasTable('supplier_responses')) { return $rows; } foreach (SupplierResponse::query()->visibleTo($actor)->whereIn('status', ['draft', 'returned', 'review']) ->whereHas('evaluation', fn ($q) => $q->where('status', 'approved'))->with('evaluation')->lazyById(100) as $record) { $evaluation = $record->evaluation; $action = $record->status === 'review' ? 'review' : 'edit'; if (! $responses->canAct($actor, $evaluation, $record, $action) || ($action === 'edit' && ! in_array($actor->id, $record->contributors, true))) { continue; } $rows[] = ['id' => 'supplier-response:'.$record->id.':'.$action.':'.$record->version, 'module' => 'purchases', 'reference' => 'AVA-'.$evaluation->id.' / CTR-'.$record->id, 'label' => $action === 'review' ? 'Analisar esclarecimento ou contraditório do fornecedor' : 'Completar e submeter esclarecimento do fornecedor', 'task' => 'supplier_response_'.$action, 'status' => $record->status, 'blocked' => false, 'created_at' => $this->date($record->created_at), 'due_on' => null, 'href' => '/fornecedores/avaliacoes/'.$evaluation->id.'#contraditorio-'.$record->id]; } return $rows; } /** @return list> */ private function supplierImprovements(User $actor): array { if (! Schema::hasTable('supplier_improvements') || ! $actor->hasFunctions(SupplierEvaluationWorkflow::ROLES)) { return []; } $workflow = app(SupplierImprovementWorkflow::class); $rows = []; foreach (SupplierImprovement::query()->visibleTo($actor)->whereIn('status', ['open', 'returned', 'review']) ->whereHas('evaluation', fn ($q) => $q->where('status', 'approved'))->with('evaluation')->lazyById(100) as $measure) { $action = $measure->status === 'review' ? 'review' : 'execute'; if (! $workflow->canAct($actor, $measure->evaluation, $measure, $action)) { continue; } $rows[] = ['id' => 'supplier-improvement:'.$measure->id.':'.$action.':'.$measure->version, 'module' => 'purchases', 'reference' => 'AVA-'.$measure->supplier_evaluation_id.' / MED-'.$measure->id, 'label' => $action === 'review' ? 'Conferir resultado e evidência da medida de melhoria' : 'Registar execução da medida de melhoria atribuída', 'task' => 'supplier_improvement_'.$action, 'status' => $measure->status, 'blocked' => false, 'created_at' => $this->date($measure->created_at), 'due_on' => $measure->due_on, 'href' => '/fornecedores/avaliacoes/'.$measure->supplier_evaluation_id.'#medida-'.$measure->id]; } return $rows; } /** @return list> */ private function purchases(User $actor): array { $rows = []; $prepare = app(OrganizationAccess::class)->canRequest($actor); $query = Requisition::query()->visibleTo($actor)->where(function ($q) use ($actor, $prepare) { $q->where('status', 'pending'); if ($prepare) { $q->orWhere(fn ($draft) => $draft->where('status', 'draft')->where('requester_id', $actor->id)); } }); foreach ($query->with('steps')->lazyById(100) as $record) { $draft = $record->status === 'draft' && $prepare && $record->requester_id === $actor->id; if (! $draft && ! $record->canDecide($actor)) { continue; } $step = $draft ? null : $record->steps->firstWhere('decision', 'pending'); $rows[] = ['id' => 'purchase:'.$record->id.':'.($step->id ?? 'draft'), 'module' => 'purchases', 'reference' => $record->number, 'label' => $draft ? 'Conferir e submeter requisição de compra' : 'Decidir a etapa de aprovação', 'task' => $draft ? 'prepare' : 'approve', 'status' => $record->status, 'blocked' => false, 'created_at' => $this->date($record->created_at), 'due_on' => null, 'href' => '/requisicoes/'.$record->id]; } return $rows; } /** @return list> */ private function suppliers(User $actor): array { $access = app(SupplierParticipation::class); if (! $access->canReviewRole($actor)) { return []; } $rows = []; foreach (Supplier::query()->where('status', 'pending')->lazyById(100) as $supplier) { if (! $access->canReview($actor, $supplier)) { continue; } $rows[] = ['id' => 'supplier:'.$supplier->id.':review:'.$supplier->version, 'module' => 'purchases', 'reference' => 'FOR-'.$supplier->id, 'label' => 'Conferir cadastro do fornecedor', 'task' => 'supplier_review', 'status' => $supplier->status, 'blocked' => false, 'created_at' => $this->date($supplier->created_at), 'due_on' => null, 'href' => '/fornecedores/'.$supplier->id.'#supplier-review']; } return $rows; } /** @return list> */ private function inventoryCorrections(User $actor): array { if (! Schema::hasTable('supply_records')) { return []; } $rows = []; foreach (['stock_event' => app(InventoryDispositions::class), 'stock_reversal' => app(InventoryReversals::class)] as $kind => $workflow) { if (! $workflow->allows($actor, 'review') && ! $workflow->allows($actor, 'execute')) { continue; } $prefix = $kind === 'stock_event' ? 'disposition' : 'reversal'; $path = $kind === 'stock_event' ? '/stock/ocorrencias' : '/stock/estornos'; foreach (app(SupplyPlanning::class)->latest($kind)->whereIn('state', ['draft', 'returned', 'approved'])->lazyById(100) as $record) { $sourceCurrent = $record->state !== 'approved' || $workflow->executionSourceCurrent($record); $action = $record->state === 'approved' ? ($workflow->canAct($actor, $record, 'execute') ? 'execute' : 'return') : 'approve'; if ($action === 'return' && $sourceCurrent) { continue; } if (! $workflow->canAct($actor, $record, $action)) { continue; } $rows[] = ['id' => $prefix.':'.$record->id.':'.$action, 'module' => 'warehouse', 'reference' => $record->data['reference'], 'label' => ! $sourceCurrent ? ($action === 'return' ? 'Conferir origem ou saldo alterados para devolução' : 'Conferir impedimento da correcção física autorizada') : ($kind === 'stock_reversal' ? ($action === 'execute' ? 'Conferir e executar estorno integral autorizado' : 'Conferir correcção integral de stock') : ($action === 'execute' ? 'Conferir e confirmar saída física autorizada' : 'Conferir devolução ou abate físico')), 'task' => $prefix.'_'.$action, 'status' => $record->state, 'blocked' => ! $sourceCurrent, 'created_at' => $this->date($record->created_at), 'due_on' => null, 'href' => $path.'?record_id='.$record->id.'#'.$prefix.'-'.$record->id]; } } return $rows; } /** @return list> */ private function lotSetups(User $actor): array { $workflow = app(InventoryLots::class); if (! Schema::hasTable('inventory_lot_allocations') || ! $workflow->allows($actor, 'inventory_lot_review')) { return []; } $rows = []; foreach (app(SupplyPlanning::class)->latest('lot_setup')->where('state', 'draft')->lazyById(100) as $record) { if (! $workflow->canActivate($actor, $record)) { continue; } $rows[] = ['id' => 'lot-setup:'.$record->id.':activate', 'module' => 'warehouse', 'reference' => $record->snapshot['item']['code'].' '.$record->snapshot['warehouse']['code'], 'label' => 'Conferir configuração e saldo inicial por lotes', 'task' => 'lot_setup_activate', 'status' => $record->state, 'blocked' => false, 'created_at' => $this->date($record->created_at), 'due_on' => null, 'href' => '/stock/lotes?record_id='.$record->id.'#lot-setup-'.$record->id]; } return $rows; } /** @return list> */ private function warehouse(User $actor): array { if (! Schema::hasTable('warehouse_requests') || ! Schema::hasTable('warehouse_request_events')) { return []; } $desk = app(WarehouseWorkdesk::class); $rows = []; foreach (['review', 'reserve', 'delivery', 'receipt'] as $view) { $query = $desk->forView($desk->visible($actor), $actor, $view); foreach ($query->lazyById(100) as $record) { $progress = app(WarehouseFulfilment::class)->progress($record); $health = in_array($view, ['reserve', 'delivery'], true) ? $desk->health($record, $progress) : null; if ($view === 'reserve' && ! $health['needs_reservation'] && ! $health['needs_check']) { continue; } $base = ['module' => 'warehouse', 'reference' => 'RA-'.$record->id, 'status' => $progress['state'], 'created_at' => $this->date($record->created_at), 'due_on' => null]; if ($view === 'receipt') { foreach ($progress['events'] as $event) { if ($event['action'] === 'deliver' && ! in_array($event['id'], $progress['received_deliveries'], true)) { $rows[] = [...$base, 'id' => 'warehouse:'.$record->id.':receive:'.$event['id'], 'task' => 'receive', 'label' => 'Confirmar recepção da entrega #'.$event['id'], 'blocked' => false, 'created_at' => $this->date($event['created_at']), 'href' => '/stock/requisicoes/'.$record->id.'#warehouse-command-receive-'.$event['id']]; } } continue; } $action = $view === 'review' ? 'authorize' : ($view === 'delivery' ? 'deliver' : 'reserve'); $blocked = $view === 'delivery' && ! $health['delivery_ready']; $rows[] = [...$base, 'id' => 'warehouse:'.$record->id.':'.$view, 'task' => $view, 'label' => match ($view) { 'review' => 'Autorizar ou recusar requisição interna', 'reserve' => 'Conferir reposição e actualizar reserva', default => $blocked ? 'Conferir stock antes da entrega' : 'Confirmar a entrega física', }, 'blocked' => $blocked, 'href' => '/stock/requisicoes/'.$record->id.($blocked ? '' : '#warehouse-command-'.$action)]; } } return $rows; } /** @return list> */ private function people(User $actor): array { $kinds = app(PeopleHiring::class)->kinds($actor); abort_unless($kinds !== [], 403); if (! Schema::hasTable('people_processes') || ! Schema::hasTable('people_process_access')) { return []; } $query = app(PeopleAccess::class)->visible(PeopleProcess::query()->whereIn('kind', $kinds), $actor); $desk = app(PeopleWorkdesk::class)->build(clone $query, $actor); $dates = (clone $query)->pluck('created_at', 'id'); $rows = []; foreach ([...$desk['tasks'], ...$desk['approvals']] as $task) { $rows[] = ['id' => 'people:'.$task['id'], 'module' => 'people', 'reference' => $task['reference'], 'label' => $task['label'], 'task' => $task['action'], 'status' => $task['status'], 'blocked' => false, 'created_at' => $this->date($dates->get($task['process_id'])), 'due_on' => $task['due_on'], 'href' => $task['href']]; } return $rows; } /** @return list> */ private function finance(User $actor): array { if (! $actor->hasFunctions(['finance'])) { return []; } $rows = []; foreach (Invoice::query()->where('status', 'pending')->lazyById(100) as $invoice) { if (! app(InvoiceWorkflow::class)->reviewerEligible($actor, $invoice)) { continue; } $rows[] = ['id' => 'invoice:'.$invoice->id, 'module' => 'finance', 'reference' => $invoice->number, 'label' => 'Conferir factura de fornecedor', 'task' => 'invoice_review', 'status' => $invoice->status, 'blocked' => false, 'created_at' => $this->date($invoice->created_at), 'due_on' => null, 'href' => '/facturas?invoice_id='.$invoice->id.'#invoice-'.$invoice->id]; } foreach (ExternalPayment::query()->where('status', 'pending')->lazyById(100) as $payment) { if (! app(ExternalPaymentWorkflow::class)->reviewerEligible($actor, $payment)) { continue; } $invoice = Invoice::query()->whereKey($payment->invoice_id)->firstOrFail(); $rows[] = ['id' => 'payment:'.$payment->id, 'module' => 'finance', 'reference' => $invoice->number, 'label' => 'Conferir registo de pagamento externo', 'task' => 'payment_review', 'status' => $payment->status, 'blocked' => false, 'created_at' => $this->date($payment->created_at), 'due_on' => null, 'href' => '/pagamentos?payment_id='.$payment->id.'#payment-'.$payment->id]; } return $rows; } /** @return list> */ private function awards(User $actor): array { if (! $actor->hasFunctions(['procurement', 'finance', 'department_head'])) { return []; } $workflow = app(PurchaseWorkflow::class); $rows = []; $requests = Requisition::query()->visibleTo($actor); $query = PurchaseAward::query()->whereIn('requisition_id', (clone $requests)->select('id'))->whereIn('status', ['pending', 'approved']); foreach ($query->lazyById(100) as $award) { $request = (clone $requests)->whereKey($award->requisition_id)->firstOrFail(); $decide = $workflow->canDecide($actor, $award, $request); if (! $decide && ! $workflow->canIssue($actor, $award, $request)) { continue; } $step = $decide ? $award->approvals()->where('decision', 'pending')->orderBy('position')->first() : null; $rows[] = ['id' => 'award:'.$award->id.':'.($step->id ?? 'issue'), 'module' => 'purchases', 'reference' => $request->number, 'label' => $decide ? 'Decidir a etapa da adjudicação' : 'Conferir adjudicação para emitir ordem de compra', 'task' => $decide ? 'award_approve' : 'order_issue_review', 'status' => $award->status, 'blocked' => false, 'created_at' => $this->date($award->created_at), 'due_on' => null, 'href' => '/requisicoes/'.$request->id.'/adjudicacao']; } return $rows; } private function date(mixed $value): ?string { return $value === null ? null : CarbonImmutable::parse($value)->utc()->toIso8601String(); } /** @return list> */ private function formalReviews(User $actor): array { if (! $actor->hasFunctions(['finance', 'department_head'])) { return []; } $rows = []; $admission = app(FormalAdmission::class); $recommendation = app(FormalRecommendation::class); $requests = Requisition::query()->visibleTo($actor)->where('status', 'approved')->whereNotIn('id', PurchaseAward::query()->select('requisition_id')); foreach ($requests->lazyById(100) as $request) { $base = ['module' => 'purchases', 'reference' => $request->number, 'blocked' => false, 'status' => 'pending', 'due_on' => null]; $adoption = $admission->adoption($request); if ($adoption !== null) { $decisions = AdmissionDecision::query()->where('admission_adoption_id', $adoption->id)->whereNotExists(fn ($query) => $query->selectRaw('1')->from('admission_decisions as newer') ->whereColumn('newer.tenant_id', 'admission_decisions.tenant_id')->whereColumn('newer.admission_adoption_id', 'admission_decisions.admission_adoption_id') ->whereColumn('newer.quotation_id', 'admission_decisions.quotation_id')->whereColumn('newer.revision', '>', 'admission_decisions.revision')); foreach ($decisions->lazyById(100) as $decision) { if ($admission->canReview($actor, $request, $decision)) { $rows[] = [...$base, 'id' => 'admission:'.$decision->id.':review', 'task' => 'admission_review', 'label' => 'Conferir proposta de admissão formal', 'created_at' => $this->date($decision->created_at), 'href' => '/requisicoes/'.$request->id.'/admissao?version='.$adoption->id.'&decision_id='.$decision->id.'#admission-'.$decision->id]; } } } $adoption = $recommendation->adoption($request); $attempt = $adoption === null ? null : RecommendationAttempt::query()->where('recommendation_adoption_id', $adoption->id)->latest('revision')->first(); if ($adoption !== null && $attempt !== null && $recommendation->canReview($actor, $request, $attempt)) { $rows[] = [...$base, 'id' => 'recommendation:'.$attempt->id.':review', 'task' => 'recommendation_review', 'label' => 'Conferir recomendação formal de adjudicação', 'created_at' => $this->date($attempt->created_at), 'href' => '/requisicoes/'.$request->id.'/recomendacao?version='.$adoption->id.'&attempt_id='.$attempt->id.'#recommendation-'.$attempt->id]; } } return $rows; } /** Only a person's own unfinished analysis, never a new mandatory analyst. * @return list> */ private function documentary(User $actor): array { if (! $actor->hasFunctions(['procurement', 'finance', 'department_head'])) { return []; } $rows = []; $workflow = app(DocumentaryQualificationWorkflow::class); $requests = Requisition::query()->visibleTo($actor)->where('status', 'approved')->whereNotIn('id', PurchaseAward::query()->select('requisition_id')); $query = QualificationSet::query()->whereIn('requisition_id', (clone $requests)->select('id'))->whereNotExists(fn ($newer) => $newer->selectRaw('1')->from('qualification_sets as newer') ->whereColumn('newer.tenant_id', 'qualification_sets.tenant_id')->whereColumn('newer.requisition_id', 'qualification_sets.requisition_id')->whereColumn('newer.revision', '>', 'qualification_sets.revision')); foreach ($query->lazyById(100) as $set) { $request = (clone $requests)->whereKey($set->requisition_id)->firstOrFail(); if (! $workflow->canAssess($actor, $request, $set)) { continue; } $records = QualificationAssessment::query()->where('qualification_set_id', $set->id)->where('analyst_id', $actor->id)->whereNotExists(fn ($newer) => $newer->selectRaw('1')->from('qualification_assessments as newer') ->whereColumn('newer.tenant_id', 'qualification_assessments.tenant_id')->whereColumn('newer.qualification_set_id', 'qualification_assessments.qualification_set_id') ->whereColumn('newer.analyst_id', 'qualification_assessments.analyst_id')->whereColumn('newer.quotation_id', 'qualification_assessments.quotation_id')->whereColumn('newer.revision', '>', 'qualification_assessments.revision')); foreach ($records->lazyById(100) as $record) { if (! collect($record->answers)->contains('result', 'pending')) { continue; } $rows[] = ['id' => 'qualification:'.$record->id.':complete', 'module' => 'purchases', 'reference' => $request->number, 'label' => 'Completar a minha análise documental', 'task' => 'qualification_complete', 'status' => 'pending', 'blocked' => false, 'created_at' => $this->date($record->created_at), 'due_on' => null, 'href' => '/requisicoes/'.$request->id.'/qualificacao?version='.$set->id.'"ation_id='.$record->quotation_id.'#qualification-assess']; } } return $rows; } /** @return list> */ private function supply(User $actor): array { $planning = app(SupplyPlanning::class); if (! Schema::hasTable('supply_records') || (! $planning->allows($actor, 'supply_review') && ! $planning->allows($actor, 'supply_history_review') && ! $planning->allows($actor, 'supply_technical_review'))) { return []; } $rows = []; foreach (['agreement', 'policy', 'auto_rule', 'history', 'alternative'] as $kind) { $permission = match ($kind) { 'history' => 'supply_history_review', 'alternative' => 'supply_technical_review', default => 'supply_review' }; if (! $planning->allows($actor, $permission)) { continue; } foreach ($planning->latest($kind)->whereIn('state', ['draft', 'returned'])->lazyById(100) as $record) { $eligible = match ($kind) { 'auto_rule' => app(AutomaticPurchases::class)->canReview($actor, $record), 'history' => app(SupplyHistory::class)->canReview($actor, $record), 'alternative' => app(SupplyCoordination::class)->canReviewAlternative($actor, $record), default => $planning->canReview($actor, $record), }; if (! $eligible) { continue; } $rows[] = ['id' => 'supply:'.$record->id.':review', 'module' => 'supply', 'reference' => trim(($record->snapshot['item']['code'] ?? 'Fornecimento').' '.($record->snapshot['warehouse']['code'] ?? '')), 'label' => match ($kind) { 'agreement' => 'Conferir acordo de fornecimento', 'policy' => 'Conferir política de reposição', 'history' => 'Conferir histórico de consumo', 'alternative' => 'Conferir alternativa técnica', default => 'Conferir parametrização de compra automática', }, 'task' => $kind.'_review', 'status' => $record->state, 'blocked' => false, 'created_at' => $this->date($record->created_at), 'due_on' => null, 'href' => $kind === 'auto_rule' ? '/stock/reposicao?rule_id='.$record->id.'#supply-rule-'.$record->id : '/compras/fornecimento?view='.match ($kind) { 'agreement' => 'agreements', 'history' => 'history', 'alternative' => 'alternatives', default => 'calendar' }.'&record_id='.$record->id.'#supply-record-'.$record->id]; } } return $rows; } /** @return list> */ private function suggestions(User $actor): array { $planning = app(SupplyPlanning::class); if (! Schema::hasTable('supply_records') || (! $planning->allows($actor, 'supply_suggest_review') && ! $planning->allows($actor, 'supply_suggest_convert'))) { return []; } $service = app(SupplySuggestions::class); $rows = []; foreach ($planning->latest('suggestion')->whereIn('state', ['draft', 'returned', 'approved', 'converted'])->lazyById(100) as $record) { $action = match ($record->state) { 'approved' => 'convert', 'converted' => 'close', default => 'approve' }; if (! $service->canAct($actor, $record, $action)) { continue; } $rows[] = ['id' => 'suggestion:'.$record->id.':'.$action, 'module' => 'supply', 'reference' => trim($record->snapshot['item']['code'].' '.$record->snapshot['warehouse']['code']), 'label' => match ($action) { 'convert' => 'Preparar compra a partir da sugestão aprovada', 'close' => 'Conferir e encerrar acompanhamento da reposição', default => 'Conferir sugestão de reposição' }, 'task' => 'suggestion_'.$action, 'status' => $record->state, 'blocked' => false, 'created_at' => $this->date($record->created_at), 'due_on' => null, 'href' => '/compras/fornecimento?view=suggestions&record_id='.$record->id.'#supply-suggestion-'.$record->id]; } return $rows; } /** @return list> */ private function quotations(User $actor): array { if (! $actor->hasFunctions(['procurement', 'finance', 'department_head'])) { return []; } $access = app(QuotationParticipation::class); $rows = []; $requests = Requisition::query()->visibleTo($actor)->where('status', 'approved') ->whereNotIn('id', PurchaseAward::query()->select('requisition_id')); foreach ($requests->lazyById(100) as $request) { $prepare = $access->canPrepare($actor, $request); if (! $prepare && ! $access->canEvaluateProcess($actor, $request)) { continue; } $comparison = app(ProposalComparison::class)->inspect($request); $path = '/requisicoes/'.$request->id.'/cotacoes'; $base = ['module' => 'purchases', 'reference' => $request->number, 'status' => $request->status, 'blocked' => false, 'created_at' => $this->date($request->created_at), 'due_on' => null]; if ($prepare) { if (count($comparison['rows']) < $comparison['policy']['minimum_proposals']) { $rows[] = [...$base, 'id' => 'quotation:'.$request->id.':prepare', 'task' => 'quotation_prepare', 'label' => 'Completar consulta ao mercado', 'href' => $path.'?prepare=1#quotation-create']; } elseif (! $comparison['ready'] && collect($comparison['rows'])->contains('eligible', false)) { $rows[] = [...$base, 'id' => 'quotation:'.$request->id.':check', 'task' => 'quotation_check', 'label' => 'Conferir propostas ou impedimentos da comparação', 'href' => $path]; } elseif ($comparison['ready'] && $actor->id !== $request->requester_id) { $rows[] = [...$base, 'id' => 'quotation:'.$request->id.':result', 'task' => 'comparison_review', 'label' => 'Conferir comparação e requisitos da adjudicação', 'href' => '/requisicoes/'.$request->id.'/adjudicacao']; } } $quotes = Quotation::query()->where('requisition_id', $request->id)->with('supplier')->get()->keyBy('id'); foreach ($comparison['rows'] as $row) { $quote = $quotes->get($row['id']); if (! $row['eligible'] || $row['evaluation_count'] >= $comparison['policy']['minimum_evaluators'] || $quote === null || ! $access->canEvaluate($actor, $request, $quote)) { continue; } $rows[] = [...$base, 'id' => 'quotation:'.$quote->id.':evaluate', 'task' => 'quotation_evaluate', 'label' => 'Avaliar proposta '.$quote->reference, 'created_at' => $this->date($quote->created_at), 'href' => $path.'?quotation_id='.$quote->id.'#quotation-'.$quote->id]; } } return $rows; } }