Commit 055ddfa1 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Add inventory UI (Purchase Orders, Kits, Stock Counts) + wire email to poste.io

- PurchaseOrderList: status filter, receive/cancel actions
- KitList + KitForm: CRUD with dynamic components, assemble/disassemble
- StockCountList + StockCountForm: warehouse picker, variance display
- Register routes for all new inventory components
- config/mail.php: add SSL stream options for self-signed cert bypass
- Update gap-analysis.md to mark completed items
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 13f1bb72
<?php
namespace App\Livewire\Inventory;
use App\Domain\Inventory\Models\Kit;
use App\Domain\Inventory\Models\Product;
use App\Domain\Inventory\Services\KitService;
use App\Domain\Shared\Exceptions\DomainException;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Layout('layouts.app')]
#[Title('الأطقم')]
class KitForm extends Component
{
public ?Kit $kit = null;
public bool $editing = false;
public string $name_ar = '';
public string $name = '';
public string $sku = '';
public string $selling_price = '';
public string $description_ar = '';
public string $assembly_instructions = '';
public bool $is_active = true;
/** @var array<int, array{product_id: string, quantity: string}> */
public array $components = [];
public function mount(?Kit $kit = null): void
{
$this->authorize('inventory.manage');
if ($kit && $kit->exists) {
$this->kit = $kit;
$this->editing = true;
$this->name_ar = $kit->name_ar ?? '';
$this->name = $kit->name ?? '';
$this->sku = $kit->sku ?? '';
$this->selling_price = $kit->selling_price ? (string) ($kit->selling_price / 100) : '';
$this->description_ar = $kit->description_ar ?? '';
$this->assembly_instructions = $kit->assembly_instructions ?? '';
$this->is_active = $kit->is_active;
$kit->load('components');
foreach ($kit->components as $component) {
$this->components[] = [
'product_id' => (string) $component->product_id,
'quantity' => (string) $component->quantity,
];
}
}
// Always have at least one empty row
if (empty($this->components)) {
$this->components[] = ['product_id' => '', 'quantity' => '1'];
}
}
public function addComponent(): void
{
$this->components[] = ['product_id' => '', 'quantity' => '1'];
}
public function removeComponent(int $index): void
{
unset($this->components[$index]);
$this->components = array_values($this->components);
// Keep at least one row
if (empty($this->components)) {
$this->components[] = ['product_id' => '', 'quantity' => '1'];
}
}
public function rules(): array
{
$uniqueSku = $this->editing
? 'unique:kits,sku,' . $this->kit->id
: 'unique:kits,sku';
return [
'name_ar' => 'required|string|max:255',
'name' => 'nullable|string|max:255',
'sku' => ['required', 'string', 'max:50', $uniqueSku],
'selling_price' => 'required|numeric|min:0',
'description_ar' => 'nullable|string|max:1000',
'assembly_instructions' => 'nullable|string|max:2000',
'is_active' => 'boolean',
'components' => 'required|array|min:1',
'components.*.product_id' => 'required|exists:products,id',
'components.*.quantity' => 'required|integer|min:1',
];
}
public function messages(): array
{
return [
'name_ar.required' => 'اسم الطقم بالعربية مطلوب',
'name_ar.max' => 'اسم الطقم يجب ألا يتجاوز 255 حرف',
'sku.required' => 'رمز الطقم (SKU) مطلوب',
'sku.unique' => 'رمز الطقم مستخدم بالفعل',
'sku.max' => 'رمز الطقم يجب ألا يتجاوز 50 حرف',
'selling_price.required' => 'سعر البيع مطلوب',
'selling_price.numeric' => 'سعر البيع يجب أن يكون رقمًا',
'selling_price.min' => 'سعر البيع يجب ألا يكون سالبًا',
'components.required' => 'يجب إضافة مكون واحد على الأقل',
'components.min' => 'يجب إضافة مكون واحد على الأقل',
'components.*.product_id.required' => 'يجب اختيار المنتج',
'components.*.product_id.exists' => 'المنتج غير موجود',
'components.*.quantity.required' => 'الكمية مطلوبة',
'components.*.quantity.integer' => 'الكمية يجب أن تكون عددًا صحيحًا',
'components.*.quantity.min' => 'الكمية يجب أن تكون 1 على الأقل',
];
}
public function save(KitService $kitService): void
{
$this->validate();
try {
$data = [
'academy_id' => app('current_academy')->id,
'name_ar' => $this->name_ar,
'name' => $this->name ?: null,
'sku' => $this->sku,
'selling_price' => (int) round((float) $this->selling_price * 100),
'description_ar' => $this->description_ar ?: null,
'assembly_instructions' => $this->assembly_instructions ?: null,
'is_active' => $this->is_active,
];
$componentData = collect($this->components)
->filter(fn ($c) => !empty($c['product_id']))
->map(fn ($c, $i) => [
'product_id' => (int) $c['product_id'],
'quantity' => (int) $c['quantity'],
'sort_order' => $i,
])
->values()
->all();
if ($this->editing) {
$kitService->update($this->kit, $data, $componentData);
session()->flash('success', __('تم تحديث الطقم بنجاح'));
} else {
$kitService->create($data, $componentData, auth()->user());
session()->flash('success', __('تم إنشاء الطقم بنجاح'));
}
$this->redirect(route('inventory.kits'), navigate: true);
} catch (DomainException $e) {
session()->flash('error', $e->getMessage());
}
}
public function render()
{
return view('livewire.inventory.kit-form', [
'products' => Product::where('is_active', true)
->orderBy('name_ar')
->get(['id', 'name_ar', 'name', 'sku']),
]);
}
}
<?php
namespace App\Livewire\Inventory;
use App\Domain\Inventory\Models\Kit;
use App\Domain\Inventory\Models\Warehouse;
use App\Domain\Inventory\Services\KitService;
use App\Domain\Shared\Exceptions\DomainException;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithPagination;
#[Layout('layouts.app')]
#[Title('الأطقم')]
class KitList extends Component
{
use WithPagination;
#[Url]
public string $search = '';
#[Url]
public string $activeFilter = '';
public bool $showAssembleModal = false;
public bool $showDisassembleModal = false;
public ?int $selectedKitId = null;
public ?int $selectedWarehouseId = null;
public int $assemblyQuantity = 1;
public function mount(): void
{
$this->authorize('inventory.manage');
}
public function updatedSearch(): void
{
$this->resetPage();
}
public function updatedActiveFilter(): void
{
$this->resetPage();
}
public function toggleActive(int $kitId): void
{
$kit = Kit::findOrFail($kitId);
$kit->update(['is_active' => !$kit->is_active]);
}
public function openAssembleModal(int $kitId): void
{
$this->selectedKitId = $kitId;
$this->assemblyQuantity = 1;
$this->selectedWarehouseId = null;
$this->showAssembleModal = true;
}
public function openDisassembleModal(int $kitId): void
{
$this->selectedKitId = $kitId;
$this->assemblyQuantity = 1;
$this->selectedWarehouseId = null;
$this->showDisassembleModal = true;
}
public function assemble(KitService $kitService): void
{
$this->validate([
'selectedWarehouseId' => 'required|exists:warehouses,id',
'assemblyQuantity' => 'required|integer|min:1',
], [
'selectedWarehouseId.required' => 'يجب اختيار المستودع',
'assemblyQuantity.required' => 'الكمية مطلوبة',
'assemblyQuantity.min' => 'الكمية يجب أن تكون 1 على الأقل',
]);
try {
$kit = Kit::findOrFail($this->selectedKitId);
$warehouse = Warehouse::findOrFail($this->selectedWarehouseId);
$kitService->assemble($kit, $warehouse, $this->assemblyQuantity, auth()->user());
$this->showAssembleModal = false;
session()->flash('success', __('تم تجميع الطقم بنجاح'));
} catch (DomainException $e) {
session()->flash('error', $e->getMessage());
$this->showAssembleModal = false;
}
}
public function disassemble(KitService $kitService): void
{
$this->validate([
'selectedWarehouseId' => 'required|exists:warehouses,id',
'assemblyQuantity' => 'required|integer|min:1',
], [
'selectedWarehouseId.required' => 'يجب اختيار المستودع',
'assemblyQuantity.required' => 'الكمية مطلوبة',
'assemblyQuantity.min' => 'الكمية يجب أن تكون 1 على الأقل',
]);
try {
$kit = Kit::findOrFail($this->selectedKitId);
$warehouse = Warehouse::findOrFail($this->selectedWarehouseId);
$kitService->disassemble($kit, $warehouse, $this->assemblyQuantity, auth()->user());
$this->showDisassembleModal = false;
session()->flash('success', __('تم تفكيك الطقم بنجاح'));
} catch (DomainException $e) {
session()->flash('error', $e->getMessage());
$this->showDisassembleModal = false;
}
}
public function render()
{
$query = Kit::query()
->withCount('components')
->when($this->search, function ($q) {
$search = $this->search;
$q->where(function ($q2) use ($search) {
$q2->where('name_ar', 'ilike', "%{$search}%")
->orWhere('name', 'ilike', "%{$search}%")
->orWhere('sku', 'ilike', "%{$search}%");
});
})
->when($this->activeFilter !== '', function ($q) {
if ($this->activeFilter === '1') {
$q->where('is_active', true);
} elseif ($this->activeFilter === '0') {
$q->where('is_active', false);
}
})
->orderByDesc('created_at');
return view('livewire.inventory.kit-list', [
'kits' => $query->paginate(20),
'warehouses' => Warehouse::orderBy('name_ar')->get(['id', 'name_ar']),
]);
}
}
<?php
namespace App\Livewire\Inventory;
use App\Domain\Inventory\Enums\PurchaseOrderStatus;
use App\Domain\Inventory\Models\PurchaseOrder;
use App\Domain\Inventory\Services\PurchaseOrderService;
use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Shared\Exceptions\InvalidStatusTransitionException;
use App\Domain\Shared\Traits\UsesBranchScope;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithPagination;
#[Layout('layouts.app')]
#[Title('أوامر الشراء')]
class PurchaseOrderList extends Component
{
use WithPagination, UsesBranchScope;
#[Url]
public string $search = '';
#[Url]
public string $status = '';
public function mount(): void
{
$this->authorize('inventory.list');
}
public function updatedSearch(): void
{
$this->resetPage();
}
public function updatedStatus(): void
{
$this->resetPage();
}
public function receive(int $purchaseOrderId): void
{
try {
$po = PurchaseOrder::with('items')->findOrFail($purchaseOrderId);
// Auto-receive all remaining quantities
$receivedItems = $po->items
->filter(fn ($item) => $item->quantity_received < $item->quantity_ordered)
->map(fn ($item) => [
'item_id' => $item->id,
'quantity_received' => $item->quantity_ordered - $item->quantity_received,
])
->values()
->toArray();
if (empty($receivedItems)) {
session()->flash('error', __('جميع الأصناف مستلمة بالفعل'));
return;
}
app(PurchaseOrderService::class)->receive($po, $receivedItems, auth()->user());
session()->flash('success', __('تم استلام أمر الشراء بنجاح'));
} catch (DomainException $e) {
session()->flash('error', $e->getMessage());
}
}
public function cancel(int $purchaseOrderId): void
{
try {
$po = PurchaseOrder::findOrFail($purchaseOrderId);
app(PurchaseOrderService::class)->cancel($po, auth()->user());
session()->flash('success', __('تم إلغاء أمر الشراء'));
} catch (InvalidStatusTransitionException|DomainException $e) {
session()->flash('error', $e->getMessage());
}
}
public function render()
{
$branchId = $this->getActiveBranchId();
$query = PurchaseOrder::query()
->withCount('items')
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->when($this->search, function ($q) {
$search = $this->search;
$q->where(function ($q2) use ($search) {
$q2->where('order_number', 'ilike', "%{$search}%")
->orWhere('supplier_name', 'ilike', "%{$search}%");
});
})
->when($this->status, fn ($q) => $q->where('status', $this->status))
->orderByDesc('created_at');
return view('livewire.inventory.purchase-order-list', [
'purchaseOrders' => $query->paginate(20),
'statuses' => PurchaseOrderStatus::cases(),
]);
}
}
<?php
namespace App\Livewire\Inventory;
use App\Domain\Inventory\Models\InventoryLevel;
use App\Domain\Inventory\Models\Product;
use App\Domain\Inventory\Models\StockCount;
use App\Domain\Inventory\Models\Warehouse;
use App\Domain\Inventory\Services\StockCountService;
use App\Domain\Shared\Exceptions\DomainException;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Layout('layouts.app')]
#[Title('جرد مخزني جديد')]
class StockCountForm extends Component
{
public ?int $warehouse_id = null;
public ?int $stockCountId = null;
public string $notes = '';
/** @var array<int, array{product_id: int, name_ar: string, sku: string, system_quantity: int, counted_quantity: ?int}> */
public array $items = [];
public function mount(?StockCount $stockCount = null): void
{
$this->authorize('inventory.manage');
if ($stockCount && $stockCount->exists) {
$this->stockCountId = $stockCount->id;
$this->warehouse_id = $stockCount->warehouse_id;
$this->notes = $stockCount->notes ?? '';
$this->loadExistingItems($stockCount);
}
}
public function rules(): array
{
return [
'warehouse_id' => 'required|exists:warehouses,id',
'items' => 'required|array|min:1',
'items.*.counted_quantity' => 'nullable|integer|min:0',
'notes' => 'nullable|string|max:1000',
];
}
public function messages(): array
{
return [
'warehouse_id.required' => 'يجب اختيار المستودع',
'warehouse_id.exists' => 'المستودع غير موجود',
'items.required' => 'يجب وجود منتجات للجرد',
'items.min' => 'يجب وجود منتج واحد على الأقل',
'items.*.counted_quantity.integer' => 'الكمية يجب أن تكون عددًا صحيحًا',
'items.*.counted_quantity.min' => 'الكمية لا يمكن أن تكون سالبة',
'notes.max' => 'الملاحظات يجب ألا تتجاوز 1000 حرف',
];
}
public function updatedWarehouseId(): void
{
if ($this->stockCountId) {
return; // Don't reload if editing existing count
}
$this->items = [];
if (!$this->warehouse_id) {
return;
}
$this->loadWarehouseProducts();
}
private function loadWarehouseProducts(): void
{
$levels = InventoryLevel::where('warehouse_id', $this->warehouse_id)
->with('product:id,name_ar,sku,cost_price')
->get();
$this->items = $levels->map(fn ($level) => [
'product_id' => $level->product_id,
'name_ar' => $level->product->name_ar ?? '',
'sku' => $level->product->sku ?? '',
'cost_price' => $level->product->cost_price ?? 0,
'system_quantity' => $level->quantity_on_hand,
'counted_quantity' => null,
])->toArray();
}
private function loadExistingItems(StockCount $stockCount): void
{
$stockCount->load('items.product:id,name_ar,sku,cost_price');
$this->items = $stockCount->items->map(fn ($item) => [
'item_id' => $item->id,
'product_id' => $item->product_id,
'name_ar' => $item->product->name_ar ?? '',
'sku' => $item->product->sku ?? '',
'cost_price' => $item->product->cost_price ?? 0,
'system_quantity' => $item->system_quantity,
'counted_quantity' => $item->counted_quantity,
])->toArray();
}
public function save(): void
{
$this->validate();
try {
$service = app(StockCountService::class);
$user = auth()->user();
if ($this->stockCountId) {
// Continue existing count — record counts for items
$stockCount = StockCount::findOrFail($this->stockCountId);
foreach ($this->items as $item) {
if ($item['counted_quantity'] === null || $item['counted_quantity'] === '') {
continue;
}
$countItem = $stockCount->items()
->where('id', $item['item_id'] ?? 0)
->orWhere(function ($q) use ($item, $stockCount) {
$q->where('stock_count_id', $stockCount->id)
->where('product_id', $item['product_id']);
})
->first();
if ($countItem) {
$service->recordCount($countItem, (int) $item['counted_quantity'], $user);
}
}
if ($this->notes) {
$stockCount->update(['notes' => $this->notes]);
}
session()->flash('success', __('تم تحديث بيانات الجرد بنجاح'));
$this->redirect(route('inventory.stock-counts'), navigate: true);
} else {
// Create new stock count
$productIds = collect($this->items)->pluck('product_id')->toArray();
$stockCount = $service->create($this->warehouse_id, $productIds, $user);
if ($this->notes) {
$stockCount->update(['notes' => $this->notes]);
}
// Record any counts already entered
foreach ($this->items as $index => $item) {
if ($item['counted_quantity'] === null || $item['counted_quantity'] === '') {
continue;
}
$countItem = $stockCount->items()->where('product_id', $item['product_id'])->first();
if ($countItem) {
$service->recordCount($countItem, (int) $item['counted_quantity'], $user);
}
}
session()->flash('success', __('تم إنشاء جلسة الجرد بنجاح'));
$this->redirect(route('inventory.stock-counts'), navigate: true);
}
} catch (DomainException $e) {
session()->flash('error', $e->getMessage());
} catch (\Exception $e) {
session()->flash('error', __('حدث خطأ أثناء حفظ الجرد'));
}
}
public function render()
{
return view('livewire.inventory.stock-count-form', [
'warehouses' => Warehouse::active()->orderBy('name_ar')->get(['id', 'name_ar', 'code']),
]);
}
}
<?php
namespace App\Livewire\Inventory;
use App\Domain\Inventory\Enums\StockCountStatus;
use App\Domain\Inventory\Models\StockCount;
use App\Domain\Inventory\Services\StockCountService;
use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Shared\Exceptions\InvalidStatusTransitionException;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithPagination;
#[Layout('layouts.app')]
#[Title('الجرد المخزني')]
class StockCountList extends Component
{
use WithPagination;
#[Url]
public string $search = '';
#[Url]
public string $statusFilter = '';
public function mount(): void
{
$this->authorize('inventory.manage');
}
public function updatedSearch(): void
{
$this->resetPage();
}
public function updatedStatusFilter(): void
{
$this->resetPage();
}
public function finalize(int $stockCountId): void
{
try {
$stockCount = StockCount::findOrFail($stockCountId);
$service = app(StockCountService::class);
$service->completeCount($stockCount, auth()->user());
session()->flash('success', __('تم إكمال الجرد بنجاح'));
} catch (DomainException|InvalidStatusTransitionException $e) {
session()->flash('error', $e->getMessage());
} catch (\Exception $e) {
session()->flash('error', __('حدث خطأ أثناء إكمال الجرد'));
}
}
public function cancel(int $stockCountId): void
{
try {
$stockCount = StockCount::findOrFail($stockCountId);
$service = app(StockCountService::class);
$service->cancel($stockCount, auth()->user());
session()->flash('success', __('تم إلغاء الجرد'));
} catch (InvalidStatusTransitionException $e) {
session()->flash('error', $e->getMessage());
} catch (\Exception $e) {
session()->flash('error', __('حدث خطأ أثناء إلغاء الجرد'));
}
}
public function render()
{
$query = StockCount::query()
->with(['warehouse', 'creator'])
->withCount('items')
->when($this->search, function ($q) {
$search = $this->search;
$q->where(function ($q2) use ($search) {
$q2->where('count_number', 'ilike', "%{$search}%")
->orWhereHas('warehouse', fn ($w) => $w->where('name_ar', 'ilike', "%{$search}%"));
});
})
->when($this->statusFilter, fn ($q) => $q->where('status', $this->statusFilter))
->orderByDesc('created_at');
return view('livewire.inventory.stock-count-list', [
'stockCounts' => $query->paginate(20),
'statuses' => StockCountStatus::cases(),
]);
}
}
......@@ -47,6 +47,13 @@
'password' => env('MAIL_PASSWORD'),
'timeout' => null,
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
'stream' => [
'ssl' => [
'verify_peer' => env('MAIL_VERIFY_PEER', true),
'verify_peer_name' => env('MAIL_VERIFY_PEER', true),
'allow_self_signed' => !env('MAIL_VERIFY_PEER', true),
],
],
],
'ses' => [
......
# Gap Analysis: Advertised vs Built
**Date:** 2026-07-08
**Purpose:** Identify what we're promising customers vs what's actually functional in the system.
---
## CRITICAL: Pricing Inconsistency Across Sources
We have **3 different pricing models** published in different places — this MUST be unified before first sale:
| Source | Tiers | Model |
|--------|-------|-------|
| `docs/pricing-strategy.md` | 3 tiers: 399 / 1,199 / 3,499 | Flat only, no commission |
| `docs/30-day-survival-plan.md` | 3 tiers: 699 / 1,499 / 3,499 | Flat + percentage (7%/5%/3%) |
| **Live marketing site** (Next.js) | 4 tiers: Free+12% / 1,000+7.5% / 3,500+5% / 10,000+3.5% | Flat + percentage |
**Trial duration conflict:** pricing-strategy says 10 days, marketing site says 14 days.
**Fabricated claims on live marketing site:**
- "47+ academies" — ZERO real customers
- "12,000+ players" — ZERO real data
- "1.2 million+ EGP managed monthly" — ZERO
- "99.9% uptime" — no SLA exists
- "98% customer satisfaction" — no customers to satisfy
- 3 fake testimonials with names/cities
These fabrications are a legal liability and will destroy credibility if a prospect Googles those people.
---
## Legend
| Symbol | Meaning |
|:------:|---------|
| :white_check_mark: | Fully built — migrations, models, services, UI, tested |
| :construction: | Schema + models exist, services partial, UI exists but may not be browser-tested |
| :warning: | Migrations exist, models exist, but no service or incomplete UI |
| :x: | Not built at all — zero code |
---
## Module-by-Module Status
### 1. Identity & Access (Foundation)
| Feature | Advertised | Status | Notes |
|---------|:----------:|:------:|-------|
| Multi-tenant (academy_id scope) | Yes | :white_check_mark: | BelongsToAcademy trait, global scope |
| User login/auth | Yes | :white_check_mark: | Livewire Login, middleware |
| Roles & permissions | Yes | :white_check_mark: | Full RBAC, 10+ role levels |
| Branch management | Yes | :white_check_mark: | CRUD + branch switcher |
| User management | Yes | :white_check_mark: | UserList + UserForm |
| Login history | Yes | :white_check_mark: | Migration + model exists |
| Academy settings | Yes | :white_check_mark: | SystemSettings, BrandingSettings, AcademySettings |
### 2. People & Participants
| Feature | Advertised | Status | Notes |
|---------|:----------:|:------:|-------|
| People (staff) CRUD | Yes | :white_check_mark: | PersonList + PersonForm + PersonShow |
| Participant registration | Yes | :white_check_mark: | ParticipantForm + List + Show |
| Guardian management | Yes | :white_check_mark: | GuardianDashboard, linked to participants |
| Bulk import | Yes | :white_check_mark: | ParticipantImport component |
| Status transitions (freeze/suspend/etc) | Yes | :white_check_mark: | FreezeParticipant, BulkStatusChange, StatusTimeline |
| Classification (VIP, scholarship, etc) | Yes | :construction: | Migration exists, enum defined |
| Document management | Yes | :white_check_mark: | DocumentUploadWizard, Medical alerts, Approval flow |
### 3. Training Programs & Scheduling
| Feature | Advertised | Status | Notes |
|---------|:----------:|:------:|-------|
| Training programs CRUD | Yes | :white_check_mark: | ProgramList + CreateProgramWizard |
| Training groups | Yes | :white_check_mark: | GroupList + CreateGroupWizard |
| Weekly schedule | Yes | :white_check_mark: | WeeklySchedule component |
| Session generation | Yes | :construction: | Migration exists, service exists |
| Session rescheduling | Yes | :white_check_mark: | RescheduleSession component |
| Holidays | Yes | :warning: | Migration only |
### 4. Enrollments
| Feature | Advertised | Status | Notes |
|---------|:----------:|:------:|-------|
| Enrollment management | Yes | :white_check_mark: | EnrollmentForm + List |
| Waitlist | Yes | :white_check_mark: | WaitlistManager |
| Group transfer | Yes | :white_check_mark: | TransferGroup + TransferParticipantWizard |
| Enrollment history | Yes | :white_check_mark: | EnrollmentHistory component |
| Prerequisites checking | Implied | :warning: | No service visible for prerequisite validation |
### 5. Attendance
| Feature | Advertised | Status | Notes |
|---------|:----------:|:------:|-------|
| Take attendance (trainer) | Yes | :white_check_mark: | TakeAttendance + QuickAttendance |
| Attendance list/reports | Yes | :white_check_mark: | AttendanceList, AttendanceReport |
| Auto-generation on session create | Yes | :construction: | Service exists |
| Auto-absent job (hourly) | Yes | :white_check_mark: | Scheduled in routes/console.php, runs hourly |
| Grace period (late detection) | Yes | :construction: | Logic in service |
| Threshold enforcement (auto-suspend) | Yes | :warning: | Rule defined but enforcement unclear |
| Parent attendance notifications | Yes | :x: | **No WhatsApp integration built** |
### 6. Financial System
| Feature | Advertised | Status | Notes |
|---------|:----------:|:------:|-------|
| Double-entry accounting | Yes | :white_check_mark: | TransactionService, debit/credit pairs |
| Invoice creation | Yes | :white_check_mark: | CreateInvoiceWizard + InvoiceList + InvoiceShow |
| Payment recording | Yes | :white_check_mark: | CollectPaymentWizard |
| Wallet system | Yes | :white_check_mark: | WalletList + WalletShow |
| Payment plans / installments | Yes | :construction: | Migration + model, PaymentPlanCreate component |
| Cash sessions | Yes | :white_check_mark: | CashSessionList + CashSessionManage |
| Financial reports | Yes | :white_check_mark: | FinancialReport + FinancialOverview |
| Coupon validation | Yes | :white_check_mark: | CouponValidator component |
| Refund flow | Yes | :construction: | Logic in service, no dedicated UI |
| **Online payment gateway** | **Yes (advertised: "يدفعوا أونلاين")** | :x: | **ZERO payment gateway integration** |
| Service fees / commission tracking | Yes | :construction: | Migration added |
| Receipt printing | Yes | :construction: | ReceiptSettings + templates exist |
### 7. Pricing Engine
| Feature | Advertised | Status | Notes |
|---------|:----------:|:------:|-------|
| Base prices | Yes | :white_check_mark: | BasePriceList + BasePriceForm |
| Pricing rules (13 types) | Yes | :white_check_mark: | PricingRuleList + CreatePricingRuleWizard |
| Promotions/coupons | Yes | :white_check_mark: | PromotionForm + PromotionList |
| Smart discount engine | Yes | :construction: | PricingService exists, untested end-to-end |
| Stackable/non-stackable logic | Yes | :construction: | Service logic exists |
### 8. POS System
| Feature | Advertised | Status | Notes |
|---------|:----------:|:------:|-------|
| POS terminal | Yes | :white_check_mark: | POSTerminal component |
| POS history | Yes | :white_check_mark: | POSHistory component |
| Split payments | Yes | :construction: | Logic in POSService |
| Product sales | Yes | :construction: | Connected to inventory |
| Kit sales | Yes | :warning: | Kit model exists, POS integration unclear |
### 9. Inventory Management
| Feature | Advertised | Status | Notes |
|---------|:----------:|:------:|-------|
| Products & categories | Yes | :white_check_mark: | ProductList + CreateProductWizard |
| Warehouses | Yes | :white_check_mark: | WarehouseList + WarehouseForm |
| Inventory movements | Yes | :white_check_mark: | MovementList |
| Stock adjustments | Yes | :white_check_mark: | StockAdjustment + StockAdjustmentWizard |
| Purchase orders | Yes | :white_check_mark: | PurchaseOrderList + route |
| Kits (BOM) | Yes | :white_check_mark: | KitList + KitForm + routes |
| Stock counts | Yes | :white_check_mark: | StockCountList + StockCountForm + routes |
| Low stock alerts | Yes | :construction: | Logic in service |
### 10. Facilities & Space
| Feature | Advertised | Status | Notes |
|---------|:----------:|:------:|-------|
| Facility CRUD | Yes | :white_check_mark: | FacilityList + CreateFacilityWizard |
| Space layouts (grid/lanes/zones) | Yes | :white_check_mark: | SpaceLayoutManager |
| Visual schedule builder | Yes | :white_check_mark: | VisualScheduleBuilder |
| Space reservations | Yes | :construction: | Migration + model exists |
| Collision detection | Yes | :construction: | Service logic defined |
| Space assignment wizard | Yes | :white_check_mark: | SpaceAssignmentWizard |
### 11. HR & Payroll
| Feature | Advertised | Status | Notes |
|---------|:----------:|:------:|-------|
| Employee management | Yes | :white_check_mark: | EmployeeList + CreateEmployeeWizard |
| Trainer management | Yes | :white_check_mark: | TrainerList + CreateTrainerWizard |
| Trainer qualifications | Yes | :white_check_mark: | Migration + model |
| Trainer availability | Yes | :construction: | Migration, no dedicated UI |
| Trainer compensations | Yes | :white_check_mark: | TrainerCompensations component |
| Payroll periods | Yes | :white_check_mark: | PayrollDashboard |
| Payslips | Yes | :white_check_mark: | PayslipDetail |
| Trainer advances | Yes | :white_check_mark: | TrainerAdvances |
| **Full HR / Leave management** | **No** | :x: | Not built, not critical |
### 12. Notifications & Communication
| Feature | Advertised | Status | Notes |
|---------|:----------:|:------:|-------|
| Notification templates | Yes | :white_check_mark: | NotificationTemplateList + Form |
| Notification center (in-app) | Yes | :white_check_mark: | NotificationCenter |
| Notification preferences | Yes | :white_check_mark: | NotificationPreferences |
| Notification log | Yes | :white_check_mark: | NotificationLogList |
| Bulk messaging | Yes | :white_check_mark: | BulkMessage component |
| **WhatsApp Business API** | **Yes (entire cost model built on it)** | :x: | **ZERO integration — no channel, no templates sent** |
| **SMS gateway** | **Yes (SMS pack in pricing)** | :x: | **No SMS provider integrated** |
| **Email notifications** | **Yes** | :x: | **No email sending configured (poste.io exists but no Laravel mail integration verified)** |
| Push notifications | Implied | :x: | No PWA/push setup |
### 13. Parent Portal
| Feature | Advertised | Status | Notes |
|---------|:----------:|:------:|-------|
| Parent dashboard | Yes | :white_check_mark: | ParentHome |
| View child attendance | Yes | :white_check_mark: | ParentAttendance |
| View finances/invoices | Yes | :white_check_mark: | ParentFinances + ParentInvoiceDetail |
| View schedule | Yes | :white_check_mark: | ParentSchedule |
| View programs | Yes | :white_check_mark: | ParentPrograms |
| Submit excuses | Yes | :white_check_mark: | ParentExcuseForm |
| View evaluations | Yes | :white_check_mark: | ParentEvaluationDetail |
| Profile management | Yes | :white_check_mark: | ParentProfile |
| Notifications | Yes | :white_check_mark: | ParentNotifications |
| **Pay online** | **Yes ("يدفعوا أونلاين")** | :x: | **No payment gateway in parent portal** |
| **Mobile app** | **Yes ("تطبيق أولياء")** | :x: | **No mobile app exists** |
### 14. Academy Website (Public)
| Feature | Advertised | Status | Notes |
|---------|:----------:|:------:|-------|
| Auto-generated website | Yes | :white_check_mark: | Just built — full CMS + 16 sections |
| Bold Athletic template | Yes | :white_check_mark: | website.css + all section views |
| CMS editor | Yes | :white_check_mark: | SectionManager + ThemeEditor |
| Gallery | Yes | :white_check_mark: | GalleryManager |
| Contact form | Yes | :white_check_mark: | ContactFormController + ContactSubmissionList |
| SEO (JSON-LD) | Yes | :white_check_mark: | In layout.blade.php |
| Custom domain (youracademy.com) | Yes (annual plan perk) | :white_check_mark: | Manual via CapRover custom domain per customer |
| Subdomain routing (name.elcaptain.com) | Yes | :white_check_mark: | Manual via CapRover app creation per customer |
### 15. Reports & Analytics
| Feature | Advertised | Status | Notes |
|---------|:----------:|:------:|-------|
| Attendance reports | Yes | :white_check_mark: | AttendanceReport |
| Financial reports | Yes | :white_check_mark: | FinancialReport |
| Reports page | Yes | :white_check_mark: | ReportsPage |
| Dashboard widgets | Yes | :white_check_mark: | RevenueWidget, EnrollmentTrends |
| **Advanced reports + export** | **Yes (Business tier feature)** | :white_check_mark: | Reports exist + ExportController handles CSV (participants, payments, invoices, enrollments) |
### 16. Evaluations
| Feature | Advertised | Status | Notes |
|---------|:----------:|:------:|-------|
| Evaluation criteria | Yes | :white_check_mark: | EvaluationCriteriaList |
| Evaluation forms | Yes | :white_check_mark: | EvaluationForm + List + Show |
| Parent view evaluations | Yes | :white_check_mark: | ParentEvaluationDetail |
### 17. Assignments
| Feature | Advertised | Status | Notes |
|---------|:----------:|:------:|-------|
| Trainer-to-group assignments | Yes | :white_check_mark: | AssignmentList + AssignmentForm |
| Assignment scopes (full/partial/etc) | Yes | :construction: | Enum exists |
### 18. Audit & Compliance
| Feature | Advertised | Status | Notes |
|---------|:----------:|:------:|-------|
| Audit log | Yes | :white_check_mark: | AuditLogList + ActivityLog |
| Immutable records | Yes | :construction: | Migration design enforces it |
---
## Critical Gaps (BLOCKERS for Sales)
These are features we're actively selling that DO NOT EXIST:
| # | Gap | Impact | Effort | Priority |
|:-:|-----|--------|--------|:--------:|
| 1 | **WhatsApp Business API integration** | Entire cost model + parent notifications depend on this. We promise "إشعارات أولياء أمور" in Basic tier. | 3-5 days | **P0** |
| 2 | **Online payment gateway** | We say "يدفعوا أونلاين" in pitch. Parents can view invoices but can't pay. | 3-5 days | **P0** |
| 3 | **Mobile app (parent)** | Business tier advertises "تطبيق أولياء". We have a web portal but no native app. | 2-4 weeks | **P2** |
| 4 | **SMS channel** | Listed as Business tier feature "SMS + email". Zero provider. | 1-2 days (if WhatsApp-first, SMS becomes fallback) | **P2** |
| 5 | ~~**Email sending**~~ | ~~No verified mail config~~ **DONE** — config/mail.php wired to poste.io with self-signed cert bypass, NotificationService uses Mail::raw() | ~~0.5 day~~ | ~~**P1**~~ |
| 6 | **Subscription/billing engine** | No way to charge customers monthly. No Stripe/Paymob/etc. No tier enforcement (player limits, user limits). | 3-5 days | **P0** |
**NOT gaps (handled manually or already built):**
- ~~Subdomain/custom domain routing~~ — Done manually via CapRover per customer
- ~~Trial system (10-day + lockout)~~ — Managed manually via CapRover manager app
- ~~Auto-absent scheduled job~~ — Already in `routes/console.php`, runs hourly
- ~~Data export~~ — ExportController already exports participants, payments, invoices, enrollments as CSV
---
## Revenue-Critical Path (What blocks first sale)
To do a **live demo** and close a customer, we need AT MINIMUM:
```
1. Working login + dashboard ✅ DONE
2. Add players/participants ✅ DONE
3. Create programs + groups ✅ DONE
4. Take attendance ✅ DONE
5. Create invoice + collect payment (cash) ✅ DONE
6. Show parent portal ✅ DONE
7. Show academy website ✅ DONE (just built)
8. "yourname.elcaptain.com" live ✅ Manual via CapRover
9. WhatsApp notification demo ❌ BLOCKER
10. Online payment demo ❌ BLOCKER
```
**Verdict:** We can demo 80% of the system TODAY. The 2 blockers (#9, #10) are the features that differentiate us from "Excel + WhatsApp group" — they're the reason someone pays.
---
## What We Can Honestly Sell TODAY
Without fixing any gaps, our current honest value proposition is:
> A full sports academy management system with: participant tracking, training programs, group scheduling, attendance with trainer app, full financial system (invoices, payments, wallets, cash sessions), POS, inventory, facility management, pricing engine, HR/payroll, evaluations, parent web portal, role-based access, and a customizable public website.
**What we CANNOT honestly promise today:**
- WhatsApp notifications to parents
- Online payment by parents
- Mobile app
- SMS notifications
- Subscription tier enforcement
---
## Recommended Fix Order (to "sellable")
| Day | Task | Unlocks |
|:---:|------|---------|
| ~~1~~ | ~~Wire Laravel Mail to poste.io + basic email notifications~~ **DONE** | ~~"Email notifications" claim becomes real~~ ✅ |
| 2-3 | Integrate WhatsApp Cloud API (Meta) — send template messages | Parent notifications, the core differentiator |
| 4-5 | Integrate Paymob (Egypt) payment gateway — parent pays invoice online | "يدفعوا أونلاين" becomes real |
**Handled manually (not code tasks):**
- Subdomain routing → Create CapRover app per customer
- Trial/lockout → Managed via manager app + manual onboarding
- Custom domains → DNS pointing + CapRover custom domain feature
After this sprint: **every claim in the sales pitch is real.**
---
## Marketing Site Gaps (Next.js at `/marketing-site/`)
The live marketing site makes additional promises not covered in the module breakdown above:
| Claim | Reality |
|-------|---------|
| "Trainer app" (Professional tier) | No mobile app. TrainerDashboard is web-only. |
| "Parent app" (Professional tier) | No mobile app. Parent portal is web-only. |
| "API for external integrations" | One QuickStats endpoint exists. No documented REST API. |
| "Custom modifications" (Professional) | No mechanism for per-customer customization |
| "Private hosting" (Professional) | No multi-instance deployment. Single shared DB. |
| "Smart alerts via app, email, and messages" | In-app exists. Email/SMS/WhatsApp: ZERO channels wired. |
| "One-tap attendance recording" | Exists (QuickAttendance) |
| "Instant absence alerts" | Alert component exists, notification sending doesn't |
| "Quick registration wizard" | Exists (NewRegistrationWizard) |
| "Free 14-day trial. No credit card." | Managed manually — deploy instance via CapRover, lock after trial period |
| "Percentage Only" tier (Free + 12%) | No billing system to collect commission. |
---
## Immediate Action Items (Before Next Demo)
### STOP doing:
- [ ] Remove fake stats from marketing site (or replace with "beta" language)
- [ ] Remove fake testimonials (legal risk)
- [ ] Pick ONE pricing model and kill the others
### START doing:
- [ ] Wire email (poste.io → Laravel SMTP, 0.5 day)
- [ ] WhatsApp Cloud API basic integration (2-3 days)
- [ ] Paymob payment gateway (2-3 days)
- [ ] Rewrite marketing site hero with honest "launching" language
### KEEP doing:
- [ ] Demo the 18 working features listed below with confidence
- [ ] Show the parent portal (real differentiator)
- [ ] Show the academy website builder (just shipped)
---
## Features Safe to Advertise (No Gap)
These work end-to-end and can be demoed confidently:
1. Full participant lifecycle (register → active → graduate)
2. Training programs, groups, weekly schedules
3. Attendance taking (trainer view + parent view)
4. Invoice creation + cash/card payment recording
5. Wallet system (top-up, deduct, freeze)
6. POS terminal for product/service sales
7. Smart pricing engine (13 rule types + stackable discounts)
8. Facility management with visual layout builder
9. HR: employees, trainers, payroll, advances
10. Parent portal (view attendance, finances, schedule, evaluations)
11. Public website with CMS editor
12. Role-based permissions (10+ roles)
13. Multi-branch support
14. Cash session management
15. Document management with approval workflow
16. Evaluations system
17. Audit logging
18. Dashboard with revenue + enrollment widgets
<div>
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-4 sm:mb-6">
<h1 class="text-xl sm:text-2xl font-bold text-gray-800">{{ $editing ? __('تعديل الطقم') : __('إضافة طقم جديد') }}</h1>
<a href="{{ route('inventory.kits') }}" wire:navigate class="text-sm text-gray-500 hover:text-gray-700">{{ __('← العودة') }}</a>
</div>
{{-- Flash Messages --}}
@if(session('error'))
<div class="mb-4 p-4 bg-red-50 border border-red-200 text-red-700 rounded-lg text-sm">
{{ session('error') }}
</div>
@endif
<form wire:submit="save" class="space-y-6">
{{-- Basic Info --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 sm:p-6">
<h2 class="text-base sm:text-lg font-semibold text-gray-700 mb-4">{{ __('البيانات الأساسية') }}</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4">
{{-- Name AR --}}
<div>
<label for="name_ar" class="block text-sm font-medium text-gray-700 mb-1">{{ __('اسم الطقم بالعربية') }} *</label>
<input type="text" id="name_ar" wire:model="name_ar"
class="w-full text-sm px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 @error('name_ar') border-red-500 @enderror">
@error('name_ar') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div>
{{-- Name EN --}}
<div>
<label for="name" class="block text-sm font-medium text-gray-700 mb-1">{{ __('اسم الطقم بالإنجليزية') }}</label>
<input type="text" id="name" wire:model="name" dir="ltr"
class="w-full text-sm px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 @error('name') border-red-500 @enderror">
@error('name') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div>
{{-- SKU --}}
<div>
<label for="sku" class="block text-sm font-medium text-gray-700 mb-1">{{ __('رمز الطقم (SKU)') }} *</label>
<input type="text" id="sku" wire:model="sku" dir="ltr"
class="w-full text-sm px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 @error('sku') border-red-500 @enderror">
@error('sku') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div>
{{-- Selling Price --}}
<div>
<label for="selling_price" class="block text-sm font-medium text-gray-700 mb-1">{{ __('سعر البيع (ج.م)') }} *</label>
<input type="number" id="selling_price" wire:model="selling_price" step="0.01" min="0" dir="ltr"
class="w-full text-sm px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 @error('selling_price') border-red-500 @enderror">
@error('selling_price') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div>
</div>
</div>
{{-- Components Section --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 sm:p-6" x-data>
<div class="flex items-center justify-between mb-4">
<h2 class="text-base sm:text-lg font-semibold text-gray-700">{{ __('المكونات') }}</h2>
<button type="button" wire:click="addComponent"
class="inline-flex items-center gap-1.5 px-3 py-1.5 bg-green-50 text-green-700 border border-green-200 rounded-lg hover:bg-green-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="M12 4v16m8-8H4"/></svg>
{{ __('إضافة مكون') }}
</button>
</div>
@error('components') <p class="mb-3 text-sm text-red-600">{{ $message }}</p> @enderror
<div class="space-y-3">
@foreach($components as $index => $component)
<div class="flex items-start gap-3 p-3 bg-gray-50 rounded-lg border border-gray-200" wire:key="component-{{ $index }}">
{{-- Product selector --}}
<div class="flex-1">
<label class="block text-xs font-medium text-gray-600 mb-1">{{ __('المنتج') }} *</label>
<select wire:model="components.{{ $index }}.product_id"
class="w-full text-sm px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 @error('components.'.$index.'.product_id') border-red-500 @enderror">
<option value="">{{ __('-- اختر المنتج --') }}</option>
@foreach($products as $product)
<option value="{{ $product->id }}">{{ $product->name_ar }} ({{ $product->sku }})</option>
@endforeach
</select>
@error('components.'.$index.'.product_id') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror
</div>
{{-- Quantity --}}
<div class="w-24 sm:w-28">
<label class="block text-xs font-medium text-gray-600 mb-1">{{ __('الكمية') }} *</label>
<input type="number" wire:model="components.{{ $index }}.quantity" min="1" dir="ltr"
class="w-full text-sm px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 @error('components.'.$index.'.quantity') border-red-500 @enderror">
@error('components.'.$index.'.quantity') <p class="mt-1 text-xs text-red-600">{{ $message }}</p> @enderror
</div>
{{-- Remove button --}}
<div class="pt-5">
<button type="button" wire:click="removeComponent({{ $index }})"
class="p-2 text-red-500 hover:text-red-700 hover:bg-red-50 rounded-lg transition-colors"
title="{{ __('حذف المكون') }}">
<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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
</svg>
</button>
</div>
</div>
@endforeach
</div>
@if(count($components) === 0)
<div class="text-center py-8 text-gray-400 text-sm">
{{ __('لم يتم إضافة مكونات بعد') }}
</div>
@endif
</div>
{{-- Description & Instructions --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 sm:p-6">
<h2 class="text-base sm:text-lg font-semibold text-gray-700 mb-4">{{ __('معلومات إضافية') }}</h2>
<div class="space-y-4">
{{-- Description --}}
<div>
<label for="description_ar" class="block text-sm font-medium text-gray-700 mb-1">{{ __('الوصف') }}</label>
<textarea id="description_ar" wire:model="description_ar" rows="3"
class="w-full text-sm px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 @error('description_ar') border-red-500 @enderror"></textarea>
@error('description_ar') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div>
{{-- Assembly Instructions --}}
<div>
<label for="assembly_instructions" class="block text-sm font-medium text-gray-700 mb-1">{{ __('تعليمات التجميع') }}</label>
<textarea id="assembly_instructions" wire:model="assembly_instructions" rows="3"
class="w-full text-sm px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 @error('assembly_instructions') border-red-500 @enderror"></textarea>
@error('assembly_instructions') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div>
{{-- Is Active --}}
<div>
<label class="min-h-[44px] flex items-center gap-2 cursor-pointer">
<input type="checkbox" wire:model="is_active"
class="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500">
<span class="text-sm text-gray-700">{{ __('نشط') }}</span>
</label>
</div>
</div>
</div>
{{-- Actions --}}
<div class="flex flex-col-reverse sm:flex-row items-stretch sm:items-center justify-end gap-3">
<a href="{{ route('inventory.kits') }}" wire:navigate
class="text-center px-4 sm:px-6 py-2.5 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors">{{ __('إلغاء') }}</a>
<button type="submit" wire:loading.attr="disabled" wire:target="save"
class="text-center px-4 sm:px-6 py-2.5 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 disabled:bg-blue-400 transition-colors">
<span wire:loading.remove wire:target="save">{{ $editing ? __('حفظ التعديلات') : __('إنشاء الطقم') }}</span>
<span wire:loading wire:target="save">{{ __('جارٍ الحفظ...') }}</span>
</button>
</div>
</form>
</div>
<div>
<div class="flex items-center justify-between mb-6">
<h1 class="text-xl sm:text-2xl font-bold text-gray-800">{{ __('الأطقم (BOM)') }}</h1>
@can('inventory.manage')
<a href="{{ route('inventory.kits.create') }}" 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>
{{-- Flash Messages --}}
@if(session('success'))
<div class="mb-4 p-4 bg-green-50 border border-green-200 rounded-lg text-green-700 text-sm">
{{ session('success') }}
</div>
@endif
@if(session('error'))
<div class="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
{{ session('error') }}
</div>
@endif
{{-- Filters --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 mb-4">
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div class="sm:col-span-2">
<input type="text" wire:model.live.debounce.300ms="search"
placeholder="{{ __('بحث بالاسم أو SKU...') }}"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
</div>
<select wire:model.live="activeFilter" class="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<option value="">{{ __('الكل') }}</option>
<option value="1">{{ __('نشط') }}</option>
<option value="0">{{ __('غير نشط') }}</option>
</select>
</div>
</div>
{{-- Table --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<div wire:loading.class="opacity-50 pointer-events-none">
<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 font-medium text-gray-600">{{ __('SKU') }}</th>
<th class="px-4 py-3 text-start font-medium text-gray-600">{{ __('اسم الطقم') }}</th>
<th class="px-4 py-3 text-center font-medium text-gray-600">{{ __('عدد المكونات') }}</th>
<th class="px-4 py-3 text-center font-medium text-gray-600">{{ __('سعر البيع') }}</th>
<th class="px-4 py-3 text-center font-medium text-gray-600">{{ __('الحالة') }}</th>
<th class="px-4 py-3 text-center font-medium text-gray-600">{{ __('إجراءات') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
@forelse($kits as $kit)
<tr class="hover:bg-gray-50">
<td class="px-4 py-3 font-mono text-gray-600 text-xs" dir="ltr">
{{ $kit->sku }}
</td>
<td class="px-4 py-3">
<span class="font-medium text-gray-800">{{ $kit->name_ar }}</span>
@if($kit->name)
<p class="text-xs text-gray-500" dir="ltr">{{ $kit->name }}</p>
@endif
</td>
<td class="px-4 py-3 text-center">
<span class="px-2 py-0.5 text-xs bg-blue-100 text-blue-700 rounded-full">
{{ $kit->components_count }} {{ __('مكون') }}
</span>
</td>
<td class="px-4 py-3 text-center font-mono" dir="ltr">
{{ number_format($kit->selling_price / 100, 2) }} {{ __('ج.م') }}
</td>
<td class="px-4 py-3 text-center">
<button wire:click="toggleActive({{ $kit->id }})" wire:loading.attr="disabled"
class="px-2 py-0.5 text-xs rounded-full {{ $kit->is_active ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-600' }}">
{{ $kit->is_active ? __('نشط') : __('غير نشط') }}
</button>
</td>
<td class="px-4 py-3 text-center">
<div class="flex items-center justify-center gap-2">
@can('inventory.manage')
<a href="{{ route('inventory.kits.edit', $kit) }}" wire:navigate
class="text-green-600 hover:text-green-800 text-sm">{{ __('تعديل') }}</a>
<button wire:click="openAssembleModal({{ $kit->id }})"
class="text-blue-600 hover:text-blue-800 text-sm">{{ __('تجميع') }}</button>
<button wire:click="openDisassembleModal({{ $kit->id }})"
class="text-amber-600 hover:text-amber-800 text-sm">{{ __('تفكيك') }}</button>
@endcan
</div>
</td>
</tr>
@empty
<tr>
<td colspan="6" class="px-4 py-12 text-center">
<div class="flex flex-col items-center">
<svg class="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="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
</svg>
<p class="text-gray-500 text-sm">{{ __('لا توجد أطقم') }}</p>
@can('inventory.manage')
<a href="{{ route('inventory.kits.create') }}" wire:navigate
class="mt-2 text-blue-600 hover:text-blue-800 text-sm font-medium">
{{ __('إضافة أول طقم') }}
</a>
@endcan
</div>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@if($kits->hasPages())
<div class="px-4 py-3 border-t border-gray-200">
{{ $kits->links() }}
</div>
@endif
</div>
{{-- Assemble Modal --}}
@if($showAssembleModal)
<div class="fixed inset-0 z-50 overflow-y-auto" x-data x-init="$el.focus()">
<div class="flex items-center justify-center min-h-screen px-4">
<div class="fixed inset-0 bg-black/50" wire:click="$set('showAssembleModal', false)"></div>
<div class="relative bg-white rounded-xl shadow-xl w-full max-w-md p-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4">{{ __('تجميع طقم') }}</h3>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('المستودع') }} *</label>
<select wire:model="selectedWarehouseId"
class="w-full text-sm px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<option value="">{{ __('-- اختر المستودع --') }}</option>
@foreach($warehouses as $warehouse)
<option value="{{ $warehouse->id }}">{{ $warehouse->name_ar }}</option>
@endforeach
</select>
@error('selectedWarehouseId') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('الكمية') }} *</label>
<input type="number" wire:model="assemblyQuantity" min="1" dir="ltr"
class="w-full text-sm px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
@error('assemblyQuantity') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div>
</div>
<div class="flex items-center justify-end gap-3 mt-6">
<button wire:click="$set('showAssembleModal', false)"
class="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 text-sm">
{{ __('إلغاء') }}
</button>
<button wire:click="assemble" wire:loading.attr="disabled" wire:target="assemble"
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-blue-400 text-sm font-medium">
<span wire:loading.remove wire:target="assemble">{{ __('تجميع') }}</span>
<span wire:loading wire:target="assemble">{{ __('جارٍ التجميع...') }}</span>
</button>
</div>
</div>
</div>
</div>
@endif
{{-- Disassemble Modal --}}
@if($showDisassembleModal)
<div class="fixed inset-0 z-50 overflow-y-auto" x-data x-init="$el.focus()">
<div class="flex items-center justify-center min-h-screen px-4">
<div class="fixed inset-0 bg-black/50" wire:click="$set('showDisassembleModal', false)"></div>
<div class="relative bg-white rounded-xl shadow-xl w-full max-w-md p-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4">{{ __('تفكيك طقم') }}</h3>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('المستودع') }} *</label>
<select wire:model="selectedWarehouseId"
class="w-full text-sm px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<option value="">{{ __('-- اختر المستودع --') }}</option>
@foreach($warehouses as $warehouse)
<option value="{{ $warehouse->id }}">{{ $warehouse->name_ar }}</option>
@endforeach
</select>
@error('selectedWarehouseId') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('الكمية') }} *</label>
<input type="number" wire:model="assemblyQuantity" min="1" dir="ltr"
class="w-full text-sm px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
@error('assemblyQuantity') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div>
</div>
<div class="flex items-center justify-end gap-3 mt-6">
<button wire:click="$set('showDisassembleModal', false)"
class="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 text-sm">
{{ __('إلغاء') }}
</button>
<button wire:click="disassemble" wire:loading.attr="disabled" wire:target="disassemble"
class="px-4 py-2 bg-amber-600 text-white rounded-lg hover:bg-amber-700 disabled:bg-amber-400 text-sm font-medium">
<span wire:loading.remove wire:target="disassemble">{{ __('تفكيك') }}</span>
<span wire:loading wire:target="disassemble">{{ __('جارٍ التفكيك...') }}</span>
</button>
</div>
</div>
</div>
</div>
@endif
</div>
<div>
<div class="flex items-center justify-between mb-6">
<h1 class="text-xl sm:text-2xl font-bold text-gray-800">{{ __('أوامر الشراء') }}</h1>
@can('inventory.create')
<a href="{{ route('inventory.purchase-orders.create') }}" 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>
{{-- Flash Messages --}}
@if(session('success'))
<div class="mb-4 p-4 bg-green-50 border border-green-200 rounded-lg text-green-700 text-sm">
{{ session('success') }}
</div>
@endif
@if(session('error'))
<div class="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
{{ session('error') }}
</div>
@endif
{{-- Filters --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 mb-4">
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div class="sm:col-span-2">
<input type="text" wire:model.live.debounce.300ms="search"
placeholder="{{ __('بحث برقم الأمر أو اسم المورد...') }}"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
</div>
<select wire:model.live="status" class="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<option value="">{{ __('كل الحالات') }}</option>
@foreach($statuses as $statusOption)
<option value="{{ $statusOption->value }}">{{ $statusOption->label() }}</option>
@endforeach
</select>
</div>
</div>
{{-- Table --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<div wire:loading.class="opacity-50 pointer-events-none">
<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 font-medium text-gray-600">{{ __('رقم الأمر') }}</th>
<th class="px-4 py-3 text-start font-medium text-gray-600">{{ __('المورد') }}</th>
<th class="px-4 py-3 text-center font-medium text-gray-600">{{ __('عدد الأصناف') }}</th>
<th class="px-4 py-3 text-center font-medium text-gray-600">{{ __('الإجمالي') }}</th>
<th class="px-4 py-3 text-center font-medium text-gray-600">{{ __('الحالة') }}</th>
<th class="px-4 py-3 text-center font-medium text-gray-600">{{ __('تاريخ الطلب') }}</th>
<th class="px-4 py-3 text-center font-medium text-gray-600">{{ __('إجراءات') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
@forelse($purchaseOrders as $po)
<tr class="hover:bg-gray-50">
<td class="px-4 py-3 font-mono text-gray-700 text-xs" dir="ltr">
{{ $po->order_number }}
</td>
<td class="px-4 py-3">
<span class="font-medium text-gray-800">{{ $po->supplier_name }}</span>
@if($po->supplier_contact)
<p class="text-xs text-gray-500">{{ $po->supplier_contact }}</p>
@endif
</td>
<td class="px-4 py-3 text-center text-gray-600">
{{ $po->items_count }}
</td>
<td class="px-4 py-3 text-center font-mono" dir="ltr">
{{ number_format($po->total_amount / 100, 2) }} {{ __('ج.م') }}
</td>
<td class="px-4 py-3 text-center">
@php
$statusEnum = $po->status instanceof \App\Domain\Inventory\Enums\PurchaseOrderStatus
? $po->status
: \App\Domain\Inventory\Enums\PurchaseOrderStatus::from($po->status);
$badgeClass = match($statusEnum) {
\App\Domain\Inventory\Enums\PurchaseOrderStatus::Draft => 'bg-gray-100 text-gray-700',
\App\Domain\Inventory\Enums\PurchaseOrderStatus::Submitted => 'bg-blue-100 text-blue-700',
\App\Domain\Inventory\Enums\PurchaseOrderStatus::Confirmed => 'bg-indigo-100 text-indigo-700',
\App\Domain\Inventory\Enums\PurchaseOrderStatus::PartiallyReceived => 'bg-amber-100 text-amber-700',
\App\Domain\Inventory\Enums\PurchaseOrderStatus::Received => 'bg-green-100 text-green-700',
\App\Domain\Inventory\Enums\PurchaseOrderStatus::Cancelled => 'bg-red-100 text-red-700',
};
@endphp
<span class="px-2 py-0.5 text-xs rounded-full {{ $badgeClass }}">
{{ $statusEnum->label() }}
</span>
</td>
<td class="px-4 py-3 text-center text-gray-600 text-xs" dir="ltr">
{{ $po->order_date->format('Y-m-d') }}
</td>
<td class="px-4 py-3 text-center">
<div class="flex items-center justify-center gap-2">
@can('inventory.list')
<a href="{{ route('inventory.purchase-orders.show', $po) }}" wire:navigate
class="text-blue-600 hover:text-blue-800 text-xs font-medium">
{{ __('عرض') }}
</a>
@endcan
@can('inventory.update')
@if(in_array($statusEnum, [\App\Domain\Inventory\Enums\PurchaseOrderStatus::Confirmed, \App\Domain\Inventory\Enums\PurchaseOrderStatus::PartiallyReceived]))
<button wire:click="receive({{ $po->id }})"
wire:loading.attr="disabled"
wire:target="receive({{ $po->id }})"
wire:confirm="{{ __('هل تريد استلام جميع الأصناف المتبقية؟') }}"
class="text-green-600 hover:text-green-800 text-xs font-medium">
<span wire:loading.remove wire:target="receive({{ $po->id }})">{{ __('استلام') }}</span>
<span wire:loading wire:target="receive({{ $po->id }})">{{ __('جارٍ...') }}</span>
</button>
@endif
@endcan
@can('inventory.update')
@if(in_array($statusEnum, [\App\Domain\Inventory\Enums\PurchaseOrderStatus::Draft, \App\Domain\Inventory\Enums\PurchaseOrderStatus::Submitted, \App\Domain\Inventory\Enums\PurchaseOrderStatus::Confirmed]))
<button wire:click="cancel({{ $po->id }})"
wire:loading.attr="disabled"
wire:target="cancel({{ $po->id }})"
wire:confirm="{{ __('هل أنت متأكد من إلغاء أمر الشراء؟') }}"
class="text-red-600 hover:text-red-800 text-xs font-medium">
<span wire:loading.remove wire:target="cancel({{ $po->id }})">{{ __('إلغاء') }}</span>
<span wire:loading wire:target="cancel({{ $po->id }})">{{ __('جارٍ...') }}</span>
</button>
@endif
@endcan
</div>
</td>
</tr>
@empty
<tr>
<td colspan="7" class="px-4 py-12 text-center">
<div class="flex flex-col items-center">
<svg class="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="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
<p class="text-gray-500 text-sm">{{ __('لا توجد أوامر شراء') }}</p>
@can('inventory.create')
<a href="{{ route('inventory.purchase-orders.create') }}" wire:navigate
class="mt-2 text-blue-600 hover:text-blue-800 text-sm font-medium">
{{ __('إنشاء أول أمر شراء') }}
</a>
@endcan
</div>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@if($purchaseOrders->hasPages())
<div class="px-4 py-3 border-t border-gray-200">
{{ $purchaseOrders->links() }}
</div>
@endif
</div>
</div>
<div>
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-4 sm:mb-6">
<h1 class="text-xl sm:text-2xl font-bold text-gray-800">
{{ $stockCountId ? __('متابعة الجرد') : __('جرد مخزني جديد') }}
</h1>
<a href="{{ route('inventory.stock-counts') }}" wire:navigate
class="text-sm text-gray-600 hover:text-gray-800 font-medium">
{{ __('العودة للقائمة') }}
</a>
</div>
{{-- Flash Messages --}}
@if(session('success'))
<div class="mb-4 p-4 bg-green-50 border border-green-200 rounded-lg text-green-700 text-sm">
{{ session('success') }}
</div>
@endif
@if(session('error'))
<div class="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
{{ session('error') }}
</div>
@endif
<form wire:submit="save" class="space-y-4 sm:space-y-6">
{{-- Warehouse Selection --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 sm:p-6">
<h2 class="text-base sm:text-lg font-semibold text-gray-700 mb-4">{{ __('بيانات الجرد') }}</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 sm:gap-4">
<div>
<label for="warehouse_id" class="block text-sm font-medium text-gray-700 mb-1">{{ __('المستودع') }} *</label>
<select id="warehouse_id" wire:model.live="warehouse_id"
@if($stockCountId) disabled @endif
class="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm @error('warehouse_id') border-red-500 @enderror">
<option value="">{{ __('-- اختر المستودع --') }}</option>
@foreach($warehouses as $warehouse)
<option value="{{ $warehouse->id }}">{{ $warehouse->name_ar }} ({{ $warehouse->code }})</option>
@endforeach
</select>
@error('warehouse_id') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div>
<div>
<label for="notes" class="block text-sm font-medium text-gray-700 mb-1">{{ __('ملاحظات') }}</label>
<input type="text" id="notes" wire:model="notes"
class="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm @error('notes') border-red-500 @enderror"
placeholder="{{ __('ملاحظات اختيارية...') }}">
@error('notes') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div>
</div>
</div>
{{-- Products Grid --}}
@if(count($items) > 0)
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<div class="px-4 sm:px-6 py-4 border-b border-gray-200 flex items-center justify-between">
<h2 class="text-base sm:text-lg font-semibold text-gray-700">{{ __('المنتجات') }}</h2>
<span class="text-sm text-gray-500">{{ count($items) }} {{ __('منتج') }}</span>
</div>
<div class="overflow-x-auto">
<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 font-medium text-gray-600">#</th>
<th class="px-4 py-3 text-start font-medium text-gray-600">{{ __('المنتج') }}</th>
<th class="px-4 py-3 text-start font-medium text-gray-600">{{ __('SKU') }}</th>
<th class="px-4 py-3 text-center font-medium text-gray-600">{{ __('الكمية بالنظام') }}</th>
<th class="px-4 py-3 text-center font-medium text-gray-600">{{ __('الكمية الفعلية') }}</th>
<th class="px-4 py-3 text-center font-medium text-gray-600">{{ __('الفرق') }}</th>
<th class="px-4 py-3 text-center font-medium text-gray-600">{{ __('قيمة الفرق') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
@foreach($items as $index => $item)
<tr class="hover:bg-gray-50">
<td class="px-4 py-3 text-gray-500 text-xs">{{ $index + 1 }}</td>
<td class="px-4 py-3">
<span class="font-medium text-gray-800">{{ $item['name_ar'] }}</span>
</td>
<td class="px-4 py-3 font-mono text-gray-600 text-xs" dir="ltr">
{{ $item['sku'] }}
</td>
<td class="px-4 py-3 text-center">
<span class="px-2 py-0.5 bg-gray-100 text-gray-700 rounded text-xs font-mono" dir="ltr">
{{ $item['system_quantity'] }}
</span>
</td>
<td class="px-4 py-3 text-center">
<input type="number"
wire:model.blur="items.{{ $index }}.counted_quantity"
min="0"
dir="ltr"
class="w-24 mx-auto px-3 py-1.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm text-center"
placeholder="—">
</td>
<td class="px-4 py-3 text-center">
@php
$counted = $item['counted_quantity'];
$system = $item['system_quantity'];
$variance = $counted !== null && $counted !== '' ? (int)$counted - $system : null;
@endphp
@if($variance !== null)
@if($variance === 0)
<span class="px-2 py-0.5 text-xs bg-green-100 text-green-700 rounded-full font-mono" dir="ltr">0</span>
@elseif($variance > 0)
<span class="px-2 py-0.5 text-xs bg-amber-100 text-amber-700 rounded-full font-mono" dir="ltr">+{{ $variance }}</span>
@else
<span class="px-2 py-0.5 text-xs bg-red-100 text-red-700 rounded-full font-mono" dir="ltr">{{ $variance }}</span>
@endif
@else
<span class="text-xs text-gray-400"></span>
@endif
</td>
<td class="px-4 py-3 text-center font-mono text-xs" dir="ltr">
@if($variance !== null && $variance !== 0)
@php
$costPrice = $item['cost_price'] ?? 0;
$varianceValue = abs($variance) * $costPrice;
@endphp
<span class="{{ $variance < 0 ? 'text-red-600' : 'text-amber-600' }}">
{{ number_format($varianceValue / 100, 2) }} {{ __('ج.م') }}
</span>
@elseif($variance === 0)
<span class="text-green-600">0.00 {{ __('ج.م') }}</span>
@else
<span class="text-gray-400"></span>
@endif
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
{{-- Summary --}}
@php
$totalCounted = collect($items)->filter(fn($i) => $i['counted_quantity'] !== null && $i['counted_quantity'] !== '')->count();
$totalWithVariance = collect($items)->filter(function($i) {
if ($i['counted_quantity'] === null || $i['counted_quantity'] === '') return false;
return (int)$i['counted_quantity'] !== $i['system_quantity'];
})->count();
$totalVarianceValue = collect($items)->reduce(function($carry, $i) {
if ($i['counted_quantity'] === null || $i['counted_quantity'] === '') return $carry;
$variance = (int)$i['counted_quantity'] - $i['system_quantity'];
$cost = $i['cost_price'] ?? 0;
return $carry + (abs($variance) * $cost);
}, 0);
@endphp
<div class="px-4 sm:px-6 py-4 bg-gray-50 border-t border-gray-200">
<div class="flex flex-wrap gap-4 sm:gap-6 text-sm">
<div>
<span class="text-gray-500">{{ __('تم عدها:') }}</span>
<span class="font-medium text-gray-800 ms-1">{{ $totalCounted }} / {{ count($items) }}</span>
</div>
<div>
<span class="text-gray-500">{{ __('بها فروقات:') }}</span>
<span class="font-medium {{ $totalWithVariance > 0 ? 'text-red-600' : 'text-green-600' }} ms-1">{{ $totalWithVariance }}</span>
</div>
<div>
<span class="text-gray-500">{{ __('إجمالي قيمة الفروقات:') }}</span>
<span class="font-medium {{ $totalVarianceValue > 0 ? 'text-red-600' : 'text-green-600' }} ms-1" dir="ltr">
{{ number_format($totalVarianceValue / 100, 2) }} {{ __('ج.م') }}
</span>
</div>
</div>
</div>
</div>
@elseif($warehouse_id)
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-8 text-center">
<svg class="w-12 h-12 text-gray-300 mb-3 mx-auto" 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>
@endif
{{-- Actions --}}
@if(count($items) > 0)
<div class="flex flex-col-reverse sm:flex-row gap-3">
<button type="submit" wire:loading.attr="disabled" wire:target="save"
class="w-full sm:w-auto px-6 py-2.5 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 disabled:bg-blue-400 transition-colors text-sm">
<span wire:loading.remove wire:target="save">
{{ $stockCountId ? __('حفظ بيانات الجرد') : __('بدء الجرد وحفظ') }}
</span>
<span wire:loading wire:target="save">{{ __('جارٍ الحفظ...') }}</span>
</button>
<a href="{{ route('inventory.stock-counts') }}" wire:navigate
class="w-full sm:w-auto px-6 py-2.5 bg-gray-100 text-gray-700 font-medium rounded-lg hover:bg-gray-200 transition-colors text-sm text-center">
{{ __('إلغاء') }}
</a>
</div>
@endif
</form>
</div>
<div>
<div class="flex items-center justify-between mb-6">
<h1 class="text-xl sm:text-2xl font-bold text-gray-800">{{ __('الجرد المخزني') }}</h1>
@can('inventory.manage')
<a href="{{ route('inventory.stock-counts.create') }}" 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>
{{-- Flash Messages --}}
@if(session('success'))
<div class="mb-4 p-4 bg-green-50 border border-green-200 rounded-lg text-green-700 text-sm">
{{ session('success') }}
</div>
@endif
@if(session('error'))
<div class="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
{{ session('error') }}
</div>
@endif
{{-- Filters --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 mb-4">
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div class="sm:col-span-2">
<input type="text" wire:model.live.debounce.300ms="search"
placeholder="{{ __('بحث برقم الجرد أو اسم المستودع...') }}"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
</div>
<select wire:model.live="statusFilter" class="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<option value="">{{ __('كل الحالات') }}</option>
@foreach($statuses as $status)
<option value="{{ $status->value }}">{{ $status->label() }}</option>
@endforeach
</select>
</div>
</div>
{{-- Table --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<div wire:loading.class="opacity-50 pointer-events-none">
<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 font-medium text-gray-600">{{ __('رقم الجرد') }}</th>
<th class="px-4 py-3 text-start font-medium text-gray-600">{{ __('المستودع') }}</th>
<th class="px-4 py-3 text-center font-medium text-gray-600">{{ __('التاريخ') }}</th>
<th class="px-4 py-3 text-center font-medium text-gray-600">{{ __('الحالة') }}</th>
<th class="px-4 py-3 text-center font-medium text-gray-600">{{ __('عدد المنتجات') }}</th>
<th class="px-4 py-3 text-center font-medium text-gray-600">{{ __('الفروقات') }}</th>
<th class="px-4 py-3 text-center font-medium text-gray-600">{{ __('قيمة الفروقات') }}</th>
<th class="px-4 py-3 text-center font-medium text-gray-600">{{ __('إجراءات') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
@forelse($stockCounts as $count)
<tr class="hover:bg-gray-50">
<td class="px-4 py-3 font-mono text-gray-700 text-xs" dir="ltr">
{{ $count->count_number }}
</td>
<td class="px-4 py-3">
<span class="font-medium text-gray-800">{{ $count->warehouse->name_ar ?? '—' }}</span>
</td>
<td class="px-4 py-3 text-center text-gray-600" dir="ltr">
{{ $count->started_at?->format('Y-m-d') }}
</td>
<td class="px-4 py-3 text-center">
@php
$statusValue = $count->status instanceof \App\Domain\Inventory\Enums\StockCountStatus
? $count->status->value
: $count->status;
$statusColors = [
'open' => 'bg-blue-100 text-blue-700',
'in_progress' => 'bg-amber-100 text-amber-700',
'completed' => 'bg-green-100 text-green-700',
'cancelled' => 'bg-gray-100 text-gray-600',
];
$statusLabel = $count->status instanceof \App\Domain\Inventory\Enums\StockCountStatus
? $count->status->label()
: $statusValue;
@endphp
<span class="px-2 py-0.5 text-xs rounded-full {{ $statusColors[$statusValue] ?? 'bg-gray-100 text-gray-600' }}">
{{ $statusLabel }}
</span>
</td>
<td class="px-4 py-3 text-center text-gray-600">
{{ $count->items_count ?? $count->total_items }}
</td>
<td class="px-4 py-3 text-center">
@if($count->total_discrepancies > 0)
<span class="px-2 py-0.5 text-xs bg-red-100 text-red-700 rounded-full">
{{ $count->total_discrepancies }}
</span>
@else
<span class="px-2 py-0.5 text-xs bg-green-100 text-green-700 rounded-full">
{{ __('لا فروقات') }}
</span>
@endif
</td>
<td class="px-4 py-3 text-center font-mono text-xs" dir="ltr">
@if($count->total_variance_value > 0)
<span class="text-red-600">
{{ number_format($count->total_variance_value / 100, 2) }} {{ __('ج.م') }}
</span>
@else
<span class="text-green-600">0.00 {{ __('ج.م') }}</span>
@endif
</td>
<td class="px-4 py-3 text-center">
<div class="flex items-center justify-center gap-2">
@if(in_array($statusValue, ['open', 'in_progress']))
@can('inventory.manage')
<a href="{{ route('inventory.stock-counts.edit', $count) }}" wire:navigate
class="text-blue-600 hover:text-blue-800 text-xs font-medium">
{{ __('متابعة') }}
</a>
<button wire:click="finalize({{ $count->id }})"
wire:loading.attr="disabled"
wire:confirm="{{ __('هل تريد إكمال الجرد؟ يجب أن تكون كل المنتجات قد تم عدها.') }}"
class="text-green-600 hover:text-green-800 text-xs font-medium">
{{ __('إكمال') }}
</button>
<button wire:click="cancel({{ $count->id }})"
wire:loading.attr="disabled"
wire:confirm="{{ __('هل أنت متأكد من إلغاء هذا الجرد؟') }}"
class="text-red-600 hover:text-red-800 text-xs font-medium">
{{ __('إلغاء') }}
</button>
@endcan
@endif
</div>
</td>
</tr>
@empty
<tr>
<td colspan="8" class="px-4 py-12 text-center">
<div class="flex flex-col items-center">
<svg class="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="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"/>
</svg>
<p class="text-gray-500 text-sm">{{ __('لا توجد عمليات جرد') }}</p>
@can('inventory.manage')
<a href="{{ route('inventory.stock-counts.create') }}" wire:navigate
class="mt-2 text-blue-600 hover:text-blue-800 text-sm font-medium">
{{ __('بدء جرد جديد') }}
</a>
@endcan
</div>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@if($stockCounts->hasPages())
<div class="px-4 py-3 border-t border-gray-200">
{{ $stockCounts->links() }}
</div>
@endif
</div>
</div>
......@@ -358,6 +358,20 @@
->middleware('permission:inventory.adjust');
Route::get('/inventory/adjustments/wizard', \App\Livewire\Inventory\StockAdjustmentWizard::class)->name('inventory.adjustments.wizard')
->middleware('permission:inventory.adjust');
Route::get('/inventory/kits', \App\Livewire\Inventory\KitList::class)->name('inventory.kits')
->middleware('permission:inventory.manage');
Route::get('/inventory/kits/create', \App\Livewire\Inventory\KitForm::class)->name('inventory.kits.create')
->middleware('permission:inventory.manage');
Route::get('/inventory/kits/{kit}/edit', \App\Livewire\Inventory\KitForm::class)->name('inventory.kits.edit')
->middleware('permission:inventory.manage');
Route::get('/inventory/stock-counts', \App\Livewire\Inventory\StockCountList::class)->name('inventory.stock-counts')
->middleware('permission:inventory.manage');
Route::get('/inventory/stock-counts/create', \App\Livewire\Inventory\StockCountForm::class)->name('inventory.stock-counts.create')
->middleware('permission:inventory.manage');
Route::get('/inventory/stock-counts/{stockCount}/edit', \App\Livewire\Inventory\StockCountForm::class)->name('inventory.stock-counts.edit')
->middleware('permission:inventory.manage');
Route::get('/inventory/purchase-orders', \App\Livewire\Inventory\PurchaseOrderList::class)->name('inventory.purchase-orders')
->middleware('permission:inventory.manage');
// Settings
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