3
0
mirror of https://github.com/snipe/snipe-it.git synced 2026-08-18 11:15:42 +00:00
Files
snipe-it/app/Http/Controllers/Api/OrderItemsController.php
2026-08-06 13:37:44 +01:00

185 lines
6.6 KiB
PHP

<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Transformers\OrderItemsTransformer;
use App\Models\Asset;
use App\Models\AssetModel;
use App\Models\OrderItem;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
/**
* Standard REST endpoint for OrderItems. Backs the Orders tab on
* accessory / consumable / component / model view pages, and any
* external API consumer that needs to page / filter / sort acquisition
* lines directly rather than through their parent inventory item.
*
* Filters:
* item_type fully-qualified model class ("App\Models\Accessory")
* item_id id of that item
* asset_model_id aggregate: all OrderItems for every Asset of this model
*
* Sort covers OrderItem-native columns and cross-table columns
* (order_number, purchase_date, supplier, currency) via a leftJoin
* onto orders + suppliers. Search hits order_number, purchase_order,
* and suppliers.name across the same joined query.
*/
class OrderItemsController extends Controller
{
/**
* Sortable columns that live on joined tables (orders / suppliers).
* Keys are the API `sort` values; values are the qualified column
* name the ORDER BY uses. Non-mapped values fall back to `id DESC`.
*/
private const SORT_COLUMN_MAP = [
'order_number' => 'orders.order_number',
'purchase_date' => 'orders.purchase_date',
'currency' => 'orders.currency',
'supplier' => 'suppliers.name',
'qty' => 'order_items.qty',
'unit_cost' => 'order_items.price',
'created_at' => 'order_items.created_at',
'created_by' => 'order_items.created_by',
];
public function index(Request $request): JsonResponse
{
$this->authorizeOrderItemsRequest($request);
$query = $this->baseOrderItemsQuery();
$this->applyScopeFilters($query, $request);
$this->applyJoinsAndSearch($query, $request);
$this->applySort($query, $request);
$total = (clone $query)->count();
$offset = ($request->input('offset') > $total) ? $total : app('api_offset_value');
$limit = app('api_limit_value');
$lines = (clone $query)->skip($offset)->take($limit)->get();
return response()->json(
(new OrderItemsTransformer)->transformOrderItems($lines, $total),
200,
['Content-Type' => 'application/json;charset=utf8'],
JSON_UNESCAPED_UNICODE,
);
}
/**
* Authorization: if the request narrows to a specific parent,
* require view permission on THAT parent. Otherwise fall back to
* requiring superuser — unfiltered "list every OrderItem" is a
* superuser-only capability until an OrderPolicy exists.
*/
private function authorizeOrderItemsRequest(Request $request): void
{
$assetModelId = $request->input('asset_model_id');
$itemType = $request->input('item_type');
$itemId = $request->input('item_id');
if ($assetModelId) {
$this->authorize('view', AssetModel::findOrFail($assetModelId));
return;
}
if ($itemType && $itemId && class_exists($itemType)) {
$this->authorize('view', $itemType::findOrFail($itemId));
return;
}
if (! auth()->user()?->isSuperUser()) {
abort(403);
}
}
private function baseOrderItemsQuery(): Builder
{
return OrderItem::query()
->with([
'admin:id,first_name,last_name,username',
'order:id,order_number,supplier_id,currency,purchase_date,notes,created_by',
'order.supplier:id,name',
'order.admin:id,first_name,last_name,username',
// action_log carries the receipt filename uploaded via
// the adjust-quantity modal; surfaced on each Orders-tab
// row so the file lives with the acquisition record
// rather than only in the item's Files tab.
'actionlog:id,order_item_id,filename,item_id,item_type',
])
// Orders tab shows purchases only. Corrections / consumption
// events (zero or negative-qty OrderItems) belong in the
// item's history tab, not on the "Orders" list.
->where('order_items.qty', '>', 0);
}
private function applyScopeFilters(Builder $query, Request $request): void
{
$assetModelId = $request->input('asset_model_id');
$itemType = $request->input('item_type');
$itemId = $request->input('item_id');
if ($assetModelId) {
$query->where('order_items.item_type', Asset::class)
->whereIn('order_items.item_id', Asset::where('model_id', $assetModelId)->select('id'));
return;
}
if ($itemType && $itemId) {
$query->where('order_items.item_type', $itemType)
->where('order_items.item_id', $itemId);
}
}
/**
* leftJoin onto orders + suppliers so cross-table sort / search
* can hit those columns. Select order_items.* so the model hydrates
* cleanly and the joined columns don't leak into the OrderItem
* attributes. Search covers order_number, supplier name, and the
* order notes field.
*/
private function applyJoinsAndSearch(Builder $query, Request $request): void
{
$query->leftJoin('orders', 'orders.id', '=', 'order_items.order_id')
->leftJoin('suppliers', 'suppliers.id', '=', 'orders.supplier_id')
->select('order_items.*');
if (! $request->filled('search')) {
return;
}
$needle = '%'.$request->input('search').'%';
$query->where(function ($q) use ($needle) {
$q->where('orders.order_number', 'like', $needle)
->orWhere('suppliers.name', 'like', $needle)
->orWhere('orders.notes', 'like', $needle);
});
}
private function applySort(Builder $query, Request $request): void
{
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$sort = $request->input('sort');
if ($sort === 'total_cost') {
// qty * price sort is computed rather than a stored column.
$query->orderByRaw('(order_items.qty * COALESCE(order_items.price, 0)) '.$order);
return;
}
$column = self::SORT_COLUMN_MAP[$sort] ?? null;
if ($column === null) {
$query->orderBy('order_items.id', 'desc');
return;
}
$query->orderBy($column, $order);
}
}