Commit 733ed4d3 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Add essential product duplicate warning in POS + update mobile app plan

POS Terminal now checks if an essential product was already purchased by
the selected participant this year. Shows amber warning with the purchase
date — allows the sale to proceed, just warns the admin.

Also expanded mobile app plan with: explore academy (news, gallery),
shop for essential products from app, event registration, and the
corresponding API endpoints needed on the system side.
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent b8cde029
This diff is collapsed.
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
namespace App\Livewire\POS; namespace App\Livewire\POS;
use App\Domain\Financial\Models\InvoiceItem;
use App\Domain\Financial\Services\CashSessionService; use App\Domain\Financial\Services\CashSessionService;
use App\Domain\Inventory\Models\Product; use App\Domain\Inventory\Models\Product;
use App\Domain\Participant\Models\Participant; use App\Domain\Participant\Models\Participant;
...@@ -49,6 +50,9 @@ class POSTerminal extends Component ...@@ -49,6 +50,9 @@ class POSTerminal extends Component
// Participant search results // Participant search results
public array $searchResults = []; public array $searchResults = [];
// Essential product warnings (product was already purchased this year)
public array $essentialWarnings = [];
public function mount(CashSessionService $cashSessionService): void public function mount(CashSessionService $cashSessionService): void
{ {
$this->authorize('pos.sell'); $this->authorize('pos.sell');
...@@ -85,12 +89,18 @@ public function clearParticipant(): void ...@@ -85,12 +89,18 @@ public function clearParticipant(): void
$this->participantId = null; $this->participantId = null;
$this->participantName = ''; $this->participantName = '';
$this->searchResults = []; $this->searchResults = [];
$this->essentialWarnings = [];
} }
public function addProduct(int $productId): void public function addProduct(int $productId): void
{ {
$product = Product::findOrFail($productId); $product = Product::findOrFail($productId);
// Check if essential product was already purchased this year by this participant
if ($product->is_essential && $this->participantId) {
$this->checkEssentialProductHistory($product);
}
$existingIndex = collect($this->cart)->search(fn ($item) => $item['item_type'] === POSItemType::Product->value && $item['item_id'] === $product->id); $existingIndex = collect($this->cart)->search(fn ($item) => $item['item_type'] === POSItemType::Product->value && $item['item_id'] === $product->id);
if ($existingIndex !== false) { if ($existingIndex !== false) {
...@@ -114,6 +124,37 @@ public function addProduct(int $productId): void ...@@ -114,6 +124,37 @@ public function addProduct(int $productId): void
]; ];
} }
private function checkEssentialProductHistory(Product $product): void
{
$yearStart = now()->startOfYear();
$productType = 'App\\Domain\\Inventory\\Models\\Product';
$previousPurchase = InvoiceItem::where('itemable_type', $productType)
->where('itemable_id', $product->id)
->where('created_at', '>=', $yearStart)
->whereHas('invoice', fn ($q) => $q
->where('billable_type', 'App\\Domain\\Participant\\Models\\Participant')
->where('billable_id', $this->participantId)
->whereNotIn('status', ['cancelled', 'draft'])
)
->orderByDesc('created_at')
->first();
if ($previousPurchase) {
$this->essentialWarnings[$product->id] = [
'product_name' => $product->name_ar,
'purchased_at' => $previousPurchase->created_at->format('Y-m-d'),
'participant_name' => $this->participantName,
];
$this->dispatch('essential-product-warning', productId: $product->id);
}
}
public function dismissEssentialWarning(int $productId): void
{
unset($this->essentialWarnings[$productId]);
}
public function addManualItem(): void public function addManualItem(): void
{ {
if (empty($this->manualItemName) || $this->manualItemPrice <= 0) { if (empty($this->manualItemName) || $this->manualItemPrice <= 0) {
......
...@@ -220,6 +220,28 @@ class="lg:hidden min-w-[44px] min-h-[44px] flex items-center justify-center text ...@@ -220,6 +220,28 @@ class="lg:hidden min-w-[44px] min-h-[44px] flex items-center justify-center text
</div> </div>
</div> </div>
{{-- Essential Product Warnings --}}
@if(count($essentialWarnings) > 0)
<div class="px-3 sm:px-4 pt-3 space-y-2">
@foreach($essentialWarnings as $productId => $warning)
<div class="flex items-start gap-2 p-3 bg-amber-50 border border-amber-300 rounded-lg">
<svg class="w-5 h-5 text-amber-600 shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"/>
</svg>
<div class="flex-1 min-w-0">
<p class="text-xs font-bold text-amber-800">{{ __('تنبيه: منتج أساسي مكرر') }}</p>
<p class="text-xs text-amber-700 mt-0.5">
{{ $warning['participant_name'] }} {{ __('اشترى') }} "{{ $warning['product_name'] }}" {{ __('بتاريخ') }} <span dir="ltr" class="font-medium">{{ $warning['purchased_at'] }}</span>
</p>
</div>
<button wire:click="dismissEssentialWarning({{ $productId }})" class="text-amber-500 hover:text-amber-700 shrink-0">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
</button>
</div>
@endforeach
</div>
@endif
{{-- Cart Items --}} {{-- Cart Items --}}
<div class="flex-1 overflow-y-auto p-3 sm:p-4 space-y-2 sm:space-y-3" wire:loading.class="opacity-50 pointer-events-none" wire:target="removeItem,updateQuantity"> <div class="flex-1 overflow-y-auto p-3 sm:p-4 space-y-2 sm:space-y-3" wire:loading.class="opacity-50 pointer-events-none" wire:target="removeItem,updateQuantity">
@forelse($cart as $index => $item) @forelse($cart as $index => $item)
......
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