Commit fdd13245 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Fix POS to sell products (not programs) + fix product visibility

- POS Terminal now shows products tab with addProduct() method
  (programs are enrolled via receptionist desk, not POS)
- Product list and POS show branch products + academy-wide (null branch)
- CreateProductWizard and ProductForm now set branch_id on create
- Product model gets ManglesUniqueOnDelete for SKU collision prevention
- Clicking a product in POS adds it to cart (or increments qty if exists)
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 2ee9537f
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
use App\Domain\Shared\Traits\Auditable; use App\Domain\Shared\Traits\Auditable;
use App\Domain\Shared\Traits\BelongsToAcademy; use App\Domain\Shared\Traits\BelongsToAcademy;
use App\Domain\Shared\Traits\HasUuid; use App\Domain\Shared\Traits\HasUuid;
use App\Domain\Shared\Traits\ManglesUniqueOnDelete;
use App\Models\User; use App\Models\User;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
...@@ -15,7 +16,9 @@ ...@@ -15,7 +16,9 @@
class Product extends Model class Product extends Model
{ {
use BelongsToAcademy, HasUuid, SoftDeletes, Auditable; use BelongsToAcademy, HasUuid, SoftDeletes, Auditable, ManglesUniqueOnDelete;
protected array $uniqueFieldsToMangle = ['sku'];
protected $fillable = [ protected $fillable = [
'academy_id', 'academy_id',
......
...@@ -130,6 +130,7 @@ public function confirm(): void ...@@ -130,6 +130,7 @@ public function confirm(): void
'sku' => $this->sku ?: null, 'sku' => $this->sku ?: null,
'barcode' => $this->barcode ?: null, 'barcode' => $this->barcode ?: null,
'category_id' => $this->categoryId, 'category_id' => $this->categoryId,
'branch_id' => session('active_branch_id'),
'type' => $this->type, 'type' => $this->type,
'description_ar' => $this->descriptionAr ?: null, 'description_ar' => $this->descriptionAr ?: null,
'selling_price' => (int) round((float) $this->sellingPrice * 100), 'selling_price' => (int) round((float) $this->sellingPrice * 100),
......
...@@ -132,6 +132,7 @@ public function save(): void ...@@ -132,6 +132,7 @@ public function save(): void
session()->flash('success', __('تم تحديث المنتج بنجاح')); session()->flash('success', __('تم تحديث المنتج بنجاح'));
} else { } else {
$data['created_by'] = auth()->id(); $data['created_by'] = auth()->id();
$data['branch_id'] = session('active_branch_id');
Product::create($data); Product::create($data);
session()->flash('success', __('تم إنشاء المنتج بنجاح')); session()->flash('success', __('تم إنشاء المنتج بنجاح'));
} }
......
...@@ -53,7 +53,9 @@ public function render() ...@@ -53,7 +53,9 @@ public function render()
$query = Product::query() $query = Product::query()
->with(['category', 'inventoryLevels']) ->with(['category', 'inventoryLevels'])
->when($branchId, fn ($q) => $q->where('branch_id', $branchId)) ->when($branchId, fn ($q) => $q->where(function ($sub) use ($branchId) {
$sub->where('branch_id', $branchId)->orWhereNull('branch_id');
}))
->when($this->search, function ($q) { ->when($this->search, function ($q) {
$search = $this->search; $search = $this->search;
$q->where(function ($q2) use ($search) { $q->where(function ($q2) use ($search) {
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
namespace App\Livewire\POS; namespace App\Livewire\POS;
use App\Domain\Financial\Services\CashSessionService; use App\Domain\Financial\Services\CashSessionService;
use App\Domain\Inventory\Models\Product;
use App\Domain\Participant\Models\Participant; use App\Domain\Participant\Models\Participant;
use App\Domain\POS\Enums\POSItemType; use App\Domain\POS\Enums\POSItemType;
use App\Domain\POS\Enums\POSPaymentMethod; use App\Domain\POS\Enums\POSPaymentMethod;
...@@ -118,6 +119,33 @@ public function addProgram(int $programId, PricingService $pricingService): void ...@@ -118,6 +119,33 @@ public function addProgram(int $programId, PricingService $pricingService): void
} }
} }
public function addProduct(int $productId): void
{
$product = Product::findOrFail($productId);
$existingIndex = collect($this->cart)->search(fn ($item) => $item['item_type'] === POSItemType::Product->value && $item['item_id'] === $product->id);
if ($existingIndex !== false) {
$this->updateQuantity($existingIndex, $this->cart[$existingIndex]['quantity'] + 1);
return;
}
$unitPrice = $product->selling_price;
$this->cart[] = [
'item_type' => POSItemType::Product->value,
'item_id' => $product->id,
'item_name_ar' => $product->name_ar,
'quantity' => 1,
'unit_price' => $unitPrice,
'base_price' => $unitPrice,
'discount_amount' => 0,
'line_total' => $unitPrice,
'pricing_snapshot' => [],
'metadata' => ['product_id' => $product->id, 'sku' => $product->sku],
];
}
public function addManualItem(): void public function addManualItem(): void
{ {
if (empty($this->manualItemName) || $this->manualItemPrice <= 0) { if (empty($this->manualItemName) || $this->manualItemPrice <= 0) {
...@@ -306,10 +334,17 @@ public function newTransaction(): void ...@@ -306,10 +334,17 @@ public function newTransaction(): void
public function render() public function render()
{ {
$programs = TrainingProgram::where('status', 'active')->orderBy('name_ar')->get(); $branchId = auth()->user()->branch_id ?? null;
$products = Product::where('is_active', true)
->when($branchId, fn ($q) => $q->where(function ($sub) use ($branchId) {
$sub->where('branch_id', $branchId)->orWhereNull('branch_id');
}))
->orderBy('name_ar')
->get();
return view('livewire.pos.pos-terminal', [ return view('livewire.pos.pos-terminal', [
'programs' => $programs, 'products' => $products,
'cartTotal' => $this->getCartTotal(), 'cartTotal' => $this->getCartTotal(),
'cartDiscount' => $this->getCartDiscount(), 'cartDiscount' => $this->getCartDiscount(),
'cartSubtotal' => $this->getCartSubtotal(), 'cartSubtotal' => $this->getCartSubtotal(),
......
<div x-data="{ <div x-data="{
activeTab: 'programs', activeTab: 'products',
showParticipantResults: false, showParticipantResults: false,
mobileCartOpen: false, mobileCartOpen: false,
}" class="min-h-[calc(100vh-4rem)]"> }" class="min-h-[calc(100vh-4rem)]">
...@@ -82,10 +82,10 @@ class="w-full text-start px-3 sm:px-4 py-2.5 min-h-[44px] hover:bg-gray-50 activ ...@@ -82,10 +82,10 @@ class="w-full text-start px-3 sm:px-4 py-2.5 min-h-[44px] hover:bg-gray-50 activ
{{-- Tabs --}} {{-- Tabs --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden"> <div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<div class="flex border-b border-gray-200"> <div class="flex border-b border-gray-200">
<button @click="activeTab = 'programs'" <button @click="activeTab = 'products'"
:class="activeTab === 'programs' ? 'border-b-2 border-blue-600 text-blue-700 bg-blue-50/50' : 'text-gray-500 hover:text-gray-700'" :class="activeTab === 'products' ? 'border-b-2 border-blue-600 text-blue-700 bg-blue-50/50' : 'text-gray-500 hover:text-gray-700'"
class="flex-1 px-3 sm:px-4 py-3 min-h-[44px] text-xs sm:text-sm font-medium transition-colors"> class="flex-1 px-3 sm:px-4 py-3 min-h-[44px] text-xs sm:text-sm font-medium transition-colors">
{{ __('البرامج') }} {{ __('المنتجات') }}
</button> </button>
<button @click="activeTab = 'manual'" <button @click="activeTab = 'manual'"
:class="activeTab === 'manual' ? 'border-b-2 border-blue-600 text-blue-700 bg-blue-50/50' : 'text-gray-500 hover:text-gray-700'" :class="activeTab === 'manual' ? 'border-b-2 border-blue-600 text-blue-700 bg-blue-50/50' : 'text-gray-500 hover:text-gray-700'"
...@@ -94,31 +94,31 @@ class="flex-1 px-3 sm:px-4 py-3 min-h-[44px] text-xs sm:text-sm font-medium tran ...@@ -94,31 +94,31 @@ class="flex-1 px-3 sm:px-4 py-3 min-h-[44px] text-xs sm:text-sm font-medium tran
</button> </button>
</div> </div>
{{-- Programs Tab --}} {{-- Products Tab --}}
<div x-show="activeTab === 'programs'" class="p-3 sm:p-4"> <div x-show="activeTab === 'products'" class="p-3 sm:p-4">
@if($programs->isEmpty()) @if($products->isEmpty())
<div class="text-center py-6 sm:py-8"> <div class="text-center py-6 sm:py-8">
<svg class="w-10 h-10 sm:w-12 sm:h-12 text-gray-300 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-10 h-10 sm:w-12 sm:h-12 text-gray-300 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"/>
</svg> </svg>
<p class="text-gray-500 text-xs sm:text-sm">{{ __('لا توجد برامج متاحة') }}</p> <p class="text-gray-500 text-xs sm:text-sm">{{ __('لا توجد منتجات متاحة') }}</p>
</div> </div>
@else @else
<div class="grid grid-cols-2 sm:grid-cols-2 lg:grid-cols-2 gap-2 sm:gap-3" wire:loading.class="opacity-50 pointer-events-none" wire:target="addProgram"> <div class="grid grid-cols-2 sm:grid-cols-2 lg:grid-cols-3 gap-2 sm:gap-3" wire:loading.class="opacity-50 pointer-events-none" wire:target="addProduct">
@foreach($programs as $program) @foreach($products as $product)
<button wire:click="addProgram({{ $program->id }})" <button wire:click="addProduct({{ $product->id }})"
class="flex items-center gap-2 sm:gap-3 p-2.5 sm:p-3 min-h-[44px] border border-gray-200 rounded-lg hover:border-blue-300 hover:bg-blue-50/50 active:bg-blue-100 active:scale-[0.97] transition-all text-start group"> class="flex flex-col items-start p-2.5 sm:p-3 min-h-[44px] border border-gray-200 rounded-lg hover:border-blue-300 hover:bg-blue-50/50 active:bg-blue-100 active:scale-[0.97] transition-all text-start group">
<div class="w-8 h-8 sm:w-10 sm:h-10 rounded-lg bg-blue-100 text-blue-600 flex items-center justify-center shrink-0 group-hover:bg-blue-200 transition-colors"> <div class="flex items-center gap-2 w-full">
<svg class="w-4 h-4 sm:w-5 sm:h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <div class="w-8 h-8 sm:w-9 sm:h-9 rounded-lg bg-emerald-100 text-emerald-600 flex items-center justify-center shrink-0 group-hover:bg-emerald-200 transition-colors">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/> <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
</svg> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"/>
</div> </svg>
<div class="flex-1 min-w-0"> </div>
<p class="text-xs sm:text-sm font-medium text-gray-800 truncate">{{ $program->name_ar }}</p> <div class="flex-1 min-w-0">
<p class="text-xs sm:text-sm font-medium text-gray-800 truncate">{{ $product->name_ar }}</p>
<p class="text-xs text-blue-600 font-bold" dir="ltr">{{ number_format($product->selling_price / 100, 2) }} {{ __('ج.م') }}</p>
</div>
</div> </div>
<svg class="w-4 h-4 text-gray-400 group-hover:text-blue-500 transition-colors shrink-0 hidden sm:block" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
</svg>
</button> </button>
@endforeach @endforeach
</div> </div>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment