fix inventory
This commit is contained in:
@@ -11,7 +11,9 @@ use App\Http\Requests\Inventory\InventoryItemUpdateRequest;
|
||||
use App\Http\Requests\Inventory\InventoryTeacherDistributionRequest;
|
||||
use App\Http\Requests\Inventory\InventoryTeacherDistributionStoreRequest;
|
||||
use App\Http\Resources\Inventory\InventoryItemResource;
|
||||
use App\Models\InventoryItem;
|
||||
use App\Services\Inventory\InventoryItemService;
|
||||
use App\Services\Inventory\InventoryMovementService;
|
||||
use App\Services\Inventory\InventorySummaryService;
|
||||
use App\Services\Inventory\InventoryTeacherDistributionService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
@@ -23,7 +25,8 @@ class InventoryController extends BaseApiController
|
||||
public function __construct(
|
||||
private InventoryItemService $items,
|
||||
private InventorySummaryService $summary,
|
||||
private InventoryTeacherDistributionService $distribution
|
||||
private InventoryTeacherDistributionService $distribution,
|
||||
private InventoryMovementService $movementService
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
@@ -182,6 +185,161 @@ class InventoryController extends BaseApiController
|
||||
return $this->success($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get low-stock items (where quantity <= reorder_point).
|
||||
*/
|
||||
public function lowStock(): JsonResponse
|
||||
{
|
||||
$items = InventoryItem::lowStock()
|
||||
->with('category', 'preferredSupplier')
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return $this->success([
|
||||
'items' => InventoryItemResource::collection($items),
|
||||
'count' => $items->count(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get reorder suggestions based on low-stock items.
|
||||
*/
|
||||
public function reorderSuggestions(): JsonResponse
|
||||
{
|
||||
$lowStockItems = InventoryItem::lowStock()
|
||||
->with('category', 'preferredSupplier')
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
$suggestions = $lowStockItems->map(function ($item) {
|
||||
$reorderQty = (int) ($item->reorder_quantity ?: 0);
|
||||
$currentQty = (int) $item->quantity;
|
||||
$reorderPoint = (int) ($item->reorder_point ?: 0);
|
||||
|
||||
// If no reorder quantity is set, suggest enough to bring back above reorder point
|
||||
if ($reorderQty <= 0) {
|
||||
$reorderQty = max(1, $reorderPoint - $currentQty + 5);
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $item->id,
|
||||
'name' => $item->name,
|
||||
'current_quantity' => $currentQty,
|
||||
'reorder_point' => $reorderPoint,
|
||||
'suggested_order_qty' => $reorderQty,
|
||||
'preferred_supplier' => $item->preferredSupplier ? [
|
||||
'id' => $item->preferredSupplier->id,
|
||||
'name' => $item->preferredSupplier->name,
|
||||
] : null,
|
||||
'category' => $item->category ? $item->category->name : null,
|
||||
];
|
||||
});
|
||||
|
||||
return $this->success([
|
||||
'suggestions' => $suggestions,
|
||||
'count' => $suggestions->count(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a reorder request for a low-stock item.
|
||||
*/
|
||||
public function reorderRequest(int $id): JsonResponse
|
||||
{
|
||||
$item = InventoryItem::with('preferredSupplier')->find($id);
|
||||
if (!$item) {
|
||||
return $this->error('Item not found.', Response::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
$reorderQty = (int) ($item->reorder_quantity ?: 0);
|
||||
if ($reorderQty <= 0) {
|
||||
$reorderQty = max(1, ((int) ($item->reorder_point ?: 0)) - ((int) $item->quantity) + 5);
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'item_id' => $item->id,
|
||||
'item_name' => $item->name,
|
||||
'current_quantity' => (int) $item->quantity,
|
||||
'suggested_quantity' => $reorderQty,
|
||||
'preferred_supplier' => $item->preferredSupplier ? [
|
||||
'id' => $item->preferredSupplier->id,
|
||||
'name' => $item->preferredSupplier->name,
|
||||
] : null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* View all items with their stock status.
|
||||
*/
|
||||
public function stockStatus(): JsonResponse
|
||||
{
|
||||
$type = request()->query('type');
|
||||
$items = InventoryItem::query()
|
||||
->with('category', 'preferredSupplier')
|
||||
->when($type, fn ($q) => $q->where('type', $type))
|
||||
->orderBy('name')
|
||||
->get()
|
||||
->map(function ($item) {
|
||||
$qty = (int) $item->quantity;
|
||||
$reorderPoint = (int) ($item->reorder_point ?: 0);
|
||||
$status = 'ok';
|
||||
if ($qty <= 0) {
|
||||
$status = 'out_of_stock';
|
||||
} elseif ($reorderPoint > 0 && $qty <= $reorderPoint) {
|
||||
$status = 'low_stock';
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $item->id,
|
||||
'name' => $item->name,
|
||||
'type' => $item->type,
|
||||
'category' => $item->category ? $item->category->name : null,
|
||||
'quantity' => $qty,
|
||||
'reorder_point' => $reorderPoint,
|
||||
'status' => $status,
|
||||
];
|
||||
});
|
||||
|
||||
$counts = [
|
||||
'ok' => $items->where('status', 'ok')->count(),
|
||||
'low_stock' => $items->where('status', 'low_stock')->count(),
|
||||
'out_of_stock' => $items->where('status', 'out_of_stock')->count(),
|
||||
];
|
||||
|
||||
return $this->success([
|
||||
'items' => $items,
|
||||
'counts' => $counts,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inventory dashboard summary.
|
||||
*/
|
||||
public function dashboard(): JsonResponse
|
||||
{
|
||||
$schoolYear = request()->query('school_year');
|
||||
|
||||
$items = InventoryItem::query()
|
||||
->when($schoolYear, fn ($q) => $q->where('school_year', $schoolYear))
|
||||
->get();
|
||||
|
||||
$totalItems = $items->count();
|
||||
$byType = $items->groupBy('type')->map(fn ($g) => $g->count());
|
||||
$lowStockCount = $items->filter(fn ($i) => $i->reorder_point && $i->quantity <= $i->reorder_point)->count();
|
||||
$outOfStockCount = $items->filter(fn ($i) => $i->quantity <= 0)->count();
|
||||
$needsRepairCount = $items->sum('needs_repair_qty');
|
||||
$missingCount = $items->sum('cannot_find_qty');
|
||||
|
||||
return $this->success([
|
||||
'total_items' => $totalItems,
|
||||
'by_type' => $byType,
|
||||
'low_stock_count' => $lowStockCount,
|
||||
'out_of_stock_count' => $outOfStockCount,
|
||||
'needs_repair_count' => (int) $needsRepairCount,
|
||||
'missing_count' => (int) $missingCount,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|JsonResponse
|
||||
*/
|
||||
|
||||
@@ -73,6 +73,57 @@ class InventoryMovementController extends BaseApiController
|
||||
return $this->success($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Void a movement (append-only correction).
|
||||
*/
|
||||
public function void(int $id): JsonResponse
|
||||
{
|
||||
$actor = $this->authenticatedUserIdOrUnauthorized();
|
||||
if ($actor instanceof JsonResponse) {
|
||||
return $actor;
|
||||
}
|
||||
|
||||
$reason = (string) request()->input('reason', '');
|
||||
if (empty(trim($reason))) {
|
||||
return $this->error('Reason is required to void a movement.', Response::HTTP_UNPROCESSABLE_ENTITY);
|
||||
}
|
||||
|
||||
$result = $this->movements->voidMovement($id, $reason, $actor);
|
||||
if (!($result['ok'] ?? false)) {
|
||||
return $this->error($result['message'] ?? 'Failed to void movement.', Response::HTTP_UNPROCESSABLE_ENTITY);
|
||||
}
|
||||
|
||||
return $this->success(['message' => 'Movement voided successfully.']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a correction movement.
|
||||
*/
|
||||
public function correct(int $id): JsonResponse
|
||||
{
|
||||
$actor = $this->authenticatedUserIdOrUnauthorized();
|
||||
if ($actor instanceof JsonResponse) {
|
||||
return $actor;
|
||||
}
|
||||
|
||||
$qtyChange = (int) request()->input('qty_change', 0);
|
||||
$reason = (string) request()->input('reason', '');
|
||||
|
||||
if ($qtyChange === 0) {
|
||||
return $this->error('Quantity change must be non-zero.', Response::HTTP_UNPROCESSABLE_ENTITY);
|
||||
}
|
||||
if (empty(trim($reason))) {
|
||||
return $this->error('Reason is required.', Response::HTTP_UNPROCESSABLE_ENTITY);
|
||||
}
|
||||
|
||||
$result = $this->movements->correctMovement($id, $qtyChange, $reason, $actor);
|
||||
if (!($result['ok'] ?? false)) {
|
||||
return $this->error($result['message'] ?? 'Failed to correct movement.', Response::HTTP_UNPROCESSABLE_ENTITY);
|
||||
}
|
||||
|
||||
return $this->success(['message' => 'Correction movement created.']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|JsonResponse
|
||||
*/
|
||||
|
||||
@@ -14,13 +14,13 @@ class InventoryItemStoreRequest extends ApiFormRequest
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'type' => ['required', 'string', 'max:20'],
|
||||
'category_id' => ['nullable', 'integer', 'min:1'],
|
||||
'type' => ['required', 'string', 'in:classroom,book,office,kitchen'],
|
||||
'category_id' => ['nullable', 'integer', 'min:1', 'exists:inventory_categories,id'],
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'description' => ['nullable', 'string'],
|
||||
'quantity' => ['nullable', 'integer'],
|
||||
'quantity' => ['nullable', 'integer', 'min:0'],
|
||||
'unit' => ['nullable', 'string', 'max:50'],
|
||||
'condition' => ['nullable', 'string', 'max:50'],
|
||||
'condition' => ['nullable', 'string', 'in:good,needs_repair,need_replace,cannot_find'],
|
||||
'isbn' => ['nullable', 'string', 'max:50'],
|
||||
'edition' => ['nullable', 'string', 'max:50'],
|
||||
'sku' => ['nullable', 'string', 'max:50'],
|
||||
|
||||
@@ -14,12 +14,12 @@ class InventoryItemUpdateRequest extends ApiFormRequest
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'category_id' => ['nullable', 'integer', 'min:1'],
|
||||
'category_id' => ['nullable', 'integer', 'min:1', 'exists:inventory_categories,id'],
|
||||
'name' => ['sometimes', 'string', 'max:255'],
|
||||
'description' => ['nullable', 'string'],
|
||||
'quantity' => ['nullable', 'integer'],
|
||||
// quantity is intentionally excluded: stock changes must go through movements
|
||||
'unit' => ['nullable', 'string', 'max:50'],
|
||||
'condition' => ['nullable', 'string', 'max:50'],
|
||||
'condition' => ['nullable', 'string', 'in:good,needs_repair,need_replace,cannot_find'],
|
||||
'isbn' => ['nullable', 'string', 'max:50'],
|
||||
'edition' => ['nullable', 'string', 'max:50'],
|
||||
'sku' => ['nullable', 'string', 'max:50'],
|
||||
|
||||
@@ -28,6 +28,16 @@ class InventoryItemResource extends JsonResource
|
||||
'semester' => $this['semester'] ?? null,
|
||||
'school_year' => $this['school_year'] ?? null,
|
||||
'updated_by' => $this['updated_by'] ?? null,
|
||||
// New fields
|
||||
'preferred_supplier_id' => $this['preferred_supplier_id'] ?? null,
|
||||
'last_purchase_price' => $this['last_purchase_price'] ?? null,
|
||||
'average_unit_cost' => $this['average_unit_cost'] ?? null,
|
||||
'reorder_point' => $this['reorder_point'] ?? null,
|
||||
'reorder_quantity' => $this['reorder_quantity'] ?? null,
|
||||
'lead_time_days' => $this['lead_time_days'] ?? null,
|
||||
'barcode' => $this['barcode'] ?? null,
|
||||
'qr_code' => $this['qr_code'] ?? null,
|
||||
'asset_tag' => $this['asset_tag'] ?? null,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user