Commit 9df1341d authored by Mahmoud Aglan's avatar Mahmoud Aglan

Add essential products: quick-buttons in wizard, delivery tracking, admin price override on invoice

- Add is_essential boolean to products (migration + model + form toggle)
- Essential products appear as quick-add buttons in registration wizard step 5 (no search needed), highlighted when added to cart
- New EssentialDeliveries page (/inventory/essential-deliveries): lists all players who bought essential products, mark استلم/لم يستلم per item with who delivered and when
- Delivery tracking: add is_delivered, delivered_at, delivered_by to invoice_items; hotbuy cart items now store itemable_type/id for proper morph link
- Admin price override now stored in invoice item metadata (original_price, overridden_price, override_reason) and displayed on invoice print with crossed-out original price
- "أساسي" badge on product list; "تسليم الأساسيات" button on product list header and sidebar
Co-Authored-By: 's avatarClaude Sonnet 4.6 <noreply@anthropic.com>
parent c6327cf7
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
namespace App\Domain\Financial\Models; namespace App\Domain\Financial\Models;
use App\Models\User;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Database\Eloquent\Relations\MorphTo;
...@@ -19,6 +20,9 @@ class InvoiceItem extends Model ...@@ -19,6 +20,9 @@ class InvoiceItem extends Model
'tax_amount', 'tax_amount',
'total_amount', 'total_amount',
'metadata', 'metadata',
'is_delivered',
'delivered_at',
'delivered_by',
]; ];
protected function casts(): array protected function casts(): array
...@@ -30,6 +34,8 @@ protected function casts(): array ...@@ -30,6 +34,8 @@ protected function casts(): array
'tax_amount' => 'integer', 'tax_amount' => 'integer',
'total_amount' => 'integer', 'total_amount' => 'integer',
'metadata' => 'array', 'metadata' => 'array',
'is_delivered' => 'boolean',
'delivered_at' => 'datetime',
]; ];
} }
...@@ -42,4 +48,9 @@ public function itemable(): MorphTo ...@@ -42,4 +48,9 @@ public function itemable(): MorphTo
{ {
return $this->morphTo(); return $this->morphTo();
} }
public function deliveredBy(): BelongsTo
{
return $this->belongsTo(User::class, 'delivered_by');
}
} }
...@@ -35,6 +35,7 @@ class Product extends Model ...@@ -35,6 +35,7 @@ class Product extends Model
'cost_price', 'cost_price',
'track_inventory', 'track_inventory',
'is_active', 'is_active',
'is_essential',
'min_stock_level', 'min_stock_level',
'max_stock_level', 'max_stock_level',
'weight_grams', 'weight_grams',
...@@ -54,6 +55,7 @@ protected function casts(): array ...@@ -54,6 +55,7 @@ protected function casts(): array
'cost_price' => 'integer', 'cost_price' => 'integer',
'track_inventory' => 'boolean', 'track_inventory' => 'boolean',
'is_active' => 'boolean', 'is_active' => 'boolean',
'is_essential' => 'boolean',
'min_stock_level' => 'integer', 'min_stock_level' => 'integer',
'max_stock_level' => 'integer', 'max_stock_level' => 'integer',
'weight_grams' => 'integer', 'weight_grams' => 'integer',
......
<?php
namespace App\Livewire\Inventory;
use App\Domain\Financial\Models\InvoiceItem;
use App\Domain\Inventory\Models\Product;
use Illuminate\Support\Facades\DB;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithPagination;
#[Layout('layouts.app')]
#[Title('تسليم المنتجات الأساسية')]
class EssentialDeliveries extends Component
{
use WithPagination;
#[Url]
public ?int $product_id = null;
#[Url]
public string $status = 'pending';
#[Url]
public string $search = '';
public function updatedSearch(): void
{
$this->resetPage();
}
public function updatedProductId(): void
{
$this->resetPage();
}
public function updatedStatus(): void
{
$this->resetPage();
}
public function markDelivered(int $itemId): void
{
$this->authorize('inventory.update');
$item = InvoiceItem::findOrFail($itemId);
$item->update([
'is_delivered' => true,
'delivered_at' => now(),
'delivered_by' => auth()->id(),
]);
session()->flash('success', __('تم تسجيل الاستلام بنجاح'));
}
public function markUndelivered(int $itemId): void
{
$this->authorize('inventory.update');
$item = InvoiceItem::findOrFail($itemId);
$item->update([
'is_delivered' => false,
'delivered_at' => null,
'delivered_by' => null,
]);
}
public function render()
{
$essentialProductIds = Product::where('is_essential', true)
->where('is_active', true)
->pluck('id');
$query = InvoiceItem::with(['invoice.billable.person', 'deliveredBy'])
->whereIn('itemable_id', $essentialProductIds)
->where('itemable_type', Product::class)
->whereHas('invoice', fn ($q) => $q->whereIn('status', ['sent', 'paid', 'partially_paid']))
->when($this->product_id, fn ($q) => $q->where('itemable_id', $this->product_id))
->when($this->status === 'pending', fn ($q) => $q->where('is_delivered', false))
->when($this->status === 'delivered', fn ($q) => $q->where('is_delivered', true))
->when($this->search, function ($q) {
$q->whereHas('invoice.billable.person', fn ($pq) =>
$pq->where('name_ar', 'ilike', "%{$this->search}%")
->orWhere('name', 'ilike', "%{$this->search}%")
->orWhere('national_id', 'ilike', "%{$this->search}%")
);
})
->orderBy('is_delivered')
->orderByDesc('created_at');
return view('livewire.inventory.essential-deliveries', [
'items' => $query->paginate(25),
'essentialProducts' => Product::where('is_essential', true)->where('is_active', true)->get(['id', 'name_ar']),
'pendingCount' => InvoiceItem::whereIn('itemable_id', $essentialProductIds)
->where('itemable_type', Product::class)
->where('is_delivered', false)
->whereHas('invoice', fn ($q) => $q->whereIn('status', ['sent', 'paid', 'partially_paid']))
->count(),
]);
}
}
...@@ -30,6 +30,7 @@ class ProductForm extends Component ...@@ -30,6 +30,7 @@ class ProductForm extends Component
public ?int $max_stock_level = null; public ?int $max_stock_level = null;
public string $tax_rate = '0'; public string $tax_rate = '0';
public bool $is_active = true; public bool $is_active = true;
public bool $is_essential = false;
public string $description_ar = ''; public string $description_ar = '';
public function mount(?Product $product = null): void public function mount(?Product $product = null): void
...@@ -54,6 +55,7 @@ public function mount(?Product $product = null): void ...@@ -54,6 +55,7 @@ public function mount(?Product $product = null): void
$this->max_stock_level = $product->max_stock_level; $this->max_stock_level = $product->max_stock_level;
$this->tax_rate = (string) ($product->tax_rate ?? 0); $this->tax_rate = (string) ($product->tax_rate ?? 0);
$this->is_active = $product->is_active; $this->is_active = $product->is_active;
$this->is_essential = $product->is_essential ?? false;
$this->description_ar = $product->description_ar ?? ''; $this->description_ar = $product->description_ar ?? '';
} }
} }
...@@ -78,6 +80,7 @@ public function rules(): array ...@@ -78,6 +80,7 @@ public function rules(): array
'max_stock_level' => 'nullable|integer|min:0', 'max_stock_level' => 'nullable|integer|min:0',
'tax_rate' => 'nullable|numeric|min:0|max:100', 'tax_rate' => 'nullable|numeric|min:0|max:100',
'is_active' => 'boolean', 'is_active' => 'boolean',
'is_essential' => 'boolean',
'description_ar' => 'nullable|string|max:1000', 'description_ar' => 'nullable|string|max:1000',
]; ];
} }
...@@ -124,6 +127,7 @@ public function save(): void ...@@ -124,6 +127,7 @@ public function save(): void
'max_stock_level' => $this->max_stock_level, 'max_stock_level' => $this->max_stock_level,
'tax_rate' => (int) $this->tax_rate, 'tax_rate' => (int) $this->tax_rate,
'is_active' => $this->is_active, 'is_active' => $this->is_active,
'is_essential' => $this->is_essential,
'description_ar' => $this->description_ar ?: null, 'description_ar' => $this->description_ar ?: null,
]; ];
......
...@@ -556,6 +556,24 @@ public function effectiveTotal(): int ...@@ -556,6 +556,24 @@ public function effectiveTotal(): int
// --- Hot-buy methods --- // --- Hot-buy methods ---
#[Computed]
public function essentialProducts(): array
{
return Product::where('is_active', true)
->where('is_essential', true)
->orderBy('sort_order')
->orderBy('name_ar')
->get()
->map(fn ($p) => [
'id' => $p->id,
'type' => 'product',
'name_ar' => $p->name_ar,
'name' => $p->name,
'sku' => $p->sku,
'price' => $p->selling_price,
])->toArray();
}
#[Computed] #[Computed]
public function hotbuyResults(): array public function hotbuyResults(): array
{ {
...@@ -861,23 +879,39 @@ public function confirm(): void ...@@ -861,23 +879,39 @@ public function confirm(): void
if ($prorationResult->applied) { if ($prorationResult->applied) {
$description .= ' (' . $prorationResult->description . ')'; $description .= ' (' . $prorationResult->description . ')';
} }
$programItemMeta = [];
if ($this->priceOverrideEnabled && $actor->is_super_admin && $finalTotal !== $computedTotal) {
$programItemMeta['original_price'] = $computedTotal;
$programItemMeta['overridden_price'] = $finalTotal;
$programItemMeta['override_reason'] = $this->priceOverrideReason;
$programItemMeta['overridden_by'] = $actor->name;
}
$invoiceItems[] = [ $invoiceItems[] = [
'description' => $description, 'description' => $description,
'quantity' => 1, 'quantity' => 1,
'unit_price' => $programFee, 'unit_price' => $programFee,
'discount_amount' => 0, 'discount_amount' => 0,
'tax_amount' => 0, 'tax_amount' => 0,
'metadata' => $programItemMeta ?: null,
]; ];
} }
foreach ($this->hotbuyCart as $cartItem) { foreach ($this->hotbuyCart as $cartItem) {
$invoiceItems[] = [ $itemEntry = [
'description' => $cartItem['name_ar'], 'description' => $cartItem['name_ar'],
'quantity' => $cartItem['quantity'], 'quantity' => $cartItem['quantity'],
'unit_price' => $cartItem['price'], 'unit_price' => $cartItem['price'],
'discount_amount' => 0, 'discount_amount' => 0,
'tax_amount' => 0, 'tax_amount' => 0,
]; ];
if ($cartItem['type'] === 'product') {
$itemEntry['itemable_type'] = Product::class;
$itemEntry['itemable_id'] = $cartItem['id'];
} elseif ($cartItem['type'] === 'kit') {
$itemEntry['itemable_type'] = Kit::class;
$itemEntry['itemable_id'] = $cartItem['id'];
}
$invoiceItems[] = $itemEntry;
} }
// If override changed total, reflect as a discount line // If override changed total, reflect as a discount line
......
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
if (!Schema::hasColumn('products', 'is_essential')) {
Schema::table('products', function (Blueprint $table) {
$table->boolean('is_essential')->default(false)->after('is_active');
});
}
}
public function down(): void
{
if (Schema::hasColumn('products', 'is_essential')) {
Schema::table('products', function (Blueprint $table) {
$table->dropColumn('is_essential');
});
}
}
};
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
if (!Schema::hasColumn('invoice_items', 'is_delivered')) {
Schema::table('invoice_items', function (Blueprint $table) {
$table->boolean('is_delivered')->default(false)->after('metadata');
$table->timestamp('delivered_at')->nullable()->after('is_delivered');
$table->foreignId('delivered_by')->nullable()->constrained('users')->after('delivered_at');
});
}
}
public function down(): void
{
Schema::table('invoice_items', function (Blueprint $table) {
$table->dropColumn(['is_delivered', 'delivered_at', 'delivered_by']);
});
}
};
...@@ -61,6 +61,7 @@ ...@@ -61,6 +61,7 @@
['label' => 'الأطقم', 'route' => 'inventory.kits', 'icon' => 'gift', 'permission' => 'inventory.list'], ['label' => 'الأطقم', 'route' => 'inventory.kits', 'icon' => 'gift', 'permission' => 'inventory.list'],
['label' => 'جرد المخزون', 'route' => 'inventory.stock-counts', 'icon' => 'clipboard-document-list', 'permission' => 'inventory.list'], ['label' => 'جرد المخزون', 'route' => 'inventory.stock-counts', 'icon' => 'clipboard-document-list', 'permission' => 'inventory.list'],
['label' => 'التسويات', 'route' => 'inventory.adjustments', 'icon' => 'clipboard-document-list', 'permission' => 'inventory.adjust'], ['label' => 'التسويات', 'route' => 'inventory.adjustments', 'icon' => 'clipboard-document-list', 'permission' => 'inventory.adjust'],
['label' => 'تسليم الأساسيات', 'route' => 'inventory.essential-deliveries', 'icon' => 'hand-raised', 'permission' => 'inventory.list'],
]], ]],
['section' => 'المنشآت', 'items' => [ ['section' => 'المنشآت', 'items' => [
......
<div class="p-6 space-y-6">
{{-- Header --}}
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-gray-800">{{ __('تسليم المنتجات الأساسية') }}</h1>
<p class="text-sm text-gray-500 mt-1">{{ __('تتبع استلام اللاعبين للمنتجات الأساسية') }}</p>
</div>
@if($pendingCount > 0)
<div class="flex items-center gap-2 px-4 py-2 bg-red-50 border border-red-200 rounded-xl">
<span class="w-2.5 h-2.5 rounded-full bg-red-500 animate-pulse"></span>
<span class="text-sm font-semibold text-red-700">{{ $pendingCount }} {{ __('لم يستلموا بعد') }}</span>
</div>
@endif
</div>
@if(session('success'))
<div class="p-3 bg-green-50 border border-green-200 rounded-lg text-sm text-green-700">
{{ session('success') }}
</div>
@endif
{{-- Filters --}}
<div class="flex flex-wrap gap-3">
<input type="text" wire:model.live.debounce.300ms="search"
class="flex-1 min-w-48 px-4 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500"
placeholder="{{ __('ابحث باسم اللاعب...') }}">
<select wire:model.live="product_id"
class="px-4 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 bg-white">
<option value="">{{ __('جميع المنتجات الأساسية') }}</option>
@foreach($essentialProducts as $ep)
<option value="{{ $ep->id }}">{{ $ep->name_ar }}</option>
@endforeach
</select>
<div class="flex rounded-lg border border-gray-300 overflow-hidden">
<button type="button" wire:click="$set('status', 'all')"
class="px-4 py-2.5 text-sm font-medium transition-colors {{ $status === 'all' ? 'bg-gray-800 text-white' : 'bg-white text-gray-600 hover:bg-gray-50' }}">
{{ __('الكل') }}
</button>
<button type="button" wire:click="$set('status', 'pending')"
class="px-4 py-2.5 text-sm font-medium transition-colors border-s border-gray-300 {{ $status === 'pending' ? 'bg-red-600 text-white' : 'bg-white text-gray-600 hover:bg-gray-50' }}">
{{ __('لم يستلموا') }}
</button>
<button type="button" wire:click="$set('status', 'delivered')"
class="px-4 py-2.5 text-sm font-medium transition-colors border-s border-gray-300 {{ $status === 'delivered' ? 'bg-green-600 text-white' : 'bg-white text-gray-600 hover:bg-gray-50' }}">
{{ __('استلموا') }}
</button>
</div>
</div>
{{-- Table --}}
<div class="bg-white rounded-xl border border-gray-200 overflow-hidden" wire:loading.class="opacity-50 pointer-events-none">
@if($items->isEmpty())
<div class="py-16 text-center">
<svg class="mx-auto w-12 h-12 text-gray-300 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"/>
</svg>
<p class="text-gray-500 text-sm">{{ __('لا توجد سجلات') }}</p>
</div>
@else
<table class="w-full text-sm">
<thead class="bg-gray-50 border-b border-gray-200">
<tr>
<th class="px-4 py-3 text-start text-xs font-semibold text-gray-500 uppercase tracking-wider">{{ __('اللاعب') }}</th>
<th class="px-4 py-3 text-start text-xs font-semibold text-gray-500 uppercase tracking-wider">{{ __('المنتج') }}</th>
<th class="px-4 py-3 text-center text-xs font-semibold text-gray-500 uppercase tracking-wider">{{ __('الكمية') }}</th>
<th class="px-4 py-3 text-start text-xs font-semibold text-gray-500 uppercase tracking-wider">{{ __('الفاتورة') }}</th>
<th class="px-4 py-3 text-start text-xs font-semibold text-gray-500 uppercase tracking-wider">{{ __('الحالة') }}</th>
<th class="px-4 py-3 text-center text-xs font-semibold text-gray-500 uppercase tracking-wider">{{ __('إجراء') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
@foreach($items as $item)
@php
$participant = $item->invoice?->billable;
$person = $participant?->person;
$playerName = $person?->name_ar ?? $person?->name ?? __('غير محدد');
@endphp
<tr class="hover:bg-gray-50 transition-colors {{ $item->is_delivered ? 'opacity-60' : '' }}">
<td class="px-4 py-3">
<p class="font-medium text-gray-800">{{ $playerName }}</p>
@if($person?->phone)
<p class="text-xs text-gray-500" dir="ltr">{{ $person->phone }}</p>
@endif
</td>
<td class="px-4 py-3">
<p class="font-medium text-gray-700">{{ $item->description }}</p>
</td>
<td class="px-4 py-3 text-center font-semibold text-gray-700">{{ $item->quantity }}</td>
<td class="px-4 py-3">
@if($item->invoice)
<span class="font-mono text-xs text-blue-600">{{ $item->invoice->number }}</span>
<p class="text-xs text-gray-400">{{ $item->invoice->created_at?->format('Y/m/d') }}</p>
@endif
</td>
<td class="px-4 py-3">
@if($item->is_delivered)
<div>
<span class="inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-medium bg-green-100 text-green-700">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7"/></svg>
{{ __('استلم') }}
</span>
@if($item->delivered_at)
<p class="text-xs text-gray-400 mt-0.5">{{ $item->delivered_at->format('Y/m/d H:i') }}</p>
@endif
@if($item->deliveredBy)
<p class="text-xs text-gray-400">{{ $item->deliveredBy->name }}</p>
@endif
</div>
@else
<span class="inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-medium bg-red-100 text-red-700">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
{{ __('لم يستلم') }}
</span>
@endif
</td>
<td class="px-4 py-3 text-center">
@if(!$item->is_delivered)
<button type="button" wire:click="markDelivered({{ $item->id }})"
wire:loading.attr="disabled" wire:target="markDelivered({{ $item->id }})"
class="px-4 py-2 bg-green-600 text-white text-xs font-semibold rounded-lg hover:bg-green-700 disabled:opacity-50 transition-colors">
<span wire:loading.remove wire:target="markDelivered({{ $item->id }})">{{ __('استلم ✓') }}</span>
<span wire:loading wire:target="markDelivered({{ $item->id }})">...</span>
</button>
@else
<button type="button" wire:click="markUndelivered({{ $item->id }})"
class="px-3 py-1.5 border border-gray-300 text-gray-500 text-xs rounded-lg hover:bg-gray-50 transition-colors">
{{ __('تراجع') }}
</button>
@endif
</td>
</tr>
@endforeach
</tbody>
</table>
@if($items->hasPages())
<div class="px-4 py-3 border-t border-gray-200">
{{ $items->links() }}
</div>
@endif
@endif
</div>
</div>
...@@ -154,6 +154,18 @@ class="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"> ...@@ -154,6 +154,18 @@ class="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500">
<span class="text-sm text-gray-700">{{ __('نشط') }}</span> <span class="text-sm text-gray-700">{{ __('نشط') }}</span>
</label> </label>
</div> </div>
{{-- Is Essential --}}
<div class="mt-2 p-3 bg-amber-50 border border-amber-200 rounded-lg">
<label class="flex items-start gap-3 cursor-pointer">
<input type="checkbox" wire:model="is_essential"
class="mt-0.5 w-4 h-4 rounded border-amber-400 text-amber-600 focus:ring-amber-500">
<div>
<span class="text-sm font-medium text-amber-800">{{ __('منتج أساسي') }}</span>
<p class="text-xs text-amber-600 mt-0.5">{{ __('يظهر كزر سريع في معالج تسجيل اللاعبين بدون حاجة للبحث') }}</p>
</div>
</label>
</div>
</div> </div>
{{-- Actions --}} {{-- Actions --}}
......
<div> <div>
<div class="flex items-center justify-between mb-6"> <div class="flex items-center justify-between mb-6">
<h1 class="text-xl sm:text-2xl font-bold text-gray-800">{{ __('المنتجات') }}</h1> <h1 class="text-xl sm:text-2xl font-bold text-gray-800">{{ __('المنتجات') }}</h1>
@can('inventory.create') <div class="flex items-center gap-2">
<a href="{{ route('inventory.products.wizard') }}" wire:navigate @can('inventory.list')
class="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-sm font-medium"> <a href="{{ route('inventory.essential-deliveries') }}" wire:navigate
<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="M12 4v16m8-8H4"/></svg> class="inline-flex items-center gap-2 px-4 py-2 bg-amber-50 text-amber-700 border border-amber-300 rounded-lg hover:bg-amber-100 text-sm font-medium">
{{ __('إضافة منتج') }} <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="M7 11.5V14m0-2.5v-6a1.5 1.5 0 113 0m-3 6a1.5 1.5 0 00-3 0v2a7.5 7.5 0 0015 0v-5a1.5 1.5 0 00-3 0m-6-3V11m0-5.5v-1a1.5 1.5 0 013 0v1m0 0V11m0-5.5a1.5 1.5 0 013 0v3m0 0V11"/></svg>
</a> {{ __('تسليم الأساسيات') }}
@endcan </a>
@endcan
@can('inventory.create')
<a href="{{ route('inventory.products.wizard') }}" wire:navigate
class="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-sm font-medium">
<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="M12 4v16m8-8H4"/></svg>
{{ __('إضافة منتج') }}
</a>
@endcan
</div>
</div> </div>
{{-- Flash Messages --}} {{-- Flash Messages --}}
...@@ -72,7 +81,12 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:rin ...@@ -72,7 +81,12 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:rin
{{ $product->sku }} {{ $product->sku }}
</td> </td>
<td class="px-4 py-3"> <td class="px-4 py-3">
<a href="{{ route('inventory.products.show', $product) }}" wire:navigate class="font-medium text-gray-800 hover:text-blue-600">{{ $product->name_ar }}</a> <div class="flex items-center gap-2">
<a href="{{ route('inventory.products.show', $product) }}" wire:navigate class="font-medium text-gray-800 hover:text-blue-600">{{ $product->name_ar }}</a>
@if($product->is_essential)
<span class="px-1.5 py-0.5 text-xs rounded bg-amber-100 text-amber-700 font-medium">{{ __('أساسي') }}</span>
@endif
</div>
@if($product->name) @if($product->name)
<p class="text-xs text-gray-500" dir="ltr">{{ $product->name }}</p> <p class="text-xs text-gray-500" dir="ltr">{{ $product->name }}</p>
@endif @endif
......
...@@ -689,10 +689,33 @@ class="flex-1 sm:flex-none inline-flex items-center justify-center gap-2 px-6 py ...@@ -689,10 +689,33 @@ class="flex-1 sm:flex-none inline-flex items-center justify-center gap-2 px-6 py
<h3 class="font-semibold text-gray-700">{{ __('بيع سريع (اختياري)') }}</h3> <h3 class="font-semibold text-gray-700">{{ __('بيع سريع (اختياري)') }}</h3>
<button type="button" @click="showSearch = !showSearch" <button type="button" @click="showSearch = !showSearch"
class="text-sm text-amber-700 hover:text-amber-900 font-medium"> class="text-sm text-amber-700 hover:text-amber-900 font-medium">
<span x-show="!showSearch">{{ __('إضافة منتج') }}</span> <span x-show="!showSearch">{{ __('بحث عن منتج') }}</span>
<span x-show="showSearch" x-cloak>{{ __('إغلاق') }}</span> <span x-show="showSearch" x-cloak>{{ __('إغلاق البحث') }}</span>
</button> </button>
</div> </div>
{{-- Essential product quick-buttons --}}
@php $essentials = $this->essentialProducts; @endphp
@if(count($essentials) > 0)
<div class="mb-3">
<p class="text-xs font-medium text-amber-700 mb-2">{{ __('منتجات أساسية') }}</p>
<div class="flex flex-wrap gap-2">
@foreach($essentials as $ep)
@php $epKey = "product_{$ep['id']}"; @endphp
<button type="button" wire:click="addHotbuyItem({{ $ep['id'] }}, 'product')"
class="flex items-center gap-1.5 px-3 py-2 rounded-lg border text-sm font-medium transition-colors
{{ isset($hotbuyCart[$epKey]) ? 'bg-amber-600 text-white border-amber-700' : 'bg-white text-amber-800 border-amber-300 hover:bg-amber-100' }}">
@if(isset($hotbuyCart[$epKey]))
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7"/></svg>
@endif
<span>{{ $ep['name_ar'] }}</span>
<span class="text-xs opacity-75" dir="ltr">{{ number_format($ep['price'] / 100, 2) }}</span>
</button>
@endforeach
</div>
</div>
@endif
<div x-show="showSearch" x-cloak x-transition class="mb-3"> <div x-show="showSearch" x-cloak x-transition class="mb-3">
<input type="text" wire:model.live.debounce.300ms="hotbuy_search" <input type="text" wire:model.live.debounce.300ms="hotbuy_search"
class="w-full px-4 py-3 border border-amber-300 rounded-lg focus:ring-2 focus:ring-amber-500 focus:border-amber-500 text-sm" class="w-full px-4 py-3 border border-amber-300 rounded-lg focus:ring-2 focus:ring-amber-500 focus:border-amber-500 text-sm"
......
...@@ -235,11 +235,32 @@ ...@@ -235,11 +235,32 @@
</thead> </thead>
<tbody> <tbody>
@forelse($invoice->items as $i => $item) @forelse($invoice->items as $i => $item)
@php
$itemMeta = is_array($item->metadata) ? $item->metadata : [];
$hasOverride = !empty($itemMeta['original_price']) && isset($itemMeta['overridden_price']);
@endphp
<tr> <tr>
<td>{{ $i + 1 }}</td> <td>{{ $i + 1 }}</td>
<td>{{ $item->description }}</td> <td>
{{ $item->description }}
@if($hasOverride)
<div style="font-size:10px; color:#dc2626; margin-top:2px;">
{{ __('السعر الأصلي') }}: <span dir="ltr" style="text-decoration:line-through">{{ format_money($itemMeta['original_price']) }}</span>
@if(!empty($itemMeta['override_reason']))
— {{ $itemMeta['override_reason'] }}
@endif
</div>
@endif
</td>
<td>{{ $item->quantity }}</td> <td>{{ $item->quantity }}</td>
<td dir="ltr">{{ format_money($item->unit_price) }}</td> <td dir="ltr">
@if($hasOverride)
<span style="text-decoration:line-through; color:#9ca3af; font-size:11px;">{{ format_money($itemMeta['original_price']) }}</span><br>
<span style="color:#dc2626; font-weight:bold;">{{ format_money($itemMeta['overridden_price']) }}</span>
@else
{{ format_money($item->unit_price) }}
@endif
</td>
<td dir="ltr">{{ format_money($item->total_amount) }}</td> <td dir="ltr">{{ format_money($item->total_amount) }}</td>
</tr> </tr>
@empty @empty
......
...@@ -389,6 +389,8 @@ ...@@ -389,6 +389,8 @@
->middleware('permission:inventory.list'); ->middleware('permission:inventory.list');
Route::get('/inventory/purchase-orders/{purchaseOrder}/edit', \App\Livewire\Inventory\PurchaseOrderForm::class)->name('inventory.purchase-orders.edit') Route::get('/inventory/purchase-orders/{purchaseOrder}/edit', \App\Livewire\Inventory\PurchaseOrderForm::class)->name('inventory.purchase-orders.edit')
->middleware('permission:inventory.manage'); ->middleware('permission:inventory.manage');
Route::get('/inventory/essential-deliveries', \App\Livewire\Inventory\EssentialDeliveries::class)->name('inventory.essential-deliveries')
->middleware('permission:inventory.list');
// Settings // Settings
Route::get('/settings', \App\Livewire\Settings\AcademySettings::class)->name('settings.academy') Route::get('/settings', \App\Livewire\Settings\AcademySettings::class)->name('settings.academy')
......
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