mirror of
https://github.com/snipe/snipe-it.git
synced 2026-08-18 11:15:42 +00:00
185 lines
6.6 KiB
PHP
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);
|
|
}
|
|
}
|