commit21
This commit is contained in:
@@ -17,37 +17,58 @@ use App\Services\InventoryReversals;
|
||||
use App\Services\OrganizationAccess;
|
||||
use App\Services\ReceiptWorkflow;
|
||||
use App\Services\SupplyCoordination;
|
||||
use App\Services\SupplyCoordinationRead;
|
||||
use App\Services\SupplyHistory;
|
||||
use App\Services\SupplyPlanning;
|
||||
use App\Services\SupplySuggestions;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
use Symfony\Component\HttpFoundation\Response as HttpResponse;
|
||||
|
||||
class SupplyPlanningController extends Controller
|
||||
{
|
||||
public function index(Request $request, SupplyPlanning $planning): Response
|
||||
public function index(Request $request, SupplyPlanning $planning): HttpResponse
|
||||
{
|
||||
$actor = $request->user('web');
|
||||
abort_unless($actor instanceof User, 403);
|
||||
|
||||
return DB::transaction(function () use ($request, $planning, $actor) {
|
||||
if (DB::getDriverName() === 'pgsql' && DB::transactionLevel() === 1) {
|
||||
DB::statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY');
|
||||
}
|
||||
|
||||
return $this->page($request, $planning, $actor);
|
||||
});
|
||||
}
|
||||
|
||||
private function page(Request $request, SupplyPlanning $planning, User $actor): HttpResponse
|
||||
{
|
||||
Inertia::encryptHistory();
|
||||
$view = $request->validate(['view' => 'nullable|in:agreements,calendar,imports,alternatives,reservations,deliveries,suggestions,history'])['view'] ?? 'agreements';
|
||||
if ($view === 'history') {
|
||||
return $this->history($actor, $planning);
|
||||
return $this->privateResponse($request, $this->history($request, $actor, $planning));
|
||||
}
|
||||
if ($view === 'suggestions') {
|
||||
return $this->suggestions($actor, $planning);
|
||||
return $this->privateResponse($request, $this->suggestions($request, $actor, $planning));
|
||||
}
|
||||
if (in_array($view, ['alternatives', 'reservations', 'deliveries'], true)) {
|
||||
return $this->coordination($actor, $view, $planning);
|
||||
return $this->privateResponse($request, $this->coordination($request, $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();
|
||||
$focus = $this->focusedRecord($request, $actor, $kind);
|
||||
[$records, $page] = $this->recordPage($request, $planning, $ready, $kind, $focus);
|
||||
$projections = [];
|
||||
if ($view === 'calendar' && $ready) {
|
||||
foreach ($records->where('state', 'approved') as $record) {
|
||||
@@ -55,39 +76,150 @@ class SupplyPlanningController extends Controller
|
||||
}
|
||||
}
|
||||
$prepare = $planning->allows($actor, $import ? 'import_prepare' : 'supply_prepare');
|
||||
$orderFilters = $import ? $request->validate([
|
||||
'order_search' => 'nullable|string|max:100',
|
||||
'selected_order_id' => ['nullable', 'integer', 'min:1', Rule::exists('purchase_orders', 'id')->where('tenant_id', $actor->tenant_id)],
|
||||
]) : [];
|
||||
$historyFilters = $kind === 'policy' ? $request->validate([
|
||||
'history_stock_item_id' => ['nullable', 'required_with:history_period_from,history_period_until,selected_history_key', 'integer', Rule::exists('stock_items', 'id')->where('tenant_id', $actor->tenant_id)],
|
||||
'history_warehouse_id' => ['nullable', 'required_with:history_stock_item_id', 'integer', Rule::exists('warehouses', 'id')->where('tenant_id', $actor->tenant_id)],
|
||||
'history_purpose' => 'nullable|required_with:history_stock_item_id|in:internal,resale',
|
||||
'history_period_from' => 'nullable|date_format:Y-m-d',
|
||||
'history_period_until' => ['nullable', 'date_format:Y-m-d', $request->filled('history_period_from') ? 'after_or_equal:history_period_from' : 'nullable'],
|
||||
'selected_history_key' => ['nullable', 'uuid', Rule::exists('supply_records', 'record_key')->where('tenant_id', $actor->tenant_id)->where('kind', 'history')],
|
||||
'history_form_key' => 'nullable|string|max:80',
|
||||
]) : [];
|
||||
|
||||
return Inertia::render('supply/index', ['view' => $view, 'kind' => $kind, 'ready' => $ready, 'records' => $records, 'projections' => $projections,
|
||||
$agreementFilters = $kind === 'policy' ? $request->validate([
|
||||
'agreement_search' => 'nullable|string|max:100',
|
||||
'agreement_stock_item_id' => ['nullable', 'required_with:agreement_search,selected_agreement_key', 'integer', Rule::exists('stock_items', 'id')->where('tenant_id', $actor->tenant_id)],
|
||||
'agreement_warehouse_id' => ['nullable', 'required_with:agreement_stock_item_id', 'integer', Rule::exists('warehouses', 'id')->where('tenant_id', $actor->tenant_id)],
|
||||
'selected_agreement_key' => ['nullable', 'uuid', Rule::exists('supply_records', 'record_key')->where('tenant_id', $actor->tenant_id)->where('kind', 'agreement')],
|
||||
'agreement_form_key' => 'nullable|string|max:80',
|
||||
]) : [];
|
||||
|
||||
return $this->privateResponse($request, Inertia::render('supply/index', ['view' => $view, 'kind' => $kind, 'ready' => $ready,
|
||||
'agreementSearch' => $agreementFilters['agreement_search'] ?? '', 'agreementFormKey' => $agreementFilters['agreement_form_key'] ?? null,
|
||||
'recordPagination' => $this->pagination($page),
|
||||
'focusRecordId' => $focus->id ?? null, 'focusChanged' => $focus !== null && $records->first()?->id !== $focus->id,
|
||||
'records' => $records->map(fn (SupplyRecord $record) => [...$record->toArray(), 'canApprove' => $planning->canReview($actor, $record), 'canReturn' => $planning->canReview($actor, $record, 'return'), 'canSuspend' => $planning->canReview($actor, $record, 'suspend')]), 'projections' => $projections,
|
||||
'asOf' => today()->toDateString(),
|
||||
'histories' => $ready && $prepare && $kind === 'policy' ? $planning->latest('history')->where('state', 'approved')->latest('id')->limit(100)->get()->map(fn ($r) => [
|
||||
'historyFormKey' => $historyFilters['history_form_key'] ?? null,
|
||||
'histories' => $ready && $prepare && $kind === 'policy' ? $this->approvedOptions($planning, 'history', $records->pluck('data.history_key')->push($historyFilters['selected_history_key'] ?? null), $historyFilters)->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']) : [],
|
||||
'agreements' => $ready && $prepare && $kind === 'policy' ? $this->approvedOptions($planning, 'agreement', $records->pluck('data.agreement_key')->push($agreementFilters['selected_agreement_key'] ?? null), $agreementFilters)->map(fn ($r) => $r->only(['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']) : [],
|
||||
]);
|
||||
'orderSearch' => $orderFilters['order_search'] ?? '',
|
||||
'orders' => $prepare && $import ? $this->orderOptions($records->pluck('purchase_order_id')->push($orderFilters['selected_order_id'] ?? null), $orderFilters['order_search'] ?? '') : [],
|
||||
]));
|
||||
}
|
||||
|
||||
private function history(User $actor, SupplyPlanning $planning): Response
|
||||
private function privateResponse(Request $request, Response $page): HttpResponse
|
||||
{
|
||||
$response = $page->toResponse($request);
|
||||
$response->headers->set('Cache-Control', 'private, no-store, max-age=0');
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
private function focusedRecord(Request $request, User $actor, string $kind): ?SupplyRecord
|
||||
{
|
||||
$filter = $request->validate(['record_id' => ['nullable', 'integer', 'min:1', Rule::exists('supply_records', 'id')->where('tenant_id', $actor->tenant_id)->where('kind', $kind)]]);
|
||||
|
||||
return isset($filter['record_id']) ? SupplyRecord::query()->where('tenant_id', $actor->tenant_id)->where('kind', $kind)->findOrFail((int) $filter['record_id']) : null;
|
||||
}
|
||||
|
||||
/** @return array{Collection<int,SupplyRecord>,LengthAwarePaginator<int,SupplyRecord>|null} */
|
||||
private function recordPage(Request $request, SupplyPlanning $planning, bool $ready, string $kind, ?SupplyRecord $focus): array
|
||||
{
|
||||
$request->validate(['records_page' => 'nullable|integer|min:1|max:1000000']);
|
||||
if (! $ready) {
|
||||
return [collect(), null];
|
||||
}
|
||||
$query = $planning->latest($kind)->latest('id');
|
||||
if ($focus !== null) {
|
||||
return [$query->where('record_key', $focus->record_key)->get(), null];
|
||||
}
|
||||
$page = $query->paginate(25, ['*'], 'records_page')->withQueryString();
|
||||
|
||||
return [$page->getCollection(), $page];
|
||||
}
|
||||
|
||||
/** @param LengthAwarePaginator<int,SupplyRecord>|null $page
|
||||
* @return array<string,mixed>|null
|
||||
*/
|
||||
private function pagination(?LengthAwarePaginator $page): ?array
|
||||
{
|
||||
return $page === null ? null : ['current_page' => $page->currentPage(), 'last_page' => $page->lastPage(), 'total' => $page->total(),
|
||||
'prev_page_url' => $page->previousPageUrl(), 'next_page_url' => $page->nextPageUrl()];
|
||||
}
|
||||
|
||||
/** Preserve already selected sources even outside the recent choices.
|
||||
* @param Collection<array-key,mixed> $selected
|
||||
* @param array<string,mixed> $filters
|
||||
* @return Collection<int,SupplyRecord>
|
||||
*/
|
||||
private function approvedOptions(SupplyPlanning $planning, string $kind, Collection $selected, array $filters = []): Collection
|
||||
{
|
||||
$query = $planning->latest($kind)->where('state', 'approved');
|
||||
if ($kind === 'agreement' && isset($filters['agreement_stock_item_id'])) {
|
||||
$query->where('stock_item_id', $filters['agreement_stock_item_id'])->where('warehouse_id', $filters['agreement_warehouse_id']);
|
||||
}
|
||||
if ($kind === 'history' && isset($filters['history_stock_item_id'])) {
|
||||
$query->where('stock_item_id', $filters['history_stock_item_id'])->where('warehouse_id', $filters['history_warehouse_id'])->where('data->purpose', $filters['history_purpose']);
|
||||
}
|
||||
$recent = (clone $query)
|
||||
->when(trim($filters['agreement_search'] ?? '') !== '', fn ($q) => $q->where(fn ($match) => $match
|
||||
->whereLike('data->reference', '%'.trim($filters['agreement_search']).'%')
|
||||
->orWhereLike('snapshot->supplier->name', '%'.trim($filters['agreement_search']).'%')))
|
||||
->when(trim($filters['policy_search'] ?? '') !== '', fn ($q) => $q->where(fn ($match) => $match
|
||||
->whereLike('snapshot->item->code', '%'.trim($filters['policy_search']).'%')
|
||||
->orWhereLike('snapshot->item->name', '%'.trim($filters['policy_search']).'%')
|
||||
->orWhereLike('snapshot->warehouse->name', '%'.trim($filters['policy_search']).'%')))
|
||||
->when($filters['history_period_from'] ?? null, fn ($q, $date) => $q->where('data->period_from', '>=', $date))
|
||||
->when($filters['history_period_until'] ?? null, fn ($q, $date) => $q->where('data->period_until', '<=', $date))
|
||||
->latest('id')->limit(100)->pluck('id');
|
||||
|
||||
return $query->where(fn ($q) => $q->whereIn('id', $recent)->orWhereIn('record_key', $selected->filter()->unique()))->latest('id')->get();
|
||||
}
|
||||
|
||||
/** @param Collection<array-key,mixed> $selected
|
||||
* @return Collection<int,PurchaseOrder>
|
||||
*/
|
||||
private function orderOptions(Collection $selected, string $search = ''): Collection
|
||||
{
|
||||
$recent = PurchaseOrder::query()->when(trim($search) !== '', fn ($q) => $q->whereRaw('LOWER(number) LIKE ?', ['%'.mb_strtolower(trim($search)).'%']))->latest('id')->limit(100)->pluck('id');
|
||||
|
||||
return PurchaseOrder::query()->where(fn ($q) => $q->whereIn('id', $recent)->orWhereIn('id', $selected->filter()->unique()))->latest('id')->get(['id', 'number']);
|
||||
}
|
||||
|
||||
private function history(Request $request, 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();
|
||||
$focus = $this->focusedRecord($request, $actor, 'history');
|
||||
[$records, $page] = $this->recordPage($request, $planning, $ready, 'history', $focus);
|
||||
$service = app(SupplyHistory::class);
|
||||
|
||||
return Inertia::render('supply/history', ['ready' => $ready, 'actorId' => $actor->id, 'records' => $records, 'canPrepare' => $prepare, 'canReview' => $review,
|
||||
return Inertia::render('supply/history', ['ready' => $ready, 'actorId' => $actor->id,
|
||||
'recordPagination' => $this->pagination($page),
|
||||
'focusRecordId' => $focus->id ?? null, 'focusChanged' => $focus !== null && $records->first()?->id !== $focus->id,
|
||||
'records' => $records->map(fn (SupplyRecord $record) => [...$record->toArray(), 'canApprove' => $service->canReview($actor, $record), 'canReturn' => $service->canReview($actor, $record, 'return'), 'canSuspend' => $service->canReview($actor, $record, 'suspend')]), '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
|
||||
private function suggestions(Request $request, User $actor, SupplyPlanning $planning): Response
|
||||
{
|
||||
$prepare = $planning->allows($actor, 'supply_suggest_prepare');
|
||||
$review = $planning->allows($actor, 'supply_suggest_review');
|
||||
@@ -95,10 +227,16 @@ class SupplyPlanningController extends Controller
|
||||
$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();
|
||||
$focus = $this->focusedRecord($request, $actor, 'suggestion');
|
||||
[$records, $page] = $this->recordPage($request, $planning, $ready, 'suggestion', $focus);
|
||||
$filters = $request->validate([
|
||||
'policy_search' => 'nullable|string|max:100',
|
||||
'selected_policy_key' => ['nullable', 'uuid', Rule::exists('supply_records', 'record_key')->where('tenant_id', $actor->tenant_id)->where('kind', 'policy')],
|
||||
'policy_form_key' => 'nullable|string|max:80',
|
||||
]);
|
||||
$policies = [];
|
||||
if ($ready && $prepare) {
|
||||
foreach ($planning->latest('policy')->where('state', 'approved')->latest('id')->limit(100)->get() as $policy) {
|
||||
foreach ($this->approvedOptions($planning, 'policy', $records->pluck('data.policy_key')->push($filters['selected_policy_key'] ?? null), $filters) 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());
|
||||
@@ -112,7 +250,16 @@ class SupplyPlanningController extends Controller
|
||||
}
|
||||
$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,
|
||||
$service = app(SupplySuggestions::class);
|
||||
|
||||
return Inertia::render('supply/suggestions', ['ready' => $ready, 'actorId' => $actor->id,
|
||||
'policySearch' => $filters['policy_search'] ?? '', 'policyFormKey' => $filters['policy_form_key'] ?? null,
|
||||
'recordPagination' => $this->pagination($page),
|
||||
'focusRecordId' => $focus->id ?? null, 'focusChanged' => $focus !== null && $records->first()?->id !== $focus->id,
|
||||
'records' => $records->map(fn (SupplyRecord $record) => [...$record->toArray(),
|
||||
'canApprove' => $service->canAct($actor, $record, 'approve'), 'canReturn' => $service->canAct($actor, $record, 'return'),
|
||||
'canConvert' => $service->canAct($actor, $record, 'convert'), 'canCancel' => $service->canAct($actor, $record, 'cancel'), 'canClose' => $service->canAct($actor, $record, 'close')]),
|
||||
'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)]),
|
||||
@@ -120,7 +267,7 @@ class SupplyPlanningController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
private function coordination(User $actor, string $view, SupplyPlanning $planning): Response
|
||||
private function coordination(Request $request, User $actor, string $view, SupplyPlanning $planning): Response
|
||||
{
|
||||
[$kind, $prepare, $review] = match ($view) {
|
||||
'alternatives' => ['alternative', 'supply_technical_prepare', 'supply_technical_review'],
|
||||
@@ -128,25 +275,79 @@ class SupplyPlanningController extends Controller
|
||||
'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);
|
||||
$read = app(SupplyCoordinationRead::class);
|
||||
$read->kind($actor, $view);
|
||||
$ready = Schema::hasTable('supply_records');
|
||||
$records = $ready ? $planning->latest($kind)->latest('id')->limit(100)->get() : collect();
|
||||
$focus = $this->focusedRecord($request, $actor, $kind);
|
||||
$request->validate(['records_page' => 'nullable|integer|min:1|max:1000000']);
|
||||
$filters = $read->filters($request, $kind);
|
||||
$query = $ready ? ($focus === null ? $read->query($kind, $filters) : $planning->latest($kind))->latest('id') : null;
|
||||
$page = $focus === null ? $query?->paginate(25, ['*'], 'records_page')->withQueryString() : null;
|
||||
$records = $focus !== null ? $query?->where('record_key', $focus->record_key)->get() ?? collect() : $page?->getCollection() ?? collect();
|
||||
$canPrepare = $planning->allows($actor, $prepare);
|
||||
$lineFilters = in_array($kind, ['reservation', 'delivery'], true) ? $request->validate([
|
||||
'line_search' => 'nullable|string|max:100', 'line_form_key' => 'nullable|string|max:80',
|
||||
'selected_order_line_id' => ['nullable', 'integer', 'min:1', Rule::exists('purchase_order_items', 'id')->where('tenant_id', $actor->tenant_id)],
|
||||
'selected_request_line_id' => ['nullable', 'integer', 'min:1', Rule::exists('requisition_items', 'id')->where('tenant_id', $actor->tenant_id)],
|
||||
]) : [];
|
||||
$lineSearch = trim($lineFilters['line_search'] ?? '');
|
||||
$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();
|
||||
$orders = collect();
|
||||
if ($canPrepare && $kind === 'delivery') {
|
||||
$recentOrders = PurchaseOrder::query()->where('status', 'issued')->when($lineSearch !== '', fn ($q) => $q->whereLike('number', '%'.$lineSearch.'%'))->latest('id')->limit(100)->pluck('id');
|
||||
$selectedOrders = $records->pluck('purchase_order_id')->filter();
|
||||
if (isset($lineFilters['selected_order_line_id'])) {
|
||||
$selectedOrders->push(PurchaseOrderItem::query()->whereKey($lineFilters['selected_order_line_id'])->value('purchase_order_id'));
|
||||
}
|
||||
$orders = PurchaseOrder::query()->where('status', 'issued')->where(fn ($q) => $q->whereIn('id', $recentOrders)->orWhereIn('id', $selectedOrders))->latest('id')->get()->keyBy('id');
|
||||
}
|
||||
$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();
|
||||
$requests = collect();
|
||||
if ($canPrepare && $kind === 'reservation') {
|
||||
$recentRequests = Requisition::query()->where('status', 'approved')->when($lineSearch !== '', fn ($q) => $q->whereLike('number', '%'.$lineSearch.'%'))->latest('id')->limit(100)->pluck('id');
|
||||
$selectedRequests = RequisitionItem::query()->whereIn('id', $records->pluck('data.requisition_item_id')->push($lineFilters['selected_request_line_id'] ?? null)->filter())->pluck('requisition_id');
|
||||
$requests = Requisition::query()->where('status', 'approved')->where(fn ($q) => $q->whereIn('id', $recentRequests)->orWhereIn('id', $selectedRequests))->latest('id')->get()->keyBy('id');
|
||||
}
|
||||
$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,
|
||||
$coordination = app(SupplyCoordination::class);
|
||||
$issueFilters = $kind === 'reservation' ? $request->validate([
|
||||
'issue_search' => 'nullable|string|max:100',
|
||||
'issue_record_id' => ['nullable', 'required_with:issue_search,selected_issue_id', 'integer', 'min:1', Rule::exists('supply_records', 'id')->where('tenant_id', $actor->tenant_id)->where('kind', 'reservation')],
|
||||
'selected_issue_id' => ['nullable', 'integer', 'min:1', Rule::exists('stock_operations', 'id')->where('tenant_id', $actor->tenant_id)->where('kind', 'issue')],
|
||||
]) : [];
|
||||
$operations = collect();
|
||||
if ($ready && $canPrepare && $kind === 'reservation') {
|
||||
$issueRecord = isset($issueFilters['issue_record_id']) ? SupplyRecord::query()->findOrFail((int) $issueFilters['issue_record_id']) : null;
|
||||
if ($issueRecord !== null) {
|
||||
$issueRecord = $planning->latest('reservation')->where('record_key', $issueRecord->record_key)->firstOrFail();
|
||||
abort_unless($coordination->canManage($actor, $issueRecord, 'fulfil'), 403);
|
||||
}
|
||||
$query = StockOperation::query()->where('kind', 'issue')->whereNotIn('id', $reversedIds)
|
||||
->when($issueRecord !== null, fn ($q) => $q->where('stock_item_id', $issueRecord->stock_item_id)->where('warehouse_id', $issueRecord->warehouse_id));
|
||||
$search = trim($issueFilters['issue_search'] ?? '');
|
||||
$recent = (clone $query)->when($search !== '', fn ($q) => $q->whereLike('reference', '%'.$search.'%'))->latest('id')->limit(100)->pluck('id');
|
||||
$operations = $query->where(fn ($q) => $q->whereIn('id', $recent)->orWhere('id', $issueFilters['selected_issue_id'] ?? 0))
|
||||
->latest('id')->get(['id', 'reference', 'stock_item_id', 'warehouse_id', 'quantity_milli']);
|
||||
}
|
||||
|
||||
return Inertia::render('supply/coordination', ['kind' => $kind, 'ready' => $ready,
|
||||
'recordFilters' => ['record_search' => trim($filters['record_search'] ?? ''), 'record_state' => $filters['record_state'] ?? ''],
|
||||
'recordPagination' => $page === null ? null : ['current_page' => $page->currentPage(), 'last_page' => $page->lastPage(), 'total' => $page->total(),
|
||||
'prev_page_url' => $page->previousPageUrl(), 'next_page_url' => $page->nextPageUrl()],
|
||||
'focusRecordId' => $focus->id ?? null, 'focusChanged' => $focus !== null && $records->first()?->id !== $focus->id,
|
||||
'records' => $records->map(fn (SupplyRecord $record) => [...$record->toArray(), 'canApprove' => $coordination->canReviewAlternative($actor, $record),
|
||||
'canReturn' => $coordination->canReviewAlternative($actor, $record, 'return'), 'canSuspend' => $coordination->canReviewAlternative($actor, $record, 'suspend'),
|
||||
'canEdit' => $coordination->canManage($actor, $record, 'edit'), 'canFulfil' => $coordination->canManage($actor, $record, 'fulfil'),
|
||||
'canCancel' => $coordination->canManage($actor, $record, 'cancel')]),
|
||||
'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,
|
||||
@@ -154,7 +355,9 @@ class SupplyPlanningController extends Controller
|
||||
'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']) : [],
|
||||
'lineSearch' => $lineSearch, 'lineFormKey' => $lineFilters['line_form_key'] ?? null,
|
||||
'operations' => $operations, 'issueSearch' => $issueFilters['issue_search'] ?? '',
|
||||
'issueRecordId' => isset($issueFilters['issue_record_id']) ? (int) $issueFilters['issue_record_id'] : null,
|
||||
'history' => $ready ? SupplyRecord::query()->where('kind', $kind)->whereIn('record_key', $records->pluck('record_key'))->orderByDesc('id')->get()->groupBy('record_key') : [],
|
||||
]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user