user('web'); abort_unless($actor instanceof User, 403); $view = $request->validate(['view' => 'nullable|in:agreements,calendar,imports,alternatives,reservations,deliveries,suggestions,history'])['view'] ?? 'agreements'; if ($view === 'history') { return $this->history($actor, $planning); } if ($view === 'suggestions') { return $this->suggestions($actor, $planning); } if (in_array($view, ['alternatives', 'reservations', 'deliveries'], true)) { return $this->coordination($actor, $view, $planning); } $import = $view === 'imports'; $capabilities = $import ? ['import_read', 'import_prepare', 'import_customs'] : ['supply_read', 'supply_prepare', 'supply_review']; abort_unless(collect($capabilities)->contains(fn ($p) => $planning->allows($actor, $p)), 403); $ready = Schema::hasTable('supply_records'); $kind = $import ? 'import' : ($view === 'calendar' ? 'policy' : 'agreement'); $records = $ready ? $planning->latest($kind)->latest('id')->limit(100)->get() : collect(); $projections = []; if ($view === 'calendar' && $ready) { foreach ($records->where('state', 'approved') as $record) { $projections[$record->record_key] = $planning->calendar($record, today()->toDateString()); } } $prepare = $planning->allows($actor, $import ? 'import_prepare' : 'supply_prepare'); return Inertia::render('supply/index', ['view' => $view, 'kind' => $kind, 'ready' => $ready, 'records' => $records, 'projections' => $projections, 'asOf' => today()->toDateString(), 'histories' => $ready && $prepare && $kind === 'policy' ? $planning->latest('history')->where('state', 'approved')->latest('id')->limit(100)->get()->map(fn ($r) => [ 'key' => $r->record_key, 'version' => $r->version, 'stock_item_id' => $r->stock_item_id, 'warehouse_id' => $r->warehouse_id, 'purpose' => $r->data['purpose'], 'period_from' => $r->data['period_from'], 'period_until' => $r->data['period_until'], 'quality' => $r->data['quality'], 'pattern' => $r->data['pattern'], 'days' => $r->snapshot['analysis']['days'], 'daily_mean_milli' => $r->snapshot['analysis']['daily_mean_milli'], ]) : [], 'agreements' => $ready && $prepare && $kind === 'policy' ? $planning->latest('agreement')->where('state', 'approved')->get(['record_key', 'version', 'stock_item_id', 'warehouse_id', 'data', 'snapshot']) : [], 'history' => $ready ? SupplyRecord::query()->where('kind', $kind)->whereIn('record_key', $records->pluck('record_key'))->orderByDesc('id')->get()->groupBy('record_key') : [], 'canPrepare' => $prepare, 'canReview' => $planning->allows($actor, 'supply_review'), 'canCustoms' => $planning->allows($actor, 'import_customs'), 'items' => $prepare && ! $import ? StockItem::query()->orderBy('code')->get(['id', 'code', 'name', 'unit']) : [], 'warehouses' => $prepare && ! $import ? Warehouse::query()->orderBy('code')->get(['id', 'code', 'name']) : [], 'suppliers' => $prepare && $kind === 'agreement' ? Supplier::query()->where('status', 'active')->orderBy('name')->get(['id', 'name']) : [], 'orders' => $prepare && $import ? PurchaseOrder::query()->latest('id')->limit(100)->get(['id', 'number']) : [], ]); } private function history(User $actor, SupplyPlanning $planning): Response { $prepare = $planning->allows($actor, 'supply_history_prepare'); $review = $planning->allows($actor, 'supply_history_review'); abort_unless($prepare || $review || $planning->allows($actor, 'supply_read'), 403); $ready = Schema::hasTable('supply_records'); $records = $ready ? $planning->latest('history')->latest('id')->limit(100)->get() : collect(); return Inertia::render('supply/history', ['ready' => $ready, 'actorId' => $actor->id, 'records' => $records, 'canPrepare' => $prepare, 'canReview' => $review, 'defaultPeriod' => ['from' => today()->subDays(30)->toDateString(), 'until' => today()->subDay()->toDateString()], 'items' => $prepare ? StockItem::query()->orderBy('code')->get(['id', 'code', 'name', 'unit']) : [], 'warehouses' => $prepare ? Warehouse::query()->orderBy('code')->get(['id', 'code', 'name']) : [], 'history' => $ready ? SupplyRecord::query()->where('kind', 'history')->whereIn('record_key', $records->pluck('record_key'))->orderByDesc('id')->get()->groupBy('record_key') : [], ]); } private function suggestions(User $actor, SupplyPlanning $planning): Response { $prepare = $planning->allows($actor, 'supply_suggest_prepare'); $review = $planning->allows($actor, 'supply_suggest_review'); $convertPermission = $planning->allows($actor, 'supply_suggest_convert'); $convert = $convertPermission && app(OrganizationAccess::class)->canRequest($actor); abort_unless($planning->allows($actor, 'supply_read') || $prepare || $review || $convertPermission, 403); $ready = Schema::hasTable('supply_records'); $records = $ready ? $planning->latest('suggestion')->latest('id')->limit(100)->get() : collect(); $policies = []; if ($ready && $prepare) { foreach ($planning->latest('policy')->where('state', 'approved')->latest('id')->limit(100)->get() as $policy) { $option = ['key' => $policy->record_key, 'version' => $policy->version, 'label' => $policy->snapshot['item']['code'].' · '.$policy->snapshot['item']['name'].' · '.$policy->snapshot['warehouse']['name'], 'issue' => null]; try { $basis = app(SupplySuggestions::class)->basis($policy->record_key, today()->toDateString()); $option += ['unit_price' => $basis['unit_price'], 'tax_rate' => $basis['tax_rate'], 'quantity_milli' => $basis['projection']['quantity_milli'] ?? 0, 'delivery_on' => $basis['projection']['risk_date'] ?? null, 'unit' => $basis['item']['unit']]; } catch (ValidationException $e) { $option['issue'] = $e->getMessage(); } $policies[] = $option; } } $requests = Requisition::query()->visibleTo($actor)->whereIn('id', $records->pluck('data.requisition_id')->filter())->get(['id', 'number'])->keyBy('id'); return Inertia::render('supply/suggestions', ['ready' => $ready, 'actorId' => $actor->id, 'records' => $records, 'policies' => $policies, 'canPrepare' => $prepare, 'canReview' => $review, 'canConvert' => $convert, 'costCenters' => $convert ? CostCenter::query()->where('department_id', $actor->department_id)->orderBy('code')->get(['id', 'code', 'name']) : [], 'requestLinks' => $requests, 'excess' => $records->mapWithKeys(fn ($r) => [$r->record_key => app(SupplySuggestions::class)->hasExcess($r)]), 'history' => $ready ? SupplyRecord::query()->where('kind', 'suggestion')->whereIn('record_key', $records->pluck('record_key'))->orderByDesc('id')->get()->groupBy('record_key') : [], ]); } private function coordination(User $actor, string $view, SupplyPlanning $planning): Response { [$kind, $prepare, $review] = match ($view) { 'alternatives' => ['alternative', 'supply_technical_prepare', 'supply_technical_review'], 'reservations' => ['reservation', 'supply_reserve', 'supply_reserve'], 'deliveries' => ['delivery', 'supply_delivery', 'supply_delivery'], default => abort(404), }; abort_unless($planning->allows($actor, 'supply_read') || $planning->allows($actor, $prepare) || $planning->allows($actor, $review), 403); $ready = Schema::hasTable('supply_records'); $records = $ready ? $planning->latest($kind)->latest('id')->limit(100)->get() : collect(); $canPrepare = $planning->allows($actor, $prepare); $reversedIds = $ready && $kind === 'reservation' ? array_keys(app(InventoryReversals::class)->corrections('operation')) : []; $orders = $canPrepare && $kind === 'delivery' ? PurchaseOrder::query()->latest('id')->limit(100)->get()->keyBy('id') : collect(); $orderLines = PurchaseOrderItem::query()->whereIn('purchase_order_id', $orders->keys())->get()->map(fn ($line) => [ 'id' => $line->id, 'label' => $orders[$line->purchase_order_id]->number.' · '.$line->description.' · '.$line->quantity.' '.$line->unit, 'stock_item_id' => $orders[$line->purchase_order_id]->snapshot['catalogue_sources'][$line->requisition_item_id]['stock_item_id'] ?? null, 'quantity_milli' => ReceiptWorkflow::milli($line->quantity), ]); $requests = $canPrepare && $kind === 'reservation' ? Requisition::query()->where('status', 'approved')->latest('id')->limit(100)->get()->keyBy('id') : collect(); $requestLines = RequisitionItem::query()->whereIn('requisition_id', $requests->keys())->get()->filter(fn ($line) => isset($line->catalogue_snapshot['stock_item_id']))->map(fn ($line) => [ 'id' => $line->id, 'label' => $requests[$line->requisition_id]->number.' · '.$line->description, 'stock_item_id' => $line->catalogue_snapshot['stock_item_id'], 'quantity_milli' => ReceiptWorkflow::milli($line->quantity), 'reference' => $requests[$line->requisition_id]->number.' / '.$line->id, 'needed_on' => $requests[$line->requisition_id]->needed_by->toDateString(), ])->values(); return Inertia::render('supply/coordination', ['kind' => $kind, 'ready' => $ready, 'records' => $records, 'eligible' => $kind === 'alternative' ? $records->mapWithKeys(fn ($r) => [$r->record_key => app(SupplyCoordination::class)->eligibleAlternative($r)]) : [], 'pending' => $kind === 'delivery' ? $records->mapWithKeys(fn ($r) => [$r->record_key => app(SupplyCoordination::class)->remainingDelivery($r)]) : ($kind === 'reservation' ? $records->mapWithKeys(fn ($r) => [$r->record_key => app(SupplyCoordination::class)->remainingReservation($r)]) : []), 'reversedOperations' => $reversedIds, 'canPrepare' => $canPrepare, 'canReview' => $planning->allows($actor, $review), 'items' => $canPrepare ? StockItem::query()->orderBy('code')->get(['id', 'code', 'name', 'unit']) : [], 'warehouses' => $canPrepare ? Warehouse::query()->orderBy('code')->get(['id', 'code', 'name']) : [], 'orderLines' => $orderLines, 'requestLines' => $requestLines, 'operations' => $canPrepare && $kind === 'reservation' ? StockOperation::query()->where('kind', 'issue')->whereNotIn('id', $reversedIds)->latest('id')->limit(100)->get(['id', 'reference', 'stock_item_id', 'warehouse_id', 'quantity_milli']) : [], 'history' => $ready ? SupplyRecord::query()->where('kind', $kind)->whereIn('record_key', $records->pluck('record_key'))->orderByDesc('id')->get()->groupBy('record_key') : [], ]); } public function save(Request $request, string $kind, SupplyPlanning $planning): RedirectResponse { $actor = $request->user('web'); abort_unless($actor instanceof User, 403); $planning->save($actor, $kind, $request->all()); return back(); } }