Commit 0c31ec65 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Add Purchase Order create/show/edit routes and components

Fixes Route [inventory.purchase-orders.create] not defined error by adding
the missing routes and creating PurchaseOrderForm + PurchaseOrderShow
Livewire components with full Arabic UI.
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 85cf6677
<?php
namespace App\Livewire\Inventory;
use App\Domain\Inventory\Models\Product;
use App\Domain\Inventory\Models\PurchaseOrder;
use App\Domain\Inventory\Models\Warehouse;
use App\Domain\Inventory\Services\PurchaseOrderService;
use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Shared\Traits\UsesBranchScope;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Layout('layouts.app')]
#[Title('أمر شراء')]
class PurchaseOrderForm extends Component
{
use UsesBranchScope;
public ?PurchaseOrder $purchaseOrder = null;
public bool $editing = false;
public string $supplier_name = '';
public string $supplier_contact = '';
public string $warehouse_id = '';
public string $order_date = '';
public string $expected_delivery_date = '';
public string $tax_amount = '0';
public string $shipping_cost = '0';
public string $notes = '';
public array $items = [];
public function mount(?PurchaseOrder $purchaseOrder = null): void
{
$this->authorize('inventory.manage');
$this->order_date = now()->format('Y-m-d');
if ($purchaseOrder && $purchaseOrder->exists) {
$this->purchaseOrder = $purchaseOrder;
$this->editing = true;
$this->supplier_name = $purchaseOrder->supplier_name ?? '';
$this->supplier_contact = $purchaseOrder->supplier_contact ?? '';
$this->warehouse_id = (string) ($purchaseOrder->warehouse_id ?? '');
$this->order_date = $purchaseOrder->order_date?->format('Y-m-d') ?? '';
$this->expected_delivery_date = $purchaseOrder->expected_delivery_date?->format('Y-m-d') ?? '';
$this->tax_amount = (string) (($purchaseOrder->tax_amount ?? 0) / 100);
$this->shipping_cost = (string) (($purchaseOrder->shipping_cost ?? 0) / 100);
$this->notes = $purchaseOrder->notes ?? '';
$purchaseOrder->load('items');
foreach ($purchaseOrder->items as $item) {
$this->items[] = [
'product_id' => (string) $item->product_id,
'quantity_ordered' => (string) $item->quantity_ordered,
'unit_cost' => (string) ($item->unit_cost / 100),
'notes' => $item->notes ?? '',
];
}
}
if (empty($this->items)) {
$this->items[] = ['product_id' => '', 'quantity_ordered' => '1', 'unit_cost' => '', 'notes' => ''];
}
}
public function addItem(): void
{
$this->items[] = ['product_id' => '', 'quantity_ordered' => '1', 'unit_cost' => '', 'notes' => ''];
}
public function removeItem(int $index): void
{
unset($this->items[$index]);
$this->items = array_values($this->items);
if (empty($this->items)) {
$this->addItem();
}
}
public function rules(): array
{
return [
'supplier_name' => 'required|string|max:255',
'supplier_contact' => 'nullable|string|max:255',
'warehouse_id' => 'required|exists:warehouses,id',
'order_date' => 'required|date',
'expected_delivery_date' => 'nullable|date|after_or_equal:order_date',
'tax_amount' => 'nullable|numeric|min:0',
'shipping_cost' => 'nullable|numeric|min:0',
'notes' => 'nullable|string|max:2000',
'items' => 'required|array|min:1',
'items.*.product_id' => 'required|exists:products,id',
'items.*.quantity_ordered' => 'required|integer|min:1',
'items.*.unit_cost' => 'required|numeric|min:0',
];
}
public function messages(): array
{
return [
'supplier_name.required' => 'اسم المورد مطلوب',
'warehouse_id.required' => 'المستودع مطلوب',
'warehouse_id.exists' => 'المستودع غير موجود',
'order_date.required' => 'تاريخ الطلب مطلوب',
'expected_delivery_date.after_or_equal' => 'تاريخ التسليم يجب أن يكون بعد تاريخ الطلب',
'items.required' => 'يجب إضافة صنف واحد على الأقل',
'items.min' => 'يجب إضافة صنف واحد على الأقل',
'items.*.product_id.required' => 'اختر المنتج',
'items.*.product_id.exists' => 'المنتج غير موجود',
'items.*.quantity_ordered.required' => 'الكمية مطلوبة',
'items.*.quantity_ordered.min' => 'الكمية يجب أن تكون 1 على الأقل',
'items.*.unit_cost.required' => 'سعر الوحدة مطلوب',
'items.*.unit_cost.min' => 'سعر الوحدة لا يمكن أن يكون سالب',
];
}
public function save(): void
{
$this->validate();
try {
$branchId = $this->getActiveBranchId() ?? auth()->user()->branch_id;
$orderNumber = 'PO-' . now()->format('Ymd') . '-' . str_pad(
PurchaseOrder::whereDate('created_at', today())->count() + 1,
3, '0', STR_PAD_LEFT
);
$data = [
'academy_id' => app('current_academy')->id,
'branch_id' => $branchId,
'warehouse_id' => (int) $this->warehouse_id,
'order_number' => $this->editing ? $this->purchaseOrder->order_number : $orderNumber,
'supplier_name' => $this->supplier_name,
'supplier_contact' => $this->supplier_contact ?: null,
'order_date' => $this->order_date,
'expected_delivery_date' => $this->expected_delivery_date ?: null,
'tax_amount' => (int) round((float) $this->tax_amount * 100),
'shipping_cost' => (int) round((float) $this->shipping_cost * 100),
'notes' => $this->notes ?: null,
'metadata' => [],
];
$items = collect($this->items)->map(fn ($item) => [
'product_id' => (int) $item['product_id'],
'quantity_ordered' => (int) $item['quantity_ordered'],
'unit_cost' => (int) round((float) $item['unit_cost'] * 100),
'notes' => $item['notes'] ?? null,
])->toArray();
$service = app(PurchaseOrderService::class);
$service->create($data, $items, auth()->user());
session()->flash('success', __('تم إنشاء أمر الشراء بنجاح'));
$this->redirect(route('inventory.purchase-orders'), navigate: true);
} catch (DomainException $e) {
session()->flash('error', $e->getMessage());
}
}
public function render()
{
$branchId = $this->getActiveBranchId();
return view('livewire.inventory.purchase-order-form', [
'warehouses' => Warehouse::query()
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->where('is_active', true)
->get(),
'products' => Product::query()
->where('is_active', true)
->select('id', 'name_ar', 'sku')
->orderBy('name_ar')
->get(),
]);
}
}
<?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 Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Layout('layouts.app')]
#[Title('تفاصيل أمر الشراء')]
class PurchaseOrderShow extends Component
{
public PurchaseOrder $purchaseOrder;
public function mount(PurchaseOrder $purchaseOrder): void
{
$this->authorize('inventory.list');
$this->purchaseOrder = $purchaseOrder->load(['items.product', 'warehouse', 'creator']);
}
public function submit(): void
{
try {
app(PurchaseOrderService::class)->submit($this->purchaseOrder, auth()->user());
session()->flash('success', __('تم إرسال أمر الشراء'));
$this->purchaseOrder->refresh();
} catch (InvalidStatusTransitionException|DomainException $e) {
session()->flash('error', $e->getMessage());
}
}
public function confirm(): void
{
try {
app(PurchaseOrderService::class)->confirm($this->purchaseOrder, auth()->user());
session()->flash('success', __('تم تأكيد أمر الشراء'));
$this->purchaseOrder->refresh();
} catch (InvalidStatusTransitionException|DomainException $e) {
session()->flash('error', $e->getMessage());
}
}
public function render()
{
return view('livewire.inventory.purchase-order-show');
}
}
<div>
<div class="flex items-center justify-between mb-6">
<div>
<h1 class="text-xl sm:text-2xl font-bold text-gray-800">{{ __('أمر الشراء') }}: <span class="font-mono" dir="ltr">{{ $purchaseOrder->order_number }}</span></h1>
<p class="text-sm text-gray-500 mt-1">{{ __('تاريخ الطلب') }}: {{ $purchaseOrder->order_date->format('Y-m-d') }}</p>
</div>
<a href="{{ route('inventory.purchase-orders') }}" wire:navigate
class="text-sm text-gray-500 hover:text-gray-700">
&larr; {{ __('العودة للقائمة') }}
</a>
</div>
@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
{{-- Status & Actions --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 mb-6">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<span class="text-sm font-medium text-gray-600">{{ __('الحالة') }}:</span>
@php
$badgeClass = match($purchaseOrder->status) {
\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-3 py-1 text-sm rounded-full {{ $badgeClass }}">{{ $purchaseOrder->status->label() }}</span>
</div>
<div class="flex items-center gap-2">
@can('inventory.manage')
@if($purchaseOrder->status === \App\Domain\Inventory\Enums\PurchaseOrderStatus::Draft)
<button wire:click="submit" wire:confirm="{{ __('هل تريد إرسال أمر الشراء؟') }}"
class="px-3 py-1.5 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700">
{{ __('إرسال') }}
</button>
@endif
@if($purchaseOrder->status === \App\Domain\Inventory\Enums\PurchaseOrderStatus::Submitted)
<button wire:click="confirm" wire:confirm="{{ __('هل تريد تأكيد أمر الشراء؟') }}"
class="px-3 py-1.5 bg-indigo-600 text-white rounded-lg text-sm hover:bg-indigo-700">
{{ __('تأكيد') }}
</button>
@endif
@endcan
</div>
</div>
</div>
{{-- Supplier & Warehouse --}}
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<h2 class="text-sm font-semibold text-gray-500 mb-3">{{ __('المورد') }}</h2>
<p class="text-gray-800 font-medium">{{ $purchaseOrder->supplier_name }}</p>
@if($purchaseOrder->supplier_contact)
<p class="text-sm text-gray-500 mt-1">{{ $purchaseOrder->supplier_contact }}</p>
@endif
</div>
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<h2 class="text-sm font-semibold text-gray-500 mb-3">{{ __('المستودع') }}</h2>
<p class="text-gray-800 font-medium">{{ $purchaseOrder->warehouse?->name_ar ?? '-' }}</p>
@if($purchaseOrder->expected_delivery_date)
<p class="text-sm text-gray-500 mt-1">{{ __('التسليم المتوقع') }}: {{ $purchaseOrder->expected_delivery_date->format('Y-m-d') }}</p>
@endif
</div>
</div>
{{-- Items Table --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden mb-6">
<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-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($purchaseOrder->items as $item)
<tr>
<td class="px-4 py-3">
<span class="font-medium text-gray-800">{{ $item->product?->name_ar ?? __('منتج محذوف') }}</span>
@if($item->product?->sku)
<span class="text-xs text-gray-400 ms-1 font-mono" dir="ltr">{{ $item->product->sku }}</span>
@endif
</td>
<td class="px-4 py-3 text-center text-gray-600">{{ $item->quantity_ordered }}</td>
<td class="px-4 py-3 text-center">
<span class="{{ $item->quantity_received >= $item->quantity_ordered ? 'text-green-600' : 'text-amber-600' }}">
{{ $item->quantity_received }}
</span>
</td>
<td class="px-4 py-3 text-center font-mono text-gray-600" dir="ltr">{{ number_format($item->unit_cost / 100, 2) }}</td>
<td class="px-4 py-3 text-center font-mono text-gray-700" dir="ltr">{{ number_format($item->line_total / 100, 2) }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
{{-- Totals --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<div class="max-w-xs ms-auto space-y-2 text-sm">
<div class="flex justify-between text-gray-600">
<span>{{ __('المجموع الفرعي') }}</span>
<span class="font-mono" dir="ltr">{{ number_format($purchaseOrder->subtotal / 100, 2) }} {{ __('ج.م') }}</span>
</div>
@if($purchaseOrder->tax_amount)
<div class="flex justify-between text-gray-600">
<span>{{ __('الضريبة') }}</span>
<span class="font-mono" dir="ltr">{{ number_format($purchaseOrder->tax_amount / 100, 2) }} {{ __('ج.م') }}</span>
</div>
@endif
@if($purchaseOrder->shipping_cost)
<div class="flex justify-between text-gray-600">
<span>{{ __('الشحن') }}</span>
<span class="font-mono" dir="ltr">{{ number_format($purchaseOrder->shipping_cost / 100, 2) }} {{ __('ج.م') }}</span>
</div>
@endif
<div class="flex justify-between font-bold text-gray-800 pt-2 border-t">
<span>{{ __('الإجمالي') }}</span>
<span class="font-mono" dir="ltr">{{ number_format($purchaseOrder->total_amount / 100, 2) }} {{ __('ج.م') }}</span>
</div>
</div>
</div>
@if($purchaseOrder->notes)
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 mt-6">
<h2 class="text-sm font-semibold text-gray-500 mb-2">{{ __('ملاحظات') }}</h2>
<p class="text-gray-700 text-sm">{{ $purchaseOrder->notes }}</p>
</div>
@endif
</div>
...@@ -373,6 +373,12 @@ ...@@ -373,6 +373,12 @@
->middleware('permission:inventory.manage'); ->middleware('permission:inventory.manage');
Route::get('/inventory/purchase-orders', \App\Livewire\Inventory\PurchaseOrderList::class)->name('inventory.purchase-orders') Route::get('/inventory/purchase-orders', \App\Livewire\Inventory\PurchaseOrderList::class)->name('inventory.purchase-orders')
->middleware('permission:inventory.manage'); ->middleware('permission:inventory.manage');
Route::get('/inventory/purchase-orders/create', \App\Livewire\Inventory\PurchaseOrderForm::class)->name('inventory.purchase-orders.create')
->middleware('permission:inventory.manage');
Route::get('/inventory/purchase-orders/{purchaseOrder}', \App\Livewire\Inventory\PurchaseOrderShow::class)->name('inventory.purchase-orders.show')
->middleware('permission:inventory.list');
Route::get('/inventory/purchase-orders/{purchaseOrder}/edit', \App\Livewire\Inventory\PurchaseOrderForm::class)->name('inventory.purchase-orders.edit')
->middleware('permission:inventory.manage');
// 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