florida_gym/app/Http/Controllers/InventoryController.php
2026-03-17 04:50:00 +05:30

314 lines
12 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Models\Product;
use App\Models\StockAdjustment;
use App\Models\ProductSale;
use App\Models\ProductSaleItem;
use App\Models\Account;
use App\Models\Branch;
use App\Models\ProductCategory;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class InventoryController extends Controller
{
public function getProducts(Request $request)
{
$user = Auth::guard('web')->user() ?? Auth::guard('receptionist')->user();
if (!$user) return response()->json(['message' => 'Unauthenticated'], 401);
$branchId = $user->isReceptionist() ? $user->branch_id : $request->query('branch_id');
$status = $request->query('status'); // In Stock, Low Stock, Out of Stock
$query = Product::with(['branch', 'category']);
if ($branchId) {
$query->where('branch_id', $branchId);
}
if ($status) {
$query->where('status', $status);
}
return response()->json($query->orderBy('name')->get());
}
public function storeProduct(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'sku' => 'nullable|string|unique:products,sku',
'product_category_id' => 'required|exists:product_categories,id',
'branch_id' => 'required|exists:branches,id',
'cost_price' => 'required|numeric|min:0',
'selling_price' => 'required|numeric|min:0',
'current_stock' => 'required|integer|min:0',
'reorder_level' => 'required|integer|min:0',
]);
$user = Auth::guard('web')->user() ?? Auth::guard('receptionist')->user();
if ($user && $user->isReceptionist() && $validated['branch_id'] != $user->branch_id) {
return response()->json(['message' => 'Unauthorized branch assignment'], 403);
}
$status = 'In Stock';
if ($validated['current_stock'] <= 0) $status = 'Out of Stock';
else if ($validated['current_stock'] <= $validated['reorder_level']) $status = 'Low Stock';
$product = Product::create(array_merge($validated, ['status' => $status]));
// Record Initial Stock Adjustment
StockAdjustment::create([
'product_id' => $product->id,
'adjustment_qty' => $product->current_stock,
'new_stock' => $product->current_stock,
'adjustment_date' => Carbon::now()->toDateString(),
'reason' => 'Initial',
'remarks' => 'System initialization'
]);
return response()->json($product->load(['branch', 'category']), 201);
}
public function adjustStock(Request $request, $id)
{
$product = Product::findOrFail($id);
$user = Auth::guard('web')->user() ?? Auth::guard('receptionist')->user();
if ($user && $user->isReceptionist() && $product->branch_id != $user->branch_id) {
return response()->json(['message' => 'Unauthorized'], 403);
}
$validated = $request->validate([
'adjustment_qty' => 'required|integer', // Positive for add, negative for remove
'adjustment_date' => 'required|date',
'reason' => 'required|string',
'remarks' => 'nullable|string'
]);
DB::transaction(function () use ($product, $validated) {
$newStock = $product->current_stock + $validated['adjustment_qty'];
$status = 'In Stock';
if ($newStock <= 0) $status = 'Out of Stock';
else if ($newStock <= $product->reorder_level) $status = 'Low Stock';
$product->update([
'current_stock' => $newStock,
'status' => $status
]);
StockAdjustment::create(array_merge($validated, [
'product_id' => $product->id,
'new_stock' => $newStock
]));
});
return response()->json($product->fresh(['branch', 'category']));
}
public function getStockHistory($id)
{
$product = Product::findOrFail($id);
$user = Auth::guard('web')->user() ?? Auth::guard('receptionist')->user();
if ($user && $user->isReceptionist() && $product->branch_id != $user->branch_id) {
return response()->json(['message' => 'Unauthorized'], 403);
}
$history = StockAdjustment::where('product_id', $id)
->orderBy('adjustment_date', 'desc')
->orderBy('created_at', 'desc')
->get();
return response()->json($history);
}
public function getSales(Request $request)
{
$user = Auth::guard('web')->user() ?? Auth::guard('receptionist')->user();
$branchId = $user && $user->isReceptionist() ? $user->branch_id : $request->query('branch_id');
$startDate = $request->query('start_date');
$endDate = $request->query('end_date');
$query = ProductSale::with(['branch', 'items.product']);
if ($branchId) {
$query->where('branch_id', $branchId);
}
if ($startDate) {
$query->where('date', '>=', $startDate);
}
if ($endDate) {
$query->where('date', '<=', $endDate);
}
$sales = $query->orderBy('date', 'desc')
->orderBy('created_at', 'desc')
->get()->map(function($s) {
$originalTotal = ($s->subtotal_amount ?? 0) + ($s->vat_amount ?? 0);
$s->is_adjusted = abs($s->total_amount - $originalTotal) > 0.01;
$s->original_amount = $originalTotal;
return $s;
});
return response()->json($sales);
}
public function storeSale(Request $request)
{
$user = Auth::guard('web')->user() ?? Auth::guard('receptionist')->user();
if (!$user) return response()->json(['message' => 'Unauthenticated'], 401);
$data = $request->all();
if ($user->isReceptionist()) {
$data['branch_id'] = $user->branch_id;
}
$validated = \Illuminate\Support\Facades\Validator::make($data, [
'branch_id' => 'required|exists:branches,id',
'payment_method' => 'required|string',
'items' => 'required|array|min:1',
'items.*.product_id' => 'required|exists:products,id',
'items.*.quantity' => 'required|integer|min:1',
'items.*.unit_price' => 'nullable|numeric|min:0',
'items.*.price' => 'nullable|numeric|min:0',
'total_amount' => 'nullable|numeric|min:0',
'remarks' => 'nullable|string'
])->validate();
// Security check for branch_id if receptionist
if ($user->isReceptionist() && $validated['branch_id'] != $user->branch_id) {
return response()->json(['message' => 'Unauthorized branch assignment'], 403);
}
return DB::transaction(function () use ($validated) {
$subtotal = 0;
$items = $validated['items'];
foreach ($items as &$item) {
$price = $item['unit_price'] ?? $item['price'] ?? 0;
$item['final_price'] = $price;
$subtotal += $item['quantity'] * $price;
}
$vatPercentage = 0.05;
$vatAmount = round($subtotal * $vatPercentage, 2);
$totalWithVat = $subtotal + $vatAmount;
// If a manual total_amount was provided, we use it, otherwise use calculated total
$totalAmount = $validated['total_amount'] ?? $totalWithVat;
$count = ProductSale::count() + 1;
$transactionId = "COL-" . (1000 + $count);
$sale = ProductSale::create([
'transaction_id' => $transactionId,
'branch_id' => $validated['branch_id'],
'subtotal_amount' => $subtotal,
'vat_amount' => $vatAmount,
'total_amount' => $totalAmount,
'payment_method' => $validated['payment_method'],
'date' => Carbon::now()->toDateString(),
'remarks' => $validated['remarks'] ?? null
]);
foreach ($items as $item) {
ProductSaleItem::create([
'product_sale_id' => $sale->id,
'product_id' => $item['product_id'],
'quantity' => $item['quantity'],
'unit_price' => $item['final_price'],
'subtotal' => $item['quantity'] * $item['final_price']
]);
// Update Stock
$product = Product::find($item['product_id']);
$newStock = $product->current_stock - $item['quantity'];
$status = 'In Stock';
if ($newStock <= 0) $status = 'Out of Stock';
else if ($newStock <= $product->reorder_level) $status = 'Low Stock';
$product->update([
'current_stock' => $newStock,
'status' => $status
]);
// Record Adjustment
StockAdjustment::create([
'product_id' => $product->id,
'adjustment_qty' => -$item['quantity'],
'new_stock' => $newStock,
'adjustment_date' => Carbon::now()->toDateString(),
'reason' => 'Sale',
'remarks' => "Sale #{$transactionId}"
]);
}
// Record to Accounts (Credit)
Account::create([
'date' => Carbon::now()->toDateString(),
'time' => Carbon::now()->toTimeString(),
'credit' => $totalAmount,
'debit' => 0,
'type' => 'sale',
'branch_id' => $validated['branch_id'],
'accountable_id' => $sale->id,
'accountable_type' => ProductSale::class,
'description' => "Product Sale #{$transactionId}"
]);
return response()->json($sale->load('items.product'), 201);
});
}
public function getCategories()
{
return response()->json(ProductCategory::where('status', 'Active')->get());
}
public function getAllMovements(Request $request)
{
$user = Auth::guard('web')->user() ?? Auth::guard('receptionist')->user();
$query = StockAdjustment::with(['product.branch']);
$branchId = $user && $user->isReceptionist() ? $user->branch_id : $request->query('branch_id');
$startDate = $request->query('start_date');
$endDate = $request->query('end_date');
if ($branchId) {
$query->whereHas('product', function($q) use ($branchId) {
$q->where('branch_id', $branchId);
});
}
if ($startDate) {
$query->where('adjustment_date', '>=', $startDate);
}
if ($endDate) {
$query->where('adjustment_date', '<=', $endDate);
}
$movements = $query->orderBy('adjustment_date', 'desc')
->orderBy('created_at', 'desc')
->get()
->map(function ($adj) {
return [
'id' => $adj->id,
'date' => $adj->adjustment_date,
'product_name' => $adj->product->name,
'sku' => $adj->product->sku,
'branch' => $adj->product->branch->name,
'reason' => $adj->reason,
'change' => $adj->adjustment_qty,
'new_stock' => $adj->new_stock,
'remarks' => $adj->remarks
];
});
return response()->json($movements);
}
}