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')->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); } }