Commit 39f6a005 authored by Mahmoud Aglan's avatar Mahmoud Aglan

هىرثىفخقغ عحيشفث

parent 78d2ad89
<?php
declare(strict_types=1);
namespace App\Modules\Inventory\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\Inventory\Models\AssetRegister;
use App\Modules\Inventory\Models\DepreciationEntry;
use App\Modules\Inventory\Models\Warehouse;
use App\Modules\Inventory\Services\DepreciationService;
class AssetController extends Controller
{
public function index(Request $request): Response
{
$filters = [
'q' => trim((string) $request->get('q', '')),
'status' => trim((string) $request->get('status', '')),
'warehouse_id' => trim((string) $request->get('warehouse_id', '')),
];
$page = max(1, (int) $request->get('page', 1));
$result = AssetRegister::search($filters, 25, $page);
return $this->view('Inventory.Views.assets.index', [
'assets' => $result['data'],
'pagination' => $result['pagination'],
'filters' => $filters,
'statuses' => AssetRegister::getStatuses(),
'warehouses' => Warehouse::allActive(),
]);
}
public function show(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$asset = $db->selectOne(
"SELECT ar.*, i.`name_ar` as item_name, i.`sku`, w.`name_ar` as warehouse_name
FROM `asset_register` ar
JOIN `inventory_items` i ON i.`id` = ar.`item_id`
JOIN `warehouses` w ON w.`id` = ar.`warehouse_id`
WHERE ar.`id` = ?",
[(int) $id]
);
if (!$asset) {
return $this->redirect('/inventory/assets')->withError('الأصل غير موجود');
}
$depreciationHistory = DepreciationEntry::getForAsset((int) $id);
return $this->view('Inventory.Views.assets.show', [
'asset' => $asset,
'depreciationHistory' => $depreciationHistory,
'methods' => AssetRegister::getDepreciationMethods(),
]);
}
public function dispose(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$asset = $db->selectOne("SELECT * FROM `asset_register` WHERE `id` = ?", [(int) $id]);
if (!$asset) {
return $this->redirect('/inventory/assets')->withError('الأصل غير موجود');
}
if ($asset['status'] !== 'active') {
return $this->redirect('/inventory/assets/' . $id)->withError('الأصل غير نشط — لا يمكن التصرف فيه');
}
$disposalValue = trim((string) $request->post('disposal_value', '0'));
$reason = trim((string) $request->post('disposal_reason', ''));
$db->update('asset_register', [
'status' => 'disposed',
'disposed_at' => date('Y-m-d'),
'disposal_value' => $disposalValue,
'disposal_reason' => $reason ?: null,
'updated_at' => date('Y-m-d H:i:s'),
'updated_by' => $employee ? (int) $employee->id : null,
], '`id` = ?', [(int) $id]);
return $this->redirect('/inventory/assets/' . $id)->withSuccess('تم تسجيل التصرف في الأصل');
}
public function runDepreciation(Request $request): Response
{
$month = trim((string) $request->post('period_month', date('Y-m')));
if (!preg_match('/^\d{4}-\d{2}$/', $month)) {
return $this->redirect('/inventory/assets')->withError('صيغة الشهر غير صحيحة');
}
$count = DepreciationService::runMonthlyDepreciation($month);
return $this->redirect('/inventory/assets')->withSuccess('تم تشغيل الإهلاك لشهر ' . $month . ' — ' . $count . ' أصل');
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Inventory\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\Inventory\Models\ItemCategory;
class CategoryController extends Controller
{
public function index(Request $request): Response
{
$filters = [
'q' => trim((string) $request->get('q', '')),
'parent_id' => trim((string) $request->get('parent_id', '')),
];
$page = max(1, (int) $request->get('page', 1));
$result = ItemCategory::search($filters, 50, $page);
$tree = ItemCategory::getTree();
$roots = ItemCategory::getRoots();
return $this->view('Inventory.Views.categories.index', [
'categories' => $result['data'],
'pagination' => $result['pagination'],
'filters' => $filters,
'tree' => $tree,
'roots' => $roots,
]);
}
public function create(Request $request): Response
{
$roots = ItemCategory::getRoots();
return $this->view('Inventory.Views.categories.form', [
'category' => null,
'roots' => $roots,
]);
}
public function store(Request $request): Response
{
$data = $this->extractData($request);
$errors = $this->validate($data);
// Unique code
if ($data['code'] !== '') {
$existing = ItemCategory::query()->where('code', '=', $data['code'])->first();
if ($existing) {
$errors[] = 'كود التصنيف مستخدم بالفعل';
}
}
if (!empty($errors)) {
return $this->flashErrorsAndRedirect($errors, $request, '/inventory/categories/create');
}
$category = ItemCategory::create($data);
return $this->redirect('/inventory/categories')->withSuccess('تم إضافة التصنيف بنجاح');
}
public function edit(Request $request, string $id): Response
{
$category = ItemCategory::find((int) $id);
if (!$category) {
return $this->redirect('/inventory/categories')->withError('التصنيف غير موجود');
}
$roots = ItemCategory::getRoots();
return $this->view('Inventory.Views.categories.form', [
'category' => $category,
'roots' => $roots,
]);
}
public function update(Request $request, string $id): Response
{
$category = ItemCategory::find((int) $id);
if (!$category) {
return $this->redirect('/inventory/categories')->withError('التصنيف غير موجود');
}
$data = $this->extractData($request);
$errors = $this->validate($data);
// Unique code (exclude current)
if ($data['code'] !== '') {
$existing = ItemCategory::query()
->where('code', '=', $data['code'])
->whereRaw('`id` != ?', [(int) $id])
->first();
if ($existing) {
$errors[] = 'كود التصنيف مستخدم بالفعل';
}
}
// Prevent self-parenting
if ($data['parent_id'] !== null && (int) $data['parent_id'] === (int) $id) {
$errors[] = 'لا يمكن أن يكون التصنيف أبًا لنفسه';
}
if (!empty($errors)) {
return $this->flashErrorsAndRedirect($errors, $request, '/inventory/categories/' . $id . '/edit');
}
$category->update($data);
return $this->redirect('/inventory/categories')->withSuccess('تم تحديث التصنيف بنجاح');
}
private function extractData(Request $request): array
{
return [
'parent_id' => ((int) $request->post('parent_id', 0)) ?: null,
'code' => strtoupper(trim((string) $request->post('code', ''))),
'name_ar' => trim((string) $request->post('name_ar', '')),
'name_en' => trim((string) $request->post('name_en', '')) ?: null,
'description_ar' => trim((string) $request->post('description_ar', '')) ?: null,
'sort_order' => (int) $request->post('sort_order', 0),
'is_active' => (int) ($request->post('is_active', 1)),
];
}
private function validate(array $data): array
{
$errors = [];
if ($data['code'] === '') {
$errors[] = 'كود التصنيف مطلوب';
}
if ($data['name_ar'] === '' || mb_strlen($data['name_ar']) < 2) {
$errors[] = 'اسم التصنيف بالعربي مطلوب (حرفان على الأقل)';
}
return $errors;
}
private function flashErrorsAndRedirect(array $errors, Request $request, string $url): Response
{
$session = App::getInstance()->session();
$session->flash('_alerts', array_map(fn($e) => ['type' => 'error', 'message' => $e], $errors));
$session->flash('_old_input', $request->all());
return $this->redirect($url);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Inventory\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\Inventory\Models\Warehouse;
use App\Modules\Inventory\Models\ItemCategory;
use App\Modules\Inventory\Models\ItemWarehouseStock;
use App\Modules\Inventory\Models\ItemBatch;
class InventoryReportController extends Controller
{
public function stockBalance(Request $request): Response
{
$warehouseId = (int) $request->get('warehouse_id', 0);
$categoryId = (int) $request->get('category_id', 0);
$db = App::getInstance()->db();
$where = 'i.`deleted_at` IS NULL';
$params = [];
if ($warehouseId > 0) {
$where .= ' AND iws.`warehouse_id` = ?';
$params[] = $warehouseId;
}
if ($categoryId > 0) {
$where .= ' AND i.`category_id` = ?';
$params[] = $categoryId;
}
$rows = $db->select(
"SELECT i.`id`, i.`sku`, i.`name_ar`, i.`unit_of_measure`, i.`cost_price`,
w.`name_ar` as warehouse_name, iws.`quantity`, iws.`min_level`, iws.`max_level`,
(i.`cost_price` * iws.`quantity`) as stock_value
FROM `item_warehouse_stock` iws
JOIN `inventory_items` i ON i.`id` = iws.`item_id`
JOIN `warehouses` w ON w.`id` = iws.`warehouse_id`
WHERE {$where}
ORDER BY i.`name_ar` ASC",
$params
);
$totalValue = array_reduce($rows, fn($carry, $r) => bcadd($carry, (string) ($r['stock_value'] ?? 0), 2), '0.00');
return $this->view('Inventory.Views.reports.stock_balance', [
'rows' => $rows,
'totalValue' => $totalValue,
'warehouses' => Warehouse::allActive(),
'categories' => ItemCategory::allActive(),
'warehouseId' => $warehouseId,
'categoryId' => $categoryId,
]);
}
public function movements(Request $request): Response
{
$db = App::getInstance()->db();
$warehouseId = (int) $request->get('warehouse_id', 0);
$dateFrom = trim((string) $request->get('date_from', date('Y-m-01')));
$dateTo = trim((string) $request->get('date_to', date('Y-m-d')));
$where = 'm.`movement_date` BETWEEN ? AND ?';
$params = [$dateFrom, $dateTo];
if ($warehouseId > 0) {
$where .= ' AND m.`warehouse_id` = ?';
$params[] = $warehouseId;
}
$summary = $db->select(
"SELECT m.`movement_type`, m.`direction`,
COUNT(*) as move_count,
SUM(m.`quantity`) as total_qty,
SUM(m.`total_cost`) as total_cost
FROM `stock_movements` m
WHERE {$where}
GROUP BY m.`movement_type`, m.`direction`
ORDER BY m.`direction`, m.`movement_type`",
$params
);
return $this->view('Inventory.Views.reports.movements', [
'summary' => $summary,
'warehouses' => Warehouse::allActive(),
'warehouseId' => $warehouseId,
'dateFrom' => $dateFrom,
'dateTo' => $dateTo,
]);
}
public function expiryAlert(Request $request): Response
{
$days = max(1, (int) $request->get('days', 30));
$nearExpiry = ItemBatch::getNearExpiry($days);
$expired = ItemBatch::getExpired();
return $this->view('Inventory.Views.reports.expiry_alert', [
'nearExpiry' => $nearExpiry,
'expired' => $expired,
'days' => $days,
]);
}
public function lowStock(Request $request): Response
{
$rows = ItemWarehouseStock::getLowStockItems();
return $this->view('Inventory.Views.reports.low_stock', [
'rows' => $rows,
]);
}
public function depreciationSchedule(Request $request): Response
{
$db = App::getInstance()->db();
$month = trim((string) $request->get('month', date('Y-m')));
$entries = $db->select(
"SELECT de.*, ar.`asset_tag`, ar.`purchase_cost`, ar.`salvage_value`, ar.`useful_life_months`,
i.`name_ar` as item_name, w.`name_ar` as warehouse_name
FROM `depreciation_entries` de
JOIN `asset_register` ar ON ar.`id` = de.`asset_id`
JOIN `inventory_items` i ON i.`id` = ar.`item_id`
JOIN `warehouses` w ON w.`id` = ar.`warehouse_id`
WHERE de.`period_month` = ?
ORDER BY ar.`asset_tag` ASC",
[$month]
);
$totalDep = array_reduce($entries, fn($c, $r) => bcadd($c, (string) ($r['depreciation_amount'] ?? 0), 2), '0.00');
return $this->view('Inventory.Views.reports.depreciation', [
'entries' => $entries,
'totalDep' => $totalDep,
'month' => $month,
]);
}
public function auditVariance(Request $request): Response
{
$db = App::getInstance()->db();
$auditId = (int) $request->get('audit_id', 0);
$audits = $db->select(
"SELECT a.`id`, a.`audit_number`, a.`audit_date`, w.`name_ar` as warehouse_name
FROM `stock_audits` a
JOIN `warehouses` w ON w.`id` = a.`warehouse_id`
WHERE a.`status` = 'approved'
ORDER BY a.`audit_date` DESC LIMIT 50"
);
$variances = [];
if ($auditId > 0) {
$variances = $db->select(
"SELECT sai.*, i.`name_ar` as item_name, i.`sku`, i.`cost_price`
FROM `stock_audit_items` sai
JOIN `inventory_items` i ON i.`id` = sai.`item_id`
WHERE sai.`audit_id` = ? AND sai.`variance` != 0
ORDER BY ABS(sai.`variance`) DESC",
[$auditId]
);
}
return $this->view('Inventory.Views.reports.audit_variance', [
'audits' => $audits,
'variances' => $variances,
'auditId' => $auditId,
]);
}
public function supplierHistory(Request $request): Response
{
$db = App::getInstance()->db();
$supplierId = (int) $request->get('supplier_id', 0);
$suppliers = $db->select(
"SELECT `id`, `name_ar`, `code` FROM `suppliers` WHERE `deleted_at` IS NULL ORDER BY `name_ar` ASC"
);
$orders = [];
if ($supplierId > 0) {
$orders = $db->select(
"SELECT po.*, w.`name_ar` as warehouse_name
FROM `purchase_orders` po
JOIN `warehouses` w ON w.`id` = po.`warehouse_id`
WHERE po.`supplier_id` = ?
ORDER BY po.`created_at` DESC LIMIT 50",
[$supplierId]
);
}
return $this->view('Inventory.Views.reports.supplier_history', [
'suppliers' => $suppliers,
'orders' => $orders,
'supplierId' => $supplierId,
]);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Inventory\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\Inventory\Models\InventoryItem;
use App\Modules\Inventory\Models\ItemCategory;
use App\Modules\Inventory\Models\Warehouse;
use App\Modules\Inventory\Models\ItemWarehouseStock;
use App\Modules\Inventory\Services\StockService;
class ItemController extends Controller
{
public function index(Request $request): Response
{
$filters = [
'q' => trim((string) $request->get('q', '')),
'category_id' => trim((string) $request->get('category_id', '')),
'tracking_type' => trim((string) $request->get('tracking_type', '')),
'is_sellable' => $request->get('is_sellable', ''),
'warehouse_id' => trim((string) $request->get('warehouse_id', '')),
];
$page = max(1, (int) $request->get('page', 1));
$result = InventoryItem::search($filters, 25, $page);
return $this->view('Inventory.Views.items.index', [
'items' => $result['data'],
'pagination' => $result['pagination'],
'filters' => $filters,
'categories' => ItemCategory::allActive(),
'trackingTypes' => InventoryItem::getTrackingTypes(),
'warehouses' => Warehouse::allActive(),
]);
}
public function create(Request $request): Response
{
return $this->view('Inventory.Views.items.form', [
'item' => null,
'categories' => ItemCategory::allActive(),
'trackingTypes' => InventoryItem::getTrackingTypes(),
'units' => InventoryItem::getUnitsOfMeasure(),
]);
}
public function store(Request $request): Response
{
$data = $this->extractData($request);
$errors = $this->validate($data);
// Unique SKU
if ($data['sku'] !== '') {
$existing = InventoryItem::query()->where('sku', '=', $data['sku'])->first();
if ($existing) {
$errors[] = 'كود الصنف (SKU) مستخدم بالفعل';
}
}
if (!empty($errors)) {
return $this->flashErrorsAndRedirect($errors, $request, '/inventory/items/create');
}
$item = InventoryItem::create($data);
return $this->redirect('/inventory/items/' . $item->id)->withSuccess('تم إضافة الصنف بنجاح');
}
public function show(Request $request, string $id): Response
{
$item = InventoryItem::find((int) $id);
if (!$item) {
return $this->redirect('/inventory/items')->withError('الصنف غير موجود');
}
$db = App::getInstance()->db();
// Category name
$category = $item->category_id ? ItemCategory::find((int) $item->category_id) : null;
// Stock per warehouse
$stockRecords = $db->select(
"SELECT iws.*, w.`name_ar` as warehouse_name, w.`code` as warehouse_code
FROM `item_warehouse_stock` iws
JOIN `warehouses` w ON w.`id` = iws.`warehouse_id`
WHERE iws.`item_id` = ?
ORDER BY w.`name_ar` ASC",
[(int) $id]
);
$totalStock = InventoryItem::getTotalStock((int) $id);
// Recent movements
$movements = StockService::getMovements((int) $id, null, 20);
return $this->view('Inventory.Views.items.show', [
'item' => $item,
'category' => $category,
'stockRecords' => $stockRecords,
'totalStock' => $totalStock,
'movements' => $movements,
'trackingTypes' => InventoryItem::getTrackingTypes(),
'units' => InventoryItem::getUnitsOfMeasure(),
]);
}
public function edit(Request $request, string $id): Response
{
$item = InventoryItem::find((int) $id);
if (!$item) {
return $this->redirect('/inventory/items')->withError('الصنف غير موجود');
}
return $this->view('Inventory.Views.items.form', [
'item' => $item,
'categories' => ItemCategory::allActive(),
'trackingTypes' => InventoryItem::getTrackingTypes(),
'units' => InventoryItem::getUnitsOfMeasure(),
]);
}
public function update(Request $request, string $id): Response
{
$item = InventoryItem::find((int) $id);
if (!$item) {
return $this->redirect('/inventory/items')->withError('الصنف غير موجود');
}
$data = $this->extractData($request);
$errors = $this->validate($data);
// Unique SKU (exclude current)
if ($data['sku'] !== '') {
$existing = InventoryItem::query()
->where('sku', '=', $data['sku'])
->whereRaw('`id` != ?', [(int) $id])
->first();
if ($existing) {
$errors[] = 'كود الصنف (SKU) مستخدم بالفعل';
}
}
if (!empty($errors)) {
return $this->flashErrorsAndRedirect($errors, $request, '/inventory/items/' . $id . '/edit');
}
$item->update($data);
return $this->redirect('/inventory/items/' . $id)->withSuccess('تم تحديث الصنف بنجاح');
}
/**
* JSON search endpoint for AJAX item lookup (POS, transfers, etc.)
*/
public function searchJson(Request $request): Response
{
$q = trim((string) $request->get('q', ''));
if (mb_strlen($q) < 2) {
return $this->json(['results' => []]);
}
$db = App::getInstance()->db();
$search = '%' . $q . '%';
$rows = $db->select(
"SELECT `id`, `sku`, `name_ar`, `sale_price_member`, `sale_price_nonmember`, `unit_of_measure`, `tracking_type`
FROM `inventory_items`
WHERE (`name_ar` LIKE ? OR `sku` LIKE ? OR `barcode` LIKE ?)
AND `is_active` = 1 AND `deleted_at` IS NULL
ORDER BY `name_ar` ASC LIMIT 20",
[$search, $search, $search]
);
return $this->json(['results' => $rows]);
}
private function extractData(Request $request): array
{
return [
'category_id' => ((int) $request->post('category_id', 0)) ?: null,
'sku' => strtoupper(trim((string) $request->post('sku', ''))),
'barcode' => trim((string) $request->post('barcode', '')) ?: null,
'name_ar' => trim((string) $request->post('name_ar', '')),
'name_en' => trim((string) $request->post('name_en', '')) ?: null,
'description_ar' => trim((string) $request->post('description_ar', '')) ?: null,
'unit_of_measure' => trim((string) $request->post('unit_of_measure', 'piece')),
'tracking_type' => trim((string) $request->post('tracking_type', 'standard')),
'cost_price' => trim((string) $request->post('cost_price', '0.00')),
'sale_price_member' => trim((string) $request->post('sale_price_member', '0.00')),
'sale_price_nonmember' => trim((string) $request->post('sale_price_nonmember', '0.00')),
'sale_price_player' => trim((string) $request->post('sale_price_player', '')) ?: null,
'tax_rate' => trim((string) $request->post('tax_rate', '14.00')),
'is_sellable' => (int) ($request->post('is_sellable', 1)),
'is_active' => (int) ($request->post('is_active', 1)),
'notes' => trim((string) $request->post('notes', '')) ?: null,
];
}
private function validate(array $data): array
{
$errors = [];
if ($data['sku'] === '') {
$errors[] = 'كود الصنف (SKU) مطلوب';
}
if ($data['name_ar'] === '' || mb_strlen($data['name_ar']) < 2) {
$errors[] = 'اسم الصنف بالعربي مطلوب (حرفان على الأقل)';
}
if (!array_key_exists($data['tracking_type'], InventoryItem::getTrackingTypes())) {
$errors[] = 'نوع التتبع غير صالح';
}
if (!array_key_exists($data['unit_of_measure'], InventoryItem::getUnitsOfMeasure())) {
$errors[] = 'وحدة القياس غير صالحة';
}
return $errors;
}
private function flashErrorsAndRedirect(array $errors, Request $request, string $url): Response
{
$session = App::getInstance()->session();
$session->flash('_alerts', array_map(fn($e) => ['type' => 'error', 'message' => $e], $errors));
$session->flash('_old_input', $request->all());
return $this->redirect($url);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Inventory\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\Inventory\Models\Supplier;
use App\Modules\Inventory\Models\PurchaseOrder;
use App\Modules\Inventory\Models\PurchaseOrderItem;
use App\Modules\Inventory\Models\InventoryItem;
use App\Modules\Inventory\Models\Warehouse;
use App\Modules\Inventory\Services\PurchaseOrderService;
class PurchaseOrderController extends Controller
{
public function index(Request $request): Response
{
$filters = [
'status' => trim((string) $request->get('status', '')),
'supplier_id' => trim((string) $request->get('supplier_id', '')),
'warehouse_id' => trim((string) $request->get('warehouse_id', '')),
'date_from' => trim((string) $request->get('date_from', '')),
'date_to' => trim((string) $request->get('date_to', '')),
];
$page = max(1, (int) $request->get('page', 1));
$result = PurchaseOrder::search($filters, 25, $page);
return $this->view('Inventory.Views.purchase_orders.index', [
'orders' => $result['data'],
'pagination' => $result['pagination'],
'filters' => $filters,
'suppliers' => Supplier::allActive(),
'warehouses' => Warehouse::allActive(),
'statuses' => PurchaseOrder::getStatuses(),
]);
}
public function create(Request $request): Response
{
return $this->view('Inventory.Views.purchase_orders.form', [
'suppliers' => Supplier::allActive(),
'warehouses' => Warehouse::allActive(),
'items' => InventoryItem::allActive(),
]);
}
public function store(Request $request): Response
{
$header = [
'supplier_id' => (int) $request->post('supplier_id', 0),
'warehouse_id' => (int) $request->post('warehouse_id', 0),
'expected_delivery_date' => trim((string) $request->post('expected_delivery_date', '')) ?: null,
'notes' => trim((string) $request->post('notes', '')) ?: null,
];
$itemIds = $request->post('item_ids', []);
$quantities = $request->post('quantities', []);
$unitPrices = $request->post('unit_prices', []);
$itemNotes = $request->post('item_notes', []);
if (!is_array($itemIds) || empty($itemIds)) {
return $this->redirect('/inventory/purchase-orders/create')->withError('يجب إضافة صنف واحد على الأقل');
}
$items = [];
foreach ($itemIds as $i => $itemId) {
$qty = $quantities[$i] ?? '0';
$price = $unitPrices[$i] ?? '0';
if ((int) $itemId > 0 && bccomp((string) $qty, '0', 3) > 0) {
$items[] = [
'item_id' => (int) $itemId,
'quantity_ordered' => (string) $qty,
'unit_price' => (string) $price,
'notes' => $itemNotes[$i] ?? null,
];
}
}
try {
$poId = PurchaseOrderService::createPO($header, $items);
return $this->redirect('/inventory/purchase-orders/' . $poId)->withSuccess('تم إنشاء أمر الشراء بنجاح');
} catch (\RuntimeException $e) {
return $this->redirect('/inventory/purchase-orders/create')->withError($e->getMessage());
}
}
public function show(Request $request, string $id): Response
{
$po = PurchaseOrder::find((int) $id);
if (!$po) {
return $this->redirect('/inventory/purchase-orders')->withError('أمر الشراء غير موجود');
}
$items = PurchaseOrderItem::getForPO((int) $id);
$db = App::getInstance()->db();
$supplier = $db->selectOne("SELECT `name_ar`, `code`, `phone`, `email` FROM `suppliers` WHERE `id` = ?", [(int) $po->supplier_id]);
return $this->view('Inventory.Views.purchase_orders.show', [
'po' => $po,
'items' => $items,
'supplier' => $supplier,
'statuses' => PurchaseOrder::getStatuses(),
]);
}
public function submit(Request $request, string $id): Response
{
try {
PurchaseOrderService::submitPO((int) $id);
return $this->redirect('/inventory/purchase-orders/' . $id)->withSuccess('تم تقديم أمر الشراء للاعتماد');
} catch (\RuntimeException $e) {
return $this->redirect('/inventory/purchase-orders/' . $id)->withError($e->getMessage());
}
}
public function approve(Request $request, string $id): Response
{
try {
PurchaseOrderService::approvePO((int) $id);
return $this->redirect('/inventory/purchase-orders/' . $id)->withSuccess('تم اعتماد أمر الشراء');
} catch (\RuntimeException $e) {
return $this->redirect('/inventory/purchase-orders/' . $id)->withError($e->getMessage());
}
}
public function receiveForm(Request $request, string $id): Response
{
$po = PurchaseOrder::find((int) $id);
if (!$po) {
return $this->redirect('/inventory/purchase-orders')->withError('أمر الشراء غير موجود');
}
if (!in_array($po->status, ['approved', 'partially_received'], true)) {
return $this->redirect('/inventory/purchase-orders/' . $id)->withError('أمر الشراء ليس في حالة تسمح بالاستلام');
}
$items = PurchaseOrderItem::getForPO((int) $id);
return $this->view('Inventory.Views.purchase_orders.receive', [
'po' => $po,
'items' => $items,
]);
}
public function receive(Request $request, string $id): Response
{
$poItemIds = $request->post('po_item_ids', []);
$quantitiesRcvd = $request->post('quantities_received', []);
$batchNumbers = $request->post('batch_numbers', []);
$expiryDates = $request->post('expiry_dates', []);
if (!is_array($poItemIds) || empty($poItemIds)) {
return $this->redirect('/inventory/purchase-orders/' . $id . '/receive')->withError('لا توجد أصناف للاستلام');
}
$receivedItems = [];
foreach ($poItemIds as $i => $poItemId) {
$qty = $quantitiesRcvd[$i] ?? '0';
if ((int) $poItemId > 0 && bccomp((string) $qty, '0', 3) > 0) {
$receivedItems[] = [
'po_item_id' => (int) $poItemId,
'quantity_received' => (string) $qty,
'batch_number' => $batchNumbers[$i] ?? null,
'expiry_date' => $expiryDates[$i] ?? null,
];
}
}
try {
PurchaseOrderService::receivePO((int) $id, $receivedItems);
return $this->redirect('/inventory/purchase-orders/' . $id)->withSuccess('تم استلام الأصناف وتحديث الأرصدة بنجاح');
} catch (\RuntimeException $e) {
return $this->redirect('/inventory/purchase-orders/' . $id . '/receive')->withError($e->getMessage());
}
}
public function cancel(Request $request, string $id): Response
{
try {
PurchaseOrderService::cancelPO((int) $id);
return $this->redirect('/inventory/purchase-orders/' . $id)->withSuccess('تم إلغاء أمر الشراء');
} catch (\RuntimeException $e) {
return $this->redirect('/inventory/purchase-orders/' . $id)->withError($e->getMessage());
}
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Inventory\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\Inventory\Models\StockAudit;
use App\Modules\Inventory\Models\StockAuditItem;
use App\Modules\Inventory\Models\Warehouse;
use App\Modules\Inventory\Models\ItemCategory;
use App\Modules\Inventory\Services\StockAuditService;
class StockAuditController extends Controller
{
public function index(Request $request): Response
{
$filters = [
'status' => trim((string) $request->get('status', '')),
'audit_type' => trim((string) $request->get('audit_type', '')),
'warehouse_id' => trim((string) $request->get('warehouse_id', '')),
];
$page = max(1, (int) $request->get('page', 1));
$result = StockAudit::search($filters, 25, $page);
return $this->view('Inventory.Views.audits.index', [
'audits' => $result['data'],
'pagination' => $result['pagination'],
'filters' => $filters,
'statuses' => StockAudit::getStatuses(),
'auditTypes' => StockAudit::getAuditTypes(),
'warehouses' => Warehouse::allActive(),
]);
}
public function create(Request $request): Response
{
return $this->view('Inventory.Views.audits.form', [
'auditTypes' => StockAudit::getAuditTypes(),
'warehouses' => Warehouse::allActive(),
'categories' => ItemCategory::allActive(),
]);
}
public function store(Request $request): Response
{
$warehouseId = (int) $request->post('warehouse_id', 0);
$auditType = trim((string) $request->post('audit_type', ''));
$categoryId = ((int) $request->post('category_id', 0)) ?: null;
$notes = trim((string) $request->post('notes', ''));
$errors = [];
if ($warehouseId <= 0) $errors[] = 'يجب اختيار المخزن';
if (!array_key_exists($auditType, StockAudit::getAuditTypes())) $errors[] = 'نوع الجرد غير صالح';
if (!empty($errors)) {
$session = App::getInstance()->session();
$session->flash('_alerts', array_map(fn($e) => ['type' => 'error', 'message' => $e], $errors));
$session->flash('_old_input', $request->all());
return $this->redirect('/inventory/audits/create');
}
try {
$auditId = StockAuditService::createAudit($warehouseId, $auditType, $categoryId, $notes ?: null);
return $this->redirect('/inventory/audits/' . $auditId . '/count')->withSuccess('تم إنشاء الجرد — يمكنك البدء في العد');
} catch (\RuntimeException $e) {
return $this->redirect('/inventory/audits/create')->withError($e->getMessage());
}
}
public function show(Request $request, string $id): Response
{
$audit = StockAudit::find((int) $id);
if (!$audit) {
return $this->redirect('/inventory/audits')->withError('الجرد غير موجود');
}
$items = StockAuditItem::getForAudit((int) $id);
$db = App::getInstance()->db();
$warehouse = $db->selectOne("SELECT `name_ar`, `code` FROM `warehouses` WHERE `id` = ?", [(int) $audit->warehouse_id]);
return $this->view('Inventory.Views.audits.show', [
'audit' => $audit,
'items' => $items,
'warehouse' => $warehouse,
]);
}
public function count(Request $request, string $id): Response
{
$audit = StockAudit::find((int) $id);
if (!$audit) {
return $this->redirect('/inventory/audits')->withError('الجرد غير موجود');
}
if (!in_array($audit->status, ['in_progress', 'draft'], true)) {
return $this->redirect('/inventory/audits/' . $id)->withError('لا يمكن إدخال العد في هذه الحالة');
}
$items = StockAuditItem::getForAudit((int) $id);
return $this->view('Inventory.Views.audits.count', [
'audit' => $audit,
'items' => $items,
]);
}
public function saveCount(Request $request, string $id): Response
{
$audit = StockAudit::find((int) $id);
if (!$audit) {
return $this->redirect('/inventory/audits')->withError('الجرد غير موجود');
}
$physicalQtys = $request->post('physical_quantities', []);
$counts = [];
foreach ($physicalQtys as $auditItemId => $qty) {
if ($qty !== '' && $qty !== null) {
$counts[(int) $auditItemId] = (string) $qty;
}
}
if (empty($counts)) {
return $this->redirect('/inventory/audits/' . $id . '/count')->withError('يجب إدخال كمية واحدة على الأقل');
}
StockAuditService::saveCounts((int) $id, $counts);
return $this->redirect('/inventory/audits/' . $id)->withSuccess('تم حفظ العد — في انتظار الاعتماد');
}
public function approve(Request $request, string $id): Response
{
try {
StockAuditService::approveAudit((int) $id);
return $this->redirect('/inventory/audits/' . $id)->withSuccess('تم اعتماد الجرد وتنفيذ التسويات');
} catch (\RuntimeException $e) {
return $this->redirect('/inventory/audits/' . $id)->withError($e->getMessage());
}
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Inventory\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\Inventory\Models\StockMovement;
use App\Modules\Inventory\Models\InventoryItem;
use App\Modules\Inventory\Models\Warehouse;
use App\Modules\Inventory\Services\StockService;
use App\Modules\Inventory\Services\BatchService;
class StockMovementController extends Controller
{
public function index(Request $request): Response
{
$filters = [
'warehouse_id' => trim((string) $request->get('warehouse_id', '')),
'movement_type' => trim((string) $request->get('movement_type', '')),
'direction' => trim((string) $request->get('direction', '')),
'date_from' => trim((string) $request->get('date_from', '')),
'date_to' => trim((string) $request->get('date_to', '')),
];
$page = max(1, (int) $request->get('page', 1));
$result = StockMovement::search($filters, 25, $page);
return $this->view('Inventory.Views.stock.movements', [
'movements' => $result['data'],
'pagination' => $result['pagination'],
'filters' => $filters,
'warehouses' => Warehouse::allActive(),
'movementTypes' => StockMovement::getMovementTypes(),
]);
}
public function create(Request $request): Response
{
return $this->view('Inventory.Views.stock.movement_form', [
'warehouses' => Warehouse::allActive(),
'items' => InventoryItem::allActive(),
]);
}
public function store(Request $request): Response
{
$itemId = (int) $request->post('item_id', 0);
$warehouseId = (int) $request->post('warehouse_id', 0);
$direction = trim((string) $request->post('direction', ''));
$moveType = trim((string) $request->post('movement_type', ''));
$quantity = trim((string) $request->post('quantity', ''));
$unitCost = trim((string) $request->post('unit_cost', ''));
$notes = trim((string) $request->post('notes', ''));
$moveDate = trim((string) $request->post('movement_date', date('Y-m-d')));
// Batch fields for expiry items
$batchNumber = trim((string) $request->post('batch_number', ''));
$expiryDate = trim((string) $request->post('expiry_date', ''));
$errors = [];
if ($itemId <= 0) $errors[] = 'يجب اختيار الصنف';
if ($warehouseId <= 0) $errors[] = 'يجب اختيار المخزن';
if (!in_array($direction, ['in', 'out'], true)) $errors[] = 'اتجاه الحركة غير صالح';
if (bccomp($quantity, '0', 3) <= 0) $errors[] = 'الكمية يجب أن تكون أكبر من صفر';
if (!empty($errors)) {
$session = App::getInstance()->session();
$session->flash('_alerts', array_map(fn($e) => ['type' => 'error', 'message' => $e], $errors));
$session->flash('_old_input', $request->all());
return $this->redirect('/inventory/movements/create');
}
try {
$batchId = null;
// Check if the item is expiry-tracked and direction is IN — create a batch
$db = App::getInstance()->db();
$item = $db->selectOne(
"SELECT `tracking_type` FROM `inventory_items` WHERE `id` = ?",
[$itemId]
);
if ($item && $item['tracking_type'] === 'expiry' && $direction === 'in') {
if ($batchNumber === '' || $expiryDate === '') {
$session = App::getInstance()->session();
$session->flash('_alerts', [['type' => 'error', 'message' => 'رقم الدفعة وتاريخ الصلاحية مطلوبان للأصناف ذات تاريخ انتهاء']]);
$session->flash('_old_input', $request->all());
return $this->redirect('/inventory/movements/create');
}
$batchId = BatchService::createBatch([
'item_id' => $itemId,
'warehouse_id' => $warehouseId,
'batch_number' => $batchNumber,
'expiry_date' => $expiryDate,
'quantity' => $quantity,
'unit_cost' => $unitCost ?: null,
'received_date' => $moveDate,
]);
}
// For expiry OUT, use FEFO allocation
if ($item && $item['tracking_type'] === 'expiry' && $direction === 'out') {
$allocations = BatchService::allocateBatch($itemId, $warehouseId, $quantity);
foreach ($allocations as $alloc) {
StockService::moveStock([
'item_id' => $itemId,
'warehouse_id' => $warehouseId,
'movement_type' => $moveType,
'direction' => 'out',
'quantity' => $alloc['quantity'],
'unit_cost' => $unitCost ?: null,
'batch_id' => $alloc['batch_id'],
'notes' => $notes,
'movement_date' => $moveDate,
]);
}
return $this->redirect('/inventory/movements')->withSuccess('تم تسجيل الحركة بنجاح');
}
StockService::moveStock([
'item_id' => $itemId,
'warehouse_id' => $warehouseId,
'movement_type' => $moveType,
'direction' => $direction,
'quantity' => $quantity,
'unit_cost' => $unitCost ?: null,
'batch_id' => $batchId,
'notes' => $notes,
'movement_date' => $moveDate,
]);
return $this->redirect('/inventory/movements')->withSuccess('تم تسجيل الحركة بنجاح');
} catch (\RuntimeException $e) {
return $this->redirect('/inventory/movements/create')->withError($e->getMessage());
}
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Inventory\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\Inventory\Models\StockTransfer;
use App\Modules\Inventory\Models\StockTransferItem;
use App\Modules\Inventory\Models\InventoryItem;
use App\Modules\Inventory\Models\Warehouse;
use App\Modules\Inventory\Services\StockTransferService;
class StockTransferController extends Controller
{
public function index(Request $request): Response
{
$filters = [
'status' => trim((string) $request->get('status', '')),
'from_warehouse_id' => trim((string) $request->get('from_warehouse_id', '')),
'to_warehouse_id' => trim((string) $request->get('to_warehouse_id', '')),
'date_from' => trim((string) $request->get('date_from', '')),
'date_to' => trim((string) $request->get('date_to', '')),
];
$page = max(1, (int) $request->get('page', 1));
$result = StockTransfer::search($filters, 25, $page);
return $this->view('Inventory.Views.stock.transfers', [
'transfers' => $result['data'],
'pagination' => $result['pagination'],
'filters' => $filters,
'warehouses' => Warehouse::allActive(),
'statuses' => StockTransfer::getStatuses(),
]);
}
public function create(Request $request): Response
{
return $this->view('Inventory.Views.stock.transfer_form', [
'warehouses' => Warehouse::allActive(),
'items' => InventoryItem::allActive(),
]);
}
public function store(Request $request): Response
{
$fromWarehouse = (int) $request->post('from_warehouse_id', 0);
$toWarehouse = (int) $request->post('to_warehouse_id', 0);
$transferDate = trim((string) $request->post('transfer_date', date('Y-m-d')));
$notes = trim((string) $request->post('notes', ''));
$itemIds = $request->post('item_ids', []);
$quantities = $request->post('quantities', []);
if (!is_array($itemIds) || empty($itemIds)) {
return $this->redirect('/inventory/transfers/create')->withError('يجب إضافة صنف واحد على الأقل');
}
$items = [];
foreach ($itemIds as $i => $itemId) {
$qty = $quantities[$i] ?? '0';
if ((int) $itemId > 0 && bccomp((string) $qty, '0', 3) > 0) {
$items[] = [
'item_id' => (int) $itemId,
'quantity' => (string) $qty,
];
}
}
try {
$transferId = StockTransferService::createTransfer([
'from_warehouse_id' => $fromWarehouse,
'to_warehouse_id' => $toWarehouse,
'transfer_date' => $transferDate,
'notes' => $notes,
], $items);
return $this->redirect('/inventory/transfers/' . $transferId)->withSuccess('تم إنشاء طلب التحويل بنجاح');
} catch (\RuntimeException $e) {
return $this->redirect('/inventory/transfers/create')->withError($e->getMessage());
}
}
public function show(Request $request, string $id): Response
{
$transfer = StockTransfer::find((int) $id);
if (!$transfer) {
return $this->redirect('/inventory/transfers')->withError('التحويل غير موجود');
}
$items = StockTransferItem::getForTransfer((int) $id);
$db = App::getInstance()->db();
$fromWarehouse = $db->selectOne("SELECT `name_ar`, `code` FROM `warehouses` WHERE `id` = ?", [(int) $transfer->from_warehouse_id]);
$toWarehouse = $db->selectOne("SELECT `name_ar`, `code` FROM `warehouses` WHERE `id` = ?", [(int) $transfer->to_warehouse_id]);
return $this->view('Inventory.Views.stock.transfer_show', [
'transfer' => $transfer,
'items' => $items,
'fromWarehouse' => $fromWarehouse,
'toWarehouse' => $toWarehouse,
'statuses' => StockTransfer::getStatuses(),
]);
}
public function approve(Request $request, string $id): Response
{
try {
StockTransferService::approve((int) $id);
return $this->redirect('/inventory/transfers/' . $id)->withSuccess('تمت الموافقة على التحويل');
} catch (\RuntimeException $e) {
return $this->redirect('/inventory/transfers/' . $id)->withError($e->getMessage());
}
}
public function receive(Request $request, string $id): Response
{
try {
StockTransferService::receive((int) $id);
return $this->redirect('/inventory/transfers/' . $id)->withSuccess('تم استلام التحويل وتحديث الأرصدة بنجاح');
} catch (\RuntimeException $e) {
return $this->redirect('/inventory/transfers/' . $id)->withError($e->getMessage());
}
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Inventory\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\Inventory\Models\Supplier;
class SupplierController extends Controller
{
public function index(Request $request): Response
{
$filters = [
'q' => trim((string) $request->get('q', '')),
'is_active' => $request->get('is_active', ''),
];
$page = max(1, (int) $request->get('page', 1));
$result = Supplier::search($filters, 25, $page);
return $this->view('Inventory.Views.suppliers.index', [
'suppliers' => $result['data'],
'pagination' => $result['pagination'],
'filters' => $filters,
]);
}
public function create(Request $request): Response
{
return $this->view('Inventory.Views.suppliers.form', [
'supplier' => null,
]);
}
public function store(Request $request): Response
{
$data = $this->extractData($request);
$errors = $this->validateSupplier($data);
// Unique code
if ($data['code'] !== '') {
$existing = Supplier::query()->where('code', '=', $data['code'])->first();
if ($existing) {
$errors[] = 'كود المورد مستخدم بالفعل';
}
}
if (!empty($errors)) {
return $this->flashErrorsAndRedirect($errors, $request, '/inventory/suppliers/create');
}
$supplier = Supplier::create($data);
return $this->redirect('/inventory/suppliers/' . $supplier->id)->withSuccess('تم إضافة المورد بنجاح');
}
public function show(Request $request, string $id): Response
{
$supplier = Supplier::find((int) $id);
if (!$supplier) {
return $this->redirect('/inventory/suppliers')->withError('المورد غير موجود');
}
$db = App::getInstance()->db();
$recentPOs = $db->select(
"SELECT * FROM `purchase_orders` WHERE `supplier_id` = ? ORDER BY `created_at` DESC LIMIT 10",
[(int) $id]
);
return $this->view('Inventory.Views.suppliers.show', [
'supplier' => $supplier,
'recentPOs' => $recentPOs,
]);
}
public function edit(Request $request, string $id): Response
{
$supplier = Supplier::find((int) $id);
if (!$supplier) {
return $this->redirect('/inventory/suppliers')->withError('المورد غير موجود');
}
return $this->view('Inventory.Views.suppliers.form', [
'supplier' => $supplier,
]);
}
public function update(Request $request, string $id): Response
{
$supplier = Supplier::find((int) $id);
if (!$supplier) {
return $this->redirect('/inventory/suppliers')->withError('المورد غير موجود');
}
$data = $this->extractData($request);
$errors = $this->validateSupplier($data);
// Unique code (exclude current)
if ($data['code'] !== '') {
$existing = Supplier::query()
->where('code', '=', $data['code'])
->whereRaw('`id` != ?', [(int) $id])
->first();
if ($existing) {
$errors[] = 'كود المورد مستخدم بالفعل';
}
}
if (!empty($errors)) {
return $this->flashErrorsAndRedirect($errors, $request, '/inventory/suppliers/' . $id . '/edit');
}
$supplier->update($data);
return $this->redirect('/inventory/suppliers/' . $id)->withSuccess('تم تحديث بيانات المورد بنجاح');
}
private function extractData(Request $request): array
{
return [
'code' => strtoupper(trim((string) $request->post('code', ''))),
'name_ar' => trim((string) $request->post('name_ar', '')),
'name_en' => trim((string) $request->post('name_en', '')) ?: null,
'contact_person' => trim((string) $request->post('contact_person', '')) ?: null,
'phone' => trim((string) $request->post('phone', '')) ?: null,
'email' => trim((string) $request->post('email', '')) ?: null,
'address_ar' => trim((string) $request->post('address_ar', '')) ?: null,
'tax_number' => trim((string) $request->post('tax_number', '')) ?: null,
'payment_terms' => trim((string) $request->post('payment_terms', '')) ?: null,
'rating' => ((int) $request->post('rating', 0)) ?: null,
'is_active' => (int) ($request->post('is_active', 1)),
'notes' => trim((string) $request->post('notes', '')) ?: null,
];
}
private function validateSupplier(array $data): array
{
$errors = [];
if ($data['code'] === '') {
$errors[] = 'كود المورد مطلوب';
}
if ($data['name_ar'] === '' || mb_strlen($data['name_ar']) < 2) {
$errors[] = 'اسم المورد بالعربي مطلوب (حرفان على الأقل)';
}
return $errors;
}
private function flashErrorsAndRedirect(array $errors, Request $request, string $url): Response
{
$session = App::getInstance()->session();
$session->flash('_alerts', array_map(fn($e) => ['type' => 'error', 'message' => $e], $errors));
$session->flash('_old_input', $request->all());
return $this->redirect($url);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Inventory\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\Inventory\Models\Warehouse;
class WarehouseController extends Controller
{
public function index(Request $request): Response
{
$filters = [
'q' => trim((string) $request->get('q', '')),
'warehouse_type' => trim((string) $request->get('warehouse_type', '')),
'is_active' => $request->get('is_active', ''),
];
$page = max(1, (int) $request->get('page', 1));
$result = Warehouse::search($filters, 25, $page);
return $this->view('Inventory.Views.warehouses.index', [
'warehouses' => $result['data'],
'pagination' => $result['pagination'],
'filters' => $filters,
'warehouseTypes' => Warehouse::getTypes(),
]);
}
public function create(Request $request): Response
{
$db = App::getInstance()->db();
$employees = $db->select(
"SELECT `id`, `full_name_ar` FROM `employees` WHERE `is_active` = 1 ORDER BY `full_name_ar` ASC"
);
return $this->view('Inventory.Views.warehouses.form', [
'warehouse' => null,
'warehouseTypes' => Warehouse::getTypes(),
'employees' => $employees,
]);
}
public function store(Request $request): Response
{
$data = $this->extractData($request);
$errors = $this->validate($data);
// Unique code
if ($data['code'] !== '') {
$existing = Warehouse::query()->where('code', '=', $data['code'])->first();
if ($existing) {
$errors[] = 'كود المخزن مستخدم بالفعل';
}
}
if (!empty($errors)) {
return $this->flashErrorsAndRedirect($errors, $request, '/inventory/warehouses/create');
}
$warehouse = Warehouse::create($data);
return $this->redirect('/inventory/warehouses/' . $warehouse->id)->withSuccess('تم إضافة المخزن بنجاح');
}
public function show(Request $request, string $id): Response
{
$warehouse = Warehouse::find((int) $id);
if (!$warehouse) {
return $this->redirect('/inventory/warehouses')->withError('المخزن غير موجود');
}
$db = App::getInstance()->db();
$keeper = $warehouse->keeper_employee_id
? $db->selectOne("SELECT `id`, `full_name_ar` FROM `employees` WHERE `id` = ?", [(int) $warehouse->keeper_employee_id])
: null;
// Stock summary in this warehouse
$stockSummary = $db->select(
"SELECT iws.*, i.`name_ar`, i.`sku`, i.`unit_of_measure`
FROM `item_warehouse_stock` iws
JOIN `inventory_items` i ON i.`id` = iws.`item_id`
WHERE iws.`warehouse_id` = ?
ORDER BY i.`name_ar` ASC",
[(int) $id]
);
return $this->view('Inventory.Views.warehouses.show', [
'warehouse' => $warehouse,
'keeper' => $keeper,
'stockSummary' => $stockSummary,
'warehouseTypes' => Warehouse::getTypes(),
]);
}
public function edit(Request $request, string $id): Response
{
$warehouse = Warehouse::find((int) $id);
if (!$warehouse) {
return $this->redirect('/inventory/warehouses')->withError('المخزن غير موجود');
}
$db = App::getInstance()->db();
$employees = $db->select(
"SELECT `id`, `full_name_ar` FROM `employees` WHERE `is_active` = 1 ORDER BY `full_name_ar` ASC"
);
return $this->view('Inventory.Views.warehouses.form', [
'warehouse' => $warehouse,
'warehouseTypes' => Warehouse::getTypes(),
'employees' => $employees,
]);
}
public function update(Request $request, string $id): Response
{
$warehouse = Warehouse::find((int) $id);
if (!$warehouse) {
return $this->redirect('/inventory/warehouses')->withError('المخزن غير موجود');
}
$data = $this->extractData($request);
$errors = $this->validate($data);
// Unique code (exclude current)
if ($data['code'] !== '') {
$existing = Warehouse::query()
->where('code', '=', $data['code'])
->whereRaw('`id` != ?', [(int) $id])
->first();
if ($existing) {
$errors[] = 'كود المخزن مستخدم بالفعل';
}
}
if (!empty($errors)) {
return $this->flashErrorsAndRedirect($errors, $request, '/inventory/warehouses/' . $id . '/edit');
}
$warehouse->update($data);
return $this->redirect('/inventory/warehouses/' . $id)->withSuccess('تم تحديث المخزن بنجاح');
}
private function extractData(Request $request): array
{
return [
'code' => strtoupper(trim((string) $request->post('code', ''))),
'name_ar' => trim((string) $request->post('name_ar', '')),
'name_en' => trim((string) $request->post('name_en', '')) ?: null,
'warehouse_type' => trim((string) $request->post('warehouse_type', 'general')),
'location_ar' => trim((string) $request->post('location_ar', '')) ?: null,
'keeper_employee_id' => ((int) $request->post('keeper_employee_id', 0)) ?: null,
'capacity_units' => ((int) $request->post('capacity_units', 0)) ?: null,
'phone' => trim((string) $request->post('phone', '')) ?: null,
'is_active' => (int) ($request->post('is_active', 1)),
'notes' => trim((string) $request->post('notes', '')) ?: null,
];
}
private function validate(array $data): array
{
$errors = [];
if ($data['code'] === '') {
$errors[] = 'كود المخزن مطلوب';
}
if ($data['name_ar'] === '' || mb_strlen($data['name_ar']) < 2) {
$errors[] = 'اسم المخزن بالعربي مطلوب (حرفان على الأقل)';
}
if (!array_key_exists($data['warehouse_type'], Warehouse::getTypes())) {
$errors[] = 'نوع المخزن غير صالح';
}
return $errors;
}
private function flashErrorsAndRedirect(array $errors, Request $request, string $url): Response
{
$session = App::getInstance()->session();
$session->flash('_alerts', array_map(fn($e) => ['type' => 'error', 'message' => $e], $errors));
$session->flash('_old_input', $request->all());
return $this->redirect($url);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Inventory\Models;
use App\Core\Model;
use App\Core\App;
use App\Core\Pagination;
class AssetRegister extends Model
{
protected static string $table = 'asset_register';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = false;
protected static bool $autoTrackAuthor = true;
protected static array $fillable = [
'item_id',
'warehouse_id',
'asset_tag',
'serial_number',
'purchase_date',
'purchase_cost',
'useful_life_months',
'salvage_value',
'depreciation_method',
'declining_rate',
'accumulated_depreciation',
'book_value',
'status',
'disposed_at',
'disposal_value',
'disposal_reason',
'condition_notes',
'last_depreciation_date',
];
public static function getStatuses(): array
{
return [
'active' => 'نشط',
'disposed' => 'تم التصرف',
'transferred' => 'محوّل',
'under_maintenance' => 'تحت الصيانة',
];
}
public static function getDepreciationMethods(): array
{
return [
'straight_line' => 'القسط الثابت',
'declining_balance' => 'القسط المتناقص',
];
}
public static function getActiveAssets(): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT ar.*, i.`name_ar` as item_name, i.`sku`, w.`name_ar` as warehouse_name
FROM `asset_register` ar
JOIN `inventory_items` i ON i.`id` = ar.`item_id`
JOIN `warehouses` w ON w.`id` = ar.`warehouse_id`
WHERE ar.`status` = 'active'
ORDER BY ar.`asset_tag` ASC"
);
}
public static function search(array $filters, int $perPage = 25, int $page = 1): array
{
$db = App::getInstance()->db();
$where = '1=1';
$params = [];
if (!empty($filters['q'])) {
$search = '%' . $filters['q'] . '%';
$where .= ' AND (ar.`asset_tag` LIKE ? OR i.`name_ar` LIKE ? OR ar.`serial_number` LIKE ?)';
$params[] = $search;
$params[] = $search;
$params[] = $search;
}
if (!empty($filters['status'])) {
$where .= ' AND ar.`status` = ?';
$params[] = $filters['status'];
}
if (!empty($filters['warehouse_id'])) {
$where .= ' AND ar.`warehouse_id` = ?';
$params[] = (int) $filters['warehouse_id'];
}
$countRow = $db->selectOne(
"SELECT COUNT(*) as cnt FROM `asset_register` ar JOIN `inventory_items` i ON i.`id` = ar.`item_id` WHERE {$where}",
$params
);
$total = (int) ($countRow['cnt'] ?? 0);
$offset = ($page - 1) * $perPage;
$rows = $db->select(
"SELECT ar.*, i.`name_ar` as item_name, i.`sku`, w.`name_ar` as warehouse_name
FROM `asset_register` ar
JOIN `inventory_items` i ON i.`id` = ar.`item_id`
JOIN `warehouses` w ON w.`id` = ar.`warehouse_id`
WHERE {$where}
ORDER BY ar.`asset_tag` ASC
LIMIT {$perPage} OFFSET {$offset}",
$params
);
return ['data' => $rows, 'pagination' => Pagination::paginate($total, $perPage, $page)];
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Inventory\Models;
use App\Core\Model;
use App\Core\App;
class DepreciationEntry extends Model
{
protected static string $table = 'depreciation_entries';
protected static string $primaryKey = 'id';
protected static bool $timestamps = false;
protected static bool $softDelete = false;
protected static bool $autoTrackAuthor = false;
protected static array $fillable = [
'asset_id',
'period_month',
'depreciation_amount',
'accumulated_total',
'book_value_after',
'method_used',
'is_manual',
'notes',
'created_by',
];
public static function getForAsset(int $assetId): array
{
return static::query()
->where('asset_id', '=', $assetId)
->orderBy('period_month', 'DESC')
->get();
}
public static function getForPeriod(string $periodMonth): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT de.*, ar.`asset_tag`, i.`name_ar` as item_name
FROM `depreciation_entries` de
JOIN `asset_register` ar ON ar.`id` = de.`asset_id`
JOIN `inventory_items` i ON i.`id` = ar.`item_id`
WHERE de.`period_month` = ?
ORDER BY ar.`asset_tag` ASC",
[$periodMonth]
);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Inventory\Models;
use App\Core\Model;
use App\Core\App;
class InventoryItem extends Model
{
protected static string $table = 'inventory_items';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = true;
protected static bool $autoTrackAuthor = true;
protected static array $fillable = [
'category_id',
'sku',
'barcode',
'name_ar',
'name_en',
'description_ar',
'unit_of_measure',
'tracking_type',
'cost_price',
'sale_price_member',
'sale_price_nonmember',
'sale_price_player',
'tax_rate',
'is_sellable',
'is_active',
'image_document_id',
'notes',
];
/**
* Get all tracking types with Arabic labels.
*/
public static function getTrackingTypes(): array
{
return [
'standard' => 'قياسي',
'expiry' => 'بتاريخ صلاحية',
'asset' => 'أصل ثابت',
];
}
/**
* Get the Arabic label for a tracking type.
*/
public static function getTrackingTypeLabel(string $type): string
{
$types = self::getTrackingTypes();
return $types[$type] ?? $type;
}
/**
* Get all units of measure with Arabic labels.
*/
public static function getUnitsOfMeasure(): array
{
return [
'piece' => 'قطعة',
'kg' => 'كيلوجرام',
'liter' => 'لتر',
'box' => 'صندوق',
'pack' => 'عبوة',
'meter' => 'متر',
'set' => 'طقم',
];
}
/**
* Get the Arabic label for a unit of measure.
*/
public static function getUnitLabel(string $unit): string
{
$units = self::getUnitsOfMeasure();
return $units[$unit] ?? $unit;
}
/**
* Get all active items ordered by name_ar.
*/
public static function allActive(): array
{
return static::query()
->where('is_active', '=', 1)
->orderBy('name_ar', 'ASC')
->get();
}
/**
* Get all sellable and active items ordered by name_ar.
*/
public static function allSellable(): array
{
return static::query()
->where('is_sellable', '=', 1)
->where('is_active', '=', 1)
->orderBy('name_ar', 'ASC')
->get();
}
/**
* Search items with filters and pagination.
*/
public static function search(array $filters, int $perPage = 25, int $page = 1): array
{
$db = App::getInstance()->db();
$where = '1=1';
$params = [];
$join = '';
if (!empty($filters['q'])) {
$search = '%' . $filters['q'] . '%';
$where .= ' AND (i.`name_ar` LIKE ? OR i.`sku` LIKE ? OR i.`barcode` LIKE ?)';
$params[] = $search;
$params[] = $search;
$params[] = $search;
}
if (!empty($filters['category_id'])) {
$where .= ' AND i.`category_id` = ?';
$params[] = (int) $filters['category_id'];
}
if (!empty($filters['tracking_type'])) {
$where .= ' AND i.`tracking_type` = ?';
$params[] = $filters['tracking_type'];
}
if (isset($filters['is_sellable']) && $filters['is_sellable'] !== '') {
$where .= ' AND i.`is_sellable` = ?';
$params[] = (int) $filters['is_sellable'];
}
if (!empty($filters['warehouse_id'])) {
$join = ' JOIN `item_warehouse_stock` iws ON iws.`item_id` = i.`id`';
$where .= ' AND iws.`warehouse_id` = ?';
$params[] = (int) $filters['warehouse_id'];
}
$where .= ' AND i.`deleted_at` IS NULL';
$countRow = $db->selectOne(
"SELECT COUNT(*) as cnt FROM `inventory_items` i{$join} WHERE {$where}",
$params
);
$total = (int) ($countRow['cnt'] ?? 0);
$offset = ($page - 1) * $perPage;
$rows = $db->select(
"SELECT i.* FROM `inventory_items` i{$join} WHERE {$where} ORDER BY i.`name_ar` ASC LIMIT {$perPage} OFFSET {$offset}",
$params
);
return ['data' => $rows, 'pagination' => \App\Core\Pagination::paginate($total, $perPage, $page)];
}
/**
* Get the current stock quantity for an item in a specific warehouse.
*/
public static function getStockInWarehouse(int $itemId, int $warehouseId): float
{
$db = App::getInstance()->db();
$row = $db->selectOne(
"SELECT `quantity` FROM `item_warehouse_stock` WHERE `item_id` = ? AND `warehouse_id` = ?",
[$itemId, $warehouseId]
);
return (float) ($row['quantity'] ?? 0);
}
/**
* Get the total stock quantity for an item across all warehouses.
*/
public static function getTotalStock(int $itemId): float
{
$db = App::getInstance()->db();
$row = $db->selectOne(
"SELECT COALESCE(SUM(`quantity`), 0) as total FROM `item_warehouse_stock` WHERE `item_id` = ?",
[$itemId]
);
return (float) ($row['total'] ?? 0);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Inventory\Models;
use App\Core\Model;
use App\Core\App;
class ItemBatch extends Model
{
protected static string $table = 'item_batches';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = false;
protected static bool $autoTrackAuthor = false;
protected static array $fillable = [
'item_id',
'warehouse_id',
'batch_number',
'expiry_date',
'quantity',
'initial_quantity',
'unit_cost',
'received_date',
'status',
'notes',
'created_by',
];
public static function getStatuses(): array
{
return [
'active' => 'نشط',
'expired' => 'منتهي',
'depleted' => 'مستنفد',
];
}
/**
* Get active batches for an item in a warehouse, ordered by expiry (FEFO).
*/
public static function getActiveBatches(int $itemId, int $warehouseId): array
{
return static::query()
->where('item_id', '=', $itemId)
->where('warehouse_id', '=', $warehouseId)
->where('status', '=', 'active')
->whereRaw('`quantity` > 0')
->orderBy('expiry_date', 'ASC')
->get();
}
/**
* Get near-expiry batches (within given days).
*/
public static function getNearExpiry(int $days = 30): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT b.*, i.`name_ar` as item_name, i.`sku`, w.`name_ar` as warehouse_name
FROM `item_batches` b
JOIN `inventory_items` i ON i.`id` = b.`item_id`
JOIN `warehouses` w ON w.`id` = b.`warehouse_id`
WHERE b.`status` = 'active'
AND b.`quantity` > 0
AND b.`expiry_date` BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL ? DAY)
ORDER BY b.`expiry_date` ASC",
[$days]
);
}
/**
* Get already expired batches that are still active.
*/
public static function getExpired(): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT b.*, i.`name_ar` as item_name, i.`sku`, w.`name_ar` as warehouse_name
FROM `item_batches` b
JOIN `inventory_items` i ON i.`id` = b.`item_id`
JOIN `warehouses` w ON w.`id` = b.`warehouse_id`
WHERE b.`status` = 'active'
AND b.`quantity` > 0
AND b.`expiry_date` < CURDATE()
ORDER BY b.`expiry_date` ASC"
);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Inventory\Models;
use App\Core\Model;
class ItemCategory extends Model
{
protected static string $table = 'item_categories';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = true;
protected static bool $autoTrackAuthor = true;
protected static array $fillable = [
'parent_id',
'code',
'name_ar',
'name_en',
'description_ar',
'sort_order',
'is_active',
];
/**
* Get all active categories ordered by sort_order.
*/
public static function allActive(): array
{
return static::query()
->where('is_active', '=', 1)
->orderBy('sort_order', 'ASC')
->orderBy('name_ar', 'ASC')
->get();
}
/**
* Get root categories (where parent_id IS NULL).
*/
public static function getRoots(): array
{
return static::query()
->whereRaw('`parent_id` IS NULL')
->orderBy('sort_order', 'ASC')
->orderBy('name_ar', 'ASC')
->get();
}
/**
* Get children of a given parent category.
*/
public static function getChildren(int $parentId): array
{
return static::query()
->where('parent_id', '=', $parentId)
->orderBy('sort_order', 'ASC')
->orderBy('name_ar', 'ASC')
->get();
}
/**
* Get full category tree (roots with nested 'children' key).
*/
public static function getTree(): array
{
$all = static::query()
->orderBy('sort_order', 'ASC')
->orderBy('name_ar', 'ASC')
->get();
$grouped = [];
foreach ($all as $cat) {
$parentId = $cat['parent_id'] ?? null;
$grouped[$parentId ?? 0][] = $cat;
}
$roots = $grouped[0] ?? [];
foreach ($roots as &$root) {
$root['children'] = $grouped[$root['id']] ?? [];
}
return $roots;
}
/**
* Search categories with filters and pagination.
*/
public static function search(array $filters, int $perPage = 25, int $page = 1): array
{
$query = static::query();
if (!empty($filters['q'])) {
$search = '%' . $filters['q'] . '%';
$query = $query->whereRaw('(`name_ar` LIKE ?)', [$search]);
}
if (!empty($filters['parent_id'])) {
$query = $query->where('parent_id', '=', (int) $filters['parent_id']);
}
$query = $query->orderBy('sort_order', 'ASC')->orderBy('name_ar', 'ASC');
return $query->paginate($perPage, $page);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Inventory\Models;
use App\Core\Model;
use App\Core\App;
class ItemWarehouseStock extends Model
{
protected static string $table = 'item_warehouse_stock';
protected static string $primaryKey = 'id';
protected static bool $timestamps = false;
protected static bool $softDelete = false;
protected static bool $autoTrackAuthor = false;
protected static array $fillable = [
'item_id',
'warehouse_id',
'quantity',
'min_level',
'max_level',
'last_counted_at',
];
/**
* Get stock records for a specific item across all warehouses.
*/
public static function getForItem(int $itemId): array
{
return static::query()
->where('item_id', '=', $itemId)
->orderBy('warehouse_id', 'ASC')
->get();
}
/**
* Get all item stock records for a specific warehouse.
*/
public static function getForWarehouse(int $warehouseId): array
{
return static::query()
->where('warehouse_id', '=', $warehouseId)
->orderBy('item_id', 'ASC')
->get();
}
/**
* Get the quantity for a specific item in a specific warehouse.
*/
public static function getQuantity(int $itemId, int $warehouseId): float
{
$db = App::getInstance()->db();
$row = $db->selectOne(
"SELECT `quantity` FROM `item_warehouse_stock` WHERE `item_id` = ? AND `warehouse_id` = ?",
[$itemId, $warehouseId]
);
return (float) ($row['quantity'] ?? 0);
}
/**
* Upsert stock: adjust quantity by direction ('in' adds, 'out' subtracts).
* If no row exists for the item/warehouse pair, a new row is inserted.
*/
public static function upsertStock(int $itemId, int $warehouseId, string $quantityChange, string $direction): void
{
$db = App::getInstance()->db();
$existing = $db->selectOne(
"SELECT `id` FROM `item_warehouse_stock` WHERE `item_id` = ? AND `warehouse_id` = ?",
[$itemId, $warehouseId]
);
if ($existing) {
$operator = $direction === 'in' ? '+' : '-';
$db->statement(
"UPDATE `item_warehouse_stock` SET `quantity` = `quantity` {$operator} ?, `updated_at` = NOW() WHERE `item_id` = ? AND `warehouse_id` = ?",
[$quantityChange, $itemId, $warehouseId]
);
} else {
$qty = $direction === 'in' ? $quantityChange : '-' . $quantityChange;
$db->insert('item_warehouse_stock', [
'item_id' => $itemId,
'warehouse_id' => $warehouseId,
'quantity' => $qty,
'updated_at' => date('Y-m-d H:i:s'),
]);
}
}
/**
* Get items where current quantity is at or below the minimum level.
*/
public static function getLowStockItems(): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT iws.*, i.`name_ar`, i.`sku`, w.`name_ar` as warehouse_name
FROM `item_warehouse_stock` iws
JOIN `inventory_items` i ON i.`id` = iws.`item_id`
JOIN `warehouses` w ON w.`id` = iws.`warehouse_id`
WHERE iws.`quantity` <= iws.`min_level` AND iws.`min_level` IS NOT NULL
ORDER BY (iws.`quantity` - iws.`min_level`) ASC"
);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Inventory\Models;
use App\Core\Model;
use App\Core\App;
use App\Core\Pagination;
class PurchaseOrder extends Model
{
protected static string $table = 'purchase_orders';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = false;
protected static bool $autoTrackAuthor = true;
protected static array $fillable = [
'po_number',
'supplier_id',
'warehouse_id',
'status',
'total_amount',
'tax_amount',
'grand_total',
'ordered_by',
'approved_by',
'approved_at',
'expected_delivery_date',
'received_at',
'notes',
];
/**
* Get all PO statuses with Arabic labels.
*/
public static function getStatuses(): array
{
return [
'draft' => 'مسودة',
'submitted' => 'مُقدّم',
'approved' => 'معتمد',
'partially_received' => 'استلام جزئي',
'received' => 'تم الاستلام',
'cancelled' => 'ملغي',
];
}
/**
* Get the Arabic label for a status.
*/
public static function getStatusLabel(string $status): string
{
$statuses = self::getStatuses();
return $statuses[$status] ?? $status;
}
/**
* Get the color for a status.
*/
public static function getStatusColor(string $status): string
{
return match ($status) {
'draft' => '#6B7280',
'submitted' => '#D97706',
'approved' => '#0D7377',
'partially_received' => '#2563EB',
'received' => '#059669',
'cancelled' => '#DC2626',
default => '#6B7280',
};
}
/**
* Search purchase orders with filters and pagination.
*/
public static function search(array $filters, int $perPage = 25, int $page = 1): array
{
$db = App::getInstance()->db();
$where = '1=1';
$params = [];
if (!empty($filters['status'])) {
$where .= ' AND po.`status` = ?';
$params[] = $filters['status'];
}
if (!empty($filters['supplier_id'])) {
$where .= ' AND po.`supplier_id` = ?';
$params[] = (int) $filters['supplier_id'];
}
if (!empty($filters['warehouse_id'])) {
$where .= ' AND po.`warehouse_id` = ?';
$params[] = (int) $filters['warehouse_id'];
}
if (!empty($filters['date_from'])) {
$where .= ' AND po.`created_at` >= ?';
$params[] = $filters['date_from'];
}
if (!empty($filters['date_to'])) {
$where .= ' AND po.`created_at` <= ?';
$params[] = $filters['date_to'] . ' 23:59:59';
}
$countRow = $db->selectOne(
"SELECT COUNT(*) as cnt FROM `purchase_orders` po WHERE {$where}",
$params
);
$total = (int) ($countRow['cnt'] ?? 0);
$offset = ($page - 1) * $perPage;
$rows = $db->select(
"SELECT po.*, s.`name_ar` as supplier_name, w.`name_ar` as warehouse_name
FROM `purchase_orders` po
JOIN `suppliers` s ON s.`id` = po.`supplier_id`
JOIN `warehouses` w ON w.`id` = po.`warehouse_id`
WHERE {$where}
ORDER BY po.`created_at` DESC
LIMIT {$perPage} OFFSET {$offset}",
$params
);
return ['data' => $rows, 'pagination' => Pagination::paginate($total, $perPage, $page)];
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Inventory\Models;
use App\Core\Model;
use App\Core\App;
class PurchaseOrderItem extends Model
{
protected static string $table = 'purchase_order_items';
protected static string $primaryKey = 'id';
protected static bool $timestamps = false;
protected static bool $softDelete = false;
protected static bool $autoTrackAuthor = false;
protected static array $fillable = [
'po_id',
'item_id',
'quantity_ordered',
'quantity_received',
'unit_price',
'total_price',
'notes',
];
/**
* Get all items for a purchase order with item details.
*/
public static function getForPO(int $poId): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT poi.*, i.`name_ar` as item_name, i.`sku`, i.`unit_of_measure`
FROM `purchase_order_items` poi
JOIN `inventory_items` i ON i.`id` = poi.`item_id`
WHERE poi.`po_id` = ?
ORDER BY i.`name_ar` ASC",
[$poId]
);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Inventory\Models;
use App\Core\Model;
use App\Core\App;
use App\Core\Pagination;
class StockAudit extends Model
{
protected static string $table = 'stock_audits';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = false;
protected static bool $autoTrackAuthor = true;
protected static array $fillable = [
'audit_number',
'warehouse_id',
'audit_type',
'category_id',
'status',
'audit_date',
'started_at',
'completed_at',
'approved_by',
'approved_at',
'total_items',
'items_counted',
'variance_count',
'notes',
];
public static function getAuditTypes(): array
{
return [
'full' => 'جرد شامل',
'partial' => 'جرد جزئي',
'spot_check' => 'جرد عشوائي',
];
}
public static function getStatuses(): array
{
return [
'draft' => 'مسودة',
'in_progress' => 'جارٍ',
'pending_approval' => 'في انتظار الاعتماد',
'approved' => 'معتمد',
'cancelled' => 'ملغي',
];
}
public static function getStatusLabel(string $status): string
{
$statuses = self::getStatuses();
return $statuses[$status] ?? $status;
}
public static function getStatusColor(string $status): string
{
return match ($status) {
'draft' => '#6B7280',
'in_progress' => '#2563EB',
'pending_approval' => '#D97706',
'approved' => '#059669',
'cancelled' => '#DC2626',
default => '#6B7280',
};
}
public static function search(array $filters, int $perPage = 25, int $page = 1): array
{
$db = App::getInstance()->db();
$where = '1=1';
$params = [];
if (!empty($filters['status'])) {
$where .= ' AND a.`status` = ?';
$params[] = $filters['status'];
}
if (!empty($filters['audit_type'])) {
$where .= ' AND a.`audit_type` = ?';
$params[] = $filters['audit_type'];
}
if (!empty($filters['warehouse_id'])) {
$where .= ' AND a.`warehouse_id` = ?';
$params[] = (int) $filters['warehouse_id'];
}
$countRow = $db->selectOne("SELECT COUNT(*) as cnt FROM `stock_audits` a WHERE {$where}", $params);
$total = (int) ($countRow['cnt'] ?? 0);
$offset = ($page - 1) * $perPage;
$rows = $db->select(
"SELECT a.*, w.`name_ar` as warehouse_name
FROM `stock_audits` a
JOIN `warehouses` w ON w.`id` = a.`warehouse_id`
WHERE {$where}
ORDER BY a.`created_at` DESC
LIMIT {$perPage} OFFSET {$offset}",
$params
);
return ['data' => $rows, 'pagination' => Pagination::paginate($total, $perPage, $page)];
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Inventory\Models;
use App\Core\Model;
use App\Core\App;
class StockAuditItem extends Model
{
protected static string $table = 'stock_audit_items';
protected static string $primaryKey = 'id';
protected static bool $timestamps = false;
protected static bool $softDelete = false;
protected static bool $autoTrackAuthor = false;
protected static array $fillable = [
'audit_id',
'item_id',
'batch_id',
'system_quantity',
'physical_quantity',
'variance',
'variance_cost',
'status',
'counted_by',
'counted_at',
'notes',
];
public static function getForAudit(int $auditId): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT sai.*, i.`name_ar` as item_name, i.`sku`, i.`unit_of_measure`, i.`cost_price`
FROM `stock_audit_items` sai
JOIN `inventory_items` i ON i.`id` = sai.`item_id`
WHERE sai.`audit_id` = ?
ORDER BY i.`name_ar` ASC",
[$auditId]
);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Inventory\Models;
use App\Core\Model;
use App\Core\App;
use App\Core\Pagination;
class StockMovement extends Model
{
protected static string $table = 'stock_movements';
protected static string $primaryKey = 'id';
protected static bool $timestamps = false;
protected static bool $softDelete = false;
protected static bool $autoTrackAuthor = false;
protected static array $fillable = [
'warehouse_id',
'item_id',
'movement_type',
'direction',
'quantity',
'unit_cost',
'total_cost',
'batch_id',
'reference_type',
'reference_id',
'transfer_id',
'notes',
'movement_date',
'created_by',
];
public static function getMovementTypes(): array
{
return [
'purchase_in' => 'شراء',
'transfer_in' => 'تحويل وارد',
'return_in' => 'مرتجع',
'adjustment_in' => 'تسوية (+)',
'opening_balance' => 'رصيد افتتاحي',
'sale_out' => 'بيع',
'transfer_out' => 'تحويل صادر',
'damage_out' => 'تالف',
'expired_out' => 'منتهي صلاحية',
'consumed_out' => 'مستهلك',
'adjustment_out' => 'تسوية (-)',
];
}
public static function getMovementTypeLabel(string $type): string
{
$types = self::getMovementTypes();
return $types[$type] ?? $type;
}
public static function search(array $filters, int $perPage = 25, int $page = 1): array
{
$db = App::getInstance()->db();
$where = '1=1';
$params = [];
if (!empty($filters['item_id'])) {
$where .= ' AND m.`item_id` = ?';
$params[] = (int) $filters['item_id'];
}
if (!empty($filters['warehouse_id'])) {
$where .= ' AND m.`warehouse_id` = ?';
$params[] = (int) $filters['warehouse_id'];
}
if (!empty($filters['movement_type'])) {
$where .= ' AND m.`movement_type` = ?';
$params[] = $filters['movement_type'];
}
if (!empty($filters['direction'])) {
$where .= ' AND m.`direction` = ?';
$params[] = $filters['direction'];
}
if (!empty($filters['date_from'])) {
$where .= ' AND m.`movement_date` >= ?';
$params[] = $filters['date_from'];
}
if (!empty($filters['date_to'])) {
$where .= ' AND m.`movement_date` <= ?';
$params[] = $filters['date_to'];
}
$countRow = $db->selectOne(
"SELECT COUNT(*) as cnt FROM `stock_movements` m WHERE {$where}",
$params
);
$total = (int) ($countRow['cnt'] ?? 0);
$offset = ($page - 1) * $perPage;
$rows = $db->select(
"SELECT m.*, i.`name_ar` as item_name, i.`sku`, w.`name_ar` as warehouse_name
FROM `stock_movements` m
JOIN `inventory_items` i ON i.`id` = m.`item_id`
JOIN `warehouses` w ON w.`id` = m.`warehouse_id`
WHERE {$where}
ORDER BY m.`created_at` DESC
LIMIT {$perPage} OFFSET {$offset}",
$params
);
return ['data' => $rows, 'pagination' => Pagination::paginate($total, $perPage, $page)];
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Inventory\Models;
use App\Core\Model;
use App\Core\App;
use App\Core\Pagination;
class StockTransfer extends Model
{
protected static string $table = 'stock_transfers';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = false;
protected static bool $autoTrackAuthor = true;
protected static array $fillable = [
'transfer_number',
'from_warehouse_id',
'to_warehouse_id',
'status',
'requested_by',
'approved_by',
'approved_at',
'received_by',
'received_at',
'transfer_date',
'notes',
];
public static function getStatuses(): array
{
return [
'draft' => 'مسودة',
'pending_approval' => 'في انتظار الموافقة',
'approved' => 'تمت الموافقة',
'in_transit' => 'قيد النقل',
'received' => 'تم الاستلام',
'cancelled' => 'ملغي',
];
}
public static function getStatusLabel(string $status): string
{
$statuses = self::getStatuses();
return $statuses[$status] ?? $status;
}
public static function getStatusColor(string $status): string
{
return match ($status) {
'draft' => '#6B7280',
'pending_approval' => '#D97706',
'approved' => '#0D7377',
'in_transit' => '#2563EB',
'received' => '#059669',
'cancelled' => '#DC2626',
default => '#6B7280',
};
}
public static function search(array $filters, int $perPage = 25, int $page = 1): array
{
$db = App::getInstance()->db();
$where = '1=1';
$params = [];
if (!empty($filters['status'])) {
$where .= ' AND t.`status` = ?';
$params[] = $filters['status'];
}
if (!empty($filters['from_warehouse_id'])) {
$where .= ' AND t.`from_warehouse_id` = ?';
$params[] = (int) $filters['from_warehouse_id'];
}
if (!empty($filters['to_warehouse_id'])) {
$where .= ' AND t.`to_warehouse_id` = ?';
$params[] = (int) $filters['to_warehouse_id'];
}
if (!empty($filters['date_from'])) {
$where .= ' AND t.`transfer_date` >= ?';
$params[] = $filters['date_from'];
}
if (!empty($filters['date_to'])) {
$where .= ' AND t.`transfer_date` <= ?';
$params[] = $filters['date_to'];
}
$countRow = $db->selectOne(
"SELECT COUNT(*) as cnt FROM `stock_transfers` t WHERE {$where}",
$params
);
$total = (int) ($countRow['cnt'] ?? 0);
$offset = ($page - 1) * $perPage;
$rows = $db->select(
"SELECT t.*, fw.`name_ar` as from_warehouse_name, tw.`name_ar` as to_warehouse_name
FROM `stock_transfers` t
JOIN `warehouses` fw ON fw.`id` = t.`from_warehouse_id`
JOIN `warehouses` tw ON tw.`id` = t.`to_warehouse_id`
WHERE {$where}
ORDER BY t.`created_at` DESC
LIMIT {$perPage} OFFSET {$offset}",
$params
);
return ['data' => $rows, 'pagination' => Pagination::paginate($total, $perPage, $page)];
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Inventory\Models;
use App\Core\Model;
use App\Core\App;
class StockTransferItem extends Model
{
protected static string $table = 'stock_transfer_items';
protected static string $primaryKey = 'id';
protected static bool $timestamps = false;
protected static bool $softDelete = false;
protected static bool $autoTrackAuthor = false;
protected static array $fillable = [
'transfer_id',
'item_id',
'quantity',
'batch_id',
'notes',
];
/**
* Get all items for a transfer with item details.
*/
public static function getForTransfer(int $transferId): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT sti.*, i.`name_ar` as item_name, i.`sku`, i.`unit_of_measure`
FROM `stock_transfer_items` sti
JOIN `inventory_items` i ON i.`id` = sti.`item_id`
WHERE sti.`transfer_id` = ?
ORDER BY i.`name_ar` ASC",
[$transferId]
);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Inventory\Models;
use App\Core\Model;
use App\Core\App;
use App\Core\Pagination;
class Supplier extends Model
{
protected static string $table = 'suppliers';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = true;
protected static bool $autoTrackAuthor = true;
protected static array $fillable = [
'code',
'name_ar',
'name_en',
'contact_person',
'phone',
'email',
'address_ar',
'tax_number',
'payment_terms',
'rating',
'is_active',
'notes',
];
/**
* Get all active suppliers ordered by name_ar.
*/
public static function allActive(): array
{
return static::query()
->where('is_active', '=', 1)
->orderBy('name_ar', 'ASC')
->get();
}
/**
* Search suppliers with filters and pagination.
*/
public static function search(array $filters, int $perPage = 25, int $page = 1): array
{
$db = App::getInstance()->db();
$where = '1=1';
$params = [];
if (!empty($filters['q'])) {
$search = '%' . $filters['q'] . '%';
$where .= ' AND (`name_ar` LIKE ? OR `code` LIKE ?)';
$params[] = $search;
$params[] = $search;
}
if (isset($filters['is_active']) && $filters['is_active'] !== '') {
$where .= ' AND `is_active` = ?';
$params[] = (int) $filters['is_active'];
}
$where .= ' AND `deleted_at` IS NULL';
$countRow = $db->selectOne(
"SELECT COUNT(*) as cnt FROM `suppliers` WHERE {$where}",
$params
);
$total = (int) ($countRow['cnt'] ?? 0);
$offset = ($page - 1) * $perPage;
$rows = $db->select(
"SELECT * FROM `suppliers` WHERE {$where} ORDER BY `name_ar` ASC LIMIT {$perPage} OFFSET {$offset}",
$params
);
return ['data' => $rows, 'pagination' => Pagination::paginate($total, $perPage, $page)];
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Inventory\Models;
use App\Core\Model;
class Warehouse extends Model
{
protected static string $table = 'warehouses';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = true;
protected static bool $autoTrackAuthor = true;
protected static array $fillable = [
'code',
'name_ar',
'name_en',
'warehouse_type',
'location_ar',
'keeper_employee_id',
'capacity_units',
'branch_id',
'phone',
'is_active',
'notes',
];
/**
* Get all warehouse types with Arabic labels.
*/
public static function getTypes(): array
{
return [
'general' => 'عام',
'sports_shop' => 'محل رياضي',
'cafeteria' => 'كافيتريا',
'gym' => 'جيم',
'medical' => 'طبي',
'maintenance' => 'صيانة',
];
}
/**
* Get the Arabic label for a warehouse type.
*/
public static function getTypeLabel(string $type): string
{
$types = self::getTypes();
return $types[$type] ?? $type;
}
/**
* Get all active warehouses ordered by name_ar.
*/
public static function allActive(): array
{
return static::query()
->where('is_active', '=', 1)
->orderBy('name_ar', 'ASC')
->get();
}
/**
* Search warehouses with filters and pagination.
*/
public static function search(array $filters, int $perPage = 25, int $page = 1): array
{
$query = static::query();
if (!empty($filters['q'])) {
$search = '%' . $filters['q'] . '%';
$query = $query->whereRaw('(`name_ar` LIKE ?)', [$search]);
}
if (!empty($filters['warehouse_type'])) {
$query = $query->where('warehouse_type', '=', $filters['warehouse_type']);
}
if (isset($filters['is_active']) && $filters['is_active'] !== '') {
$query = $query->where('is_active', '=', (int) $filters['is_active']);
}
$query = $query->orderBy('name_ar', 'ASC');
return $query->paginate($perPage, $page);
}
}
<?php
declare(strict_types=1);
return [
// Warehouses
['GET', '/inventory/warehouses', 'Inventory\Controllers\WarehouseController@index', ['auth'], 'warehouse.view'],
['GET', '/inventory/warehouses/create', 'Inventory\Controllers\WarehouseController@create', ['auth'], 'warehouse.manage'],
['POST', '/inventory/warehouses', 'Inventory\Controllers\WarehouseController@store', ['auth', 'csrf'], 'warehouse.manage'],
['GET', '/inventory/warehouses/{id:\d+}', 'Inventory\Controllers\WarehouseController@show', ['auth'], 'warehouse.view'],
['GET', '/inventory/warehouses/{id:\d+}/edit', 'Inventory\Controllers\WarehouseController@edit', ['auth'], 'warehouse.manage'],
['POST', '/inventory/warehouses/{id:\d+}', 'Inventory\Controllers\WarehouseController@update', ['auth', 'csrf'], 'warehouse.manage'],
// Categories
['GET', '/inventory/categories', 'Inventory\Controllers\CategoryController@index', ['auth'], 'inventory.view'],
['GET', '/inventory/categories/create', 'Inventory\Controllers\CategoryController@create', ['auth'], 'inventory.manage'],
['POST', '/inventory/categories', 'Inventory\Controllers\CategoryController@store', ['auth', 'csrf'], 'inventory.manage'],
['GET', '/inventory/categories/{id:\d+}/edit', 'Inventory\Controllers\CategoryController@edit', ['auth'], 'inventory.manage'],
['POST', '/inventory/categories/{id:\d+}', 'Inventory\Controllers\CategoryController@update', ['auth', 'csrf'], 'inventory.manage'],
// Items
['GET', '/inventory/items', 'Inventory\Controllers\ItemController@index', ['auth'], 'inventory.view'],
['GET', '/inventory/items/create', 'Inventory\Controllers\ItemController@create', ['auth'], 'inventory.manage'],
['POST', '/inventory/items', 'Inventory\Controllers\ItemController@store', ['auth', 'csrf'], 'inventory.manage'],
['GET', '/inventory/items/{id:\d+}', 'Inventory\Controllers\ItemController@show', ['auth'], 'inventory.view'],
['GET', '/inventory/items/{id:\d+}/edit', 'Inventory\Controllers\ItemController@edit', ['auth'], 'inventory.manage'],
['POST', '/inventory/items/{id:\d+}', 'Inventory\Controllers\ItemController@update', ['auth', 'csrf'], 'inventory.manage'],
['GET', '/inventory/items/search-json', 'Inventory\Controllers\ItemController@searchJson', ['auth'], 'inventory.view'],
// Stock Movements
['GET', '/inventory/movements', 'Inventory\Controllers\StockMovementController@index', ['auth'], 'stock.view'],
['GET', '/inventory/movements/create', 'Inventory\Controllers\StockMovementController@create', ['auth'], 'stock.move'],
['POST', '/inventory/movements', 'Inventory\Controllers\StockMovementController@store', ['auth', 'csrf'], 'stock.move'],
// Stock Transfers
['GET', '/inventory/transfers', 'Inventory\Controllers\StockTransferController@index', ['auth'], 'stock.view'],
['GET', '/inventory/transfers/create', 'Inventory\Controllers\StockTransferController@create', ['auth'], 'stock.move'],
['POST', '/inventory/transfers', 'Inventory\Controllers\StockTransferController@store', ['auth', 'csrf'], 'stock.move'],
['GET', '/inventory/transfers/{id:\d+}', 'Inventory\Controllers\StockTransferController@show', ['auth'], 'stock.view'],
['POST', '/inventory/transfers/{id:\d+}/approve', 'Inventory\Controllers\StockTransferController@approve',['auth', 'csrf'], 'stock.approve_transfer'],
['POST', '/inventory/transfers/{id:\d+}/receive', 'Inventory\Controllers\StockTransferController@receive',['auth', 'csrf'], 'stock.move'],
// Stock Audits
['GET', '/inventory/audits', 'Inventory\Controllers\StockAuditController@index', ['auth'], 'stock.audit'],
['GET', '/inventory/audits/create', 'Inventory\Controllers\StockAuditController@create', ['auth'], 'stock.audit'],
['POST', '/inventory/audits', 'Inventory\Controllers\StockAuditController@store', ['auth', 'csrf'], 'stock.audit'],
['GET', '/inventory/audits/{id:\d+}', 'Inventory\Controllers\StockAuditController@show', ['auth'], 'stock.audit'],
['GET', '/inventory/audits/{id:\d+}/count', 'Inventory\Controllers\StockAuditController@count', ['auth'], 'stock.audit'],
['POST', '/inventory/audits/{id:\d+}/count', 'Inventory\Controllers\StockAuditController@saveCount', ['auth', 'csrf'], 'stock.audit'],
['POST', '/inventory/audits/{id:\d+}/approve', 'Inventory\Controllers\StockAuditController@approve', ['auth', 'csrf'], 'stock.approve_adjustment'],
// Suppliers
['GET', '/inventory/suppliers', 'Inventory\Controllers\SupplierController@index', ['auth'], 'supplier.view'],
['GET', '/inventory/suppliers/create', 'Inventory\Controllers\SupplierController@create', ['auth'], 'supplier.manage'],
['POST', '/inventory/suppliers', 'Inventory\Controllers\SupplierController@store', ['auth', 'csrf'], 'supplier.manage'],
['GET', '/inventory/suppliers/{id:\d+}', 'Inventory\Controllers\SupplierController@show', ['auth'], 'supplier.view'],
['GET', '/inventory/suppliers/{id:\d+}/edit', 'Inventory\Controllers\SupplierController@edit', ['auth'], 'supplier.manage'],
['POST', '/inventory/suppliers/{id:\d+}', 'Inventory\Controllers\SupplierController@update', ['auth', 'csrf'], 'supplier.manage'],
// Purchase Orders
['GET', '/inventory/purchase-orders', 'Inventory\Controllers\PurchaseOrderController@index', ['auth'], 'purchase.view'],
['GET', '/inventory/purchase-orders/create', 'Inventory\Controllers\PurchaseOrderController@create', ['auth'], 'purchase.create'],
['POST', '/inventory/purchase-orders', 'Inventory\Controllers\PurchaseOrderController@store', ['auth', 'csrf'], 'purchase.create'],
['GET', '/inventory/purchase-orders/{id:\d+}', 'Inventory\Controllers\PurchaseOrderController@show', ['auth'], 'purchase.view'],
['POST', '/inventory/purchase-orders/{id:\d+}/submit', 'Inventory\Controllers\PurchaseOrderController@submit', ['auth', 'csrf'], 'purchase.create'],
['POST', '/inventory/purchase-orders/{id:\d+}/approve', 'Inventory\Controllers\PurchaseOrderController@approve',['auth', 'csrf'], 'purchase.approve'],
['GET', '/inventory/purchase-orders/{id:\d+}/receive', 'Inventory\Controllers\PurchaseOrderController@receiveForm', ['auth'], 'purchase.create'],
['POST', '/inventory/purchase-orders/{id:\d+}/receive', 'Inventory\Controllers\PurchaseOrderController@receive',['auth', 'csrf'], 'purchase.create'],
['POST', '/inventory/purchase-orders/{id:\d+}/cancel', 'Inventory\Controllers\PurchaseOrderController@cancel', ['auth', 'csrf'], 'purchase.approve'],
// Assets
['GET', '/inventory/assets', 'Inventory\Controllers\AssetController@index', ['auth'], 'asset.view'],
['GET', '/inventory/assets/{id:\d+}', 'Inventory\Controllers\AssetController@show', ['auth'], 'asset.view'],
['POST', '/inventory/assets/{id:\d+}/dispose', 'Inventory\Controllers\AssetController@dispose', ['auth', 'csrf'], 'asset.manage'],
['POST', '/inventory/assets/run-depreciation', 'Inventory\Controllers\AssetController@runDepreciation',['auth', 'csrf'], 'asset.manage'],
// Reports
['GET', '/inventory/reports/stock-balance', 'Inventory\Controllers\InventoryReportController@stockBalance', ['auth'], 'report.inventory'],
['GET', '/inventory/reports/movements', 'Inventory\Controllers\InventoryReportController@movements', ['auth'], 'report.inventory'],
['GET', '/inventory/reports/expiry-alert', 'Inventory\Controllers\InventoryReportController@expiryAlert', ['auth'], 'report.inventory'],
['GET', '/inventory/reports/low-stock', 'Inventory\Controllers\InventoryReportController@lowStock', ['auth'], 'report.inventory'],
['GET', '/inventory/reports/depreciation', 'Inventory\Controllers\InventoryReportController@depreciationSchedule',['auth'], 'report.inventory'],
['GET', '/inventory/reports/audit-variance', 'Inventory\Controllers\InventoryReportController@auditVariance', ['auth'], 'report.inventory'],
['GET', '/inventory/reports/supplier-history', 'Inventory\Controllers\InventoryReportController@supplierHistory', ['auth'], 'report.inventory'],
];
<?php
declare(strict_types=1);
namespace App\Modules\Inventory\Services;
use App\Core\App;
use App\Core\EventBus;
use App\Core\Logger;
use App\Modules\Inventory\Models\ItemBatch;
/**
* FEFO (First Expiry First Out) batch allocation for expiry-tracked items.
*/
final class BatchService
{
/**
* Create a new batch when stock comes in (purchase, etc.).
*/
public static function createBatch(array $data): int
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$batchId = $db->insert('item_batches', [
'item_id' => (int) $data['item_id'],
'warehouse_id' => (int) $data['warehouse_id'],
'batch_number' => $data['batch_number'],
'expiry_date' => $data['expiry_date'],
'quantity' => $data['quantity'],
'initial_quantity' => $data['quantity'],
'unit_cost' => $data['unit_cost'] ?? null,
'received_date' => $data['received_date'] ?? date('Y-m-d'),
'status' => 'active',
'notes' => $data['notes'] ?? null,
'created_by' => $employee ? (int) $employee->id : null,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
Logger::info("Batch #{$batchId} created for item #{$data['item_id']}, qty: {$data['quantity']}, expiry: {$data['expiry_date']}");
return $batchId;
}
/**
* FEFO allocation: pick the earliest-expiring active batch(es) to fulfill a quantity.
* Returns array of ['batch_id' => int, 'quantity' => string] allocations.
*
* @throws \RuntimeException if insufficient batch quantity
*/
public static function allocateBatch(int $itemId, int $warehouseId, string $quantity): array
{
$batches = ItemBatch::getActiveBatches($itemId, $warehouseId);
$remaining = $quantity;
$allocations = [];
foreach ($batches as $batch) {
if (bccomp($remaining, '0', 3) <= 0) {
break;
}
$available = (string) $batch['quantity'];
$take = bccomp($available, $remaining, 3) >= 0 ? $remaining : $available;
$allocations[] = [
'batch_id' => (int) $batch['id'],
'quantity' => $take,
];
$remaining = bcsub($remaining, $take, 3);
}
if (bccomp($remaining, '0', 3) > 0) {
throw new \RuntimeException(
'كمية الدُفعات غير كافية — المطلوب: ' . $quantity . ' المتاح في الدفعات: ' . bcsub($quantity, $remaining, 3)
);
}
return $allocations;
}
/**
* Mark expired batches and dispatch alert events.
* Called by the daily cron job.
*/
public static function flagExpiredBatches(): int
{
$db = App::getInstance()->db();
$expired = ItemBatch::getExpired();
$count = 0;
foreach ($expired as $batch) {
$db->update('item_batches', [
'status' => 'expired',
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [(int) $batch['id']]);
EventBus::dispatch('inventory.batch_expired', [
'batch_id' => (int) $batch['id'],
'item_id' => (int) $batch['item_id'],
'warehouse_id' => (int) $batch['warehouse_id'],
'expiry_date' => $batch['expiry_date'],
'quantity' => $batch['quantity'],
]);
$count++;
}
if ($count > 0) {
Logger::info("Flagged {$count} expired batch(es)");
}
return $count;
}
/**
* Get near-expiry batches and dispatch warnings.
* Called by the daily cron job.
*/
public static function alertNearExpiry(int $days = 30): int
{
$batches = ItemBatch::getNearExpiry($days);
foreach ($batches as $batch) {
EventBus::dispatch('inventory.expiry_warning', [
'batch_id' => (int) $batch['id'],
'item_id' => (int) $batch['item_id'],
'warehouse_id' => (int) $batch['warehouse_id'],
'expiry_date' => $batch['expiry_date'],
'days_left' => (int) ((strtotime($batch['expiry_date']) - time()) / 86400),
]);
}
return count($batches);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Inventory\Services;
use App\Core\App;
use App\Core\EventBus;
use App\Core\Logger;
final class DepreciationService
{
/**
* Run monthly depreciation for all active assets.
*
* @param string $periodMonth YYYY-MM format
* @return int Number of assets depreciated
*/
public static function runMonthlyDepreciation(string $periodMonth): int
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$count = 0;
$assets = $db->select(
"SELECT * FROM `asset_register` WHERE `status` = 'active'"
);
foreach ($assets as $asset) {
// Skip if already depreciated for this period
$existing = $db->selectOne(
"SELECT `id` FROM `depreciation_entries` WHERE `asset_id` = ? AND `period_month` = ?",
[(int) $asset['id'], $periodMonth]
);
if ($existing) continue;
// Skip if book value already at salvage
$bookValue = (string) $asset['book_value'];
$salvageValue = (string) $asset['salvage_value'];
if (bccomp($bookValue, $salvageValue, 2) <= 0) continue;
// Calculate depreciation amount
$depAmount = self::calculateDepreciation($asset);
// Ensure book value doesn't go below salvage
$maxDep = bcsub($bookValue, $salvageValue, 2);
if (bccomp($depAmount, $maxDep, 2) > 0) {
$depAmount = $maxDep;
}
if (bccomp($depAmount, '0.00', 2) <= 0) continue;
$newAccumulated = bcadd((string) $asset['accumulated_depreciation'], $depAmount, 2);
$newBookValue = bcsub($bookValue, $depAmount, 2);
$db->beginTransaction();
try {
$db->insert('depreciation_entries', [
'asset_id' => (int) $asset['id'],
'period_month' => $periodMonth,
'depreciation_amount' => $depAmount,
'accumulated_total' => $newAccumulated,
'book_value_after' => $newBookValue,
'method_used' => $asset['depreciation_method'],
'is_manual' => 0,
'created_by' => $employee ? (int) $employee->id : null,
'created_at' => date('Y-m-d H:i:s'),
]);
$db->update('asset_register', [
'accumulated_depreciation' => $newAccumulated,
'book_value' => $newBookValue,
'last_depreciation_date' => $periodMonth . '-01',
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [(int) $asset['id']]);
$db->commit();
$count++;
} catch (\Throwable $e) {
$db->rollBack();
Logger::error("Depreciation failed for asset #{$asset['id']}: " . $e->getMessage());
}
}
if ($count > 0) {
EventBus::dispatch('inventory.depreciation_run', [
'period_month' => $periodMonth,
'assets_count' => $count,
]);
Logger::info("Monthly depreciation for {$periodMonth}: {$count} asset(s) processed");
}
return $count;
}
/**
* Calculate monthly depreciation for an asset.
*/
private static function calculateDepreciation(array $asset): string
{
$method = $asset['depreciation_method'] ?? 'straight_line';
if ($method === 'straight_line') {
// Monthly = (Purchase Cost - Salvage Value) / Useful Life in Months
$depreciableBase = bcsub((string) $asset['purchase_cost'], (string) $asset['salvage_value'], 2);
$months = max(1, (int) ($asset['useful_life_months'] ?? 60));
return bcdiv($depreciableBase, (string) $months, 2);
}
if ($method === 'declining_balance') {
// Monthly = Book Value × Annual Rate / 12
$rate = (float) ($asset['declining_rate'] ?? 20.00);
$annualDep = bcmul((string) $asset['book_value'], (string) ($rate / 100), 2);
return bcdiv($annualDep, '12', 2);
}
return '0.00';
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Inventory\Services;
use App\Core\App;
final class InventoryNumberGenerator
{
/**
* Generate a transfer number: TRF-YYYY-NNNNNN
*/
public static function nextTransferNumber(): string
{
return self::generate('TRF', 'stock_transfers', 'transfer_number');
}
/**
* Generate a purchase order number: PO-YYYY-NNNNNN
*/
public static function nextPONumber(): string
{
return self::generate('PO', 'purchase_orders', 'po_number');
}
/**
* Generate an audit number: AUD-YYYY-NNNNNN
*/
public static function nextAuditNumber(): string
{
return self::generate('AUD', 'stock_audits', 'audit_number');
}
/**
* Generate an asset tag: AST-YYYY-NNNNNN
*/
public static function nextAssetTag(): string
{
return self::generate('AST', 'asset_register', 'asset_tag');
}
/**
* Core sequence generator: PREFIX-YYYY-NNNNNN
*/
private static function generate(string $prefix, string $table, string $column): string
{
$db = App::getInstance()->db();
$year = date('Y');
$pattern = $prefix . '-' . $year . '-';
$last = $db->selectOne(
"SELECT `{$column}` FROM `{$table}` WHERE `{$column}` LIKE ? ORDER BY `id` DESC LIMIT 1",
[$pattern . '%']
);
if ($last) {
$parts = explode('-', $last[$column]);
$seq = (int) end($parts) + 1;
} else {
$seq = 1;
}
return $pattern . str_pad((string) $seq, 6, '0', STR_PAD_LEFT);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Inventory\Services;
use App\Core\App;
use App\Core\EventBus;
use App\Core\Logger;
/**
* Service for purchase order lifecycle management.
*/
final class PurchaseOrderService
{
/**
* Create a new purchase order with its items.
*
* @param array $header Keys: supplier_id, warehouse_id, expected_delivery_date, notes
* @param array $items Each: ['item_id' => int, 'quantity_ordered' => string, 'unit_price' => string, 'notes' => ?string]
* @return int PO ID
*/
public static function createPO(array $header, array $items): int
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
if (empty($items)) {
throw new \RuntimeException('يجب إضافة صنف واحد على الأقل');
}
$supplierId = (int) ($header['supplier_id'] ?? 0);
$warehouseId = (int) ($header['warehouse_id'] ?? 0);
if ($supplierId <= 0) {
throw new \RuntimeException('يجب تحديد المورد');
}
if ($warehouseId <= 0) {
throw new \RuntimeException('يجب تحديد المخزن');
}
$poNumber = InventoryNumberGenerator::nextPONumber();
// Calculate totals
$totalAmount = '0.00';
foreach ($items as $item) {
$qty = (string) ($item['quantity_ordered'] ?? '0');
$price = (string) ($item['unit_price'] ?? '0');
$lineTotal = bcmul($qty, $price, 2);
$totalAmount = bcadd($totalAmount, $lineTotal, 2);
}
$taxAmount = bcmul($totalAmount, '0.14', 2);
$grandTotal = bcadd($totalAmount, $taxAmount, 2);
$db->beginTransaction();
try {
$poId = $db->insert('purchase_orders', [
'po_number' => $poNumber,
'supplier_id' => $supplierId,
'warehouse_id' => $warehouseId,
'status' => 'draft',
'total_amount' => $totalAmount,
'tax_amount' => $taxAmount,
'grand_total' => $grandTotal,
'ordered_by' => $employee ? (int) $employee->id : null,
'expected_delivery_date' => $header['expected_delivery_date'] ?? null,
'notes' => $header['notes'] ?? null,
'created_by' => $employee ? (int) $employee->id : null,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
foreach ($items as $item) {
$qty = (string) ($item['quantity_ordered'] ?? '0');
$price = (string) ($item['unit_price'] ?? '0');
$lineTotal = bcmul($qty, $price, 2);
$db->insert('purchase_order_items', [
'po_id' => $poId,
'item_id' => (int) $item['item_id'],
'quantity_ordered' => $qty,
'quantity_received' => '0',
'unit_price' => $price,
'total_price' => $lineTotal,
'notes' => $item['notes'] ?? null,
]);
}
$db->commit();
} catch (\Throwable $e) {
$db->rollBack();
throw $e;
}
Logger::info("PO #{$poId} ({$poNumber}) created for supplier #{$supplierId}, grand total: {$grandTotal}");
return $poId;
}
/**
* Submit a draft PO for approval.
*/
public static function submitPO(int $poId): void
{
$db = App::getInstance()->db();
$po = $db->selectOne("SELECT * FROM `purchase_orders` WHERE `id` = ?", [$poId]);
if (!$po) {
throw new \RuntimeException('أمر الشراء غير موجود');
}
if ($po['status'] !== 'draft') {
throw new \RuntimeException('أمر الشراء ليس في حالة مسودة');
}
$db->update('purchase_orders', [
'status' => 'submitted',
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [$poId]);
Logger::info("PO #{$poId} submitted for approval");
}
/**
* Approve a submitted PO.
*/
public static function approvePO(int $poId): void
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$po = $db->selectOne("SELECT * FROM `purchase_orders` WHERE `id` = ?", [$poId]);
if (!$po) {
throw new \RuntimeException('أمر الشراء غير موجود');
}
if ($po['status'] !== 'submitted') {
throw new \RuntimeException('أمر الشراء ليس في حالة مُقدّم');
}
$db->update('purchase_orders', [
'status' => 'approved',
'approved_by' => $employee ? (int) $employee->id : null,
'approved_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [$poId]);
Logger::info("PO #{$poId} approved");
}
/**
* Receive items against an approved/partially_received PO.
*
* @param int $poId
* @param array $receivedItems Each: ['po_item_id' => int, 'quantity_received' => string, 'batch_number' => ?string, 'expiry_date' => ?string]
*/
public static function receivePO(int $poId, array $receivedItems): void
{
$db = App::getInstance()->db();
$po = $db->selectOne("SELECT * FROM `purchase_orders` WHERE `id` = ?", [$poId]);
if (!$po) {
throw new \RuntimeException('أمر الشراء غير موجود');
}
if (!in_array($po['status'], ['approved', 'partially_received'], true)) {
throw new \RuntimeException('أمر الشراء ليس في حالة تسمح بالاستلام');
}
$warehouseId = (int) $po['warehouse_id'];
$db->beginTransaction();
try {
foreach ($receivedItems as $received) {
$poItemId = (int) ($received['po_item_id'] ?? 0);
$qtyReceived = (string) ($received['quantity_received'] ?? '0');
if ($poItemId <= 0 || bccomp($qtyReceived, '0', 3) <= 0) {
continue;
}
$poItem = $db->selectOne(
"SELECT poi.*, i.`tracking_type`
FROM `purchase_order_items` poi
JOIN `inventory_items` i ON i.`id` = poi.`item_id`
WHERE poi.`id` = ? AND poi.`po_id` = ?",
[$poItemId, $poId]
);
if (!$poItem) {
continue;
}
// Update quantity_received on the PO item
$newReceived = bcadd((string) $poItem['quantity_received'], $qtyReceived, 3);
$db->update('purchase_order_items', [
'quantity_received' => $newReceived,
], '`id` = ?', [$poItemId]);
// Create stock movement via StockService
$batchId = null;
if ($poItem['tracking_type'] === 'expiry' && !empty($received['batch_number']) && !empty($received['expiry_date'])) {
$batchId = BatchService::createBatch([
'item_id' => (int) $poItem['item_id'],
'warehouse_id' => $warehouseId,
'batch_number' => $received['batch_number'],
'expiry_date' => $received['expiry_date'],
'quantity' => $qtyReceived,
'unit_cost' => (string) $poItem['unit_price'],
'received_date' => date('Y-m-d'),
]);
}
StockService::moveStock([
'item_id' => (int) $poItem['item_id'],
'warehouse_id' => $warehouseId,
'movement_type' => 'purchase_in',
'direction' => 'in',
'quantity' => $qtyReceived,
'unit_cost' => (string) $poItem['unit_price'],
'batch_id' => $batchId,
'reference_type' => 'purchase_orders',
'reference_id' => $poId,
'notes' => 'استلام أمر شراء — ' . $po['po_number'],
]);
}
// Determine new PO status
$allItems = $db->select(
"SELECT `quantity_ordered`, `quantity_received` FROM `purchase_order_items` WHERE `po_id` = ?",
[$poId]
);
$allFullyReceived = true;
foreach ($allItems as $item) {
if (bccomp((string) $item['quantity_received'], (string) $item['quantity_ordered'], 3) < 0) {
$allFullyReceived = false;
break;
}
}
$newStatus = $allFullyReceived ? 'received' : 'partially_received';
$updateData = [
'status' => $newStatus,
'updated_at' => date('Y-m-d H:i:s'),
];
if ($newStatus === 'received') {
$updateData['received_at'] = date('Y-m-d H:i:s');
}
$db->update('purchase_orders', $updateData, '`id` = ?', [$poId]);
$db->commit();
} catch (\Throwable $e) {
$db->rollBack();
throw $e;
}
EventBus::dispatch('inventory.po_received', [
'po_id' => $poId,
'warehouse_id' => $warehouseId,
'status' => $newStatus,
]);
Logger::info("PO #{$poId} received — new status: {$newStatus}");
}
/**
* Cancel a PO (only from draft or submitted).
*/
public static function cancelPO(int $poId): void
{
$db = App::getInstance()->db();
$po = $db->selectOne("SELECT * FROM `purchase_orders` WHERE `id` = ?", [$poId]);
if (!$po) {
throw new \RuntimeException('أمر الشراء غير موجود');
}
if (!in_array($po['status'], ['draft', 'submitted'], true)) {
throw new \RuntimeException('لا يمكن إلغاء أمر الشراء في حالته الحالية');
}
$db->update('purchase_orders', [
'status' => 'cancelled',
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [$poId]);
Logger::info("PO #{$poId} cancelled");
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Inventory\Services;
use App\Core\App;
use App\Core\EventBus;
use App\Core\Logger;
final class StockAuditService
{
/**
* Create a new stock audit and populate its items.
*/
public static function createAudit(int $warehouseId, string $auditType, ?int $categoryId = null, ?string $notes = null): int
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$auditNumber = InventoryNumberGenerator::nextAuditNumber();
// Determine items to audit
$itemWhere = 'iws.`warehouse_id` = ?';
$itemParams = [$warehouseId];
if ($auditType === 'partial' && $categoryId) {
$itemWhere .= ' AND i.`category_id` = ?';
$itemParams[] = $categoryId;
}
$stockItems = $db->select(
"SELECT iws.`item_id`, iws.`quantity` as system_quantity
FROM `item_warehouse_stock` iws
JOIN `inventory_items` i ON i.`id` = iws.`item_id` AND i.`deleted_at` IS NULL
WHERE {$itemWhere}
ORDER BY i.`name_ar` ASC",
$itemParams
);
// For spot_check, only take a random subset (max 20 items)
if ($auditType === 'spot_check' && count($stockItems) > 20) {
shuffle($stockItems);
$stockItems = array_slice($stockItems, 0, 20);
}
$db->beginTransaction();
try {
$auditId = $db->insert('stock_audits', [
'audit_number' => $auditNumber,
'warehouse_id' => $warehouseId,
'audit_type' => $auditType,
'category_id' => $categoryId,
'status' => 'in_progress',
'audit_date' => date('Y-m-d'),
'started_at' => date('Y-m-d H:i:s'),
'total_items' => count($stockItems),
'items_counted' => 0,
'variance_count' => 0,
'notes' => $notes,
'created_by' => $employee ? (int) $employee->id : null,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
foreach ($stockItems as $si) {
$db->insert('stock_audit_items', [
'audit_id' => $auditId,
'item_id' => (int) $si['item_id'],
'system_quantity' => (string) $si['system_quantity'],
'status' => 'pending',
]);
}
$db->commit();
} catch (\Throwable $e) {
$db->rollBack();
throw $e;
}
Logger::info("Audit #{$auditId} ({$auditNumber}) created: {$auditType} for warehouse #{$warehouseId}, {$stockItems} items");
return $auditId;
}
/**
* Save physical counts for audit items.
*
* @param array $counts [audit_item_id => physical_quantity, ...]
*/
public static function saveCounts(int $auditId, array $counts): void
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$ts = date('Y-m-d H:i:s');
foreach ($counts as $auditItemId => $physicalQty) {
$item = $db->selectOne(
"SELECT * FROM `stock_audit_items` WHERE `id` = ? AND `audit_id` = ?",
[(int) $auditItemId, $auditId]
);
if (!$item) continue;
$systemQty = (string) $item['system_quantity'];
$physicalQty = (string) $physicalQty;
$variance = bcsub($physicalQty, $systemQty, 3);
// Get cost for variance calculation
$itemData = $db->selectOne("SELECT `cost_price` FROM `inventory_items` WHERE `id` = ?", [(int) $item['item_id']]);
$varianceCost = $itemData ? bcmul($variance, (string) ($itemData['cost_price'] ?? '0'), 2) : null;
$db->update('stock_audit_items', [
'physical_quantity' => $physicalQty,
'variance' => $variance,
'variance_cost' => $varianceCost,
'status' => 'counted',
'counted_by' => $employee ? (int) $employee->id : null,
'counted_at' => $ts,
], '`id` = ?', [(int) $auditItemId]);
}
// Update audit summary
$counted = $db->selectOne(
"SELECT COUNT(*) as cnt FROM `stock_audit_items` WHERE `audit_id` = ? AND `status` = 'counted'",
[$auditId]
);
$variances = $db->selectOne(
"SELECT COUNT(*) as cnt FROM `stock_audit_items` WHERE `audit_id` = ? AND `status` = 'counted' AND `variance` != 0",
[$auditId]
);
$db->update('stock_audits', [
'items_counted' => (int) ($counted['cnt'] ?? 0),
'variance_count' => (int) ($variances['cnt'] ?? 0),
'status' => 'pending_approval',
'completed_at' => $ts,
'updated_at' => $ts,
], '`id` = ?', [$auditId]);
}
/**
* Approve an audit and create adjustment movements for variances.
*/
public static function approveAudit(int $auditId): void
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$audit = $db->selectOne("SELECT * FROM `stock_audits` WHERE `id` = ?", [$auditId]);
if (!$audit) {
throw new \RuntimeException('الجرد غير موجود');
}
if ($audit['status'] !== 'pending_approval') {
throw new \RuntimeException('الجرد ليس في حالة انتظار الاعتماد');
}
$warehouseId = (int) $audit['warehouse_id'];
// Get items with variances
$items = $db->select(
"SELECT * FROM `stock_audit_items` WHERE `audit_id` = ? AND `status` = 'counted' AND `variance` != 0",
[$auditId]
);
$db->beginTransaction();
try {
foreach ($items as $item) {
$variance = (string) $item['variance'];
$direction = bccomp($variance, '0', 3) > 0 ? 'in' : 'out';
$absVariance = ltrim($variance, '-');
StockService::moveStock([
'item_id' => (int) $item['item_id'],
'warehouse_id' => $warehouseId,
'movement_type' => $direction === 'in' ? 'adjustment_in' : 'adjustment_out',
'direction' => $direction,
'quantity' => $absVariance,
'reference_type' => 'stock_audits',
'reference_id' => $auditId,
'notes' => 'تسوية جرد — ' . $audit['audit_number'],
]);
$db->update('stock_audit_items', [
'status' => 'approved',
], '`id` = ?', [(int) $item['id']]);
}
$db->update('stock_audits', [
'status' => 'approved',
'approved_by' => $employee ? (int) $employee->id : null,
'approved_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [$auditId]);
$db->commit();
} catch (\Throwable $e) {
$db->rollBack();
throw $e;
}
EventBus::dispatch('inventory.audit_completed', [
'audit_id' => $auditId,
'warehouse_id' => $warehouseId,
'variance_count' => count($items),
]);
Logger::info("Audit #{$auditId} approved — " . count($items) . " adjustment(s) created");
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Inventory\Services;
use App\Core\App;
use App\Core\EventBus;
use App\Core\Logger;
/**
* Single entry point for ALL stock changes.
* Every stock in/out MUST go through moveStock().
*/
final class StockService
{
/**
* Record a stock movement and update warehouse stock.
*
* @param array $data Keys:
* - item_id (int, required)
* - warehouse_id (int, required)
* - movement_type (string, required) e.g. purchase_in, sale_out, transfer_in, ...
* - direction (string, required) 'in' or 'out'
* - quantity (string, required) positive decimal
* - unit_cost (string|null)
* - batch_id (int|null)
* - reference_type (string|null) e.g. 'purchase_orders', 'sales'
* - reference_id (int|null)
* - transfer_id (int|null)
* - notes (string|null)
* - movement_date (string|null) defaults to today
* @return int The stock_movements.id
* @throws \RuntimeException on validation/stock failure
*/
public static function moveStock(array $data): int
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$itemId = (int) ($data['item_id'] ?? 0);
$warehouseId = (int) ($data['warehouse_id'] ?? 0);
$moveType = (string) ($data['movement_type'] ?? '');
$direction = (string) ($data['direction'] ?? '');
$quantity = (string) ($data['quantity'] ?? '0');
$unitCost = isset($data['unit_cost']) ? (string) $data['unit_cost'] : null;
$batchId = isset($data['batch_id']) ? (int) $data['batch_id'] : null;
$refType = $data['reference_type'] ?? null;
$refId = isset($data['reference_id']) ? (int) $data['reference_id'] : null;
$transferId = isset($data['transfer_id']) ? (int) $data['transfer_id'] : null;
$notes = $data['notes'] ?? null;
$moveDate = $data['movement_date'] ?? date('Y-m-d');
// ── Validation ──
if ($itemId <= 0) {
throw new \RuntimeException('الصنف غير محدد');
}
if ($warehouseId <= 0) {
throw new \RuntimeException('المخزن غير محدد');
}
if (!in_array($direction, ['in', 'out'], true)) {
throw new \RuntimeException('اتجاه الحركة غير صالح');
}
if (bccomp($quantity, '0', 3) <= 0) {
throw new \RuntimeException('الكمية يجب أن تكون أكبر من صفر');
}
// Verify item exists and is active
$item = $db->selectOne(
"SELECT `id`, `tracking_type`, `name_ar` FROM `inventory_items` WHERE `id` = ? AND `deleted_at` IS NULL",
[$itemId]
);
if (!$item) {
throw new \RuntimeException('الصنف غير موجود');
}
// Verify warehouse exists and is active
$warehouse = $db->selectOne(
"SELECT `id` FROM `warehouses` WHERE `id` = ? AND `is_active` = 1 AND `deleted_at` IS NULL",
[$warehouseId]
);
if (!$warehouse) {
throw new \RuntimeException('المخزن غير موجود أو غير نشط');
}
// For OUT movements, verify sufficient stock
if ($direction === 'out') {
$currentQty = self::getAvailableQuantity($itemId, $warehouseId);
if (bccomp($currentQty, $quantity, 3) < 0) {
throw new \RuntimeException(
'الرصيد غير كافٍ — المتاح: ' . $currentQty . ' المطلوب: ' . $quantity
);
}
}
$totalCost = null;
if ($unitCost !== null) {
$totalCost = bcmul($unitCost, $quantity, 2);
}
$db->beginTransaction();
try {
// Lock the stock row for concurrency safety
$db->selectOne(
"SELECT `id` FROM `item_warehouse_stock` WHERE `item_id` = ? AND `warehouse_id` = ? FOR UPDATE",
[$itemId, $warehouseId]
);
// Insert movement record
$movementId = $db->insert('stock_movements', [
'warehouse_id' => $warehouseId,
'item_id' => $itemId,
'movement_type' => $moveType,
'direction' => $direction,
'quantity' => $quantity,
'unit_cost' => $unitCost,
'total_cost' => $totalCost,
'batch_id' => $batchId,
'reference_type' => $refType,
'reference_id' => $refId,
'transfer_id' => $transferId,
'notes' => $notes,
'movement_date' => $moveDate,
'created_by' => $employee ? (int) $employee->id : null,
'created_at' => date('Y-m-d H:i:s'),
]);
// Upsert stock level
self::upsertStockLevel($itemId, $warehouseId, $quantity, $direction);
// If batch-tracked OUT, deduct from batch
if ($direction === 'out' && $batchId) {
self::deductBatch($batchId, $quantity);
}
$db->commit();
} catch (\Throwable $e) {
$db->rollBack();
throw $e;
}
// Fire event outside transaction
EventBus::dispatch('inventory.stock_moved', [
'movement_id' => $movementId,
'item_id' => $itemId,
'warehouse_id' => $warehouseId,
'direction' => $direction,
'quantity' => $quantity,
'movement_type' => $moveType,
]);
// Check low stock after OUT
if ($direction === 'out') {
$newQty = self::getAvailableQuantity($itemId, $warehouseId);
$minLevel = self::getMinLevel($itemId, $warehouseId);
if ($minLevel !== null && bccomp($newQty, (string) $minLevel, 3) <= 0) {
EventBus::dispatch('inventory.low_stock_alert', [
'item_id' => $itemId,
'warehouse_id' => $warehouseId,
'quantity' => $newQty,
'min_level' => $minLevel,
]);
}
}
Logger::info("Stock movement #{$movementId}: {$direction} {$quantity} of item #{$itemId} in warehouse #{$warehouseId} ({$moveType})");
return $movementId;
}
/**
* Get the available quantity for an item in a warehouse.
*/
public static function getAvailableQuantity(int $itemId, int $warehouseId): string
{
$db = App::getInstance()->db();
$row = $db->selectOne(
"SELECT `quantity` FROM `item_warehouse_stock` WHERE `item_id` = ? AND `warehouse_id` = ?",
[$itemId, $warehouseId]
);
return (string) ($row['quantity'] ?? '0.000');
}
/**
* Get total stock across all warehouses.
*/
public static function getTotalStock(int $itemId): string
{
$db = App::getInstance()->db();
$row = $db->selectOne(
"SELECT COALESCE(SUM(`quantity`), 0) as total FROM `item_warehouse_stock` WHERE `item_id` = ?",
[$itemId]
);
return (string) ($row['total'] ?? '0.000');
}
/**
* Get movement history for an item (optionally in a specific warehouse).
*/
public static function getMovements(int $itemId, ?int $warehouseId = null, int $limit = 50): array
{
$db = App::getInstance()->db();
$where = '`item_id` = ?';
$params = [$itemId];
if ($warehouseId) {
$where .= ' AND `warehouse_id` = ?';
$params[] = $warehouseId;
}
return $db->select(
"SELECT * FROM `stock_movements` WHERE {$where} ORDER BY `created_at` DESC LIMIT {$limit}",
$params
);
}
// ── Private helpers ──
private static function upsertStockLevel(int $itemId, int $warehouseId, string $quantity, string $direction): void
{
$db = App::getInstance()->db();
$existing = $db->selectOne(
"SELECT `id` FROM `item_warehouse_stock` WHERE `item_id` = ? AND `warehouse_id` = ?",
[$itemId, $warehouseId]
);
if ($existing) {
$op = $direction === 'in' ? '+' : '-';
$db->statement(
"UPDATE `item_warehouse_stock` SET `quantity` = `quantity` {$op} ?, `updated_at` = NOW() WHERE `item_id` = ? AND `warehouse_id` = ?",
[$quantity, $itemId, $warehouseId]
);
} else {
$qty = $direction === 'in' ? $quantity : '-' . $quantity;
$db->insert('item_warehouse_stock', [
'item_id' => $itemId,
'warehouse_id' => $warehouseId,
'quantity' => $qty,
'updated_at' => date('Y-m-d H:i:s'),
]);
}
}
private static function deductBatch(int $batchId, string $quantity): void
{
$db = App::getInstance()->db();
$db->statement(
"UPDATE `item_batches` SET `quantity` = `quantity` - ? WHERE `id` = ?",
[$quantity, $batchId]
);
// Mark depleted if quantity reaches zero
$db->statement(
"UPDATE `item_batches` SET `status` = 'depleted' WHERE `id` = ? AND `quantity` <= 0",
[$batchId]
);
}
private static function getMinLevel(int $itemId, int $warehouseId): ?string
{
$db = App::getInstance()->db();
$row = $db->selectOne(
"SELECT `min_level` FROM `item_warehouse_stock` WHERE `item_id` = ? AND `warehouse_id` = ?",
[$itemId, $warehouseId]
);
return $row['min_level'] ?? null;
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Inventory\Services;
use App\Core\App;
use App\Core\EventBus;
use App\Core\Logger;
use App\Modules\Inventory\Models\StockTransfer;
use App\Modules\Inventory\Models\StockTransferItem;
final class StockTransferService
{
/**
* Create a new transfer with its items.
*
* @param array $header Keys: from_warehouse_id, to_warehouse_id, transfer_date, notes
* @param array $items Each: ['item_id' => int, 'quantity' => string, 'batch_id' => ?int, 'notes' => ?string]
* @return int transfer ID
*/
public static function createTransfer(array $header, array $items): int
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
if (empty($items)) {
throw new \RuntimeException('يجب إضافة صنف واحد على الأقل');
}
$fromId = (int) ($header['from_warehouse_id'] ?? 0);
$toId = (int) ($header['to_warehouse_id'] ?? 0);
if ($fromId <= 0 || $toId <= 0) {
throw new \RuntimeException('يجب تحديد المخزن المصدر والمخزن المستقبل');
}
if ($fromId === $toId) {
throw new \RuntimeException('لا يمكن التحويل لنفس المخزن');
}
$transferNumber = InventoryNumberGenerator::nextTransferNumber();
$db->beginTransaction();
try {
$transferId = $db->insert('stock_transfers', [
'transfer_number' => $transferNumber,
'from_warehouse_id' => $fromId,
'to_warehouse_id' => $toId,
'status' => 'pending_approval',
'requested_by' => $employee ? (int) $employee->id : null,
'transfer_date' => $header['transfer_date'] ?? date('Y-m-d'),
'notes' => $header['notes'] ?? null,
'created_by' => $employee ? (int) $employee->id : null,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
foreach ($items as $item) {
$db->insert('stock_transfer_items', [
'transfer_id' => $transferId,
'item_id' => (int) $item['item_id'],
'quantity' => $item['quantity'],
'batch_id' => isset($item['batch_id']) ? (int) $item['batch_id'] : null,
'notes' => $item['notes'] ?? null,
]);
}
$db->commit();
} catch (\Throwable $e) {
$db->rollBack();
throw $e;
}
Logger::info("Transfer #{$transferId} ({$transferNumber}) created: warehouse #{$fromId} → #{$toId}");
return $transferId;
}
/**
* Approve a pending transfer.
*/
public static function approve(int $transferId): void
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$transfer = $db->selectOne("SELECT * FROM `stock_transfers` WHERE `id` = ?", [$transferId]);
if (!$transfer) {
throw new \RuntimeException('التحويل غير موجود');
}
if ($transfer['status'] !== 'pending_approval') {
throw new \RuntimeException('التحويل ليس في حالة انتظار الموافقة');
}
$db->update('stock_transfers', [
'status' => 'approved',
'approved_by' => $employee ? (int) $employee->id : null,
'approved_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [$transferId]);
Logger::info("Transfer #{$transferId} approved");
}
/**
* Receive a transfer — executes all stock movements.
*/
public static function receive(int $transferId): void
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$transfer = $db->selectOne("SELECT * FROM `stock_transfers` WHERE `id` = ?", [$transferId]);
if (!$transfer) {
throw new \RuntimeException('التحويل غير موجود');
}
if (!in_array($transfer['status'], ['approved', 'in_transit'], true)) {
throw new \RuntimeException('التحويل ليس في حالة تسمح بالاستلام');
}
$items = StockTransferItem::getForTransfer($transferId);
if (empty($items)) {
throw new \RuntimeException('التحويل لا يحتوي على أصناف');
}
$fromWarehouse = (int) $transfer['from_warehouse_id'];
$toWarehouse = (int) $transfer['to_warehouse_id'];
$db->beginTransaction();
try {
foreach ($items as $item) {
// OUT from source warehouse
StockService::moveStock([
'item_id' => (int) $item['item_id'],
'warehouse_id' => $fromWarehouse,
'movement_type' => 'transfer_out',
'direction' => 'out',
'quantity' => (string) $item['quantity'],
'batch_id' => $item['batch_id'] ? (int) $item['batch_id'] : null,
'reference_type' => 'stock_transfers',
'reference_id' => $transferId,
'transfer_id' => $transferId,
'notes' => 'تحويل صادر — ' . $transfer['transfer_number'],
]);
// IN to destination warehouse
StockService::moveStock([
'item_id' => (int) $item['item_id'],
'warehouse_id' => $toWarehouse,
'movement_type' => 'transfer_in',
'direction' => 'in',
'quantity' => (string) $item['quantity'],
'batch_id' => $item['batch_id'] ? (int) $item['batch_id'] : null,
'reference_type' => 'stock_transfers',
'reference_id' => $transferId,
'transfer_id' => $transferId,
'notes' => 'تحويل وارد — ' . $transfer['transfer_number'],
]);
}
$db->update('stock_transfers', [
'status' => 'received',
'received_by' => $employee ? (int) $employee->id : null,
'received_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [$transferId]);
$db->commit();
} catch (\Throwable $e) {
$db->rollBack();
throw $e;
}
EventBus::dispatch('inventory.transfer_completed', [
'transfer_id' => $transferId,
'from_warehouse_id' => $fromWarehouse,
'to_warehouse_id' => $toWarehouse,
]);
Logger::info("Transfer #{$transferId} received: all stock movements executed");
}
}
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>سجل الأصول<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<form method="POST" action="/inventory/assets/run-depreciation" style="display:inline-flex;align-items:center;gap:8px;" onsubmit="return confirm('هل أنت متأكد من تشغيل الإهلاك لهذا الشهر؟');">
<?= csrf_field() ?>
<input type="month" name="month" value="<?= e(date('Y-m')) ?>" class="form-input" style="width:160px;padding:6px 10px;font-size:13px;">
<button type="submit" class="btn" style="background:#D97706;color:#fff;border:none;">
<i data-lucide="calculator" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> تشغيل الإهلاك
</button>
</form>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$statusMap = [
'active' => ['bg' => '#ECFDF5', 'color' => '#059669', 'label' => 'نشط'],
'disposed' => ['bg' => '#FEE2E2', 'color' => '#DC2626', 'label' => 'مستبعد'],
'fully_depreciated' => ['bg' => '#FFF7ED', 'color' => '#D97706', 'label' => 'مهلك بالكامل'],
'inactive' => ['bg' => '#F3F4F6', 'color' => '#6B7280', 'label' => 'غير نشط'],
];
$depMethodLabels = [
'straight_line' => 'القسط الثابت',
'declining_balance' => 'القسط المتناقص',
];
?>
<!-- Search & Filters -->
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/inventory/assets" style="display:flex;gap:10px;flex-wrap:wrap;align-items:end;">
<div style="flex:1;min-width:200px;">
<label class="form-label" style="font-size:12px;">بحث</label>
<input type="text" name="q" value="<?= e($filters['q'] ?? '') ?>" placeholder="رقم الأصل، اسم الصنف..." class="form-input">
</div>
<div style="min-width:160px;">
<label class="form-label" style="font-size:12px;">الحالة</label>
<select name="status" class="form-select">
<option value="">الكل</option>
<?php foreach ($statuses as $key => $label): ?>
<option value="<?= e($key) ?>" <?= ($filters['status'] ?? '') === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="min-width:160px;">
<label class="form-label" style="font-size:12px;">المخزن</label>
<select name="warehouse_id" class="form-select">
<option value="">الكل</option>
<?php foreach ($warehouses as $wh): ?>
<option value="<?= (int) $wh['id'] ?>" <?= ($filters['warehouse_id'] ?? '') == $wh['id'] ? 'selected' : '' ?>><?= e($wh['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<button type="submit" class="btn btn-outline"><i data-lucide="search" style="width:16px;height:16px;vertical-align:middle;"></i> بحث</button>
<a href="/inventory/assets" class="btn btn-sm btn-outline" style="color:#6B7280;">مسح</a>
</form>
</div>
<!-- Assets Table -->
<?php if (!empty($assets)): ?>
<div class="card">
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>رقم الأصل</th>
<th>الصنف</th>
<th>المخزن</th>
<th>تكلفة الشراء</th>
<th>القيمة الدفترية</th>
<th>طريقة الإهلاك</th>
<th>الحالة</th>
<th>الإجراءات</th>
</tr>
</thead>
<tbody>
<?php foreach ($assets as $asset): ?>
<?php
$asSt = $asset['status'] ?? 'active';
$asStInfo = $statusMap[$asSt] ?? $statusMap['active'];
$dm = $asset['depreciation_method'] ?? 'straight_line';
$dmLabel = $depMethodLabels[$dm] ?? $dm;
?>
<tr>
<td>
<a href="/inventory/assets/<?= (int) $asset['id'] ?>" style="color:#0D7377;font-weight:600;text-decoration:none;font-family:monospace;">
<?= e($asset['asset_tag'] ?? '') ?>
</a>
</td>
<td style="font-weight:600;"><?= e($asset['item_name'] ?? '') ?></td>
<td><?= e($asset['warehouse_name'] ?? '') ?></td>
<td style="font-weight:700;direction:ltr;text-align:left;"><?= money($asset['purchase_cost'] ?? 0) ?></td>
<td style="font-weight:700;direction:ltr;text-align:left;"><?= money($asset['book_value'] ?? 0) ?></td>
<td style="font-size:13px;"><?= e($dmLabel) ?></td>
<td>
<span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;background:<?= $asStInfo['bg'] ?>;color:<?= $asStInfo['color'] ?>;">
<?= e($asStInfo['label']) ?>
</span>
</td>
<td>
<a href="/inventory/assets/<?= (int) $asset['id'] ?>" class="btn btn-sm btn-outline" style="font-size:12px;padding:4px 10px;">
<i data-lucide="eye" style="width:13px;height:13px;vertical-align:middle;"></i> عرض
</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php if (isset($pagination) && ($pagination['last_page'] ?? 1) > 1): ?>
<div style="padding:15px;">
<?php $__template->include('Shared.Components.pagination', ['pagination' => $pagination]); ?>
</div>
<?php endif; ?>
<?php else: ?>
<div class="card" style="padding:60px 20px;text-align:center;">
<div style="margin-bottom:15px;">
<i data-lucide="landmark" style="width:48px;height:48px;color:#D1D5DB;"></i>
</div>
<h3 style="color:#6B7280;margin:0 0 8px;">لا توجد أصول مسجلة</h3>
<p style="color:#9CA3AF;font-size:14px;margin:0 0 20px;">
<?php if (!empty($filters['q']) || !empty($filters['status']) || !empty($filters['warehouse_id'])): ?>
لا توجد نتائج مطابقة لبحثك. جرب تغيير معايير البحث.
<?php else: ?>
يتم إنشاء الأصول تلقائيا عند استلام أصناف من نوع "أصل".
<?php endif; ?>
</p>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>أصل <?= e($asset['asset_tag']) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/inventory/assets" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة للقائمة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$statusMap = [
'active' => ['bg' => '#ECFDF5', 'color' => '#059669', 'label' => 'نشط'],
'disposed' => ['bg' => '#FEE2E2', 'color' => '#DC2626', 'label' => 'مستبعد'],
'fully_depreciated' => ['bg' => '#FFF7ED', 'color' => '#D97706', 'label' => 'مهلك بالكامل'],
'inactive' => ['bg' => '#F3F4F6', 'color' => '#6B7280', 'label' => 'غير نشط'],
];
$depMethodLabels = [
'straight_line' => 'القسط الثابت',
'declining_balance' => 'القسط المتناقص',
];
$asSt = $asset['status'] ?? 'active';
$asStInfo = $statusMap[$asSt] ?? $statusMap['active'];
$dm = $asset['depreciation_method'] ?? 'straight_line';
$dmLabel = $depMethodLabels[$dm] ?? $dm;
$purchaseCost = (float) ($asset['purchase_cost'] ?? 0);
$salvageValue = (float) ($asset['salvage_value'] ?? 0);
$depreciableAmount = $purchaseCost - $salvageValue;
$accumulatedDep = (float) ($asset['accumulated_depreciation'] ?? 0);
$bookValue = (float) ($asset['book_value'] ?? 0);
$depPercent = $depreciableAmount > 0 ? min(100, round(($accumulatedDep / $depreciableAmount) * 100, 1)) : 0;
?>
<!-- Asset Info Card -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="landmark" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">بيانات الأصل</h3>
<div style="margin-right:auto;">
<span style="display:inline-block;padding:3px 12px;border-radius:10px;font-size:12px;font-weight:600;background:<?= $asStInfo['bg'] ?>;color:<?= $asStInfo['color'] ?>;"><?= e($asStInfo['label']) ?></span>
</div>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:0;">
<table style="width:100%;font-size:14px;">
<tr>
<td style="padding:10px 0;color:#6B7280;width:40%;">رقم الأصل</td>
<td style="padding:10px 0;font-weight:600;">
<code style="font-size:12px;background:#F3F4F6;padding:2px 8px;border-radius:4px;"><?= e($asset['asset_tag']) ?></code>
</td>
</tr>
<tr>
<td style="padding:10px 0;color:#6B7280;">الصنف</td>
<td style="padding:10px 0;font-weight:600;"><?= e($asset['item_name'] ?? '') ?></td>
</tr>
<tr>
<td style="padding:10px 0;color:#6B7280;">SKU</td>
<td style="padding:10px 0;">
<code style="font-size:11px;background:#F3F4F6;padding:2px 6px;border-radius:4px;"><?= e($asset['sku'] ?? '—') ?></code>
</td>
</tr>
<tr>
<td style="padding:10px 0;color:#6B7280;">المخزن</td>
<td style="padding:10px 0;font-weight:600;"><?= e($asset['warehouse_name'] ?? '') ?></td>
</tr>
<?php if (!empty($asset['serial_number'])): ?>
<tr>
<td style="padding:10px 0;color:#6B7280;">الرقم التسلسلي</td>
<td style="padding:10px 0;font-family:monospace;direction:ltr;text-align:left;"><?= e($asset['serial_number']) ?></td>
</tr>
<?php endif; ?>
</table>
<table style="width:100%;font-size:14px;">
<tr>
<td style="padding:10px 0;color:#6B7280;width:40%;">تاريخ الشراء</td>
<td style="padding:10px 0;"><?= e($asset['purchase_date'] ?? '—') ?></td>
</tr>
<?php if (!empty($asset['warranty_expiry'])): ?>
<tr>
<td style="padding:10px 0;color:#6B7280;">انتهاء الضمان</td>
<td style="padding:10px 0;"><?= e($asset['warranty_expiry']) ?></td>
</tr>
<?php endif; ?>
<tr>
<td style="padding:10px 0;color:#6B7280;">طريقة الإهلاك</td>
<td style="padding:10px 0;font-weight:600;"><?= e($dmLabel) ?></td>
</tr>
<tr>
<td style="padding:10px 0;color:#6B7280;">العمر الافتراضي (شهور)</td>
<td style="padding:10px 0;font-weight:600;"><?= (int) ($asset['useful_life_months'] ?? 0) ?></td>
</tr>
<tr>
<td style="padding:10px 0;color:#6B7280;">الحالة</td>
<td style="padding:10px 0;">
<span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;background:<?= $asStInfo['bg'] ?>;color:<?= $asStInfo['color'] ?>;"><?= e($asStInfo['label']) ?></span>
</td>
</tr>
</table>
</div>
</div>
</div>
<!-- Financial Summary -->
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:15px;margin-bottom:20px;">
<div class="card" style="padding:20px;text-align:center;">
<div style="font-size:13px;color:#6B7280;margin-bottom:6px;">تكلفة الشراء</div>
<div style="font-size:22px;font-weight:800;color:#1A1A2E;direction:ltr;"><?= money($purchaseCost) ?></div>
</div>
<div class="card" style="padding:20px;text-align:center;">
<div style="font-size:13px;color:#6B7280;margin-bottom:6px;">قيمة الإنقاذ</div>
<div style="font-size:22px;font-weight:800;color:#6B7280;direction:ltr;"><?= money($salvageValue) ?></div>
</div>
<div class="card" style="padding:20px;text-align:center;">
<div style="font-size:13px;color:#6B7280;margin-bottom:6px;">الإهلاك المتراكم</div>
<div style="font-size:22px;font-weight:800;color:#D97706;direction:ltr;"><?= money($accumulatedDep) ?></div>
</div>
<div class="card" style="padding:20px;text-align:center;">
<div style="font-size:13px;color:#6B7280;margin-bottom:6px;">القيمة الدفترية</div>
<div style="font-size:22px;font-weight:800;color:#0D7377;direction:ltr;"><?= money($bookValue) ?></div>
</div>
</div>
<!-- Depreciation Progress Bar -->
<div class="card" style="margin-bottom:20px;padding:20px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
<span style="font-size:13px;font-weight:600;color:#374151;">نسبة الإهلاك</span>
<span style="font-size:13px;font-weight:700;color:#D97706;"><?= $depPercent ?>%</span>
</div>
<div style="width:100%;height:16px;background:#F3F4F6;border-radius:8px;overflow:hidden;">
<div style="width:<?= $depPercent ?>%;height:100%;background:linear-gradient(90deg, #0D7377, #059669);border-radius:8px;transition:width 0.5s ease;"></div>
</div>
<div style="display:flex;justify-content:space-between;margin-top:6px;font-size:11px;color:#9CA3AF;">
<span>المبلغ القابل للإهلاك: <?= money($depreciableAmount) ?></span>
<span>المتبقي: <?= money(max(0, $depreciableAmount - $accumulatedDep)) ?></span>
</div>
</div>
<!-- Dispose Form (only for active assets) -->
<?php if ($asSt === 'active'): ?>
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="trash-2" style="width:18px;height:18px;color:#DC2626;"></i>
<h3 style="margin:0;color:#DC2626;font-size:15px;">استبعاد الأصل</h3>
</div>
<div style="padding:20px;">
<form method="POST" action="/inventory/assets/<?= (int) $asset['id'] ?>/dispose" id="disposeForm" onsubmit="return confirm('هل أنت متأكد من استبعاد هذا الأصل؟ هذا الإجراء لا يمكن التراجع عنه.');">
<?= csrf_field() ?>
<div style="display:grid;grid-template-columns:1fr 2fr;gap:20px;align-items:end;">
<div class="form-group">
<label class="form-label">قيمة الاستبعاد</label>
<input type="number" name="disposal_value" value="0" class="form-input" step="0.01" min="0" placeholder="0.00" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">سبب الاستبعاد <span style="color:#DC2626;">*</span></label>
<input type="text" name="disposal_reason" class="form-input" required placeholder="مثال: تالف، نهاية العمر الافتراضي...">
</div>
</div>
<div style="margin-top:15px;">
<button type="submit" class="btn" style="background:#DC2626;color:#fff;border:none;padding:10px 24px;">
<i data-lucide="trash-2" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> استبعاد الأصل
</button>
</div>
</form>
</div>
</div>
<?php endif; ?>
<!-- Depreciation History -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="history" style="width:18px;height:18px;color:#D97706;"></i>
<h3 style="margin:0;color:#D97706;font-size:15px;">سجل الإهلاك</h3>
</div>
<?php if (!empty($depreciationHistory)): ?>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>الفترة</th>
<th>مبلغ الإهلاك</th>
<th>الإهلاك المتراكم</th>
<th>القيمة الدفترية</th>
<th>الطريقة</th>
</tr>
</thead>
<tbody>
<?php foreach ($depreciationHistory as $dep): ?>
<?php
$depMethod = $dep['method_used'] ?? 'straight_line';
$depMethodLabel = $depMethodLabels[$depMethod] ?? $depMethod;
?>
<tr>
<td style="font-weight:600;white-space:nowrap;"><?= e($dep['period_month'] ?? '') ?></td>
<td style="font-weight:700;direction:ltr;text-align:left;color:#DC2626;"><?= money($dep['depreciation_amount'] ?? 0) ?></td>
<td style="font-weight:600;direction:ltr;text-align:left;color:#D97706;"><?= money($dep['accumulated_total'] ?? 0) ?></td>
<td style="font-weight:700;direction:ltr;text-align:left;color:#0D7377;"><?= money($dep['book_value_after'] ?? 0) ?></td>
<td style="font-size:13px;"><?= e($depMethodLabel) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div style="padding:40px 20px;text-align:center;color:#6B7280;">
<i data-lucide="calendar-x" style="width:36px;height:36px;color:#D1D5DB;margin-bottom:8px;"></i>
<p style="margin:0;">لا يوجد سجل إهلاك لهذا الأصل بعد</p>
</div>
<?php endif; ?>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>العد الفعلي — <?= e($audit->audit_number) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/inventory/audits/<?= (int) $audit->id ?>" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة للجرد</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<form method="POST" action="/inventory/audits/<?= (int) $audit->id ?>/count" id="countForm">
<?= csrf_field() ?>
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="list-checks" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">العد الفعلي للأصناف</h3>
<div style="margin-right:auto;font-size:13px;color:#6B7280;">
رقم الجرد: <code style="font-size:12px;background:#F3F4F6;padding:2px 8px;border-radius:4px;"><?= e($audit->audit_number) ?></code>
</div>
</div>
<?php if (!empty($items)): ?>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>#</th>
<th>الصنف</th>
<th>SKU</th>
<th>الوحدة</th>
<th>الكمية بالنظام</th>
<th>الكمية الفعلية</th>
<th>الحالة</th>
</tr>
</thead>
<tbody>
<?php $rowNum = 0; ?>
<?php foreach ($items as $item): ?>
<?php
$rowNum++;
$sysQty = (float) ($item['system_quantity'] ?? 0);
$physQty = $item['physical_quantity'];
$hasVariance = $physQty !== null && (float) $physQty !== $sysQty;
$rowBg = $hasVariance ? '#FFF7ED' : '';
?>
<tr style="<?= $rowBg ? 'background:' . $rowBg . ';' : '' ?>">
<td style="color:#6B7280;font-size:13px;"><?= $rowNum ?></td>
<td style="font-weight:600;"><?= e($item['item_name'] ?? '') ?></td>
<td>
<code style="font-size:11px;background:#F3F4F6;padding:2px 6px;border-radius:4px;"><?= e($item['sku'] ?? '—') ?></code>
</td>
<td style="color:#6B7280;"><?= e($item['unit_of_measure'] ?? '—') ?></td>
<td style="font-weight:700;direction:ltr;text-align:left;color:#6B7280;"><?= number_format($sysQty, 2) ?></td>
<td style="width:150px;">
<input type="number"
name="physical_quantities[<?= (int) $item['id'] ?>]"
value="<?= e($physQty ?? '') ?>"
class="form-input physical-qty"
data-system="<?= $sysQty ?>"
step="0.01"
min="0"
placeholder="0.00"
style="direction:ltr;text-align:left;width:130px;">
</td>
<td>
<?php
$st = $item['status'] ?? 'pending';
if ($st === 'counted'):
?>
<span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;background:#ECFDF5;color:#059669;">تم العد</span>
<?php else: ?>
<span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;background:#F3F4F6;color:#6B7280;">معلق</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div style="padding:40px 20px;text-align:center;color:#6B7280;">
<i data-lucide="package-x" style="width:36px;height:36px;color:#D1D5DB;margin-bottom:8px;"></i>
<p style="margin:0;">لا توجد أصناف في هذا الجرد</p>
</div>
<?php endif; ?>
</div>
<?php if (!empty($items)): ?>
<div style="display:flex;gap:10px;justify-content:flex-start;">
<button type="submit" class="btn btn-primary" style="padding:12px 30px;font-size:15px;">
<i data-lucide="save" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i>
حفظ العد
</button>
<a href="/inventory/audits/<?= (int) $audit->id ?>" class="btn btn-outline" style="padding:12px 30px;font-size:15px;">إلغاء</a>
</div>
<?php endif; ?>
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
// Highlight rows with variance on input change
document.querySelectorAll('.physical-qty').forEach(function(input) {
input.addEventListener('input', function() {
var row = this.closest('tr');
var sysQty = parseFloat(this.dataset.system) || 0;
var physQty = parseFloat(this.value);
if (!isNaN(physQty) && physQty !== sysQty) {
row.style.background = '#FFF7ED';
} else {
row.style.background = '';
}
});
});
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>إنشاء جرد جديد<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/inventory/audits" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة للقائمة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<form method="POST" action="/inventory/audits" id="auditForm">
<?= csrf_field() ?>
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="clipboard-list" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">بيانات الجرد</h3>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:20px;">
<div class="form-group">
<label class="form-label">المخزن <span style="color:#DC2626;">*</span></label>
<select name="warehouse_id" class="form-select" required>
<option value="">-- اختر المخزن --</option>
<?php foreach ($warehouses as $wh): ?>
<option value="<?= (int) $wh['id'] ?>" <?= (int) old('warehouse_id') === (int) $wh['id'] && old('warehouse_id') !== '' ? 'selected' : '' ?>><?= e($wh['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">نوع الجرد <span style="color:#DC2626;">*</span></label>
<select name="audit_type" id="auditType" class="form-select" required>
<?php foreach ($auditTypes as $key => $label): ?>
<option value="<?= e($key) ?>" <?= old('audit_type', 'full') === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group" id="categoryGroup" style="display:none;">
<label class="form-label">التصنيف</label>
<select name="category_id" class="form-select">
<option value="">-- كل التصنيفات --</option>
<?php foreach ($categories as $cat): ?>
<option value="<?= (int) $cat['id'] ?>" <?= (int) old('category_id') === (int) $cat['id'] && old('category_id') !== '' ? 'selected' : '' ?>><?= e($cat['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="form-group" style="margin-top:15px;">
<label class="form-label">ملاحظات</label>
<textarea name="notes" class="form-input" rows="3" placeholder="ملاحظات إضافية حول عملية الجرد..."><?= e(old('notes')) ?></textarea>
</div>
</div>
</div>
<!-- Submit Buttons -->
<div style="display:flex;gap:10px;justify-content:flex-start;">
<button type="submit" class="btn btn-primary" style="padding:12px 30px;font-size:15px;">
<i data-lucide="check" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i>
إنشاء الجرد
</button>
<a href="/inventory/audits" class="btn btn-outline" style="padding:12px 30px;font-size:15px;">إلغاء</a>
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
var auditTypeSelect = document.getElementById('auditType');
var categoryGroup = document.getElementById('categoryGroup');
function toggleCategory() {
categoryGroup.style.display = auditTypeSelect.value === 'partial' ? '' : 'none';
}
auditTypeSelect.addEventListener('change', toggleCategory);
toggleCategory();
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>عمليات الجرد<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/inventory/audits/create" class="btn btn-primary"><i data-lucide="plus" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> جرد جديد</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$statusColors = [
'draft' => ['bg' => '#F3F4F6', 'color' => '#6B7280', 'label' => 'مسودة'],
'in_progress' => ['bg' => '#EFF6FF', 'color' => '#2563EB', 'label' => 'قيد التنفيذ'],
'pending_approval' => ['bg' => '#FFF7ED', 'color' => '#D97706', 'label' => 'بانتظار الموافقة'],
'approved' => ['bg' => '#ECFDF5', 'color' => '#059669', 'label' => 'معتمد'],
'cancelled' => ['bg' => '#FEE2E2', 'color' => '#DC2626', 'label' => 'ملغى'],
];
$auditTypeLabels = [
'full' => 'جرد شامل',
'partial' => 'جرد جزئي',
'spot_check' => 'جرد عشوائي',
];
$auditTypeColors = [
'full' => ['bg' => '#F0FDFA', 'color' => '#0D7377'],
'partial' => ['bg' => '#FFF7ED', 'color' => '#D97706'],
'spot_check' => ['bg' => '#EFF6FF', 'color' => '#2563EB'],
];
?>
<!-- Search & Filters -->
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/inventory/audits" style="display:flex;gap:10px;flex-wrap:wrap;align-items:end;">
<div style="min-width:160px;">
<label class="form-label" style="font-size:12px;">الحالة</label>
<select name="status" class="form-select">
<option value="">الكل</option>
<?php foreach ($statuses as $key => $label): ?>
<option value="<?= e($key) ?>" <?= ($filters['status'] ?? '') === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="min-width:160px;">
<label class="form-label" style="font-size:12px;">نوع الجرد</label>
<select name="audit_type" class="form-select">
<option value="">الكل</option>
<?php foreach ($auditTypes as $key => $label): ?>
<option value="<?= e($key) ?>" <?= ($filters['audit_type'] ?? '') === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="min-width:160px;">
<label class="form-label" style="font-size:12px;">المخزن</label>
<select name="warehouse_id" class="form-select">
<option value="">الكل</option>
<?php foreach ($warehouses as $wh): ?>
<option value="<?= (int) $wh['id'] ?>" <?= ($filters['warehouse_id'] ?? '') == $wh['id'] ? 'selected' : '' ?>><?= e($wh['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<button type="submit" class="btn btn-outline"><i data-lucide="search" style="width:16px;height:16px;vertical-align:middle;"></i> بحث</button>
<a href="/inventory/audits" class="btn btn-sm btn-outline" style="color:#6B7280;">مسح</a>
</form>
</div>
<!-- Audits Table -->
<?php if (!empty($audits)): ?>
<div class="card">
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>رقم الجرد</th>
<th>المخزن</th>
<th>النوع</th>
<th>الحالة</th>
<th>تاريخ الجرد</th>
<th>إجمالي الأصناف</th>
<th>تم العد</th>
<th>الفروقات</th>
<th>الإجراءات</th>
</tr>
</thead>
<tbody>
<?php foreach ($audits as $audit): ?>
<?php
$st = $audit['status'] ?? 'draft';
$stInfo = $statusColors[$st] ?? $statusColors['draft'];
$at = $audit['audit_type'] ?? 'full';
$atLabel = $auditTypeLabels[$at] ?? $at;
$atColors = $auditTypeColors[$at] ?? $auditTypeColors['full'];
?>
<tr>
<td>
<a href="/inventory/audits/<?= (int) $audit['id'] ?>" style="color:#0D7377;font-weight:600;text-decoration:none;font-family:monospace;">
<?= e($audit['audit_number'] ?? '') ?>
</a>
</td>
<td style="font-weight:600;"><?= e($audit['warehouse_name'] ?? '') ?></td>
<td>
<span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;background:<?= $atColors['bg'] ?>;color:<?= $atColors['color'] ?>;">
<?= e($atLabel) ?>
</span>
</td>
<td>
<span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;background:<?= $stInfo['bg'] ?>;color:<?= $stInfo['color'] ?>;">
<?= e($stInfo['label']) ?>
</span>
</td>
<td style="white-space:nowrap;"><?= e($audit['audit_date'] ?? '—') ?></td>
<td style="text-align:center;font-weight:600;"><?= (int) ($audit['total_items'] ?? 0) ?></td>
<td style="text-align:center;font-weight:600;"><?= (int) ($audit['items_counted'] ?? 0) ?></td>
<td style="text-align:center;">
<?php $vc = (int) ($audit['variance_count'] ?? 0); ?>
<?php if ($vc > 0): ?>
<span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;background:#FEE2E2;color:#DC2626;"><?= $vc ?></span>
<?php else: ?>
<span style="color:#059669;font-weight:600;">0</span>
<?php endif; ?>
</td>
<td>
<a href="/inventory/audits/<?= (int) $audit['id'] ?>" class="btn btn-sm btn-outline" style="font-size:12px;padding:4px 10px;">
<i data-lucide="eye" style="width:13px;height:13px;vertical-align:middle;"></i> عرض
</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php if (isset($pagination) && ($pagination['last_page'] ?? 1) > 1): ?>
<div style="padding:15px;">
<?php $__template->include('Shared.Components.pagination', ['pagination' => $pagination]); ?>
</div>
<?php endif; ?>
<?php else: ?>
<div class="card" style="padding:60px 20px;text-align:center;">
<div style="margin-bottom:15px;">
<i data-lucide="clipboard-list" style="width:48px;height:48px;color:#D1D5DB;"></i>
</div>
<h3 style="color:#6B7280;margin:0 0 8px;">لا توجد عمليات جرد</h3>
<p style="color:#9CA3AF;font-size:14px;margin:0 0 20px;">
<?php if (!empty($filters['status']) || !empty($filters['audit_type']) || !empty($filters['warehouse_id'])): ?>
لا توجد نتائج مطابقة لبحثك. جرب تغيير معايير البحث.
<?php else: ?>
ابدأ بإنشاء عملية جرد جديدة.
<?php endif; ?>
</p>
<a href="/inventory/audits/create" class="btn btn-primary"><i data-lucide="plus" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> جرد جديد</a>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>جرد <?= e($audit->audit_number) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<?php if ($audit->status === 'pending_approval'): ?>
<form method="POST" action="/inventory/audits/<?= (int) $audit->id ?>/approve" style="display:inline;">
<?= csrf_field() ?>
<button type="submit" class="btn" style="background:#059669;color:#fff;border:none;" onclick="return confirm('هل أنت متأكد من اعتماد هذا الجرد؟');">
<i data-lucide="check-circle" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> اعتماد الجرد
</button>
</form>
<?php endif; ?>
<?php if ($audit->status === 'in_progress'): ?>
<a href="/inventory/audits/<?= (int) $audit->id ?>/count" class="btn" style="background:#2563EB;color:#fff;border:none;">
<i data-lucide="list-checks" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> متابعة العد
</a>
<?php endif; ?>
<a href="/inventory/audits" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة للقائمة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$statusColors = [
'draft' => ['bg' => '#F3F4F6', 'color' => '#6B7280', 'label' => 'مسودة'],
'in_progress' => ['bg' => '#EFF6FF', 'color' => '#2563EB', 'label' => 'قيد التنفيذ'],
'pending_approval' => ['bg' => '#FFF7ED', 'color' => '#D97706', 'label' => 'بانتظار الموافقة'],
'approved' => ['bg' => '#ECFDF5', 'color' => '#059669', 'label' => 'معتمد'],
'cancelled' => ['bg' => '#FEE2E2', 'color' => '#DC2626', 'label' => 'ملغى'],
];
$auditTypeLabels = [
'full' => 'جرد شامل',
'partial' => 'جرد جزئي',
'spot_check' => 'جرد عشوائي',
];
$st = $audit->status ?? 'draft';
$stInfo = $statusColors[$st] ?? $statusColors['draft'];
$at = $audit->audit_type ?? 'full';
$atLabel = $auditTypeLabels[$at] ?? $at;
?>
<!-- Audit Info Card -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="clipboard-list" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">بيانات الجرد</h3>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:0;">
<table style="width:100%;font-size:14px;">
<tr>
<td style="padding:10px 0;color:#6B7280;width:40%;">رقم الجرد</td>
<td style="padding:10px 0;font-weight:600;">
<code style="font-size:12px;background:#F3F4F6;padding:2px 8px;border-radius:4px;"><?= e($audit->audit_number) ?></code>
</td>
</tr>
<tr>
<td style="padding:10px 0;color:#6B7280;">المخزن</td>
<td style="padding:10px 0;font-weight:600;"><?= e($warehouse['name_ar'] ?? '') ?></td>
</tr>
<tr>
<td style="padding:10px 0;color:#6B7280;">نوع الجرد</td>
<td style="padding:10px 0;font-weight:600;"><?= e($atLabel) ?></td>
</tr>
</table>
<table style="width:100%;font-size:14px;">
<tr>
<td style="padding:10px 0;color:#6B7280;width:40%;">تاريخ الجرد</td>
<td style="padding:10px 0;"><?= e($audit->audit_date ?? '—') ?></td>
</tr>
<tr>
<td style="padding:10px 0;color:#6B7280;">الحالة</td>
<td style="padding:10px 0;">
<span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;background:<?= $stInfo['bg'] ?>;color:<?= $stInfo['color'] ?>;">
<?= e($stInfo['label']) ?>
</span>
</td>
</tr>
<?php if (!empty($audit->notes)): ?>
<tr>
<td style="padding:10px 0;color:#6B7280;">ملاحظات</td>
<td style="padding:10px 0;"><?= e($audit->notes) ?></td>
</tr>
<?php endif; ?>
</table>
</div>
</div>
</div>
<!-- Summary Cards -->
<?php
$totalItems = count($items);
$countedItems = 0;
$varianceItems = 0;
foreach ($items as $itm) {
if ($itm['physical_quantity'] !== null) $countedItems++;
$v = (float) ($itm['variance'] ?? 0);
if ($v != 0) $varianceItems++;
}
?>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:15px;margin-bottom:20px;">
<div class="card" style="padding:20px;text-align:center;">
<div style="font-size:13px;color:#6B7280;margin-bottom:6px;">إجمالي الأصناف</div>
<div style="font-size:28px;font-weight:800;color:#0D7377;"><?= $totalItems ?></div>
</div>
<div class="card" style="padding:20px;text-align:center;">
<div style="font-size:13px;color:#6B7280;margin-bottom:6px;">تم العد</div>
<div style="font-size:28px;font-weight:800;color:#2563EB;"><?= $countedItems ?></div>
</div>
<div class="card" style="padding:20px;text-align:center;">
<div style="font-size:13px;color:#6B7280;margin-bottom:6px;">الفروقات</div>
<div style="font-size:28px;font-weight:800;color:<?= $varianceItems > 0 ? '#DC2626' : '#059669' ?>;"><?= $varianceItems ?></div>
</div>
</div>
<!-- Items Table -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="package" style="width:18px;height:18px;color:#D97706;"></i>
<h3 style="margin:0;color:#D97706;font-size:15px;">أصناف الجرد</h3>
</div>
<?php if (!empty($items)): ?>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>#</th>
<th>الصنف</th>
<th>SKU</th>
<th>كمية النظام</th>
<th>الكمية الفعلية</th>
<th>الفرق</th>
<th>تكلفة الفرق</th>
<th>الحالة</th>
</tr>
</thead>
<tbody>
<?php $rowNum = 0; ?>
<?php foreach ($items as $item): ?>
<?php
$rowNum++;
$variance = (float) ($item['variance'] ?? 0);
$varianceCost = (float) ($item['variance_cost'] ?? 0);
$itemStatus = $item['status'] ?? 'pending';
if ($variance > 0) {
$varColor = '#059669';
$varBg = '#ECFDF5';
$varSign = '+';
} elseif ($variance < 0) {
$varColor = '#DC2626';
$varBg = '#FEE2E2';
$varSign = '';
} else {
$varColor = '#6B7280';
$varBg = '';
$varSign = '';
}
$rowBg = $variance != 0 ? ($variance > 0 ? '#F0FDF4' : '#FEF2F2') : '';
?>
<tr style="<?= $rowBg ? 'background:' . $rowBg . ';' : '' ?>">
<td style="color:#6B7280;font-size:13px;"><?= $rowNum ?></td>
<td style="font-weight:600;"><?= e($item['item_name'] ?? '') ?></td>
<td>
<code style="font-size:11px;background:#F3F4F6;padding:2px 6px;border-radius:4px;"><?= e($item['sku'] ?? '—') ?></code>
</td>
<td style="font-weight:600;direction:ltr;text-align:left;"><?= number_format((float) ($item['system_quantity'] ?? 0), 2) ?></td>
<td style="font-weight:600;direction:ltr;text-align:left;">
<?= $item['physical_quantity'] !== null ? number_format((float) $item['physical_quantity'], 2) : '—' ?>
</td>
<td style="font-weight:700;direction:ltr;text-align:left;color:<?= $varColor ?>;">
<?php if ($variance != 0): ?>
<span style="display:inline-block;padding:2px 8px;border-radius:8px;background:<?= $varBg ?>;color:<?= $varColor ?>;">
<?= $varSign ?><?= number_format($variance, 2) ?>
</span>
<?php else: ?>
<span style="color:#6B7280;">0.00</span>
<?php endif; ?>
</td>
<td style="font-weight:600;direction:ltr;text-align:left;color:<?= $varColor ?>;">
<?php if ($varianceCost != 0): ?>
<?= $varSign ?><?= money(abs($varianceCost)) ?>
<?php else: ?>
<span style="color:#6B7280;"></span>
<?php endif; ?>
</td>
<td>
<?php if ($itemStatus === 'counted'): ?>
<span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;background:#ECFDF5;color:#059669;">تم العد</span>
<?php else: ?>
<span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;background:#F3F4F6;color:#6B7280;">معلق</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div style="padding:40px 20px;text-align:center;color:#6B7280;">
<i data-lucide="package-x" style="width:36px;height:36px;color:#D1D5DB;margin-bottom:8px;"></i>
<p style="margin:0;">لا توجد أصناف في هذا الجرد</p>
</div>
<?php endif; ?>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php
$isEdit = isset($category) && $category !== null;
$pageTitle = $isEdit ? 'تعديل التصنيف: ' . e($category->name_ar) : 'إضافة تصنيف جديد';
$formAction = $isEdit ? '/inventory/categories/' . (int) $category->id : '/inventory/categories';
?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?><?= $pageTitle ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/inventory/categories" class="btn btn-outline"><i data-lucide="arrow-right" style="width:16px;height:16px;"></i> رجوع</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<form method="POST" action="<?= $formAction ?>">
<?= csrf_field() ?>
<div class="card" style="padding:20px;margin-bottom:20px;">
<h3 style="margin:0 0 20px;font-size:16px;font-weight:700;color:#1F2937;border-bottom:1px solid #E5E7EB;padding-bottom:12px;">
<i data-lucide="tag" style="width:18px;height:18px;vertical-align:middle;color:#0D7377;"></i>
بيانات التصنيف
</h3>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;">
<!-- Parent Category -->
<div class="form-group">
<label class="form-label">التصنيف الرئيسي</label>
<select name="parent_id" class="form-select">
<option value="">— تصنيف رئيسي —</option>
<?php foreach ($roots as $root): ?>
<option value="<?= (int) $root['id'] ?>" <?= (string) e(old('parent_id', $category->parent_id ?? '')) === (string) $root['id'] ? 'selected' : '' ?>><?= e($root['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<!-- Code -->
<div class="form-group">
<label class="form-label">الكود <span style="color:#DC2626;">*</span></label>
<input type="text" name="code" value="<?= e(old('code', $category->code ?? '')) ?>" class="form-input" required style="direction:ltr;text-align:right;text-transform:uppercase;">
</div>
<!-- Name AR -->
<div class="form-group">
<label class="form-label">الاسم بالعربي <span style="color:#DC2626;">*</span></label>
<input type="text" name="name_ar" value="<?= e(old('name_ar', $category->name_ar ?? '')) ?>" class="form-input" required>
</div>
<!-- Name EN -->
<div class="form-group">
<label class="form-label">الاسم بالإنجليزي</label>
<input type="text" name="name_en" value="<?= e(old('name_en', $category->name_en ?? '')) ?>" class="form-input" style="direction:ltr;text-align:right;">
</div>
<!-- Description AR -->
<div class="form-group" style="grid-column:1/-1;">
<label class="form-label">الوصف</label>
<textarea name="description_ar" class="form-textarea" rows="3"><?= e(old('description_ar', $category->description_ar ?? '')) ?></textarea>
</div>
<!-- Sort Order -->
<div class="form-group">
<label class="form-label">ترتيب العرض</label>
<input type="number" name="sort_order" value="<?= e(old('sort_order', $category->sort_order ?? '0')) ?>" class="form-input" min="0">
</div>
<!-- Is Active -->
<div class="form-group" style="display:flex;align-items:end;">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;padding-bottom:8px;">
<input type="hidden" name="is_active" value="0">
<input type="checkbox" name="is_active" value="1" <?= old('is_active', $category->is_active ?? '1') ? 'checked' : '' ?> style="width:18px;height:18px;accent-color:#0D7377;cursor:pointer;">
<span class="form-label" style="margin:0;">نشط</span>
</label>
</div>
</div>
</div>
<div style="display:flex;gap:10px;">
<button type="submit" class="btn btn-primary">
<i data-lucide="<?= $isEdit ? 'save' : 'plus-circle' ?>" style="width:16px;height:16px;"></i>
<?= $isEdit ? 'حفظ التعديلات' : 'إضافة التصنيف' ?>
</button>
<a href="/inventory/categories" class="btn btn-outline">إلغاء</a>
</div>
</form>
<?php $__template->endSection(); ?>
<script>
document.addEventListener('DOMContentLoaded', function() { lucide.createIcons(); });
</script>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تصنيفات الأصناف<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/inventory/categories/create" class="btn btn-primary"><i data-lucide="plus" style="width:16px;height:16px;"></i> تصنيف جديد</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/inventory/categories" style="display:flex;gap:10px;flex-wrap:wrap;align-items:end;">
<div>
<label class="form-label" style="font-size:12px;">بحث</label>
<input type="text" name="q" value="<?= e($filters['q'] ?? '') ?>" placeholder="الكود، الاسم..." class="form-input" style="min-width:200px;">
</div>
<div>
<label class="form-label" style="font-size:12px;">التصنيف الرئيسي</label>
<select name="parent_id" class="form-select" style="min-width:150px;">
<option value="">الكل</option>
<?php foreach ($roots as $root): ?>
<option value="<?= (int) $root['id'] ?>" <?= ($filters['parent_id'] ?? '') == $root['id'] ? 'selected' : '' ?>><?= e($root['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<button type="submit" class="btn btn-outline">بحث</button>
<a href="/inventory/categories" class="btn btn-sm btn-outline" style="color:#6B7280;">مسح</a>
</form>
</div>
<?php if (!empty($tree)): ?>
<div class="card">
<?php foreach ($tree as $root): ?>
<div style="border-right:4px solid #0D7377;margin-bottom:0;<?= $root !== end($tree) ? 'border-bottom:1px solid #E5E7EB;' : '' ?>">
<!-- Root category row -->
<div style="display:flex;align-items:center;justify-content:space-between;padding:15px 20px;background:#F9FAFB;">
<div style="display:flex;align-items:center;gap:12px;">
<i data-lucide="folder" style="width:20px;height:20px;color:#0D7377;"></i>
<div>
<div style="font-weight:700;font-size:15px;color:#1F2937;"><?= e($root['name_ar']) ?></div>
<div style="display:flex;gap:10px;margin-top:3px;">
<code style="font-size:12px;background:#E5E7EB;padding:1px 6px;border-radius:3px;"><?= e($root['code']) ?></code>
<span style="font-size:12px;color:#6B7280;"><?= count($root['children'] ?? []) ?> تصنيف فرعي</span>
</div>
</div>
</div>
<div style="display:flex;align-items:center;gap:10px;">
<?php if ($root['is_active']): ?>
<span style="color:#059669;font-weight:600;font-size:13px;">● نشط</span>
<?php else: ?>
<span style="color:#DC2626;font-weight:600;font-size:13px;">● معطل</span>
<?php endif; ?>
<a href="/inventory/categories/<?= (int) $root['id'] ?>/edit" class="btn btn-sm btn-outline">
<i data-lucide="edit-2" style="width:14px;height:14px;"></i> تعديل
</a>
</div>
</div>
<!-- Children -->
<?php if (!empty($root['children'])): ?>
<?php foreach ($root['children'] as $child): ?>
<div style="display:flex;align-items:center;justify-content:space-between;padding:12px 20px;padding-right:50px;border-top:1px solid #F3F4F6;">
<div style="display:flex;align-items:center;gap:10px;">
<i data-lucide="corner-down-left" style="width:16px;height:16px;color:#9CA3AF;transform:scaleX(-1);"></i>
<i data-lucide="file" style="width:16px;height:16px;color:#6B7280;"></i>
<div>
<span style="font-weight:600;color:#374151;"><?= e($child['name_ar']) ?></span>
<code style="font-size:12px;background:#E5E7EB;padding:1px 6px;border-radius:3px;margin-right:8px;"><?= e($child['code']) ?></code>
</div>
</div>
<div style="display:flex;align-items:center;gap:10px;">
<?php if ($child['is_active']): ?>
<span style="color:#059669;font-weight:600;font-size:13px;">● نشط</span>
<?php else: ?>
<span style="color:#DC2626;font-weight:600;font-size:13px;">● معطل</span>
<?php endif; ?>
<a href="/inventory/categories/<?= (int) $child['id'] ?>/edit" class="btn btn-sm btn-outline">
<i data-lucide="edit-2" style="width:14px;height:14px;"></i> تعديل
</a>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
<?php endforeach; ?>
<?php if (isset($pagination) && ($pagination['last_page'] ?? 1) > 1): ?>
<div style="padding:15px;">
<?php $__template->include('Shared.Components.pagination', ['pagination' => $pagination]); ?>
</div>
<?php endif; ?>
</div>
<?php else: ?>
<div class="card" style="padding:60px 20px;text-align:center;">
<i data-lucide="folder-open" style="width:48px;height:48px;color:#D1D5DB;margin:0 auto 15px;display:block;"></i>
<p style="color:#6B7280;font-size:15px;margin:0;">لا توجد تصنيفات</p>
<a href="/inventory/categories/create" style="color:#0D7377;font-size:14px;margin-top:10px;display:inline-block;">+ إضافة تصنيف جديد</a>
</div>
<?php endif; ?>
<?php $__template->endSection(); ?>
<script>
document.addEventListener('DOMContentLoaded', function() { lucide.createIcons(); });
</script>
<?php
$isEdit = $item !== null;
$__template->layout('Layout.main');
?>
<?php $__template->section('title'); ?><?= $isEdit ? 'تعديل الصنف' : 'إضافة صنف جديد' ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<?php if ($isEdit): ?>
<a href="/inventory/items/<?= (int) $item->id ?>" class="btn btn-outline"><i data-lucide="eye" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> عرض الصنف</a>
<?php endif; ?>
<a href="/inventory/items" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة للقائمة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<form method="POST" action="<?= $isEdit ? '/inventory/items/' . (int) $item->id : '/inventory/items' ?>" id="itemForm">
<?= csrf_field() ?>
<!-- Card 1: Basic Information -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="file-text" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">البيانات الأساسية</h3>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:20px;">
<div class="form-group">
<label class="form-label">كود الصنف (SKU) <span style="color:#DC2626;">*</span></label>
<input type="text" name="sku" value="<?= e(old('sku', $item->sku ?? '')) ?>" class="form-input" required placeholder="مثال: ITM-001" style="direction:ltr;text-align:left;text-transform:uppercase;">
</div>
<div class="form-group">
<label class="form-label">الباركود</label>
<input type="text" name="barcode" value="<?= e(old('barcode', $item->barcode ?? '')) ?>" class="form-input" placeholder="باركود الصنف" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">الاسم بالعربي <span style="color:#DC2626;">*</span></label>
<input type="text" name="name_ar" value="<?= e(old('name_ar', $item->name_ar ?? '')) ?>" class="form-input" required placeholder="اسم الصنف بالعربي">
</div>
<div class="form-group">
<label class="form-label">الاسم بالإنجليزي</label>
<input type="text" name="name_en" value="<?= e(old('name_en', $item->name_en ?? '')) ?>" class="form-input" placeholder="Item name in English" style="direction:ltr;text-align:left;">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:20px;margin-top:15px;">
<div class="form-group">
<label class="form-label">التصنيف</label>
<select name="category_id" class="form-select">
<option value="">-- بدون تصنيف --</option>
<?php foreach ($categories as $cat): ?>
<option value="<?= (int) $cat['id'] ?>" <?= (int) old('category_id', $item->category_id ?? '') === (int) $cat['id'] && old('category_id', $item->category_id ?? '') !== '' ? 'selected' : '' ?>><?= e($cat['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">وحدة القياس <span style="color:#DC2626;">*</span></label>
<select name="unit_of_measure" class="form-select" required>
<?php foreach ($units as $key => $label): ?>
<option value="<?= e($key) ?>" <?= old('unit_of_measure', $item->unit_of_measure ?? 'piece') === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">نوع التتبع <span style="color:#DC2626;">*</span></label>
<select name="tracking_type" class="form-select" required>
<?php foreach ($trackingTypes as $key => $label): ?>
<option value="<?= e($key) ?>" <?= old('tracking_type', $item->tracking_type ?? 'standard') === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="form-group" style="margin-top:15px;">
<label class="form-label">وصف الصنف</label>
<textarea name="description_ar" class="form-input" rows="3" placeholder="وصف تفصيلي للصنف..."><?= e(old('description_ar', $item->description_ar ?? '')) ?></textarea>
</div>
</div>
</div>
<!-- Card 2: Pricing -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="banknote" style="width:18px;height:18px;color:#059669;"></i>
<h3 style="margin:0;color:#059669;font-size:15px;">الأسعار</h3>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:20px;">
<div class="form-group">
<label class="form-label">سعر التكلفة</label>
<input type="number" name="cost_price" value="<?= e(old('cost_price', $item->cost_price ?? '0.00')) ?>" class="form-input" step="0.01" min="0" placeholder="0.00" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">سعر البيع (عضو)</label>
<input type="number" name="sale_price_member" value="<?= e(old('sale_price_member', $item->sale_price_member ?? '0.00')) ?>" class="form-input" step="0.01" min="0" placeholder="0.00" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">سعر البيع (غير عضو)</label>
<input type="number" name="sale_price_nonmember" value="<?= e(old('sale_price_nonmember', $item->sale_price_nonmember ?? '0.00')) ?>" class="form-input" step="0.01" min="0" placeholder="0.00" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">سعر البيع (لاعب)</label>
<input type="number" name="sale_price_player" value="<?= e(old('sale_price_player', $item->sale_price_player ?? '')) ?>" class="form-input" step="0.01" min="0" placeholder="0.00" style="direction:ltr;text-align:left;">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 3fr;gap:20px;margin-top:15px;">
<div class="form-group">
<label class="form-label">نسبة الضريبة (%)</label>
<input type="number" name="tax_rate" value="<?= e(old('tax_rate', $item->tax_rate ?? '14.00')) ?>" class="form-input" step="0.01" min="0" max="100" placeholder="14.00" style="direction:ltr;text-align:left;">
</div>
</div>
</div>
</div>
<!-- Card 3: Settings -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="settings" style="width:18px;height:18px;color:#D97706;"></i>
<h3 style="margin:0;color:#D97706;font-size:15px;">إعدادات</h3>
</div>
<div style="padding:20px;">
<div style="display:flex;gap:30px;margin-bottom:20px;">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;padding:12px 16px;border:1px solid #E5E7EB;border-radius:8px;">
<input type="hidden" name="is_sellable" value="0">
<input type="checkbox" name="is_sellable" value="1" <?= (int) old('is_sellable', $item->is_sellable ?? 1) ? 'checked' : '' ?> style="width:18px;height:18px;accent-color:#0D7377;">
<div>
<div style="font-size:13px;font-weight:600;color:#1A1A2E;">متاح للبيع</div>
<div style="font-size:11px;color:#9CA3AF;">يظهر في شاشة البيع ونقاط البيع</div>
</div>
</label>
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;padding:12px 16px;border:1px solid #E5E7EB;border-radius:8px;">
<input type="hidden" name="is_active" value="0">
<input type="checkbox" name="is_active" value="1" <?= (int) old('is_active', $item->is_active ?? 1) ? 'checked' : '' ?> style="width:18px;height:18px;accent-color:#0D7377;">
<div>
<div style="font-size:13px;font-weight:600;color:#1A1A2E;">فعّال</div>
<div style="font-size:11px;color:#9CA3AF;">الصنف نشط ويمكن استخدامه</div>
</div>
</label>
</div>
<div class="form-group">
<label class="form-label">ملاحظات</label>
<textarea name="notes" class="form-input" rows="3" placeholder="ملاحظات إضافية..."><?= e(old('notes', $item->notes ?? '')) ?></textarea>
</div>
</div>
</div>
<!-- Submit Buttons -->
<div style="display:flex;gap:10px;justify-content:flex-start;">
<button type="submit" class="btn btn-primary" style="padding:12px 30px;font-size:15px;">
<i data-lucide="check" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i>
<?= $isEdit ? 'حفظ التعديلات' : 'إضافة الصنف' ?>
</button>
<a href="/inventory/items" class="btn btn-outline" style="padding:12px 30px;font-size:15px;">إلغاء</a>
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>الأصناف<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/inventory/items/create" class="btn btn-primary"><i data-lucide="plus" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> إضافة صنف جديد</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Search & Filters -->
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/inventory/items" style="display:flex;gap:10px;flex-wrap:wrap;align-items:end;">
<div style="flex:1;min-width:200px;">
<label class="form-label" style="font-size:12px;">بحث</label>
<input type="text" name="q" value="<?= e($filters['q'] ?? '') ?>" placeholder="اسم الصنف، SKU، باركود..." class="form-input">
</div>
<div style="min-width:160px;">
<label class="form-label" style="font-size:12px;">التصنيف</label>
<select name="category_id" class="form-select">
<option value="">الكل</option>
<?php foreach ($categories as $cat): ?>
<option value="<?= (int) $cat['id'] ?>" <?= ($filters['category_id'] ?? '') == $cat['id'] ? 'selected' : '' ?>><?= e($cat['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="min-width:150px;">
<label class="form-label" style="font-size:12px;">نوع التتبع</label>
<select name="tracking_type" class="form-select">
<option value="">الكل</option>
<?php foreach ($trackingTypes as $key => $label): ?>
<option value="<?= e($key) ?>" <?= ($filters['tracking_type'] ?? '') === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="min-width:160px;">
<label class="form-label" style="font-size:12px;">المستودع</label>
<select name="warehouse_id" class="form-select">
<option value="">الكل</option>
<?php foreach ($warehouses as $wh): ?>
<option value="<?= (int) $wh['id'] ?>" <?= ($filters['warehouse_id'] ?? '') == $wh['id'] ? 'selected' : '' ?>><?= e($wh['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<button type="submit" class="btn btn-outline"><i data-lucide="search" style="width:16px;height:16px;vertical-align:middle;"></i> بحث</button>
<a href="/inventory/items" class="btn btn-sm btn-outline" style="color:#6B7280;">مسح</a>
</form>
</div>
<!-- Items Table -->
<?php if (!empty($items)): ?>
<div class="card">
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>SKU</th>
<th>الاسم</th>
<th>التصنيف</th>
<th>نوع التتبع</th>
<th>الوحدة</th>
<th>سعر البيع (عضو)</th>
<th>للبيع</th>
<th>الإجراءات</th>
</tr>
</thead>
<tbody>
<?php
$trackingColors = [
'standard' => ['bg' => '#F3F4F6', 'color' => '#6B7280'],
'expiry' => ['bg' => '#FFF7ED', 'color' => '#D97706'],
'asset' => ['bg' => '#EFF6FF', 'color' => '#2563EB'],
];
$unitLabels = [
'piece' => 'قطعة', 'kg' => 'كيلوجرام', 'liter' => 'لتر',
'box' => 'صندوق', 'pack' => 'عبوة', 'meter' => 'متر', 'set' => 'طقم',
];
?>
<?php foreach ($items as $item): ?>
<tr>
<td style="direction:ltr;text-align:left;font-family:monospace;font-weight:600;"><?= e($item['sku'] ?? '') ?></td>
<td>
<a href="/inventory/items/<?= (int) $item['id'] ?>" style="color:#0D7377;font-weight:600;text-decoration:none;">
<?= e($item['name_ar'] ?? '') ?>
</a>
<?php if (!empty($item['name_en'])): ?>
<div style="font-size:11px;color:#9CA3AF;"><?= e($item['name_en']) ?></div>
<?php endif; ?>
</td>
<td><?= (int) ($item['category_id'] ?? 0) ?: '—' ?></td>
<td>
<?php
$tt = $item['tracking_type'] ?? 'standard';
$ttColors = $trackingColors[$tt] ?? $trackingColors['standard'];
$ttLabel = $trackingTypes[$tt] ?? $tt;
?>
<span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;background:<?= $ttColors['bg'] ?>;color:<?= $ttColors['color'] ?>;">
<?= e($ttLabel) ?>
</span>
</td>
<td><?= e($unitLabels[$item['unit_of_measure'] ?? ''] ?? ($item['unit_of_measure'] ?? '')) ?></td>
<td style="font-weight:700;direction:ltr;text-align:left;"><?= money($item['sale_price_member'] ?? 0) ?></td>
<td>
<?php if ((int) ($item['is_sellable'] ?? 0)): ?>
<span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;background:#ECFDF5;color:#059669;">نعم</span>
<?php else: ?>
<span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;background:#FEE2E2;color:#DC2626;">لا</span>
<?php endif; ?>
</td>
<td>
<div style="display:flex;gap:6px;">
<a href="/inventory/items/<?= (int) $item['id'] ?>" class="btn btn-sm btn-outline" style="font-size:12px;padding:4px 10px;">
<i data-lucide="eye" style="width:13px;height:13px;vertical-align:middle;"></i> عرض
</a>
<a href="/inventory/items/<?= (int) $item['id'] ?>/edit" class="btn btn-sm btn-outline" style="font-size:12px;padding:4px 10px;">
<i data-lucide="edit-3" style="width:13px;height:13px;vertical-align:middle;"></i> تعديل
</a>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php if (isset($pagination) && ($pagination['last_page'] ?? 1) > 1): ?>
<div style="padding:15px;">
<?php $__template->include('Shared.Components.pagination', ['pagination' => $pagination]); ?>
</div>
<?php endif; ?>
<?php else: ?>
<div class="card" style="padding:60px 20px;text-align:center;">
<div style="margin-bottom:15px;">
<i data-lucide="package" style="width:48px;height:48px;color:#D1D5DB;"></i>
</div>
<h3 style="color:#6B7280;margin:0 0 8px;">لا توجد أصناف</h3>
<p style="color:#9CA3AF;font-size:14px;margin:0 0 20px;">
<?php if (!empty($filters['q']) || !empty($filters['category_id']) || !empty($filters['tracking_type']) || !empty($filters['warehouse_id'])): ?>
لا توجد نتائج مطابقة لبحثك. جرب تغيير معايير البحث.
<?php else: ?>
ابدأ بإضافة صنف جديد للمخزون.
<?php endif; ?>
</p>
<a href="/inventory/items/create" class="btn btn-primary"><i data-lucide="plus" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> إضافة صنف جديد</a>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?><?= e($item->name_ar) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/inventory/items/<?= (int) $item->id ?>/edit" class="btn btn-outline"><i data-lucide="edit-3" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> تعديل</a>
<a href="/inventory/items" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة للقائمة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$trackingColors = [
'standard' => ['bg' => '#F3F4F6', 'color' => '#6B7280'],
'expiry' => ['bg' => '#FFF7ED', 'color' => '#D97706'],
'asset' => ['bg' => '#EFF6FF', 'color' => '#2563EB'],
];
$unitLabels = $units ?? [];
$tt = $item->tracking_type ?? 'standard';
$ttColors = $trackingColors[$tt] ?? $trackingColors['standard'];
$ttLabel = $trackingTypes[$tt] ?? $tt;
$unitLabel = $unitLabels[$item->unit_of_measure ?? ''] ?? ($item->unit_of_measure ?? '');
$movementTypeLabels = [
'purchase_in' => 'شراء',
'sale_out' => 'بيع',
'transfer_in' => 'تحويل وارد',
'transfer_out' => 'تحويل صادر',
'adjustment_in' => 'تسوية (+)',
'adjustment_out' => 'تسوية (-)',
'damage_out' => 'تالف',
'expired_out' => 'منتهي صلاحية',
'return_in' => 'مرتجع',
'opening_balance' => 'رصيد افتتاحي',
'consumed_out' => 'مستهلك',
];
?>
<!-- Item Details Card -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="package" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">بيانات الصنف</h3>
<div style="margin-right:auto;display:flex;gap:8px;">
<?php if ((int) $item->is_active): ?>
<span style="display:inline-block;padding:3px 12px;border-radius:10px;font-size:12px;font-weight:600;background:#ECFDF5;color:#059669;">فعّال</span>
<?php else: ?>
<span style="display:inline-block;padding:3px 12px;border-radius:10px;font-size:12px;font-weight:600;background:#FEE2E2;color:#DC2626;">معطّل</span>
<?php endif; ?>
<span style="display:inline-block;padding:3px 12px;border-radius:10px;font-size:12px;font-weight:600;background:<?= $ttColors['bg'] ?>;color:<?= $ttColors['color'] ?>;"><?= e($ttLabel) ?></span>
</div>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(220px, 1fr));gap:20px;">
<div>
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">كود الصنف (SKU)</div>
<div style="font-weight:700;font-family:monospace;direction:ltr;text-align:left;font-size:15px;"><?= e($item->sku ?? '') ?></div>
</div>
<div>
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">الباركود</div>
<div style="font-weight:600;font-family:monospace;direction:ltr;text-align:left;"><?= e($item->barcode ?? '') ?: '—' ?></div>
</div>
<div>
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">الاسم بالعربي</div>
<div style="font-weight:700;font-size:15px;"><?= e($item->name_ar ?? '') ?></div>
</div>
<div>
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">الاسم بالإنجليزي</div>
<div style="font-weight:600;direction:ltr;text-align:left;"><?= e($item->name_en ?? '') ?: '—' ?></div>
</div>
<div>
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">التصنيف</div>
<div style="font-weight:600;"><?= $category ? e($category->name_ar) : '—' ?></div>
</div>
<div>
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">وحدة القياس</div>
<div style="font-weight:600;"><?= e($unitLabel) ?></div>
</div>
<div>
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">نوع التتبع</div>
<div><span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;background:<?= $ttColors['bg'] ?>;color:<?= $ttColors['color'] ?>;"><?= e($ttLabel) ?></span></div>
</div>
<div>
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">للبيع</div>
<div>
<?php if ((int) $item->is_sellable): ?>
<span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;background:#ECFDF5;color:#059669;">نعم</span>
<?php else: ?>
<span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;background:#FEE2E2;color:#DC2626;">لا</span>
<?php endif; ?>
</div>
</div>
</div>
<?php if (!empty($item->description_ar)): ?>
<div style="margin-top:20px;padding-top:15px;border-top:1px solid #F3F4F6;">
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">وصف الصنف</div>
<div style="font-size:14px;color:#4B5563;line-height:1.7;"><?= e($item->description_ar) ?></div>
</div>
<?php endif; ?>
<!-- Pricing Section -->
<div style="margin-top:20px;padding-top:15px;border-top:1px solid #F3F4F6;">
<div style="font-size:13px;font-weight:600;color:#059669;margin-bottom:12px;display:flex;align-items:center;gap:6px;">
<i data-lucide="banknote" style="width:16px;height:16px;"></i> الأسعار
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(180px, 1fr));gap:15px;">
<div style="padding:12px;background:#F9FAFB;border-radius:8px;">
<div style="font-size:11px;color:#6B7280;">سعر التكلفة</div>
<div style="font-weight:700;font-size:16px;color:#1A1A2E;direction:ltr;text-align:left;"><?= money($item->cost_price ?? 0) ?></div>
</div>
<div style="padding:12px;background:#F0FDF4;border-radius:8px;">
<div style="font-size:11px;color:#6B7280;">سعر البيع (عضو)</div>
<div style="font-weight:700;font-size:16px;color:#059669;direction:ltr;text-align:left;"><?= money($item->sale_price_member ?? 0) ?></div>
</div>
<div style="padding:12px;background:#F9FAFB;border-radius:8px;">
<div style="font-size:11px;color:#6B7280;">سعر البيع (غير عضو)</div>
<div style="font-weight:700;font-size:16px;color:#1A1A2E;direction:ltr;text-align:left;"><?= money($item->sale_price_nonmember ?? 0) ?></div>
</div>
<div style="padding:12px;background:#F9FAFB;border-radius:8px;">
<div style="font-size:11px;color:#6B7280;">سعر البيع (لاعب)</div>
<div style="font-weight:700;font-size:16px;color:#1A1A2E;direction:ltr;text-align:left;"><?= $item->sale_price_player !== null ? money($item->sale_price_player) : '—' ?></div>
</div>
<div style="padding:12px;background:#F9FAFB;border-radius:8px;">
<div style="font-size:11px;color:#6B7280;">نسبة الضريبة</div>
<div style="font-weight:700;font-size:16px;color:#1A1A2E;direction:ltr;text-align:left;"><?= e($item->tax_rate ?? '0.00') ?>%</div>
</div>
</div>
</div>
<?php if (!empty($item->notes)): ?>
<div style="margin-top:20px;padding-top:15px;border-top:1px solid #F3F4F6;">
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">ملاحظات</div>
<div style="font-size:14px;color:#4B5563;line-height:1.7;"><?= e($item->notes) ?></div>
</div>
<?php endif; ?>
</div>
</div>
<!-- Total Stock -->
<div class="card" style="margin-bottom:20px;padding:20px;text-align:center;background:linear-gradient(135deg, #0D737710, #0D737720);">
<div style="font-size:14px;color:#6B7280;margin-bottom:6px;">إجمالي المخزون</div>
<div style="font-size:36px;font-weight:800;color:#0D7377;"><?= number_format((float) $totalStock, 2) ?></div>
<div style="font-size:13px;color:#6B7280;"><?= e($unitLabel) ?></div>
</div>
<!-- Stock Per Warehouse -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="warehouse" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">المخزون حسب المستودع</h3>
</div>
<?php if (!empty($stockRecords)): ?>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>المستودع</th>
<th>الكود</th>
<th>الكمية</th>
<th>الحد الأدنى</th>
<th>الحد الأقصى</th>
<th>الحالة</th>
</tr>
</thead>
<tbody>
<?php foreach ($stockRecords as $stock): ?>
<?php
$qty = (float) ($stock['quantity'] ?? 0);
$minLevel = (float) ($stock['min_level'] ?? 0);
$maxLevel = (float) ($stock['max_level'] ?? 0);
// Determine status
if ($qty <= 0) {
$statusLabel = 'نفذ';
$statusColor = '#DC2626';
$statusBg = '#FEE2E2';
} elseif ($minLevel > 0 && $qty <= $minLevel) {
$statusLabel = 'منخفض';
$statusColor = '#D97706';
$statusBg = '#FFF7ED';
} elseif ($maxLevel > 0 && $qty >= $maxLevel) {
$statusLabel = 'ممتلئ';
$statusColor = '#2563EB';
$statusBg = '#EFF6FF';
} else {
$statusLabel = 'طبيعي';
$statusColor = '#059669';
$statusBg = '#ECFDF5';
}
?>
<tr>
<td style="font-weight:600;"><?= e($stock['warehouse_name'] ?? '') ?></td>
<td style="font-family:monospace;direction:ltr;text-align:left;font-size:12px;color:#6B7280;"><?= e($stock['warehouse_code'] ?? '') ?></td>
<td style="font-weight:700;font-size:15px;"><?= number_format($qty, 2) ?></td>
<td style="color:#6B7280;"><?= $minLevel > 0 ? number_format($minLevel, 2) : '—' ?></td>
<td style="color:#6B7280;"><?= $maxLevel > 0 ? number_format($maxLevel, 2) : '—' ?></td>
<td>
<span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;background:<?= $statusBg ?>;color:<?= $statusColor ?>;">
<?= $statusLabel ?>
</span>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div style="padding:40px 20px;text-align:center;color:#6B7280;">
<i data-lucide="package-x" style="width:36px;height:36px;color:#D1D5DB;margin-bottom:8px;"></i>
<p style="margin:0;">لا يوجد مخزون مسجّل في أي مستودع</p>
</div>
<?php endif; ?>
</div>
<!-- Recent Movements -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="arrow-left-right" style="width:18px;height:18px;color:#D97706;"></i>
<h3 style="margin:0;color:#D97706;font-size:15px;">آخر الحركات</h3>
</div>
<?php if (!empty($movements)): ?>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>تاريخ</th>
<th>النوع</th>
<th>الاتجاه</th>
<th>الكمية</th>
<th>ملاحظات</th>
</tr>
</thead>
<tbody>
<?php foreach ($movements as $mv): ?>
<?php
$direction = $mv['direction'] ?? '';
$isIn = $direction === 'in';
$mvType = $mv['movement_type'] ?? '';
$mvLabel = $movementTypeLabels[$mvType] ?? $mvType;
?>
<tr>
<td style="font-size:13px;white-space:nowrap;"><?= e($mv['movement_date'] ?? $mv['created_at'] ?? '') ?></td>
<td style="font-weight:600;"><?= e($mvLabel) ?></td>
<td>
<?php if ($isIn): ?>
<span style="display:inline-flex;align-items:center;gap:4px;color:#059669;font-weight:600;">
<i data-lucide="arrow-down-circle" style="width:16px;height:16px;"></i> وارد
</span>
<?php else: ?>
<span style="display:inline-flex;align-items:center;gap:4px;color:#DC2626;font-weight:600;">
<i data-lucide="arrow-up-circle" style="width:16px;height:16px;"></i> صادر
</span>
<?php endif; ?>
</td>
<td style="font-weight:700;font-size:15px;color:<?= $isIn ? '#059669' : '#DC2626' ?>;">
<?= $isIn ? '+' : '-' ?><?= number_format((float) ($mv['quantity'] ?? 0), 2) ?>
</td>
<td style="font-size:13px;color:#6B7280;max-width:250px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"><?= e($mv['notes'] ?? '') ?: '—' ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div style="padding:40px 20px;text-align:center;color:#6B7280;">
<i data-lucide="activity" style="width:36px;height:36px;color:#D1D5DB;margin-bottom:8px;"></i>
<p style="margin:0;">لا توجد حركات مسجّلة لهذا الصنف</p>
</div>
<?php endif; ?>
</div>
<!-- Metadata -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="info" style="width:18px;height:18px;color:#6B7280;"></i>
<h3 style="margin:0;color:#6B7280;font-size:15px;">معلومات النظام</h3>
</div>
<div style="padding:15px 20px;">
<table style="width:100%;font-size:13px;">
<tr>
<td style="padding:6px 0;color:#6B7280;width:30%;">رقم السجل</td>
<td style="padding:6px 0;font-weight:600;">#<?= (int) $item->id ?></td>
</tr>
<?php if ($item->created_at): ?>
<tr>
<td style="padding:6px 0;color:#6B7280;">تاريخ الإنشاء</td>
<td style="padding:6px 0;"><?= e($item->created_at) ?></td>
</tr>
<?php endif; ?>
<?php if ($item->updated_at): ?>
<tr>
<td style="padding:6px 0;color:#6B7280;">آخر تحديث</td>
<td style="padding:6px 0;"><?= e($item->updated_at) ?></td>
</tr>
<?php endif; ?>
</table>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>إنشاء أمر شراء جديد<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/inventory/purchase-orders" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة للقائمة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<form method="POST" action="/inventory/purchase-orders" id="poForm">
<?= csrf_field() ?>
<!-- Header Information -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="file-text" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">بيانات أمر الشراء</h3>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:20px;">
<div class="form-group">
<label class="form-label">المورد <span style="color:#DC2626;">*</span></label>
<select name="supplier_id" class="form-select" required>
<option value="">-- اختر المورد --</option>
<?php foreach ($suppliers as $sup): ?>
<option value="<?= (int) $sup['id'] ?>" <?= (int) old('supplier_id', '') === (int) $sup['id'] && old('supplier_id', '') !== '' ? 'selected' : '' ?>><?= e($sup['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">المستودع <span style="color:#DC2626;">*</span></label>
<select name="warehouse_id" class="form-select" required>
<option value="">-- اختر المستودع --</option>
<?php foreach ($warehouses as $wh): ?>
<option value="<?= (int) $wh['id'] ?>" <?= (int) old('warehouse_id', '') === (int) $wh['id'] && old('warehouse_id', '') !== '' ? 'selected' : '' ?>><?= e($wh['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">تاريخ التسليم المتوقع</label>
<input type="date" name="expected_delivery_date" value="<?= e(old('expected_delivery_date', '')) ?>" class="form-input">
</div>
</div>
<div class="form-group" style="margin-top:15px;">
<label class="form-label">ملاحظات</label>
<textarea name="notes" class="form-input" rows="2" placeholder="ملاحظات إضافية عن أمر الشراء..."><?= e(old('notes', '')) ?></textarea>
</div>
</div>
</div>
<!-- Items Table -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;justify-content:space-between;">
<div style="display:flex;align-items:center;gap:8px;">
<i data-lucide="package" style="width:18px;height:18px;color:#D97706;"></i>
<h3 style="margin:0;color:#D97706;font-size:15px;">الأصناف</h3>
</div>
<button type="button" id="addItemBtn" class="btn btn-sm" style="background:#0D7377;color:#fff;border:none;font-size:12px;padding:6px 14px;">
<i data-lucide="plus" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> إضافة صنف
</button>
</div>
<div class="table-responsive">
<table class="data-table" id="itemsTable">
<thead>
<tr>
<th style="min-width:250px;">الصنف <span style="color:#DC2626;">*</span></th>
<th style="min-width:120px;">الكمية <span style="color:#DC2626;">*</span></th>
<th style="min-width:140px;">سعر الوحدة <span style="color:#DC2626;">*</span></th>
<th style="min-width:140px;">الإجمالي</th>
<th style="width:60px;"></th>
</tr>
</thead>
<tbody id="itemsBody">
<!-- Rows added by JS -->
</tbody>
<tfoot>
<tr style="background:#F9FAFB;">
<td colspan="3" style="text-align:left;font-weight:700;font-size:15px;padding:12px 15px;">الإجمالي الكلي</td>
<td style="font-weight:800;font-size:16px;color:#0D7377;direction:ltr;text-align:left;padding:12px 15px;" id="grandTotal">0.00</td>
<td></td>
</tr>
</tfoot>
</table>
</div>
<div id="noItemsMsg" style="padding:40px 20px;text-align:center;color:#9CA3AF;font-size:14px;">
<i data-lucide="package-open" style="width:36px;height:36px;color:#D1D5DB;margin-bottom:10px;"></i>
<div>اضغط "إضافة صنف" لإضافة أصناف لأمر الشراء</div>
</div>
</div>
<!-- Submit Buttons -->
<div style="display:flex;gap:10px;justify-content:flex-start;">
<button type="submit" class="btn btn-primary" style="padding:12px 30px;font-size:15px;">
<i data-lucide="check" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i>
إنشاء أمر الشراء
</button>
<a href="/inventory/purchase-orders" class="btn btn-outline" style="padding:12px 30px;font-size:15px;">إلغاء</a>
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
var items = <?= json_encode($items, JSON_UNESCAPED_UNICODE) ?>;
var rowIndex = 0;
var tbody = document.getElementById('itemsBody');
var noItemsMsg = document.getElementById('noItemsMsg');
var grandTotalEl = document.getElementById('grandTotal');
function buildItemOptions() {
var html = '<option value="">-- اختر الصنف --</option>';
for (var i = 0; i < items.length; i++) {
html += '<option value="' + items[i].id + '" data-cost="' + (items[i].cost_price || 0) + '">'
+ items[i].name_ar + (items[i].sku ? ' (' + items[i].sku + ')' : '') + '</option>';
}
return html;
}
function addRow() {
var idx = rowIndex++;
var tr = document.createElement('tr');
tr.setAttribute('data-row', idx);
tr.innerHTML = '<td>'
+ '<select name="items[' + idx + '][item_id]" class="form-select item-select" required style="width:100%;">'
+ buildItemOptions()
+ '</select></td>'
+ '<td><input type="number" name="items[' + idx + '][quantity_ordered]" class="form-input item-qty" min="1" step="1" value="1" required style="direction:ltr;text-align:left;"></td>'
+ '<td><input type="number" name="items[' + idx + '][unit_price]" class="form-input item-price" min="0" step="0.01" value="0.00" required style="direction:ltr;text-align:left;"></td>'
+ '<td class="row-total" style="font-weight:700;direction:ltr;text-align:left;padding:10px 15px;">0.00</td>'
+ '<td style="text-align:center;">'
+ '<button type="button" class="btn btn-sm remove-row" style="background:#FEE2E2;color:#DC2626;border:none;padding:6px 10px;">'
+ '<i data-lucide="trash-2" style="width:14px;height:14px;vertical-align:middle;"></i></button></td>';
tbody.appendChild(tr);
updateVisibility();
// Auto-fill cost price on item select
var select = tr.querySelector('.item-select');
var priceInput = tr.querySelector('.item-price');
select.addEventListener('change', function() {
var opt = this.options[this.selectedIndex];
var cost = opt.getAttribute('data-cost') || 0;
priceInput.value = parseFloat(cost).toFixed(2);
calcRowTotal(tr);
});
tr.querySelector('.item-qty').addEventListener('input', function() { calcRowTotal(tr); });
priceInput.addEventListener('input', function() { calcRowTotal(tr); });
tr.querySelector('.remove-row').addEventListener('click', function() {
tr.remove();
calcGrandTotal();
updateVisibility();
});
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
}
function calcRowTotal(tr) {
var qty = parseFloat(tr.querySelector('.item-qty').value) || 0;
var price = parseFloat(tr.querySelector('.item-price').value) || 0;
var total = qty * price;
tr.querySelector('.row-total').textContent = total.toFixed(2);
calcGrandTotal();
}
function calcGrandTotal() {
var totals = document.querySelectorAll('.row-total');
var sum = 0;
totals.forEach(function(el) {
sum += parseFloat(el.textContent) || 0;
});
grandTotalEl.textContent = sum.toFixed(2);
}
function updateVisibility() {
var hasRows = tbody.querySelectorAll('tr').length > 0;
noItemsMsg.style.display = hasRows ? 'none' : 'block';
}
document.getElementById('addItemBtn').addEventListener('click', addRow);
// Start with one row
addRow();
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>أوامر الشراء<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/inventory/purchase-orders/create" class="btn btn-primary"><i data-lucide="plus" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> أمر شراء جديد</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$statusLabels = [
'draft' => 'مسودة', 'submitted' => 'مقدم', 'approved' => 'معتمد',
'partially_received' => 'استلام جزئي', 'received' => 'مستلم', 'cancelled' => 'ملغي',
];
$statusColors = [
'draft' => '#6B7280', 'submitted' => '#D97706', 'approved' => '#0D7377',
'partially_received' => '#2563EB', 'received' => '#059669', 'cancelled' => '#DC2626',
];
$statusBgs = [
'draft' => '#F3F4F6', 'submitted' => '#FFF7ED', 'approved' => '#F0FDFA',
'partially_received' => '#EFF6FF', 'received' => '#ECFDF5', 'cancelled' => '#FEE2E2',
];
?>
<!-- Search & Filters -->
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/inventory/purchase-orders" style="display:flex;gap:10px;flex-wrap:wrap;align-items:end;">
<div style="min-width:140px;">
<label class="form-label" style="font-size:12px;">الحالة</label>
<select name="status" class="form-select">
<option value="">الكل</option>
<?php foreach ($statuses as $val => $label): ?>
<option value="<?= e($val) ?>" <?= ($filters['status'] ?? '') === $val ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="min-width:160px;">
<label class="form-label" style="font-size:12px;">المورد</label>
<select name="supplier_id" class="form-select">
<option value="">الكل</option>
<?php foreach ($suppliers as $sup): ?>
<option value="<?= (int) $sup['id'] ?>" <?= ($filters['supplier_id'] ?? '') == $sup['id'] ? 'selected' : '' ?>><?= e($sup['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="min-width:160px;">
<label class="form-label" style="font-size:12px;">المستودع</label>
<select name="warehouse_id" class="form-select">
<option value="">الكل</option>
<?php foreach ($warehouses as $wh): ?>
<option value="<?= (int) $wh['id'] ?>" <?= ($filters['warehouse_id'] ?? '') == $wh['id'] ? 'selected' : '' ?>><?= e($wh['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label" style="font-size:12px;">من</label>
<input type="date" name="date_from" value="<?= e($filters['date_from'] ?? '') ?>" class="form-input">
</div>
<div>
<label class="form-label" style="font-size:12px;">إلى</label>
<input type="date" name="date_to" value="<?= e($filters['date_to'] ?? '') ?>" class="form-input">
</div>
<button type="submit" class="btn btn-outline"><i data-lucide="search" style="width:16px;height:16px;vertical-align:middle;"></i> بحث</button>
<a href="/inventory/purchase-orders" class="btn btn-sm btn-outline" style="color:#6B7280;">مسح</a>
</form>
</div>
<!-- Orders Table -->
<?php if (!empty($orders)): ?>
<div class="card">
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>رقم الأمر</th>
<th>المورد</th>
<th>المستودع</th>
<th>الحالة</th>
<th>الإجمالي</th>
<th>التاريخ</th>
<th>الإجراءات</th>
</tr>
</thead>
<tbody>
<?php foreach ($orders as $o): ?>
<?php
$st = $o['status'] ?? 'draft';
$stColor = $statusColors[$st] ?? '#6B7280';
$stBg = $statusBgs[$st] ?? '#F3F4F6';
$stLabel = $statusLabels[$st] ?? $st;
?>
<tr>
<td style="font-weight:600;">
<a href="/inventory/purchase-orders/<?= (int) $o['id'] ?>" style="color:#0D7377;text-decoration:none;">
<code style="font-size:12px;background:#F3F4F6;padding:2px 8px;border-radius:4px;"><?= e($o['po_number']) ?></code>
</a>
</td>
<td style="font-weight:600;"><?= e($o['supplier_name'] ?? '—') ?></td>
<td style="font-size:13px;"><?= e($o['warehouse_name'] ?? '—') ?></td>
<td>
<span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;background:<?= $stBg ?>;color:<?= $stColor ?>;">
<?= e($stLabel) ?>
</span>
</td>
<td style="font-weight:700;direction:ltr;text-align:left;"><?= money($o['grand_total'] ?? 0) ?></td>
<td style="font-size:12px;"><?= e(substr($o['created_at'] ?? '', 0, 10)) ?></td>
<td>
<a href="/inventory/purchase-orders/<?= (int) $o['id'] ?>" class="btn btn-sm btn-outline" style="font-size:12px;padding:4px 10px;">
<i data-lucide="eye" style="width:13px;height:13px;vertical-align:middle;"></i> عرض
</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php if (isset($pagination) && ($pagination['last_page'] ?? 1) > 1): ?>
<div style="padding:15px;">
<?php $__template->include('Shared.Components.pagination', ['pagination' => $pagination]); ?>
</div>
<?php endif; ?>
<?php else: ?>
<div class="card" style="padding:60px 20px;text-align:center;">
<div style="margin-bottom:15px;">
<i data-lucide="shopping-cart" style="width:48px;height:48px;color:#D1D5DB;"></i>
</div>
<h3 style="color:#6B7280;margin:0 0 8px;">لا توجد أوامر شراء</h3>
<p style="color:#9CA3AF;font-size:14px;margin:0 0 20px;">
<?php if (!empty($filters['status']) || !empty($filters['supplier_id']) || !empty($filters['warehouse_id']) || !empty($filters['date_from']) || !empty($filters['date_to'])): ?>
لا توجد نتائج مطابقة لبحثك. جرب تغيير معايير البحث.
<?php else: ?>
ابدأ بإنشاء أمر شراء جديد.
<?php endif; ?>
</p>
<a href="/inventory/purchase-orders/create" class="btn btn-primary"><i data-lucide="plus" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> أمر شراء جديد</a>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>استلام أمر شراء <?= e($order->po_number) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/inventory/purchase-orders/<?= (int) $order->id ?>" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة لأمر الشراء</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Order Summary -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="shopping-cart" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">ملخص أمر الشراء</h3>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(180px, 1fr));gap:15px;">
<div style="padding:12px;background:#F9FAFB;border-radius:8px;">
<div style="font-size:11px;color:#6B7280;">رقم الأمر</div>
<div style="font-weight:700;font-size:14px;">
<code style="font-size:12px;background:#F3F4F6;padding:2px 8px;border-radius:4px;"><?= e($order->po_number) ?></code>
</div>
</div>
<div style="padding:12px;background:#F9FAFB;border-radius:8px;">
<div style="font-size:11px;color:#6B7280;">المورد</div>
<div style="font-weight:700;font-size:14px;"><?= e($order->supplier_name) ?></div>
</div>
<div style="padding:12px;background:#F9FAFB;border-radius:8px;">
<div style="font-size:11px;color:#6B7280;">المستودع</div>
<div style="font-weight:700;font-size:14px;"><?= e($order->warehouse_name) ?></div>
</div>
<div style="padding:12px;background:#F0FDF4;border-radius:8px;">
<div style="font-size:11px;color:#6B7280;">الإجمالي</div>
<div style="font-weight:700;font-size:14px;color:#059669;direction:ltr;text-align:left;"><?= money($order->grand_total ?? 0) ?></div>
</div>
</div>
</div>
</div>
<!-- Receive Form -->
<form method="POST" action="/inventory/purchase-orders/<?= (int) $order->id ?>/receive">
<?= csrf_field() ?>
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="package-check" style="width:18px;height:18px;color:#059669;"></i>
<h3 style="margin:0;color:#059669;font-size:15px;">استلام الأصناف</h3>
</div>
<div style="padding:10px 20px 5px;background:#FFF7ED;border-bottom:1px solid #FDE68A;">
<p style="margin:0;font-size:13px;color:#92400E;display:flex;align-items:center;gap:6px;">
<i data-lucide="info" style="width:15px;height:15px;"></i>
أدخل الكميات المستلمة لكل صنف. الكمية المتبقية هي الحد الأقصى الذي يمكن إدخاله.
</p>
</div>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>الصنف</th>
<th>الكمية المطلوبة</th>
<th>المستلم سابقاً</th>
<th>المتبقي</th>
<th style="min-width:120px;">الكمية المستلمة</th>
<th style="min-width:130px;">رقم الدفعة</th>
<th style="min-width:140px;">تاريخ الانتهاء</th>
</tr>
</thead>
<tbody>
<?php foreach ($items as $idx => $item): ?>
<?php
$qtyOrdered = (float) ($item['quantity_ordered'] ?? 0);
$qtyReceived = (float) ($item['quantity_received'] ?? 0);
$remaining = max(0, $qtyOrdered - $qtyReceived);
$isFullyReceived = $remaining <= 0;
?>
<tr style="<?= $isFullyReceived ? 'opacity:0.5;' : '' ?>">
<td style="font-weight:600;">
<?= e($item['item_name'] ?? '') ?>
<?php if (!empty($item['sku'])): ?>
<div style="font-size:11px;color:#9CA3AF;">
<code style="font-size:10px;background:#F3F4F6;padding:1px 4px;border-radius:3px;"><?= e($item['sku']) ?></code>
</div>
<?php endif; ?>
<input type="hidden" name="items[<?= $idx ?>][po_item_id]" value="<?= (int) ($item['id'] ?? 0) ?>">
</td>
<td style="font-weight:600;direction:ltr;text-align:left;"><?= number_format($qtyOrdered) ?></td>
<td style="direction:ltr;text-align:left;color:#2563EB;font-weight:600;"><?= number_format($qtyReceived) ?></td>
<td>
<?php if ($isFullyReceived): ?>
<span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;background:#ECFDF5;color:#059669;">مكتمل</span>
<?php else: ?>
<span style="font-weight:700;color:#D97706;direction:ltr;"><?= number_format($remaining) ?></span>
<?php endif; ?>
</td>
<td>
<input type="number"
name="items[<?= $idx ?>][quantity_received]"
class="form-input"
min="0"
max="<?= $remaining ?>"
value="0"
step="1"
style="direction:ltr;text-align:left;"
<?= $isFullyReceived ? 'disabled' : '' ?>>
</td>
<td>
<input type="text"
name="items[<?= $idx ?>][batch_number]"
class="form-input"
placeholder="رقم الدفعة"
style="direction:ltr;text-align:left;"
<?= $isFullyReceived ? 'disabled' : '' ?>>
</td>
<td>
<input type="date"
name="items[<?= $idx ?>][expiry_date]"
class="form-input"
<?= $isFullyReceived ? 'disabled' : '' ?>>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<!-- Submit Buttons -->
<div style="display:flex;gap:10px;justify-content:flex-start;">
<button type="submit" class="btn" style="background:#059669;color:#fff;border:none;padding:12px 30px;font-size:15px;">
<i data-lucide="package-check" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i>
تأكيد الاستلام
</button>
<a href="/inventory/purchase-orders/<?= (int) $order->id ?>" class="btn btn-outline" style="padding:12px 30px;font-size:15px;">إلغاء</a>
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>أمر شراء <?= e($order->po_number) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<?php if ($order->status === 'draft'): ?>
<form method="POST" action="/inventory/purchase-orders/<?= (int) $order->id ?>/submit" style="display:inline;">
<?= csrf_field() ?>
<button type="submit" class="btn" style="background:#D97706;color:#fff;border:none;" onclick="return confirm('هل أنت متأكد من تقديم أمر الشراء؟');">
<i data-lucide="send" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> تقديم
</button>
</form>
<?php endif; ?>
<?php if ($order->status === 'submitted'): ?>
<form method="POST" action="/inventory/purchase-orders/<?= (int) $order->id ?>/approve" style="display:inline;">
<?= csrf_field() ?>
<button type="submit" class="btn" style="background:#0D7377;color:#fff;border:none;" onclick="return confirm('هل أنت متأكد من اعتماد أمر الشراء؟');">
<i data-lucide="check-circle" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> اعتماد
</button>
</form>
<?php endif; ?>
<?php if ($order->status === 'approved' || $order->status === 'partially_received'): ?>
<a href="/inventory/purchase-orders/<?= (int) $order->id ?>/receive" class="btn" style="background:#059669;color:#fff;border:none;">
<i data-lucide="package-check" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> استلام
</a>
<?php endif; ?>
<?php if ($order->status === 'draft' || $order->status === 'submitted'): ?>
<form method="POST" action="/inventory/purchase-orders/<?= (int) $order->id ?>/cancel" style="display:inline;">
<?= csrf_field() ?>
<button type="submit" class="btn" style="background:#FEE2E2;color:#DC2626;border:none;" onclick="return confirm('هل أنت متأكد من إلغاء أمر الشراء؟ لا يمكن التراجع عن هذا الإجراء.');">
<i data-lucide="x-circle" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> إلغاء
</button>
</form>
<?php endif; ?>
<a href="/inventory/purchase-orders" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة للقائمة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$statusLabels = [
'draft' => 'مسودة', 'submitted' => 'مقدم', 'approved' => 'معتمد',
'partially_received' => 'استلام جزئي', 'received' => 'مستلم', 'cancelled' => 'ملغي',
];
$statusColors = [
'draft' => ['bg' => '#F3F4F6', 'color' => '#6B7280'],
'submitted' => ['bg' => '#FFF7ED', 'color' => '#D97706'],
'approved' => ['bg' => '#F0FDFA', 'color' => '#0D7377'],
'partially_received' => ['bg' => '#EFF6FF', 'color' => '#2563EB'],
'received' => ['bg' => '#ECFDF5', 'color' => '#059669'],
'cancelled' => ['bg' => '#FEE2E2', 'color' => '#DC2626'],
];
$st = $order->status ?? 'draft';
$stColors = $statusColors[$st] ?? $statusColors['draft'];
$stLabel = $statusLabels[$st] ?? $st;
?>
<!-- Order Info Card -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="shopping-cart" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">بيانات أمر الشراء</h3>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:0;">
<table style="width:100%;font-size:14px;">
<tr>
<td style="padding:10px 0;color:#6B7280;width:40%;">رقم الأمر</td>
<td style="padding:10px 0;font-weight:600;">
<code style="font-size:12px;background:#F3F4F6;padding:2px 8px;border-radius:4px;"><?= e($order->po_number) ?></code>
</td>
</tr>
<tr>
<td style="padding:10px 0;color:#6B7280;">المورد</td>
<td style="padding:10px 0;font-weight:600;">
<a href="/inventory/suppliers/<?= (int) $order->supplier_id ?>" style="color:#0D7377;text-decoration:none;"><?= e($order->supplier_name) ?></a>
</td>
</tr>
<tr>
<td style="padding:10px 0;color:#6B7280;">المستودع</td>
<td style="padding:10px 0;font-weight:600;"><?= e($order->warehouse_name) ?></td>
</tr>
<tr>
<td style="padding:10px 0;color:#6B7280;">تاريخ التسليم المتوقع</td>
<td style="padding:10px 0;"><?= e($order->expected_delivery_date ?? '—') ?></td>
</tr>
</table>
<table style="width:100%;font-size:14px;">
<tr>
<td style="padding:10px 0;color:#6B7280;width:40%;">الحالة</td>
<td style="padding:10px 0;">
<span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;background:<?= $stColors['bg'] ?>;color:<?= $stColors['color'] ?>;">
<?= e($stLabel) ?>
</span>
</td>
</tr>
<tr>
<td style="padding:10px 0;color:#6B7280;">تاريخ الإنشاء</td>
<td style="padding:10px 0;"><?= e($order->created_at ?? '—') ?></td>
</tr>
<?php if (!empty($order->approved_at)): ?>
<tr>
<td style="padding:10px 0;color:#6B7280;">تاريخ الاعتماد</td>
<td style="padding:10px 0;"><?= e($order->approved_at) ?></td>
</tr>
<?php endif; ?>
<?php if (!empty($order->received_at)): ?>
<tr>
<td style="padding:10px 0;color:#6B7280;">تاريخ الاستلام</td>
<td style="padding:10px 0;"><?= e($order->received_at) ?></td>
</tr>
<?php endif; ?>
</table>
</div>
<?php if (!empty($order->notes)): ?>
<div style="margin-top:20px;padding-top:15px;border-top:1px solid #F3F4F6;">
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">ملاحظات</div>
<div style="font-size:14px;color:#4B5563;line-height:1.7;"><?= e($order->notes) ?></div>
</div>
<?php endif; ?>
</div>
</div>
<!-- Grand Total -->
<div class="card" style="margin-bottom:20px;padding:20px;text-align:center;background:linear-gradient(135deg, #0D737710, #0D737720);">
<div style="font-size:14px;color:#6B7280;margin-bottom:6px;">الإجمالي الكلي</div>
<div style="font-size:36px;font-weight:800;color:#0D7377;direction:ltr;"><?= money($order->grand_total ?? 0) ?></div>
</div>
<!-- Items Table -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="package" style="width:18px;height:18px;color:#D97706;"></i>
<h3 style="margin:0;color:#D97706;font-size:15px;">الأصناف</h3>
</div>
<?php if (!empty($items)): ?>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>الصنف</th>
<th>SKU</th>
<th>الكمية المطلوبة</th>
<th>المستلم</th>
<th>التقدم</th>
<th>سعر الوحدة</th>
<th>الإجمالي</th>
</tr>
</thead>
<tbody>
<?php foreach ($items as $item): ?>
<?php
$qtyOrdered = (float) ($item['quantity_ordered'] ?? 0);
$qtyReceived = (float) ($item['quantity_received'] ?? 0);
$pct = $qtyOrdered > 0 ? round(($qtyReceived / $qtyOrdered) * 100) : 0;
$pctColor = $pct >= 100 ? '#059669' : ($pct > 0 ? '#2563EB' : '#6B7280');
?>
<tr>
<td style="font-weight:600;"><?= e($item['item_name'] ?? '') ?></td>
<td>
<code style="font-size:11px;background:#F3F4F6;padding:2px 6px;border-radius:4px;"><?= e($item['sku'] ?? '—') ?></code>
</td>
<td style="font-weight:600;direction:ltr;text-align:left;"><?= number_format($qtyOrdered) ?></td>
<td style="font-weight:600;direction:ltr;text-align:left;color:<?= $pctColor ?>;"><?= number_format($qtyReceived) ?></td>
<td style="min-width:140px;">
<div style="display:flex;align-items:center;gap:8px;">
<div style="flex:1;height:8px;background:#E5E7EB;border-radius:4px;overflow:hidden;">
<div style="width:<?= min($pct, 100) ?>%;height:100%;background:<?= $pctColor ?>;border-radius:4px;"></div>
</div>
<span style="font-size:11px;font-weight:600;color:<?= $pctColor ?>;min-width:35px;"><?= $pct ?>%</span>
</div>
</td>
<td style="direction:ltr;text-align:left;"><?= money($item['unit_price'] ?? 0) ?></td>
<td style="font-weight:700;direction:ltr;text-align:left;"><?= money($item['total_price'] ?? 0) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div style="padding:40px 20px;text-align:center;color:#6B7280;">
<i data-lucide="package-open" style="width:36px;height:36px;color:#D1D5DB;margin-bottom:8px;"></i>
<p style="margin:0;">لا توجد أصناف في هذا الأمر</p>
</div>
<?php endif; ?>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تقرير فروقات الجرد<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<button onclick="window.print()" class="btn btn-outline"><i data-lucide="printer" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> طباعة</button>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Filter -->
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/inventory/reports/audit-variance" style="display:flex;gap:10px;align-items:end;">
<div style="min-width:280px;">
<label class="form-label" style="font-size:12px;">اختر الجرد</label>
<select name="audit_id" class="form-select">
<option value="">-- اختر جرد --</option>
<?php foreach ($audits as $audit): ?>
<option value="<?= (int) $audit['id'] ?>" <?= $auditId == $audit['id'] ? 'selected' : '' ?>>
<?= e($audit['audit_number']) ?><?= e($audit['warehouse_name']) ?> (<?= e($audit['audit_date']) ?>)
</option>
<?php endforeach; ?>
</select>
</div>
<button type="submit" class="btn btn-primary"><i data-lucide="search" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> عرض</button>
</form>
</div>
<?php if ($auditId && !empty($variances)): ?>
<?php
$totalPositive = 0;
$totalNegative = 0;
$totalCost = 0;
foreach ($variances as $v) {
$var = (float) ($v['variance'] ?? 0);
if ($var > 0) $totalPositive += $var;
else $totalNegative += abs($var);
$totalCost += (float) ($v['variance_cost'] ?? 0);
}
?>
<!-- Variance Summary -->
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:15px;margin-bottom:20px;">
<div class="card" style="padding:20px;text-align:center;background:#ECFDF5;border:1px solid #BBF7D0;">
<div style="font-size:28px;font-weight:700;color:#059669;">+<?= number_format($totalPositive) ?></div>
<div style="color:#6B7280;font-size:14px;">فائض</div>
</div>
<div class="card" style="padding:20px;text-align:center;background:#FEF2F2;border:1px solid #FECACA;">
<div style="font-size:28px;font-weight:700;color:#DC2626;">-<?= number_format($totalNegative) ?></div>
<div style="color:#6B7280;font-size:14px;">عجز</div>
</div>
<div class="card" style="padding:20px;text-align:center;background:#EFF6FF;border:1px solid #BFDBFE;">
<div style="font-size:28px;font-weight:700;color:#2563EB;"><?= money(abs($totalCost)) ?></div>
<div style="color:#6B7280;font-size:14px;">تكلفة الفروقات</div>
</div>
</div>
<!-- Variances Table -->
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="clipboard-list" style="width:20px;height:20px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;">تفاصيل فروقات الجرد</h3>
</div>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>الصنف</th>
<th>SKU</th>
<th>كمية النظام</th>
<th>الكمية الفعلية</th>
<th>الفرق</th>
<th>تكلفة الفرق</th>
</tr>
</thead>
<tbody>
<?php foreach ($variances as $v): ?>
<?php
$variance = (float) ($v['variance'] ?? 0);
$varColor = $variance > 0 ? '#059669' : ($variance < 0 ? '#DC2626' : '#6B7280');
$varBg = $variance > 0 ? '#ECFDF5' : ($variance < 0 ? '#FEF2F2' : 'transparent');
$varSign = $variance > 0 ? '+' : '';
?>
<tr>
<td style="font-weight:600;"><?= e($v['item_name']) ?></td>
<td style="direction:ltr;text-align:right;font-family:monospace;font-size:13px;"><?= e($v['sku'] ?? '—') ?></td>
<td style="font-weight:600;"><?= number_format((float) ($v['system_quantity'] ?? 0)) ?></td>
<td style="font-weight:600;"><?= number_format((float) ($v['physical_quantity'] ?? 0)) ?></td>
<td>
<span style="display:inline-block;padding:3px 12px;border-radius:10px;font-size:13px;font-weight:700;background:<?= $varBg ?>;color:<?= $varColor ?>;">
<?= $varSign ?><?= number_format($variance) ?>
</span>
</td>
<td style="font-weight:700;color:<?= $varColor ?>;direction:ltr;text-align:right;"><?= money(abs((float) ($v['variance_cost'] ?? 0))) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php elseif ($auditId && empty($variances)): ?>
<div class="card" style="padding:60px 20px;text-align:center;">
<div style="margin-bottom:15px;">
<i data-lucide="check-circle" style="width:48px;height:48px;color:#059669;"></i>
</div>
<h3 style="color:#059669;margin:0 0 8px;">لا توجد فروقات</h3>
<p style="color:#9CA3AF;font-size:14px;margin:0;">جميع الأصناف متطابقة بين النظام والجرد الفعلي</p>
</div>
<?php else: ?>
<div class="card" style="padding:60px 20px;text-align:center;">
<div style="margin-bottom:15px;">
<i data-lucide="clipboard-list" style="width:48px;height:48px;color:#D1D5DB;"></i>
</div>
<h3 style="color:#6B7280;margin:0 0 8px;">اختر جرد لعرض الفروقات</h3>
<p style="color:#9CA3AF;font-size:14px;margin:0;">اختر أحد عمليات الجرد من القائمة أعلاه</p>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تقرير الإهلاك<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<button onclick="window.print()" class="btn btn-outline"><i data-lucide="printer" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> طباعة</button>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Filter -->
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/inventory/reports/depreciation" style="display:flex;gap:10px;align-items:end;">
<div style="min-width:200px;">
<label class="form-label" style="font-size:12px;">الشهر</label>
<input type="month" name="month" value="<?= e($month ?? '') ?>" class="form-input">
</div>
<button type="submit" class="btn btn-primary"><i data-lucide="search" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> عرض التقرير</button>
</form>
</div>
<?php if (!empty($entries)): ?>
<!-- Total Depreciation Card -->
<div class="card" style="margin-bottom:20px;padding:20px;text-align:center;background:#EFF6FF;border:1px solid #BFDBFE;">
<div style="font-size:14px;color:#6B7280;margin-bottom:6px;">إجمالي إهلاك الفترة</div>
<div style="font-size:32px;font-weight:700;color:#2563EB;"><?= money($totalDep ?? 0) ?></div>
<?php if (!empty($month)): ?>
<div style="font-size:13px;color:#9CA3AF;margin-top:4px;"><?= e($month) ?></div>
<?php endif; ?>
</div>
<!-- Depreciation Table -->
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="trending-down" style="width:20px;height:20px;color:#2563EB;"></i>
<h3 style="margin:0;color:#0D7377;">تفاصيل الإهلاك</h3>
</div>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>رقم الأصل</th>
<th>الصنف</th>
<th>المخزن</th>
<th>تكلفة الشراء</th>
<th>إهلاك الفترة</th>
<th>المتراكم</th>
<th>القيمة الدفترية</th>
<th>الطريقة</th>
</tr>
</thead>
<tbody>
<?php foreach ($entries as $entry): ?>
<tr>
<td style="font-family:monospace;font-weight:600;"><?= e($entry['asset_tag'] ?? '—') ?></td>
<td style="font-weight:600;"><?= e($entry['item_name']) ?></td>
<td><?= e($entry['warehouse_name'] ?? '—') ?></td>
<td style="font-weight:600;direction:ltr;text-align:right;"><?= money($entry['purchase_cost'] ?? 0) ?></td>
<td style="font-weight:700;color:#2563EB;direction:ltr;text-align:right;"><?= money($entry['depreciation_amount'] ?? 0) ?></td>
<td style="font-weight:600;color:#D97706;direction:ltr;text-align:right;"><?= money($entry['accumulated_total'] ?? 0) ?></td>
<td style="font-weight:700;color:#059669;direction:ltr;text-align:right;"><?= money($entry['book_value_after'] ?? 0) ?></td>
<td>
<span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;background:#F3F4F6;color:#6B7280;">
<?= e($entry['method_used'] ?? '—') ?>
</span>
</td>
</tr>
<?php endforeach; ?>
</tbody>
<tfoot>
<tr style="background:#EFF6FF;font-weight:700;">
<td colspan="4" style="padding:12px 15px;">الإجمالي</td>
<td style="padding:12px 15px;direction:ltr;text-align:right;font-size:16px;color:#2563EB;"><?= money($totalDep ?? 0) ?></td>
<td colspan="3"></td>
</tr>
</tfoot>
</table>
</div>
</div>
<?php else: ?>
<div class="card" style="padding:60px 20px;text-align:center;">
<div style="margin-bottom:15px;">
<i data-lucide="calendar" style="width:48px;height:48px;color:#D1D5DB;"></i>
</div>
<h3 style="color:#6B7280;margin:0 0 8px;">لا توجد بيانات إهلاك</h3>
<p style="color:#9CA3AF;font-size:14px;margin:0;">اختر الشهر لعرض تقرير الإهلاك</p>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تنبيهات الصلاحية<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<button onclick="window.print()" class="btn btn-outline"><i data-lucide="printer" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> طباعة</button>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Filter -->
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/inventory/reports/expiry-alert" style="display:flex;gap:10px;align-items:end;">
<div style="min-width:160px;">
<label class="form-label" style="font-size:12px;">عدد الأيام (قرب الانتهاء)</label>
<input type="number" name="days" value="<?= (int) ($days ?? 30) ?>" min="1" max="365" class="form-input">
</div>
<button type="submit" class="btn btn-primary"><i data-lucide="search" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> عرض</button>
</form>
</div>
<!-- Summary Cards -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;margin-bottom:20px;">
<div class="card" style="padding:20px;text-align:center;background:#FEF2F2;border:1px solid #FECACA;">
<div style="font-size:32px;font-weight:700;color:#DC2626;"><?= count($expired) ?></div>
<div style="color:#6B7280;font-size:14px;">أصناف منتهية الصلاحية</div>
</div>
<div class="card" style="padding:20px;text-align:center;background:#FFFBEB;border:1px solid #FDE68A;">
<div style="font-size:32px;font-weight:700;color:#D97706;"><?= count($nearExpiry) ?></div>
<div style="color:#6B7280;font-size:14px;">أصناف قاربت على الانتهاء</div>
</div>
</div>
<!-- Expired Items -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:2px solid #DC2626;display:flex;align-items:center;gap:8px;">
<i data-lucide="alert-triangle" style="width:20px;height:20px;color:#DC2626;"></i>
<h3 style="margin:0;color:#DC2626;">أصناف منتهية الصلاحية</h3>
<span style="display:inline-block;padding:2px 10px;border-radius:10px;font-size:12px;font-weight:600;background:#FEE2E2;color:#DC2626;margin-right:8px;"><?= count($expired) ?></span>
</div>
<?php if (!empty($expired)): ?>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>الصنف</th>
<th>SKU</th>
<th>المخزن</th>
<th>رقم الدفعة</th>
<th>تاريخ الانتهاء</th>
<th>الكمية</th>
</tr>
</thead>
<tbody>
<?php foreach ($expired as $item): ?>
<tr style="background:#FEF2F2;">
<td style="font-weight:600;"><?= e($item['item_name']) ?></td>
<td style="direction:ltr;text-align:right;font-family:monospace;font-size:13px;"><?= e($item['sku'] ?? '—') ?></td>
<td><?= e($item['warehouse_name']) ?></td>
<td style="font-family:monospace;font-size:13px;"><?= e($item['batch_number'] ?? '—') ?></td>
<td style="color:#DC2626;font-weight:600;"><?= e($item['expiry_date']) ?></td>
<td style="font-weight:700;"><?= number_format((float) ($item['quantity'] ?? 0)) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div style="padding:30px;text-align:center;color:#059669;font-size:14px;">
<i data-lucide="check-circle" style="width:24px;height:24px;vertical-align:middle;margin-left:6px;color:#059669;"></i>
لا توجد أصناف منتهية الصلاحية
</div>
<?php endif; ?>
</div>
<!-- Near Expiry Items -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:2px solid #D97706;display:flex;align-items:center;gap:8px;">
<i data-lucide="clock" style="width:20px;height:20px;color:#D97706;"></i>
<h3 style="margin:0;color:#D97706;">أصناف قاربت على الانتهاء (خلال <?= (int) ($days ?? 30) ?> يوم)</h3>
<span style="display:inline-block;padding:2px 10px;border-radius:10px;font-size:12px;font-weight:600;background:#FEF3C7;color:#D97706;margin-right:8px;"><?= count($nearExpiry) ?></span>
</div>
<?php if (!empty($nearExpiry)): ?>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>الصنف</th>
<th>SKU</th>
<th>المخزن</th>
<th>رقم الدفعة</th>
<th>تاريخ الانتهاء</th>
<th>الكمية</th>
</tr>
</thead>
<tbody>
<?php foreach ($nearExpiry as $item): ?>
<tr style="background:#FFFBEB;">
<td style="font-weight:600;"><?= e($item['item_name']) ?></td>
<td style="direction:ltr;text-align:right;font-family:monospace;font-size:13px;"><?= e($item['sku'] ?? '—') ?></td>
<td><?= e($item['warehouse_name']) ?></td>
<td style="font-family:monospace;font-size:13px;"><?= e($item['batch_number'] ?? '—') ?></td>
<td style="color:#D97706;font-weight:600;"><?= e($item['expiry_date']) ?></td>
<td style="font-weight:700;"><?= number_format((float) ($item['quantity'] ?? 0)) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div style="padding:30px;text-align:center;color:#059669;font-size:14px;">
<i data-lucide="check-circle" style="width:24px;height:24px;vertical-align:middle;margin-left:6px;color:#059669;"></i>
لا توجد أصناف قاربت على الانتهاء
</div>
<?php endif; ?>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>أصناف تحت الحد الأدنى<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<button onclick="window.print()" class="btn btn-outline"><i data-lucide="printer" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> طباعة</button>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php if (!empty($rows)): ?>
<!-- Summary -->
<div class="card" style="margin-bottom:20px;padding:20px;text-align:center;background:#FEF2F2;border:1px solid #FECACA;">
<div style="font-size:32px;font-weight:700;color:#DC2626;"><?= count($rows) ?></div>
<div style="color:#6B7280;font-size:14px;">صنف تحت الحد الأدنى</div>
</div>
<!-- Low Stock Table -->
<div class="card">
<div style="padding:15px 20px;border-bottom:2px solid #DC2626;display:flex;align-items:center;gap:8px;">
<i data-lucide="alert-circle" style="width:20px;height:20px;color:#DC2626;"></i>
<h3 style="margin:0;color:#DC2626;">أصناف تحتاج إعادة توريد</h3>
</div>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>الصنف</th>
<th>SKU</th>
<th>المخزن</th>
<th>الكمية الحالية</th>
<th>الحد الأدنى</th>
<th>الفرق</th>
</tr>
</thead>
<tbody>
<?php foreach ($rows as $row): ?>
<?php
$qty = (float) ($row['quantity'] ?? 0);
$minLevel = (float) ($row['min_level'] ?? 0);
$diff = $qty - $minLevel;
$ratio = $minLevel > 0 ? $qty / $minLevel : 0;
if ($ratio <= 0) {
$rowBg = '#FEF2F2';
$diffColor = '#DC2626';
} elseif ($ratio <= 0.25) {
$rowBg = '#FEF2F2';
$diffColor = '#DC2626';
} elseif ($ratio <= 0.5) {
$rowBg = '#FFFBEB';
$diffColor = '#D97706';
} else {
$rowBg = '#FFF7ED';
$diffColor = '#D97706';
}
?>
<tr style="background:<?= $rowBg ?>;">
<td style="font-weight:600;"><?= e($row['name_ar']) ?></td>
<td style="direction:ltr;text-align:right;font-family:monospace;font-size:13px;"><?= e($row['sku'] ?? '—') ?></td>
<td><?= e($row['warehouse_name'] ?? '—') ?></td>
<td style="font-weight:700;color:<?= $diffColor ?>;"><?= number_format($qty) ?></td>
<td style="font-weight:600;color:#6B7280;"><?= number_format($minLevel) ?></td>
<td>
<span style="display:inline-block;padding:3px 12px;border-radius:10px;font-size:13px;font-weight:700;background:<?= $ratio <= 0.25 ? '#FEE2E2' : '#FEF3C7' ?>;color:<?= $diffColor ?>;">
<?= number_format($diff) ?>
</span>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php else: ?>
<div class="card" style="padding:60px 20px;text-align:center;">
<div style="margin-bottom:15px;">
<i data-lucide="check-circle" style="width:48px;height:48px;color:#059669;"></i>
</div>
<h3 style="color:#059669;margin:0 0 8px;">جميع الأصناف فوق الحد الأدنى</h3>
<p style="color:#9CA3AF;font-size:14px;margin:0;">لا توجد أصناف تحتاج إعادة توريد حالياً</p>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تقرير الحركات<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<button onclick="window.print()" class="btn btn-outline"><i data-lucide="printer" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> طباعة</button>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$typeLabels = [
'purchase_in' => 'شراء',
'sale_out' => 'بيع',
'transfer_in' => 'تحويل (وارد)',
'transfer_out' => 'تحويل (صادر)',
'adjustment_in' => 'تسوية (وارد)',
'adjustment_out' => 'تسوية (صادر)',
'damage_out' => 'تالف',
'expired_out' => 'منتهي صلاحية',
'return_in' => 'مرتجع',
'opening_balance' => 'رصيد افتتاحي',
'consumed_out' => 'مستهلك',
];
?>
<!-- Filters -->
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/inventory/reports/movements" style="display:flex;gap:10px;flex-wrap:wrap;align-items:end;">
<div style="min-width:160px;">
<label class="form-label" style="font-size:12px;">المخزن</label>
<select name="warehouse_id" class="form-select">
<option value="">كل المخازن</option>
<?php foreach ($warehouses as $wh): ?>
<option value="<?= (int) $wh['id'] ?>" <?= $warehouseId == $wh['id'] ? 'selected' : '' ?>><?= e($wh['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="min-width:150px;">
<label class="form-label" style="font-size:12px;">من تاريخ</label>
<input type="date" name="date_from" value="<?= e($dateFrom ?? '') ?>" class="form-input">
</div>
<div style="min-width:150px;">
<label class="form-label" style="font-size:12px;">إلى تاريخ</label>
<input type="date" name="date_to" value="<?= e($dateTo ?? '') ?>" class="form-input">
</div>
<button type="submit" class="btn btn-primary"><i data-lucide="search" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> عرض التقرير</button>
<a href="/inventory/reports/movements" class="btn btn-outline" style="color:#6B7280;">مسح</a>
</form>
</div>
<?php
$inbound = array_filter($summary, fn($r) => ($r['direction'] ?? '') === 'in');
$outbound = array_filter($summary, fn($r) => ($r['direction'] ?? '') === 'out');
?>
<!-- Inbound Section -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:2px solid #059669;display:flex;align-items:center;gap:8px;">
<i data-lucide="arrow-down-circle" style="width:20px;height:20px;color:#059669;"></i>
<h3 style="margin:0;color:#059669;">الحركات الواردة</h3>
</div>
<?php if (!empty($inbound)): ?>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>نوع الحركة</th>
<th>عدد الحركات</th>
<th>إجمالي الكمية</th>
<th>إجمالي التكلفة</th>
</tr>
</thead>
<tbody>
<?php foreach ($inbound as $row): ?>
<tr>
<td style="font-weight:600;"><?= e($typeLabels[$row['movement_type']] ?? $row['movement_type']) ?></td>
<td style="font-weight:600;"><?= (int) $row['move_count'] ?></td>
<td style="font-weight:700;color:#059669;"><?= number_format((float) ($row['total_qty'] ?? 0)) ?></td>
<td style="font-weight:700;direction:ltr;text-align:right;"><?= money($row['total_cost'] ?? 0) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div style="padding:30px;text-align:center;color:#6B7280;font-size:14px;">لا توجد حركات واردة في الفترة المحددة</div>
<?php endif; ?>
</div>
<!-- Outbound Section -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:2px solid #DC2626;display:flex;align-items:center;gap:8px;">
<i data-lucide="arrow-up-circle" style="width:20px;height:20px;color:#DC2626;"></i>
<h3 style="margin:0;color:#DC2626;">الحركات الصادرة</h3>
</div>
<?php if (!empty($outbound)): ?>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>نوع الحركة</th>
<th>عدد الحركات</th>
<th>إجمالي الكمية</th>
<th>إجمالي التكلفة</th>
</tr>
</thead>
<tbody>
<?php foreach ($outbound as $row): ?>
<tr>
<td style="font-weight:600;"><?= e($typeLabels[$row['movement_type']] ?? $row['movement_type']) ?></td>
<td style="font-weight:600;"><?= (int) $row['move_count'] ?></td>
<td style="font-weight:700;color:#DC2626;"><?= number_format((float) ($row['total_qty'] ?? 0)) ?></td>
<td style="font-weight:700;direction:ltr;text-align:right;"><?= money($row['total_cost'] ?? 0) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div style="padding:30px;text-align:center;color:#6B7280;font-size:14px;">لا توجد حركات صادرة في الفترة المحددة</div>
<?php endif; ?>
</div>
<?php if (empty($summary)): ?>
<div class="card" style="padding:60px 20px;text-align:center;">
<div style="margin-bottom:15px;">
<i data-lucide="bar-chart-3" style="width:48px;height:48px;color:#D1D5DB;"></i>
</div>
<h3 style="color:#6B7280;margin:0 0 8px;">لا توجد بيانات</h3>
<p style="color:#9CA3AF;font-size:14px;margin:0;">اختر الفترة الزمنية والمخزن لعرض تقرير الحركات</p>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تقرير أرصدة المخزون<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Filters -->
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/inventory/reports/stock-balance" style="display:flex;gap:10px;flex-wrap:wrap;align-items:end;">
<div style="min-width:180px;">
<label class="form-label" style="font-size:12px;">المخزن</label>
<select name="warehouse_id" class="form-select">
<option value="">الكل</option>
<?php foreach ($warehouses as $wh): ?>
<option value="<?= (int) $wh['id'] ?>" <?= ($warehouseId ?? '') == $wh['id'] ? 'selected' : '' ?>><?= e($wh['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="min-width:180px;">
<label class="form-label" style="font-size:12px;">التصنيف</label>
<select name="category_id" class="form-select">
<option value="">الكل</option>
<?php foreach ($categories as $cat): ?>
<option value="<?= (int) $cat['id'] ?>" <?= ($categoryId ?? '') == $cat['id'] ? 'selected' : '' ?>><?= e($cat['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<button type="submit" class="btn btn-outline"><i data-lucide="search" style="width:16px;height:16px;vertical-align:middle;"></i> عرض</button>
<a href="/inventory/reports/stock-balance" class="btn btn-sm btn-outline" style="color:#6B7280;">مسح</a>
</form>
</div>
<!-- Total Value Card -->
<div class="card" style="margin-bottom:20px;padding:20px;text-align:center;background:linear-gradient(135deg, #0D737710, #0D737720);">
<div style="font-size:14px;color:#6B7280;margin-bottom:6px;">إجمالي قيمة المخزون</div>
<div style="font-size:36px;font-weight:800;color:#0D7377;direction:ltr;"><?= money($totalValue ?? 0) ?></div>
</div>
<!-- Stock Balance Table -->
<?php if (!empty($rows)): ?>
<div class="card">
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>SKU</th>
<th>الصنف</th>
<th>المخزن</th>
<th>الكمية</th>
<th>الحد الأدنى</th>
<th>الحد الأقصى</th>
<th>الوحدة</th>
<th>سعر التكلفة</th>
<th>قيمة المخزون</th>
</tr>
</thead>
<tbody>
<?php foreach ($rows as $row): ?>
<?php
$qty = (float) ($row['quantity'] ?? 0);
$minLvl = (float) ($row['min_level'] ?? 0);
$maxLvl = (float) ($row['max_level'] ?? 0);
// Row color based on stock level
if ($qty <= 0) {
$qtyColor = '#DC2626';
$rowBg = '#FEF2F2';
} elseif ($minLvl > 0 && $qty <= $minLvl) {
$qtyColor = '#D97706';
$rowBg = '#FFFBEB';
} else {
$qtyColor = '#1A1A2E';
$rowBg = '';
}
?>
<tr style="<?= $rowBg ? 'background:' . $rowBg . ';' : '' ?>">
<td style="direction:ltr;text-align:left;font-family:monospace;font-weight:600;"><?= e($row['sku'] ?? '') ?></td>
<td style="font-weight:600;"><?= e($row['name_ar'] ?? '') ?></td>
<td><?= e($row['warehouse_name'] ?? '') ?></td>
<td style="font-weight:700;color:<?= $qtyColor ?>;direction:ltr;text-align:left;"><?= number_format($qty, 2) ?></td>
<td style="color:#6B7280;direction:ltr;text-align:left;"><?= $minLvl > 0 ? number_format($minLvl, 2) : '—' ?></td>
<td style="color:#6B7280;direction:ltr;text-align:left;"><?= $maxLvl > 0 ? number_format($maxLvl, 2) : '—' ?></td>
<td><?= e($row['unit_of_measure'] ?? '') ?></td>
<td style="font-weight:600;direction:ltr;text-align:left;"><?= money($row['cost_price'] ?? 0) ?></td>
<td style="font-weight:700;direction:ltr;text-align:left;color:#0D7377;"><?= money($row['stock_value'] ?? 0) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
<tfoot>
<tr style="background:#F9FAFB;border-top:2px solid #E5E7EB;">
<td colspan="8" style="font-weight:700;font-size:15px;padding:12px 15px;">الإجمالي</td>
<td style="font-weight:800;font-size:16px;direction:ltr;text-align:left;color:#0D7377;padding:12px 15px;"><?= money($totalValue ?? 0) ?></td>
</tr>
</tfoot>
</table>
</div>
</div>
<?php else: ?>
<div class="card" style="padding:60px 20px;text-align:center;">
<div style="margin-bottom:15px;">
<i data-lucide="bar-chart-3" style="width:48px;height:48px;color:#D1D5DB;"></i>
</div>
<h3 style="color:#6B7280;margin:0 0 8px;">لا توجد بيانات</h3>
<p style="color:#9CA3AF;font-size:14px;margin:0;">
<?php if (!empty($warehouseId) || !empty($categoryId)): ?>
لا توجد نتائج مطابقة. جرب تغيير معايير التصفية.
<?php else: ?>
لا توجد أرصدة مخزون مسجلة حاليا.
<?php endif; ?>
</p>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تاريخ الموردين<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<button onclick="window.print()" class="btn btn-outline"><i data-lucide="printer" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> طباعة</button>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$statusLabels = [
'draft' => ['label' => 'مسودة', 'bg' => '#F3F4F6', 'color' => '#6B7280'],
'pending_approval' => ['label' => 'بانتظار الموافقة', 'bg' => '#FEF3C7', 'color' => '#D97706'],
'approved' => ['label' => 'معتمد', 'bg' => '#DBEAFE', 'color' => '#2563EB'],
'partially_received' => ['label' => 'استلام جزئي', 'bg' => '#E0E7FF', 'color' => '#4F46E5'],
'received' => ['label' => 'مستلم', 'bg' => '#ECFDF5', 'color' => '#059669'],
'cancelled' => ['label' => 'ملغى', 'bg' => '#FEE2E2', 'color' => '#DC2626'],
];
?>
<!-- Filter -->
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/inventory/reports/supplier-history" style="display:flex;gap:10px;align-items:end;">
<div style="min-width:250px;">
<label class="form-label" style="font-size:12px;">المورد</label>
<select name="supplier_id" class="form-select">
<option value="">-- اختر مورد --</option>
<?php foreach ($suppliers as $sup): ?>
<option value="<?= (int) $sup['id'] ?>" <?= $supplierId == $sup['id'] ? 'selected' : '' ?>>
<?= e($sup['name_ar']) ?>
<?php if (!empty($sup['code'])): ?> (<?= e($sup['code']) ?>)<?php endif; ?>
</option>
<?php endforeach; ?>
</select>
</div>
<button type="submit" class="btn btn-primary"><i data-lucide="search" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> عرض</button>
</form>
</div>
<?php if ($supplierId && !empty($orders)): ?>
<?php
$totalAmount = 0;
foreach ($orders as $o) {
$totalAmount = bcadd((string) $totalAmount, (string) ($o['grand_total'] ?? 0), 2);
}
?>
<!-- Summary -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;margin-bottom:20px;">
<div class="card" style="padding:20px;text-align:center;background:#F0FDF4;border:1px solid #BBF7D0;">
<div style="font-size:28px;font-weight:700;color:#059669;"><?= count($orders) ?></div>
<div style="color:#6B7280;font-size:14px;">عدد أوامر الشراء</div>
</div>
<div class="card" style="padding:20px;text-align:center;background:#EFF6FF;border:1px solid #BFDBFE;">
<div style="font-size:28px;font-weight:700;color:#2563EB;"><?= money($totalAmount) ?></div>
<div style="color:#6B7280;font-size:14px;">إجمالي المشتريات</div>
</div>
</div>
<!-- Orders Table -->
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="file-text" style="width:20px;height:20px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;">أوامر الشراء</h3>
</div>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>رقم الأمر</th>
<th>المخزن</th>
<th>الحالة</th>
<th>الإجمالي</th>
<th>التاريخ</th>
</tr>
</thead>
<tbody>
<?php foreach ($orders as $order): ?>
<?php
$st = $statusLabels[$order['status'] ?? ''] ?? ['label' => $order['status'] ?? '—', 'bg' => '#F3F4F6', 'color' => '#6B7280'];
?>
<tr>
<td style="font-family:monospace;font-weight:600;">
<a href="/inventory/purchase-orders/<?= (int) ($order['id'] ?? 0) ?>" style="color:#0D7377;text-decoration:none;"><?= e($order['po_number'] ?? '—') ?></a>
</td>
<td><?= e($order['warehouse_name'] ?? '—') ?></td>
<td>
<span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;background:<?= $st['bg'] ?>;color:<?= $st['color'] ?>;">
<?= e($st['label']) ?>
</span>
</td>
<td style="font-weight:700;direction:ltr;text-align:right;"><?= money($order['grand_total'] ?? 0) ?></td>
<td style="font-size:13px;color:#6B7280;white-space:nowrap;"><?= e($order['created_at'] ?? '—') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
<tfoot>
<tr style="background:#F0FDF4;font-weight:700;">
<td colspan="3" style="padding:12px 15px;">الإجمالي</td>
<td style="padding:12px 15px;direction:ltr;text-align:right;font-size:16px;color:#059669;"><?= money($totalAmount) ?></td>
<td></td>
</tr>
</tfoot>
</table>
</div>
</div>
<?php elseif ($supplierId && empty($orders)): ?>
<div class="card" style="padding:60px 20px;text-align:center;">
<div style="margin-bottom:15px;">
<i data-lucide="inbox" style="width:48px;height:48px;color:#D1D5DB;"></i>
</div>
<h3 style="color:#6B7280;margin:0 0 8px;">لا توجد أوامر شراء</h3>
<p style="color:#9CA3AF;font-size:14px;margin:0;">لم يتم تسجيل أي أوامر شراء لهذا المورد</p>
</div>
<?php else: ?>
<div class="card" style="padding:60px 20px;text-align:center;">
<div style="margin-bottom:15px;">
<i data-lucide="truck" style="width:48px;height:48px;color:#D1D5DB;"></i>
</div>
<h3 style="color:#6B7280;margin:0 0 8px;">اختر مورد</h3>
<p style="color:#9CA3AF;font-size:14px;margin:0;">اختر أحد الموردين من القائمة لعرض تاريخ أوامر الشراء</p>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تسجيل حركة مخزون<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/inventory/movements" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة للحركات</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<form method="POST" action="/inventory/movements" id="movementForm">
<?= csrf_field() ?>
<!-- Card 1: Movement Data -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="arrow-left-right" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">بيانات الحركة</h3>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;">
<div class="form-group">
<label class="form-label">الصنف <span style="color:#DC2626;">*</span></label>
<select name="item_id" id="item_id" class="form-select" required>
<option value="">-- اختر الصنف --</option>
<?php foreach ($items as $item): ?>
<option value="<?= (int) $item['id'] ?>" data-tracking="<?= e($item['tracking_type'] ?? 'standard') ?>" <?= old('item_id') == $item['id'] ? 'selected' : '' ?>>
<?= e($item['name_ar']) ?> (<?= e($item['sku']) ?>)
</option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">المخزن <span style="color:#DC2626;">*</span></label>
<select name="warehouse_id" class="form-select" required>
<option value="">-- اختر المخزن --</option>
<?php foreach ($warehouses as $wh): ?>
<option value="<?= (int) $wh['id'] ?>" <?= old('warehouse_id') == $wh['id'] ? 'selected' : '' ?>><?= e($wh['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:15px;">
<div class="form-group">
<label class="form-label">الاتجاه <span style="color:#DC2626;">*</span></label>
<select name="direction" id="direction" class="form-select" required>
<option value="">-- اختر الاتجاه --</option>
<option value="in" <?= old('direction') === 'in' ? 'selected' : '' ?>>وارد</option>
<option value="out" <?= old('direction') === 'out' ? 'selected' : '' ?>>صادر</option>
</select>
</div>
<div class="form-group">
<label class="form-label">نوع الحركة <span style="color:#DC2626;">*</span></label>
<select name="movement_type" id="movement_type" class="form-select" required>
<option value="">-- اختر الاتجاه أولاً --</option>
</select>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:20px;margin-top:15px;">
<div class="form-group">
<label class="form-label">الكمية <span style="color:#DC2626;">*</span></label>
<input type="number" name="quantity" value="<?= e(old('quantity', '')) ?>" class="form-input" step="0.001" min="0.001" required placeholder="0.000" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">تكلفة الوحدة</label>
<input type="number" name="unit_cost" value="<?= e(old('unit_cost', '')) ?>" class="form-input" step="0.01" min="0" placeholder="0.00" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">تاريخ الحركة <span style="color:#DC2626;">*</span></label>
<input type="date" name="movement_date" value="<?= e(old('movement_date', date('Y-m-d'))) ?>" class="form-input" required>
</div>
</div>
</div>
</div>
<!-- Card 2: Batch Data (hidden by default) -->
<div class="card" style="margin-bottom:20px;display:none;" id="batchCard">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="calendar-clock" style="width:18px;height:18px;color:#D97706;"></i>
<h3 style="margin:0;color:#D97706;font-size:15px;">بيانات الدفعة (للأصناف ذات تاريخ صلاحية)</h3>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;">
<div class="form-group">
<label class="form-label">رقم الدفعة</label>
<input type="text" name="batch_number" value="<?= e(old('batch_number', '')) ?>" class="form-input" placeholder="مثال: BATCH-2026-001" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">تاريخ الصلاحية</label>
<input type="date" name="expiry_date" value="<?= e(old('expiry_date', '')) ?>" class="form-input">
</div>
</div>
</div>
</div>
<!-- Notes -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:20px;">
<div class="form-group" style="margin:0;">
<label class="form-label">ملاحظات</label>
<textarea name="notes" class="form-input" rows="3" placeholder="ملاحظات إضافية عن الحركة..."><?= e(old('notes', '')) ?></textarea>
</div>
</div>
</div>
<!-- Submit -->
<div style="display:flex;gap:10px;justify-content:flex-start;">
<button type="submit" class="btn btn-primary" style="padding:12px 30px;font-size:15px;">
<i data-lucide="check" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i>
تسجيل الحركة
</button>
<a href="/inventory/movements" class="btn btn-outline" style="padding:12px 30px;font-size:15px;">إلغاء</a>
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
// Items data with tracking types
var itemsData = {};
<?php foreach ($items as $item): ?>
itemsData[<?= (int) $item['id'] ?>] = {
tracking_type: '<?= e($item['tracking_type'] ?? 'standard') ?>'
};
<?php endforeach; ?>
// Movement types by direction
var movementTypesByDirection = {
'in': {
'purchase': 'شراء',
'return_in': 'مرتجع وارد',
'transfer_in': 'تحويل وارد',
'adjustment_in': 'تسوية وارد',
'opening_balance': 'رصيد افتتاحي'
},
'out': {
'sale': 'بيع',
'return_out': 'مرتجع صادر',
'transfer_out': 'تحويل صادر',
'adjustment_out': 'تسوية صادر',
'damage': 'تالف',
'consumption': 'استهلاك'
}
};
var directionSelect = document.getElementById('direction');
var movementTypeSelect = document.getElementById('movement_type');
var itemSelect = document.getElementById('item_id');
var batchCard = document.getElementById('batchCard');
var oldMovementType = '<?= e(old('movement_type', '')) ?>';
// Update movement types based on direction
directionSelect.addEventListener('change', function() {
var dir = this.value;
movementTypeSelect.innerHTML = '';
if (!dir) {
movementTypeSelect.innerHTML = '<option value="">-- اختر الاتجاه أولاً --</option>';
return;
}
movementTypeSelect.innerHTML = '<option value="">-- اختر نوع الحركة --</option>';
var types = movementTypesByDirection[dir] || {};
for (var key in types) {
var opt = document.createElement('option');
opt.value = key;
opt.textContent = types[key];
if (key === oldMovementType) {
opt.selected = true;
oldMovementType = '';
}
movementTypeSelect.appendChild(opt);
}
updateBatchVisibility();
});
// Show/hide batch card based on item tracking type and direction
function updateBatchVisibility() {
var selectedItemId = itemSelect.value;
var dir = directionSelect.value;
var item = itemsData[selectedItemId];
if (item && item.tracking_type === 'expiry' && dir === 'in') {
batchCard.style.display = '';
} else {
batchCard.style.display = 'none';
}
}
itemSelect.addEventListener('change', updateBatchVisibility);
// Trigger initial state if old values exist
if (directionSelect.value) {
directionSelect.dispatchEvent(new Event('change'));
}
updateBatchVisibility();
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>حركات المخزون<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/inventory/movements/create" class="btn btn-primary"><i data-lucide="plus" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> حركة جديدة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Search & Filters -->
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/inventory/movements" style="display:flex;gap:10px;flex-wrap:wrap;align-items:end;">
<div style="min-width:160px;">
<label class="form-label" style="font-size:12px;">المخزن</label>
<select name="warehouse_id" class="form-select">
<option value="">الكل</option>
<?php foreach ($warehouses as $wh): ?>
<option value="<?= (int) $wh['id'] ?>" <?= ($filters['warehouse_id'] ?? '') == $wh['id'] ? 'selected' : '' ?>><?= e($wh['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="min-width:160px;">
<label class="form-label" style="font-size:12px;">نوع الحركة</label>
<select name="movement_type" class="form-select">
<option value="">الكل</option>
<?php foreach ($movementTypes as $key => $label): ?>
<option value="<?= e($key) ?>" <?= ($filters['movement_type'] ?? '') === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="min-width:140px;">
<label class="form-label" style="font-size:12px;">الاتجاه</label>
<select name="direction" class="form-select">
<option value="">الكل</option>
<option value="in" <?= ($filters['direction'] ?? '') === 'in' ? 'selected' : '' ?>>وارد</option>
<option value="out" <?= ($filters['direction'] ?? '') === 'out' ? 'selected' : '' ?>>صادر</option>
</select>
</div>
<div style="min-width:150px;">
<label class="form-label" style="font-size:12px;">من تاريخ</label>
<input type="date" name="date_from" value="<?= e($filters['date_from'] ?? '') ?>" class="form-input">
</div>
<div style="min-width:150px;">
<label class="form-label" style="font-size:12px;">إلى تاريخ</label>
<input type="date" name="date_to" value="<?= e($filters['date_to'] ?? '') ?>" class="form-input">
</div>
<button type="submit" class="btn btn-outline"><i data-lucide="search" style="width:16px;height:16px;vertical-align:middle;"></i> بحث</button>
<a href="/inventory/movements" class="btn btn-sm btn-outline" style="color:#6B7280;">مسح</a>
</form>
</div>
<!-- Movements Table -->
<?php if (!empty($movements)): ?>
<div class="card">
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>التاريخ</th>
<th>الصنف</th>
<th>المخزن</th>
<th>النوع</th>
<th>الاتجاه</th>
<th>الكمية</th>
<th>التكلفة</th>
<th>ملاحظات</th>
</tr>
</thead>
<tbody>
<?php foreach ($movements as $mv): ?>
<tr>
<td style="white-space:nowrap;font-size:13px;color:#6B7280;"><?= e($mv['movement_date'] ?? '') ?></td>
<td>
<div style="font-weight:600;"><?= e($mv['item_name'] ?? '') ?></div>
<?php if (!empty($mv['sku'])): ?>
<div style="font-size:11px;color:#9CA3AF;direction:ltr;text-align:right;">
<code style="font-size:11px;background:#F3F4F6;padding:1px 6px;border-radius:4px;"><?= e($mv['sku']) ?></code>
</div>
<?php endif; ?>
</td>
<td><?= e($mv['warehouse_name'] ?? '') ?></td>
<td>
<span style="font-size:12px;font-weight:600;"><?= e($movementTypes[$mv['movement_type'] ?? ''] ?? ($mv['movement_type'] ?? '')) ?></span>
</td>
<td>
<?php if (($mv['direction'] ?? '') === 'in'): ?>
<span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;background:#ECFDF5;color:#059669;">وارد</span>
<?php else: ?>
<span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;background:#FEE2E2;color:#DC2626;">صادر</span>
<?php endif; ?>
</td>
<td style="font-weight:700;direction:ltr;text-align:left;"><?= e($mv['quantity'] ?? 0) ?></td>
<td style="font-weight:600;direction:ltr;text-align:left;">
<?php if (!empty($mv['total_cost'])): ?>
<?= e(number_format((float) $mv['total_cost'], 2)) ?>
<?php if (!empty($mv['unit_cost'])): ?>
<div style="font-size:11px;color:#9CA3AF;">@ <?= e(number_format((float) $mv['unit_cost'], 2)) ?></div>
<?php endif; ?>
<?php else: ?>
<?php endif; ?>
</td>
<td style="font-size:13px;color:#6B7280;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">
<?= e($mv['notes'] ?? '—') ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php if (isset($pagination) && ($pagination['last_page'] ?? 1) > 1): ?>
<div style="padding:15px;">
<?php $__template->include('Shared.Components.pagination', ['pagination' => $pagination]); ?>
</div>
<?php endif; ?>
<?php else: ?>
<div class="card" style="padding:60px 20px;text-align:center;">
<div style="margin-bottom:15px;">
<i data-lucide="arrow-left-right" style="width:48px;height:48px;color:#D1D5DB;"></i>
</div>
<h3 style="color:#6B7280;margin:0 0 8px;">لا توجد حركات مخزون</h3>
<p style="color:#9CA3AF;font-size:14px;margin:0 0 20px;">
<?php if (!empty($filters['warehouse_id']) || !empty($filters['movement_type']) || !empty($filters['direction']) || !empty($filters['date_from']) || !empty($filters['date_to'])): ?>
لا توجد نتائج مطابقة لبحثك. جرب تغيير معايير البحث.
<?php else: ?>
لم يتم تسجيل أي حركات مخزون بعد.
<?php endif; ?>
</p>
<a href="/inventory/movements/create" class="btn btn-primary"><i data-lucide="plus" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> حركة جديدة</a>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>طلب تحويل مخزون جديد<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/inventory/transfers" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة للتحويلات</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<form method="POST" action="/inventory/transfers" id="transferForm">
<?= csrf_field() ?>
<!-- Transfer Details Card -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="repeat" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">بيانات التحويل</h3>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:20px;">
<div class="form-group">
<label class="form-label">من المخزن <span style="color:#DC2626;">*</span></label>
<select name="from_warehouse_id" id="from_warehouse_id" class="form-select" required>
<option value="">-- اختر المخزن --</option>
<?php foreach ($warehouses as $wh): ?>
<option value="<?= (int) $wh['id'] ?>" <?= old('from_warehouse_id') == $wh['id'] ? 'selected' : '' ?>><?= e($wh['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">إلى المخزن <span style="color:#DC2626;">*</span></label>
<select name="to_warehouse_id" id="to_warehouse_id" class="form-select" required>
<option value="">-- اختر المخزن --</option>
<?php foreach ($warehouses as $wh): ?>
<option value="<?= (int) $wh['id'] ?>" <?= old('to_warehouse_id') == $wh['id'] ? 'selected' : '' ?>><?= e($wh['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">تاريخ التحويل <span style="color:#DC2626;">*</span></label>
<input type="date" name="transfer_date" value="<?= e(old('transfer_date', date('Y-m-d'))) ?>" class="form-input" required>
</div>
</div>
<div class="form-group" style="margin-top:15px;">
<label class="form-label">ملاحظات</label>
<textarea name="notes" class="form-input" rows="3" placeholder="ملاحظات إضافية عن التحويل..."><?= e(old('notes', '')) ?></textarea>
</div>
</div>
</div>
<!-- Items Card -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;justify-content:space-between;">
<div style="display:flex;align-items:center;gap:8px;">
<i data-lucide="package" style="width:18px;height:18px;color:#D97706;"></i>
<h3 style="margin:0;color:#D97706;font-size:15px;">الأصناف المحولة</h3>
</div>
<button type="button" id="addItemBtn" class="btn btn-sm btn-outline" style="font-size:12px;padding:5px 12px;color:#0D7377;border-color:#0D7377;">
<i data-lucide="plus" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> إضافة صنف
</button>
</div>
<div style="padding:20px;">
<table class="data-table" id="itemsTable" style="width:100%;">
<thead>
<tr>
<th style="width:50%;">الصنف</th>
<th style="width:30%;">الكمية</th>
<th style="width:20%;text-align:center;">حذف</th>
</tr>
</thead>
<tbody id="itemsBody">
<!-- Rows added dynamically -->
</tbody>
</table>
<div id="noItemsMsg" style="padding:30px;text-align:center;color:#9CA3AF;font-size:14px;">
<i data-lucide="package-open" style="width:32px;height:32px;color:#D1D5DB;margin-bottom:8px;"></i>
<div>لم يتم إضافة أصناف بعد. اضغط "إضافة صنف" للبدء.</div>
</div>
</div>
</div>
<!-- Submit -->
<div style="display:flex;gap:10px;justify-content:flex-start;">
<button type="submit" class="btn btn-primary" style="padding:12px 30px;font-size:15px;">
<i data-lucide="check" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i>
إنشاء طلب التحويل
</button>
<a href="/inventory/transfers" class="btn btn-outline" style="padding:12px 30px;font-size:15px;">إلغاء</a>
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
// Items data for select options
var availableItems = [
<?php foreach ($items as $item): ?>
{ id: <?= (int) $item['id'] ?>, name: '<?= e($item['name_ar']) ?>', sku: '<?= e($item['sku']) ?>' },
<?php endforeach; ?>
];
var itemsBody = document.getElementById('itemsBody');
var noItemsMsg = document.getElementById('noItemsMsg');
var addItemBtn = document.getElementById('addItemBtn');
var rowCounter = 0;
function buildItemOptions() {
var html = '<option value="">-- اختر الصنف --</option>';
for (var i = 0; i < availableItems.length; i++) {
var it = availableItems[i];
html += '<option value="' + it.id + '">' + it.name + ' (' + it.sku + ')</option>';
}
return html;
}
function updateVisibility() {
var rows = itemsBody.querySelectorAll('tr');
noItemsMsg.style.display = rows.length > 0 ? 'none' : '';
}
function addRow() {
rowCounter++;
var tr = document.createElement('tr');
tr.id = 'itemRow_' + rowCounter;
tr.innerHTML =
'<td>' +
'<select name="item_ids[]" class="form-select" required>' + buildItemOptions() + '</select>' +
'</td>' +
'<td>' +
'<input type="number" name="quantities[]" class="form-input" step="0.001" min="0.001" required placeholder="0.000" style="direction:ltr;text-align:left;">' +
'</td>' +
'<td style="text-align:center;">' +
'<button type="button" class="btn btn-sm" style="background:#FEE2E2;color:#DC2626;border:none;padding:6px 10px;border-radius:6px;cursor:pointer;" onclick="removeRow(\'' + tr.id + '\')">' +
'<i data-lucide="trash-2" style="width:14px;height:14px;vertical-align:middle;"></i>' +
'</button>' +
'</td>';
itemsBody.appendChild(tr);
updateVisibility();
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
}
window.removeRow = function(rowId) {
var row = document.getElementById(rowId);
if (row) {
row.remove();
updateVisibility();
}
};
addItemBtn.addEventListener('click', addRow);
// Add one row by default
addRow();
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تحويل <?= e($transfer->transfer_number) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<?php if ($transfer->status === 'pending_approval'): ?>
<form method="POST" action="/inventory/transfers/<?= (int) $transfer->id ?>/approve" style="display:inline;">
<?= csrf_field() ?>
<button type="submit" class="btn" style="background:#059669;color:#fff;border:none;" onclick="return confirm('هل أنت متأكد من الموافقة على هذا التحويل؟');">
<i data-lucide="check-circle" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> موافقة
</button>
</form>
<?php endif; ?>
<?php if ($transfer->status === 'approved' || $transfer->status === 'in_transit'): ?>
<form method="POST" action="/inventory/transfers/<?= (int) $transfer->id ?>/receive" style="display:inline;">
<?= csrf_field() ?>
<button type="submit" class="btn" style="background:#0D7377;color:#fff;border:none;" onclick="return confirm('هل أنت متأكد من تأكيد استلام هذا التحويل؟');">
<i data-lucide="package-check" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> استلام
</button>
</form>
<?php endif; ?>
<a href="/inventory/transfers" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة للقائمة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$statusColors = [
'draft' => ['bg' => '#F3F4F6', 'color' => '#6B7280'],
'pending_approval' => ['bg' => '#FFF7ED', 'color' => '#D97706'],
'approved' => ['bg' => '#F0FDFA', 'color' => '#0D7377'],
'in_transit' => ['bg' => '#EFF6FF', 'color' => '#2563EB'],
'received' => ['bg' => '#ECFDF5', 'color' => '#059669'],
'cancelled' => ['bg' => '#FEE2E2', 'color' => '#DC2626'],
];
$st = $transfer->status ?? 'draft';
$stColors = $statusColors[$st] ?? $statusColors['draft'];
$stLabel = $statuses[$st] ?? $st;
?>
<!-- Transfer Info Card -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="repeat" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">بيانات التحويل</h3>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:0;">
<table style="width:100%;font-size:14px;">
<tr>
<td style="padding:10px 0;color:#6B7280;width:40%;">رقم التحويل</td>
<td style="padding:10px 0;font-weight:600;">
<code style="font-size:12px;background:#F3F4F6;padding:2px 8px;border-radius:4px;"><?= e($transfer->transfer_number) ?></code>
</td>
</tr>
<tr>
<td style="padding:10px 0;color:#6B7280;">من المخزن</td>
<td style="padding:10px 0;font-weight:600;">
<?= e($fromWarehouse['name_ar'] ?? '') ?>
<?php if (!empty($fromWarehouse['code'])): ?>
<code style="font-size:11px;background:#F3F4F6;padding:1px 6px;border-radius:4px;margin-right:6px;"><?= e($fromWarehouse['code']) ?></code>
<?php endif; ?>
</td>
</tr>
<tr>
<td style="padding:10px 0;color:#6B7280;">إلى المخزن</td>
<td style="padding:10px 0;font-weight:600;">
<?= e($toWarehouse['name_ar'] ?? '') ?>
<?php if (!empty($toWarehouse['code'])): ?>
<code style="font-size:11px;background:#F3F4F6;padding:1px 6px;border-radius:4px;margin-right:6px;"><?= e($toWarehouse['code']) ?></code>
<?php endif; ?>
</td>
</tr>
</table>
<table style="width:100%;font-size:14px;">
<tr>
<td style="padding:10px 0;color:#6B7280;width:40%;">تاريخ التحويل</td>
<td style="padding:10px 0;"><?= e($transfer->transfer_date ?? '—') ?></td>
</tr>
<tr>
<td style="padding:10px 0;color:#6B7280;">الحالة</td>
<td style="padding:10px 0;">
<span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;background:<?= $stColors['bg'] ?>;color:<?= $stColors['color'] ?>;">
<?= e($stLabel) ?>
</span>
</td>
</tr>
<?php if (!empty($transfer->approved_at)): ?>
<tr>
<td style="padding:10px 0;color:#6B7280;">تاريخ الموافقة</td>
<td style="padding:10px 0;"><?= e($transfer->approved_at) ?></td>
</tr>
<?php endif; ?>
<?php if (!empty($transfer->received_at)): ?>
<tr>
<td style="padding:10px 0;color:#6B7280;">تاريخ الاستلام</td>
<td style="padding:10px 0;"><?= e($transfer->received_at) ?></td>
</tr>
<?php endif; ?>
</table>
</div>
</div>
</div>
<!-- Transfer Items -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="package" style="width:18px;height:18px;color:#D97706;"></i>
<h3 style="margin:0;color:#D97706;font-size:15px;">الأصناف المحولة</h3>
</div>
<?php if (!empty($items)): ?>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>الصنف</th>
<th>SKU</th>
<th>الوحدة</th>
<th>الكمية</th>
</tr>
</thead>
<tbody>
<?php foreach ($items as $item): ?>
<tr>
<td style="font-weight:600;"><?= e($item['item_name'] ?? '') ?></td>
<td>
<code style="font-size:11px;background:#F3F4F6;padding:2px 6px;border-radius:4px;"><?= e($item['sku'] ?? '—') ?></code>
</td>
<td><?= e($item['unit_of_measure'] ?? '—') ?></td>
<td style="font-weight:700;direction:ltr;text-align:left;"><?= e($item['quantity'] ?? 0) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div style="padding:40px;text-align:center;color:#9CA3AF;font-size:14px;">
<i data-lucide="package-open" style="width:36px;height:36px;color:#D1D5DB;margin-bottom:10px;"></i>
<div>لا توجد أصناف في هذا التحويل</div>
</div>
<?php endif; ?>
</div>
<!-- Notes -->
<?php if (!empty($transfer->notes)): ?>
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="file-text" style="width:18px;height:18px;color:#6B7280;"></i>
<h3 style="margin:0;color:#6B7280;font-size:15px;">ملاحظات</h3>
</div>
<div style="padding:20px;">
<p style="margin:0;color:#374151;font-size:14px;line-height:1.7;white-space:pre-wrap;"><?= e($transfer->notes) ?></p>
</div>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تحويلات المخزون<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/inventory/transfers/create" class="btn btn-primary"><i data-lucide="plus" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> طلب تحويل جديد</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$statusColors = [
'draft' => ['bg' => '#F3F4F6', 'color' => '#6B7280'],
'pending_approval' => ['bg' => '#FFF7ED', 'color' => '#D97706'],
'approved' => ['bg' => '#F0FDFA', 'color' => '#0D7377'],
'in_transit' => ['bg' => '#EFF6FF', 'color' => '#2563EB'],
'received' => ['bg' => '#ECFDF5', 'color' => '#059669'],
'cancelled' => ['bg' => '#FEE2E2', 'color' => '#DC2626'],
];
?>
<!-- Search & Filters -->
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/inventory/transfers" style="display:flex;gap:10px;flex-wrap:wrap;align-items:end;">
<div style="min-width:150px;">
<label class="form-label" style="font-size:12px;">الحالة</label>
<select name="status" class="form-select">
<option value="">الكل</option>
<?php foreach ($statuses as $key => $label): ?>
<option value="<?= e($key) ?>" <?= ($filters['status'] ?? '') === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="min-width:160px;">
<label class="form-label" style="font-size:12px;">من المخزن</label>
<select name="from_warehouse_id" class="form-select">
<option value="">الكل</option>
<?php foreach ($warehouses as $wh): ?>
<option value="<?= (int) $wh['id'] ?>" <?= ($filters['from_warehouse_id'] ?? '') == $wh['id'] ? 'selected' : '' ?>><?= e($wh['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="min-width:160px;">
<label class="form-label" style="font-size:12px;">إلى المخزن</label>
<select name="to_warehouse_id" class="form-select">
<option value="">الكل</option>
<?php foreach ($warehouses as $wh): ?>
<option value="<?= (int) $wh['id'] ?>" <?= ($filters['to_warehouse_id'] ?? '') == $wh['id'] ? 'selected' : '' ?>><?= e($wh['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="min-width:150px;">
<label class="form-label" style="font-size:12px;">من تاريخ</label>
<input type="date" name="date_from" value="<?= e($filters['date_from'] ?? '') ?>" class="form-input">
</div>
<div style="min-width:150px;">
<label class="form-label" style="font-size:12px;">إلى تاريخ</label>
<input type="date" name="date_to" value="<?= e($filters['date_to'] ?? '') ?>" class="form-input">
</div>
<button type="submit" class="btn btn-outline"><i data-lucide="search" style="width:16px;height:16px;vertical-align:middle;"></i> بحث</button>
<a href="/inventory/transfers" class="btn btn-sm btn-outline" style="color:#6B7280;">مسح</a>
</form>
</div>
<!-- Transfers Table -->
<?php if (!empty($transfers)): ?>
<div class="card">
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>رقم التحويل</th>
<th>من المخزن</th>
<th>إلى المخزن</th>
<th>التاريخ</th>
<th>الحالة</th>
<th>إجراءات</th>
</tr>
</thead>
<tbody>
<?php foreach ($transfers as $tr): ?>
<tr>
<td>
<a href="/inventory/transfers/<?= (int) $tr['id'] ?>" style="color:#0D7377;font-weight:600;text-decoration:none;">
<code style="font-size:12px;background:#F3F4F6;padding:2px 8px;border-radius:4px;"><?= e($tr['transfer_number'] ?? '') ?></code>
</a>
</td>
<td><?= e($tr['from_warehouse_name'] ?? '') ?></td>
<td><?= e($tr['to_warehouse_name'] ?? '') ?></td>
<td style="white-space:nowrap;font-size:13px;color:#6B7280;"><?= e($tr['transfer_date'] ?? '') ?></td>
<td>
<?php
$st = $tr['status'] ?? 'draft';
$stColors = $statusColors[$st] ?? $statusColors['draft'];
$stLabel = $statuses[$st] ?? $st;
?>
<span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;background:<?= $stColors['bg'] ?>;color:<?= $stColors['color'] ?>;">
<?= e($stLabel) ?>
</span>
</td>
<td>
<a href="/inventory/transfers/<?= (int) $tr['id'] ?>" class="btn btn-sm btn-outline" style="font-size:12px;padding:4px 10px;">
<i data-lucide="eye" style="width:13px;height:13px;vertical-align:middle;"></i> عرض
</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php if (isset($pagination) && ($pagination['last_page'] ?? 1) > 1): ?>
<div style="padding:15px;">
<?php $__template->include('Shared.Components.pagination', ['pagination' => $pagination]); ?>
</div>
<?php endif; ?>
<?php else: ?>
<div class="card" style="padding:60px 20px;text-align:center;">
<div style="margin-bottom:15px;">
<i data-lucide="repeat" style="width:48px;height:48px;color:#D1D5DB;"></i>
</div>
<h3 style="color:#6B7280;margin:0 0 8px;">لا توجد تحويلات</h3>
<p style="color:#9CA3AF;font-size:14px;margin:0 0 20px;">
<?php if (!empty($filters['status']) || !empty($filters['from_warehouse_id']) || !empty($filters['to_warehouse_id']) || !empty($filters['date_from']) || !empty($filters['date_to'])): ?>
لا توجد نتائج مطابقة لبحثك. جرب تغيير معايير البحث.
<?php else: ?>
لم يتم إنشاء أي طلبات تحويل بعد.
<?php endif; ?>
</p>
<a href="/inventory/transfers/create" class="btn btn-primary"><i data-lucide="plus" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> طلب تحويل جديد</a>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php
$isEdit = $supplier !== null;
$__template->layout('Layout.main');
?>
<?php $__template->section('title'); ?><?= $isEdit ? 'تعديل المورد' : 'إضافة مورد جديد' ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<?php if ($isEdit): ?>
<a href="/inventory/suppliers/<?= (int) $supplier->id ?>" class="btn btn-outline"><i data-lucide="eye" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> عرض المورد</a>
<?php endif; ?>
<a href="/inventory/suppliers" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة للقائمة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<form method="POST" action="<?= $isEdit ? '/inventory/suppliers/' . (int) $supplier->id : '/inventory/suppliers' ?>">
<?= csrf_field() ?>
<!-- Basic Information -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="file-text" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">البيانات الأساسية</h3>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:20px;">
<div class="form-group">
<label class="form-label">كود المورد <span style="color:#DC2626;">*</span></label>
<input type="text" name="code" value="<?= e(old('code', $supplier->code ?? '')) ?>" class="form-input" required placeholder="مثال: SUP-001" style="direction:ltr;text-align:left;text-transform:uppercase;">
</div>
<div class="form-group">
<label class="form-label">الاسم بالعربي <span style="color:#DC2626;">*</span></label>
<input type="text" name="name_ar" value="<?= e(old('name_ar', $supplier->name_ar ?? '')) ?>" class="form-input" required placeholder="اسم المورد بالعربي">
</div>
<div class="form-group">
<label class="form-label">الاسم بالإنجليزي</label>
<input type="text" name="name_en" value="<?= e(old('name_en', $supplier->name_en ?? '')) ?>" class="form-input" placeholder="Supplier name in English" style="direction:ltr;text-align:left;">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:20px;margin-top:15px;">
<div class="form-group">
<label class="form-label">جهة الاتصال</label>
<input type="text" name="contact_person" value="<?= e(old('contact_person', $supplier->contact_person ?? '')) ?>" class="form-input" placeholder="اسم الشخص المسؤول">
</div>
<div class="form-group">
<label class="form-label">الهاتف</label>
<input type="text" name="phone" value="<?= e(old('phone', $supplier->phone ?? '')) ?>" class="form-input" placeholder="رقم الهاتف" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">البريد الإلكتروني</label>
<input type="email" name="email" value="<?= e(old('email', $supplier->email ?? '')) ?>" class="form-input" placeholder="example@domain.com" style="direction:ltr;text-align:left;">
</div>
</div>
<div class="form-group" style="margin-top:15px;">
<label class="form-label">العنوان</label>
<textarea name="address_ar" class="form-input" rows="2" placeholder="عنوان المورد بالتفصيل..."><?= e(old('address_ar', $supplier->address_ar ?? '')) ?></textarea>
</div>
</div>
</div>
<!-- Financial & Rating -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="banknote" style="width:18px;height:18px;color:#059669;"></i>
<h3 style="margin:0;color:#059669;font-size:15px;">البيانات المالية والتقييم</h3>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:20px;">
<div class="form-group">
<label class="form-label">الرقم الضريبي</label>
<input type="text" name="tax_number" value="<?= e(old('tax_number', $supplier->tax_number ?? '')) ?>" class="form-input" placeholder="الرقم الضريبي" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">شروط الدفع</label>
<input type="text" name="payment_terms" value="<?= e(old('payment_terms', $supplier->payment_terms ?? '')) ?>" class="form-input" placeholder="مثال: 30 يوم">
</div>
<div class="form-group">
<label class="form-label">التقييم</label>
<select name="rating" class="form-select">
<option value="">-- بدون تقييم --</option>
<?php for ($i = 1; $i <= 5; $i++): ?>
<option value="<?= $i ?>" <?= (int) old('rating', $supplier->rating ?? '') === $i ? 'selected' : '' ?>><?= $i ?> <?= str_repeat('★', $i) ?></option>
<?php endfor; ?>
</select>
</div>
</div>
</div>
</div>
<!-- Settings -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="settings" style="width:18px;height:18px;color:#D97706;"></i>
<h3 style="margin:0;color:#D97706;font-size:15px;">إعدادات</h3>
</div>
<div style="padding:20px;">
<div style="display:flex;gap:30px;margin-bottom:20px;">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;padding:12px 16px;border:1px solid #E5E7EB;border-radius:8px;">
<input type="hidden" name="is_active" value="0">
<input type="checkbox" name="is_active" value="1" <?= (int) old('is_active', $supplier->is_active ?? 1) ? 'checked' : '' ?> style="width:18px;height:18px;accent-color:#0D7377;">
<div>
<div style="font-size:13px;font-weight:600;color:#1A1A2E;">مورد نشط</div>
<div style="font-size:11px;color:#9CA3AF;">المورد فعال ويمكن التعامل معه</div>
</div>
</label>
</div>
<div class="form-group">
<label class="form-label">ملاحظات</label>
<textarea name="notes" class="form-input" rows="3" placeholder="ملاحظات إضافية عن المورد..."><?= e(old('notes', $supplier->notes ?? '')) ?></textarea>
</div>
</div>
</div>
<!-- Submit Buttons -->
<div style="display:flex;gap:10px;justify-content:flex-start;">
<button type="submit" class="btn btn-primary" style="padding:12px 30px;font-size:15px;">
<i data-lucide="check" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i>
<?= $isEdit ? 'حفظ التعديلات' : 'إضافة المورد' ?>
</button>
<a href="/inventory/suppliers" class="btn btn-outline" style="padding:12px 30px;font-size:15px;">إلغاء</a>
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>الموردين<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/inventory/suppliers/create" class="btn btn-primary"><i data-lucide="plus" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> إضافة مورد</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Search & Filters -->
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/inventory/suppliers" style="display:flex;gap:10px;flex-wrap:wrap;align-items:end;">
<div style="flex:1;min-width:200px;">
<label class="form-label" style="font-size:12px;">بحث</label>
<input type="text" name="q" value="<?= e($filters['q'] ?? '') ?>" placeholder="اسم المورد، الكود، الهاتف..." class="form-input">
</div>
<div style="min-width:140px;">
<label class="form-label" style="font-size:12px;">الحالة</label>
<select name="is_active" class="form-select">
<option value="">الكل</option>
<option value="1" <?= ($filters['is_active'] ?? '') === '1' ? 'selected' : '' ?>>نشط</option>
<option value="0" <?= ($filters['is_active'] ?? '') === '0' ? 'selected' : '' ?>>معطل</option>
</select>
</div>
<button type="submit" class="btn btn-outline"><i data-lucide="search" style="width:16px;height:16px;vertical-align:middle;"></i> بحث</button>
<a href="/inventory/suppliers" class="btn btn-sm btn-outline" style="color:#6B7280;">مسح</a>
</form>
</div>
<!-- Suppliers Table -->
<?php if (!empty($suppliers)): ?>
<div class="card">
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>الكود</th>
<th>الاسم</th>
<th>الهاتف</th>
<th>البريد</th>
<th>التقييم</th>
<th>الحالة</th>
<th>الإجراءات</th>
</tr>
</thead>
<tbody>
<?php foreach ($suppliers as $s): ?>
<tr>
<td>
<code style="font-size:12px;background:#F3F4F6;padding:2px 8px;border-radius:4px;"><?= e($s['code'] ?? '—') ?></code>
</td>
<td>
<a href="/inventory/suppliers/<?= (int) $s['id'] ?>" style="color:#0D7377;font-weight:600;text-decoration:none;"><?= e($s['name_ar']) ?></a>
</td>
<td style="direction:ltr;text-align:right;font-size:13px;"><?= e($s['phone'] ?? '—') ?></td>
<td style="direction:ltr;text-align:right;font-size:13px;"><?= e($s['email'] ?? '—') ?></td>
<td>
<?php
$rating = (int) ($s['rating'] ?? 0);
for ($i = 1; $i <= 5; $i++):
?>
<i data-lucide="star" style="width:14px;height:14px;vertical-align:middle;<?= $i <= $rating ? 'color:#D97706;fill:#D97706;' : 'color:#D1D5DB;' ?>"></i>
<?php endfor; ?>
</td>
<td>
<?php if ((int) ($s['is_active'] ?? 0)): ?>
<span style="color:#059669;font-weight:600;font-size:13px;">● نشط</span>
<?php else: ?>
<span style="color:#DC2626;font-weight:600;font-size:13px;">● معطل</span>
<?php endif; ?>
</td>
<td>
<div style="display:flex;gap:6px;">
<a href="/inventory/suppliers/<?= (int) $s['id'] ?>" class="btn btn-sm btn-outline" style="font-size:12px;padding:4px 10px;">
<i data-lucide="eye" style="width:13px;height:13px;vertical-align:middle;"></i> عرض
</a>
<a href="/inventory/suppliers/<?= (int) $s['id'] ?>/edit" class="btn btn-sm btn-outline" style="font-size:12px;padding:4px 10px;">
<i data-lucide="edit-3" style="width:13px;height:13px;vertical-align:middle;"></i> تعديل
</a>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php if (isset($pagination) && ($pagination['last_page'] ?? 1) > 1): ?>
<div style="padding:15px;">
<?php $__template->include('Shared.Components.pagination', ['pagination' => $pagination]); ?>
</div>
<?php endif; ?>
<?php else: ?>
<div class="card" style="padding:60px 20px;text-align:center;">
<div style="margin-bottom:15px;">
<i data-lucide="truck" style="width:48px;height:48px;color:#D1D5DB;"></i>
</div>
<h3 style="color:#6B7280;margin:0 0 8px;">لا يوجد موردين</h3>
<p style="color:#9CA3AF;font-size:14px;margin:0 0 20px;">
<?php if (!empty($filters['q']) || ($filters['is_active'] ?? '') !== ''): ?>
لا توجد نتائج مطابقة لبحثك. جرب تغيير معايير البحث.
<?php else: ?>
ابدأ بإضافة مورد جديد للنظام.
<?php endif; ?>
</p>
<a href="/inventory/suppliers/create" class="btn btn-primary"><i data-lucide="plus" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> إضافة مورد</a>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?><?= e($supplier->name_ar) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/inventory/suppliers/<?= (int) $supplier->id ?>/edit" class="btn btn-outline"><i data-lucide="edit-3" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> تعديل</a>
<a href="/inventory/suppliers" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة للقائمة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Supplier Info Card -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="truck" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">بيانات المورد</h3>
<div style="margin-right:auto;">
<?php if ((int) $supplier->is_active): ?>
<span style="display:inline-block;padding:3px 12px;border-radius:10px;font-size:12px;font-weight:600;background:#ECFDF5;color:#059669;">نشط</span>
<?php else: ?>
<span style="display:inline-block;padding:3px 12px;border-radius:10px;font-size:12px;font-weight:600;background:#FEE2E2;color:#DC2626;">معطل</span>
<?php endif; ?>
</div>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:0;">
<table style="width:100%;font-size:14px;">
<tr>
<td style="padding:10px 0;color:#6B7280;width:40%;">الكود</td>
<td style="padding:10px 0;font-weight:600;">
<code style="font-size:12px;background:#F3F4F6;padding:2px 8px;border-radius:4px;"><?= e($supplier->code) ?></code>
</td>
</tr>
<tr>
<td style="padding:10px 0;color:#6B7280;">الاسم بالعربي</td>
<td style="padding:10px 0;font-weight:600;"><?= e($supplier->name_ar) ?></td>
</tr>
<tr>
<td style="padding:10px 0;color:#6B7280;">الاسم بالإنجليزي</td>
<td style="padding:10px 0;"><?= e($supplier->name_en ?? '—') ?></td>
</tr>
<tr>
<td style="padding:10px 0;color:#6B7280;">جهة الاتصال</td>
<td style="padding:10px 0;font-weight:600;"><?= e($supplier->contact_person ?? '—') ?></td>
</tr>
<tr>
<td style="padding:10px 0;color:#6B7280;">الهاتف</td>
<td style="padding:10px 0;direction:ltr;text-align:right;"><?= e($supplier->phone ?? '—') ?></td>
</tr>
<tr>
<td style="padding:10px 0;color:#6B7280;">البريد الإلكتروني</td>
<td style="padding:10px 0;direction:ltr;text-align:right;"><?= e($supplier->email ?? '—') ?></td>
</tr>
</table>
<table style="width:100%;font-size:14px;">
<tr>
<td style="padding:10px 0;color:#6B7280;width:40%;">العنوان</td>
<td style="padding:10px 0;"><?= e($supplier->address_ar ?? '—') ?></td>
</tr>
<tr>
<td style="padding:10px 0;color:#6B7280;">الرقم الضريبي</td>
<td style="padding:10px 0;direction:ltr;text-align:right;"><?= e($supplier->tax_number ?? '—') ?></td>
</tr>
<tr>
<td style="padding:10px 0;color:#6B7280;">شروط الدفع</td>
<td style="padding:10px 0;"><?= e($supplier->payment_terms ?? '—') ?></td>
</tr>
<tr>
<td style="padding:10px 0;color:#6B7280;">التقييم</td>
<td style="padding:10px 0;">
<?php
$rating = (int) ($supplier->rating ?? 0);
if ($rating > 0):
for ($i = 1; $i <= 5; $i++):
?>
<i data-lucide="star" style="width:16px;height:16px;vertical-align:middle;<?= $i <= $rating ? 'color:#D97706;fill:#D97706;' : 'color:#D1D5DB;' ?>"></i>
<?php
endfor;
else:
?>
<span style="color:#9CA3AF;"></span>
<?php endif; ?>
</td>
</tr>
<tr>
<td style="padding:10px 0;color:#6B7280;">ملاحظات</td>
<td style="padding:10px 0;"><?= e($supplier->notes ?? '—') ?></td>
</tr>
</table>
</div>
</div>
</div>
<!-- Recent Purchase Orders -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="shopping-cart" style="width:18px;height:18px;color:#D97706;"></i>
<h3 style="margin:0;color:#D97706;font-size:15px;">آخر أوامر الشراء</h3>
</div>
<?php if (!empty($recentPOs)): ?>
<?php
$poStatusLabels = [
'draft' => 'مسودة', 'submitted' => 'مقدم', 'approved' => 'معتمد',
'partially_received' => 'استلام جزئي', 'received' => 'مستلم', 'cancelled' => 'ملغي',
];
$poStatusColors = [
'draft' => '#6B7280', 'submitted' => '#D97706', 'approved' => '#0D7377',
'partially_received' => '#2563EB', 'received' => '#059669', 'cancelled' => '#DC2626',
];
$poStatusBgs = [
'draft' => '#F3F4F6', 'submitted' => '#FFF7ED', 'approved' => '#F0FDFA',
'partially_received' => '#EFF6FF', 'received' => '#ECFDF5', 'cancelled' => '#FEE2E2',
];
?>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>رقم الأمر</th>
<th>الحالة</th>
<th>الإجمالي</th>
<th>التاريخ</th>
<th>الإجراءات</th>
</tr>
</thead>
<tbody>
<?php foreach ($recentPOs as $po): ?>
<?php
$poSt = $po['status'] ?? 'draft';
$poColor = $poStatusColors[$poSt] ?? '#6B7280';
$poBg = $poStatusBgs[$poSt] ?? '#F3F4F6';
$poLabel = $poStatusLabels[$poSt] ?? $poSt;
?>
<tr>
<td style="font-weight:600;">
<code style="font-size:12px;background:#F3F4F6;padding:2px 8px;border-radius:4px;"><?= e($po['po_number']) ?></code>
</td>
<td>
<span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;background:<?= $poBg ?>;color:<?= $poColor ?>;">
<?= e($poLabel) ?>
</span>
</td>
<td style="font-weight:700;direction:ltr;text-align:left;"><?= money($po['grand_total'] ?? 0) ?></td>
<td style="font-size:13px;"><?= e(substr($po['created_at'] ?? '', 0, 10)) ?></td>
<td>
<a href="/inventory/purchase-orders/<?= (int) $po['id'] ?>" class="btn btn-sm btn-outline" style="font-size:12px;padding:4px 10px;">
<i data-lucide="eye" style="width:13px;height:13px;vertical-align:middle;"></i> عرض
</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div style="padding:40px 20px;text-align:center;color:#6B7280;">
<i data-lucide="shopping-cart" style="width:36px;height:36px;color:#D1D5DB;margin-bottom:8px;"></i>
<p style="margin:0;">لا توجد أوامر شراء لهذا المورد</p>
</div>
<?php endif; ?>
</div>
<!-- Metadata -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="info" style="width:18px;height:18px;color:#6B7280;"></i>
<h3 style="margin:0;color:#6B7280;font-size:15px;">معلومات النظام</h3>
</div>
<div style="padding:15px 20px;">
<table style="width:100%;font-size:13px;">
<tr>
<td style="padding:6px 0;color:#6B7280;width:30%;">رقم السجل</td>
<td style="padding:6px 0;font-weight:600;">#<?= (int) $supplier->id ?></td>
</tr>
<?php if ($supplier->created_at): ?>
<tr>
<td style="padding:6px 0;color:#6B7280;">تاريخ الإنشاء</td>
<td style="padding:6px 0;"><?= e($supplier->created_at) ?></td>
</tr>
<?php endif; ?>
<?php if ($supplier->updated_at): ?>
<tr>
<td style="padding:6px 0;color:#6B7280;">آخر تحديث</td>
<td style="padding:6px 0;"><?= e($supplier->updated_at) ?></td>
</tr>
<?php endif; ?>
</table>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?><?= $warehouse ? 'تعديل المخزن: ' . e($warehouse->name_ar) : 'إنشاء مخزن جديد' ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/inventory/warehouses" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة للقائمة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<form method="POST" action="<?= $warehouse ? '/inventory/warehouses/' . (int) $warehouse->id : '/inventory/warehouses' ?>">
<?= csrf_field() ?>
<!-- البيانات الأساسية -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="warehouse" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">البيانات الأساسية</h3>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;">
<div class="form-group">
<label class="form-label">كود المخزن <span style="color:#DC2626;">*</span></label>
<input type="text" name="code" value="<?= e(old('code', $warehouse->code ?? '')) ?>" class="form-input" style="direction:ltr;text-align:left;text-transform:uppercase;" required>
</div>
<div class="form-group">
<label class="form-label">الاسم بالعربي <span style="color:#DC2626;">*</span></label>
<input type="text" name="name_ar" value="<?= e(old('name_ar', $warehouse->name_ar ?? '')) ?>" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label">الاسم بالإنجليزي</label>
<input type="text" name="name_en" value="<?= e(old('name_en', $warehouse->name_en ?? '')) ?>" class="form-input" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">نوع المخزن <span style="color:#DC2626;">*</span></label>
<select name="warehouse_type" class="form-select" required>
<option value="">-- اختر النوع --</option>
<?php foreach ($warehouseTypes as $typeKey => $typeLabel): ?>
<option value="<?= e($typeKey) ?>" <?= old('warehouse_type', $warehouse->warehouse_type ?? '') === $typeKey ? 'selected' : '' ?>><?= e($typeLabel) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group" style="grid-column:1/-1;">
<label class="form-label">الموقع</label>
<input type="text" name="location_ar" value="<?= e(old('location_ar', $warehouse->location_ar ?? '')) ?>" class="form-input" placeholder="مثال: المبنى الرئيسي - الدور الأرضي">
</div>
</div>
</div>
</div>
<!-- بيانات إضافية -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="settings" style="width:18px;height:18px;color:#6B7280;"></i>
<h3 style="margin:0;color:#6B7280;font-size:15px;">بيانات إضافية</h3>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;">
<div class="form-group">
<label class="form-label">أمين المخزن</label>
<select name="keeper_employee_id" class="form-select">
<option value="">-- بدون --</option>
<?php foreach ($employees as $emp): ?>
<option value="<?= (int) $emp['id'] ?>" <?= old('keeper_employee_id', $warehouse->keeper_employee_id ?? '') == $emp['id'] ? 'selected' : '' ?>><?= e($emp['full_name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">السعة (وحدات)</label>
<input type="number" name="capacity_units" value="<?= e(old('capacity_units', $warehouse->capacity_units ?? '')) ?>" class="form-input" min="0">
</div>
<div class="form-group">
<label class="form-label">الهاتف</label>
<input type="text" name="phone" value="<?= e(old('phone', $warehouse->phone ?? '')) ?>" class="form-input" style="direction:ltr;text-align:left;">
</div>
<div class="form-group" style="display:flex;align-items:center;gap:10px;padding-top:28px;">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;font-size:14px;">
<input type="hidden" name="is_active" value="0">
<input type="checkbox" name="is_active" value="1" <?= old('is_active', $warehouse->is_active ?? '1') == '1' ? 'checked' : '' ?> style="width:18px;height:18px;accent-color:#0D7377;">
نشط
</label>
</div>
<div class="form-group" style="grid-column:1/-1;">
<label class="form-label">ملاحظات</label>
<textarea name="notes" class="form-textarea" rows="3" placeholder="ملاحظات إضافية..."><?= e(old('notes', $warehouse->notes ?? '')) ?></textarea>
</div>
</div>
</div>
</div>
<div style="display:flex;gap:10px;">
<button type="submit" class="btn btn-primary">
<i data-lucide="<?= $warehouse ? 'check' : 'plus' ?>" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i>
<?= $warehouse ? 'حفظ التعديلات' : 'إنشاء المخزن' ?>
</button>
<a href="/inventory/warehouses" class="btn btn-outline">إلغاء</a>
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>المخازن<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/inventory/warehouses/create" class="btn btn-primary"><i data-lucide="plus" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> مخزن جديد</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Search / Filters -->
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/inventory/warehouses" style="display:flex;gap:10px;align-items:end;flex-wrap:wrap;">
<div style="flex:1;min-width:200px;">
<label class="form-label" style="font-size:12px;">بحث</label>
<input type="text" name="q" value="<?= e($filters['q'] ?? '') ?>" placeholder="ابحث بالاسم أو الكود..." class="form-input">
</div>
<div style="min-width:160px;">
<label class="form-label" style="font-size:12px;">نوع المخزن</label>
<select name="warehouse_type" class="form-select">
<option value="">الكل</option>
<?php foreach ($warehouseTypes as $typeKey => $typeLabel): ?>
<option value="<?= e($typeKey) ?>" <?= ($filters['warehouse_type'] ?? '') === $typeKey ? 'selected' : '' ?>><?= e($typeLabel) ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="min-width:120px;">
<label class="form-label" style="font-size:12px;">الحالة</label>
<select name="is_active" class="form-select">
<option value="">الكل</option>
<option value="1" <?= ($filters['is_active'] ?? '') === '1' ? 'selected' : '' ?>>نشط</option>
<option value="0" <?= ($filters['is_active'] ?? '') === '0' ? 'selected' : '' ?>>معطل</option>
</select>
</div>
<button type="submit" class="btn btn-outline"><i data-lucide="search" style="width:16px;height:16px;vertical-align:middle;"></i> بحث</button>
</form>
</div>
<!-- Warehouses Table -->
<?php if (!empty($warehouses)): ?>
<div class="card">
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>الكود</th>
<th>الاسم</th>
<th>النوع</th>
<th>الموقع</th>
<th>أمين المخزن</th>
<th>الحالة</th>
<th>إجراءات</th>
</tr>
</thead>
<tbody>
<?php foreach ($warehouses as $w): ?>
<tr>
<td><code style="font-size:12px;background:#F3F4F6;padding:2px 8px;border-radius:4px;"><?= e($w['code']) ?></code></td>
<td style="font-weight:600;"><?= e($w['name_ar']) ?></td>
<td>
<span style="display:inline-block;padding:2px 10px;border-radius:10px;font-size:12px;font-weight:600;background:#F0FDFA;color:#0D7377;">
<?= e($warehouseTypes[$w['warehouse_type']] ?? $w['warehouse_type']) ?>
</span>
</td>
<td><?= e($w['location_ar'] ?? '—') ?></td>
<td><?= e($w['keeper_name'] ?? '—') ?></td>
<td>
<?php if ($w['is_active']): ?>
<span style="color:#059669;font-weight:600;">● نشط</span>
<?php else: ?>
<span style="color:#DC2626;font-weight:600;">● معطل</span>
<?php endif; ?>
</td>
<td>
<div style="display:flex;gap:5px;">
<a href="/inventory/warehouses/<?= (int) $w['id'] ?>" class="btn btn-sm btn-outline" style="font-size:12px;">
<i data-lucide="eye" style="width:13px;height:13px;vertical-align:middle;margin-left:3px;"></i> عرض
</a>
<a href="/inventory/warehouses/<?= (int) $w['id'] ?>/edit" class="btn btn-sm btn-outline" style="font-size:12px;">
<i data-lucide="edit-3" style="width:13px;height:13px;vertical-align:middle;margin-left:3px;"></i> تعديل
</a>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php if (isset($pagination) && ($pagination['last_page'] ?? 1) > 1): ?>
<div style="padding:15px;">
<?php $__template->include('Shared.Components.pagination', ['pagination' => $pagination]); ?>
</div>
<?php endif; ?>
<?php else: ?>
<div class="card" style="padding:60px 20px;text-align:center;">
<div style="margin-bottom:15px;">
<i data-lucide="warehouse" style="width:48px;height:48px;color:#D1D5DB;"></i>
</div>
<h3 style="color:#6B7280;margin:0 0 8px;">لا توجد مخازن</h3>
<p style="color:#9CA3AF;font-size:14px;margin:0 0 20px;">
<?php if (!empty($filters['q']) || !empty($filters['warehouse_type']) || ($filters['is_active'] ?? '') !== ''): ?>
لا توجد نتائج مطابقة لبحثك. جرب تغيير معايير البحث.
<?php else: ?>
ابدأ بإضافة مخزن جديد.
<?php endif; ?>
</p>
<a href="/inventory/warehouses/create" class="btn btn-primary"><i data-lucide="plus" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> مخزن جديد</a>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?><?= e($warehouse->name_ar) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/inventory/warehouses/<?= (int) $warehouse->id ?>/edit" class="btn btn-outline"><i data-lucide="edit-3" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> تعديل</a>
<a href="/inventory/warehouses" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة للقائمة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$isActive = (int) $warehouse->is_active;
$typeLabel = $warehouseTypes[$warehouse->warehouse_type] ?? $warehouse->warehouse_type;
?>
<!-- Warehouse Info Card -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="warehouse" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">بيانات المخزن</h3>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:0;">
<table style="width:100%;font-size:14px;">
<tr>
<td style="padding:10px 0;color:#6B7280;width:40%;">الكود</td>
<td style="padding:10px 0;font-weight:600;">
<code style="font-size:12px;background:#F3F4F6;padding:2px 8px;border-radius:4px;"><?= e($warehouse->code) ?></code>
</td>
</tr>
<tr>
<td style="padding:10px 0;color:#6B7280;">الاسم بالعربي</td>
<td style="padding:10px 0;font-weight:600;"><?= e($warehouse->name_ar) ?></td>
</tr>
<tr>
<td style="padding:10px 0;color:#6B7280;">الاسم بالإنجليزي</td>
<td style="padding:10px 0;"><?= e($warehouse->name_en ?? '—') ?></td>
</tr>
<tr>
<td style="padding:10px 0;color:#6B7280;">النوع</td>
<td style="padding:10px 0;">
<span style="display:inline-block;padding:2px 10px;border-radius:10px;font-size:12px;font-weight:600;background:#F0FDFA;color:#0D7377;">
<?= e($typeLabel) ?>
</span>
</td>
</tr>
<tr>
<td style="padding:10px 0;color:#6B7280;">الموقع</td>
<td style="padding:10px 0;"><?= e($warehouse->location_ar ?? '—') ?></td>
</tr>
</table>
<table style="width:100%;font-size:14px;">
<tr>
<td style="padding:10px 0;color:#6B7280;width:40%;">أمين المخزن</td>
<td style="padding:10px 0;font-weight:600;"><?= $keeper ? e($keeper['full_name_ar']) : '—' ?></td>
</tr>
<tr>
<td style="padding:10px 0;color:#6B7280;">السعة</td>
<td style="padding:10px 0;"><?= $warehouse->capacity_units ? (int) $warehouse->capacity_units . ' وحدة' : '—' ?></td>
</tr>
<tr>
<td style="padding:10px 0;color:#6B7280;">الهاتف</td>
<td style="padding:10px 0;direction:ltr;text-align:right;"><?= e($warehouse->phone ?? '—') ?></td>
</tr>
<tr>
<td style="padding:10px 0;color:#6B7280;">الحالة</td>
<td style="padding:10px 0;">
<?php if ($isActive): ?>
<span style="color:#059669;font-weight:600;">● نشط</span>
<?php else: ?>
<span style="color:#DC2626;font-weight:600;">● معطل</span>
<?php endif; ?>
</td>
</tr>
<tr>
<td style="padding:10px 0;color:#6B7280;">ملاحظات</td>
<td style="padding:10px 0;"><?= e($warehouse->notes ?? '—') ?></td>
</tr>
</table>
</div>
</div>
</div>
<!-- Stock Summary -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="package" style="width:18px;height:18px;color:#D97706;"></i>
<h3 style="margin:0;color:#D97706;font-size:15px;">ملخص المخزون</h3>
</div>
<?php if (!empty($stockSummary)): ?>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>الصنف</th>
<th>SKU</th>
<th>وحدة القياس</th>
<th>الكمية</th>
<th>الحد الأدنى</th>
<th>الحد الأقصى</th>
<th>الحالة</th>
</tr>
</thead>
<tbody>
<?php foreach ($stockSummary as $item): ?>
<?php $isLow = $item['min_level'] !== null && (float) $item['quantity'] <= (float) $item['min_level']; ?>
<tr>
<td style="font-weight:600;"><?= e($item['name_ar']) ?></td>
<td><code style="font-size:11px;background:#F3F4F6;padding:2px 6px;border-radius:4px;"><?= e($item['sku'] ?? '—') ?></code></td>
<td><?= e($item['unit_of_measure'] ?? '—') ?></td>
<td style="font-weight:700;color:<?= $isLow ? '#DC2626' : '#059669' ?>;"><?= (int) $item['quantity'] ?></td>
<td><?= $item['min_level'] !== null ? (int) $item['min_level'] : '—' ?></td>
<td><?= $item['max_level'] !== null ? (int) $item['max_level'] : '—' ?></td>
<td>
<?php if ($isLow): ?>
<span style="display:inline-flex;align-items:center;gap:4px;padding:2px 10px;border-radius:10px;font-size:12px;font-weight:600;background:#FEE2E2;color:#DC2626;">
<i data-lucide="alert-triangle" style="width:13px;height:13px;"></i> منخفض
</span>
<?php else: ?>
<span style="display:inline-flex;align-items:center;gap:4px;padding:2px 10px;border-radius:10px;font-size:12px;font-weight:600;background:#ECFDF5;color:#059669;">
<i data-lucide="check-circle" style="width:13px;height:13px;"></i> طبيعي
</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div style="padding:40px;text-align:center;color:#9CA3AF;font-size:14px;">
<i data-lucide="package-open" style="width:36px;height:36px;color:#D1D5DB;margin-bottom:10px;"></i>
<div>لا توجد أصناف في هذا المخزن حالياً</div>
</div>
<?php endif; ?>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php
declare(strict_types=1);
use App\Core\Registries\MenuRegistry;
use App\Core\Registries\PermissionRegistry;
// ────────────────────────────────────────────────────────────
// Inventory — Permissions
// ────────────────────────────────────────────────────────────
PermissionRegistry::register('inventory', [
'inventory.view' => ['ar' => 'عرض الأصناف والتصنيفات', 'en' => 'View Items & Categories'],
'inventory.manage' => ['ar' => 'إدارة الأصناف والتصنيفات', 'en' => 'Manage Items & Categories'],
'warehouse.view' => ['ar' => 'عرض المخازن', 'en' => 'View Warehouses'],
'warehouse.manage' => ['ar' => 'إدارة المخازن', 'en' => 'Manage Warehouses'],
'stock.view' => ['ar' => 'عرض حركات المخزون', 'en' => 'View Stock Movements'],
'stock.move' => ['ar' => 'إجراء حركات مخزون', 'en' => 'Create Stock Movements'],
'stock.approve_transfer' => ['ar' => 'اعتماد نقل بين المخازن', 'en' => 'Approve Stock Transfers'],
'stock.audit' => ['ar' => 'إجراء جرد المخزون', 'en' => 'Conduct Stock Audits'],
'stock.approve_adjustment' => ['ar' => 'اعتماد تسويات الجرد', 'en' => 'Approve Audit Adjustments'],
'purchase.view' => ['ar' => 'عرض أوامر الشراء', 'en' => 'View Purchase Orders'],
'purchase.create' => ['ar' => 'إنشاء أوامر شراء', 'en' => 'Create Purchase Orders'],
'purchase.approve' => ['ar' => 'اعتماد أوامر الشراء', 'en' => 'Approve Purchase Orders'],
'supplier.view' => ['ar' => 'عرض الموردين', 'en' => 'View Suppliers'],
'supplier.manage' => ['ar' => 'إدارة الموردين', 'en' => 'Manage Suppliers'],
'asset.view' => ['ar' => 'عرض الأصول', 'en' => 'View Assets'],
'asset.manage' => ['ar' => 'إدارة الأصول والإهلاك', 'en' => 'Manage Assets & Depreciation'],
'report.inventory' => ['ar' => 'تقارير المخزون', 'en' => 'Inventory Reports'],
]);
// ────────────────────────────────────────────────────────────
// Inventory — Sidebar menu
// ────────────────────────────────────────────────────────────
MenuRegistry::register('inventory', [
'label_ar' => 'إدارة المخازن',
'label_en' => 'Inventory Management',
'icon' => 'warehouse',
'route' => '/inventory/items',
'permission' => 'inventory.view',
'parent' => null,
'order' => 700,
'children' => [
['label_ar' => 'الأصناف', 'label_en' => 'Items', 'route' => '/inventory/items', 'permission' => 'inventory.view', 'order' => 1],
['label_ar' => 'التصنيفات', 'label_en' => 'Categories', 'route' => '/inventory/categories', 'permission' => 'inventory.view', 'order' => 2],
['label_ar' => 'المخازن', 'label_en' => 'Warehouses', 'route' => '/inventory/warehouses', 'permission' => 'warehouse.view', 'order' => 3],
['label_ar' => 'حركات المخزون', 'label_en' => 'Stock Movements', 'route' => '/inventory/movements', 'permission' => 'stock.view', 'order' => 4],
['label_ar' => 'النقل بين المخازن', 'label_en' => 'Stock Transfers', 'route' => '/inventory/transfers', 'permission' => 'stock.view', 'order' => 5],
['label_ar' => 'الجرد', 'label_en' => 'Stock Audits', 'route' => '/inventory/audits', 'permission' => 'stock.audit', 'order' => 6],
['label_ar' => 'الموردين', 'label_en' => 'Suppliers', 'route' => '/inventory/suppliers', 'permission' => 'supplier.view', 'order' => 7],
['label_ar' => 'أوامر الشراء', 'label_en' => 'Purchase Orders', 'route' => '/inventory/purchase-orders', 'permission' => 'purchase.view', 'order' => 8],
['label_ar' => 'الأصول والإهلاك', 'label_en' => 'Assets', 'route' => '/inventory/assets', 'permission' => 'asset.view', 'order' => 9],
['label_ar' => 'تقارير المخزون', 'label_en' => 'Inventory Reports', 'route' => '/inventory/reports/stock-balance','permission' => 'report.inventory', 'order' => 10],
],
]);
...@@ -311,6 +311,7 @@ final class PaymentService ...@@ -311,6 +311,7 @@ final class PaymentService
'carnet_replacement' => 'بدل فاقد كارنيه', 'carnet_replacement' => 'بدل فاقد كارنيه',
'seasonal_fee' => 'رسوم عضوية موسمية', 'seasonal_fee' => 'رسوم عضوية موسمية',
'sports_conversion' => 'رسوم تحويل رياضي', 'sports_conversion' => 'رسوم تحويل رياضي',
'inventory_sale' => 'مبيعات مخزون',
'other' => 'أخرى', 'other' => 'أخرى',
default => $type, default => $type,
}; };
......
<?php
declare(strict_types=1);
namespace App\Modules\Sales\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\Sales\Models\Package;
use App\Modules\Sales\Models\PackageItem;
use App\Modules\Sales\Services\PackageService;
use App\Modules\Inventory\Models\InventoryItem;
class PackageController extends Controller
{
public function index(Request $request): Response
{
$filters = [
'q' => trim((string) $request->get('q', '')),
'is_active' => $request->get('is_active', ''),
];
$page = max(1, (int) $request->get('page', 1));
$result = Package::search($filters, 25, $page);
return $this->view('Sales.Views.packages.index', [
'packages' => $result['data'],
'pagination' => $result['pagination'],
'filters' => $filters,
]);
}
public function create(Request $request): Response
{
return $this->view('Sales.Views.packages.form', [
'package' => null,
'items' => InventoryItem::allSellable(),
'packageItems' => [],
]);
}
public function store(Request $request): Response
{
$header = $this->extractHeader($request);
$items = $this->extractItems($request);
$errors = $this->validate($header);
// Unique code
if ($header['code'] !== '') {
$existing = Package::query()->where('code', '=', $header['code'])->first();
if ($existing) {
$errors[] = 'كود الباقة مستخدم بالفعل';
}
}
if (empty($items)) {
$errors[] = 'يجب إضافة صنف واحد على الأقل';
}
if (!empty($errors)) {
$session = App::getInstance()->session();
$session->flash('_alerts', array_map(fn($e) => ['type' => 'error', 'message' => $e], $errors));
$session->flash('_old_input', $request->all());
return $this->redirect('/sales/packages/create');
}
try {
$packageId = PackageService::createPackage($header, $items);
return $this->redirect('/sales/packages/' . $packageId)->withSuccess('تم إنشاء الباقة بنجاح');
} catch (\RuntimeException $e) {
return $this->redirect('/sales/packages/create')->withError($e->getMessage());
}
}
public function show(Request $request, string $id): Response
{
$package = Package::find((int) $id);
if (!$package) {
return $this->redirect('/sales/packages')->withError('الباقة غير موجودة');
}
$packageItems = PackageItem::getForPackage((int) $id);
return $this->view('Sales.Views.packages.show', [
'package' => $package,
'packageItems' => $packageItems,
]);
}
public function edit(Request $request, string $id): Response
{
$package = Package::find((int) $id);
if (!$package) {
return $this->redirect('/sales/packages')->withError('الباقة غير موجودة');
}
return $this->view('Sales.Views.packages.form', [
'package' => $package,
'items' => InventoryItem::allSellable(),
'packageItems' => PackageItem::getForPackage((int) $id),
]);
}
public function update(Request $request, string $id): Response
{
$package = Package::find((int) $id);
if (!$package) {
return $this->redirect('/sales/packages')->withError('الباقة غير موجودة');
}
$header = $this->extractHeader($request);
$items = $this->extractItems($request);
$errors = $this->validate($header);
// Unique code (exclude current)
if ($header['code'] !== '') {
$existing = Package::query()
->where('code', '=', $header['code'])
->whereRaw('`id` != ?', [(int) $id])
->first();
if ($existing) {
$errors[] = 'كود الباقة مستخدم بالفعل';
}
}
if (empty($items)) {
$errors[] = 'يجب إضافة صنف واحد على الأقل';
}
if (!empty($errors)) {
$session = App::getInstance()->session();
$session->flash('_alerts', array_map(fn($e) => ['type' => 'error', 'message' => $e], $errors));
$session->flash('_old_input', $request->all());
return $this->redirect('/sales/packages/' . $id . '/edit');
}
try {
PackageService::updatePackage((int) $id, $header, $items);
return $this->redirect('/sales/packages/' . $id)->withSuccess('تم تحديث الباقة بنجاح');
} catch (\RuntimeException $e) {
return $this->redirect('/sales/packages/' . $id . '/edit')->withError($e->getMessage());
}
}
private function extractHeader(Request $request): array
{
return [
'code' => strtoupper(trim((string) $request->post('code', ''))),
'name_ar' => trim((string) $request->post('name_ar', '')),
'name_en' => trim((string) $request->post('name_en', '')) ?: null,
'description_ar' => trim((string) $request->post('description_ar', '')) ?: null,
'package_price_member' => trim((string) $request->post('package_price_member', '0')),
'package_price_nonmember' => trim((string) $request->post('package_price_nonmember', '0')),
'active_from' => $request->post('active_from') ?: null,
'active_to' => $request->post('active_to') ?: null,
'notes' => trim((string) $request->post('notes', '')) ?: null,
];
}
private function extractItems(Request $request): array
{
$itemIds = $request->post('item_ids', []);
$quantities = $request->post('quantities', []);
$items = [];
foreach ($itemIds as $i => $itemId) {
$qty = (string) ($quantities[$i] ?? '0');
if ((int) $itemId > 0 && bccomp($qty, '0', 3) > 0) {
$items[] = ['item_id' => (int) $itemId, 'quantity' => $qty];
}
}
return $items;
}
private function validate(array $header): array
{
$errors = [];
if ($header['code'] === '') $errors[] = 'كود الباقة مطلوب';
if ($header['name_ar'] === '' || mb_strlen($header['name_ar']) < 2) $errors[] = 'اسم الباقة بالعربي مطلوب';
if (bccomp($header['package_price_member'], '0', 2) <= 0) $errors[] = 'سعر الباقة للأعضاء يجب أن يكون أكبر من صفر';
if (bccomp($header['package_price_nonmember'], '0', 2) <= 0) $errors[] = 'سعر الباقة لغير الأعضاء يجب أن يكون أكبر من صفر';
return $errors;
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Sales\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\Sales\Models\Sale;
use App\Modules\Sales\Models\SaleItem;
use App\Modules\Sales\Models\SaleRefund;
use App\Modules\Sales\Services\SaleService;
use App\Modules\Sales\Services\RefundService;
use App\Modules\Inventory\Models\Warehouse;
use App\Modules\Inventory\Models\InventoryItem;
use App\Modules\Sales\Models\Package;
class SaleController extends Controller
{
public function index(Request $request): Response
{
$filters = [
'q' => trim((string) $request->get('q', '')),
'status' => trim((string) $request->get('status', '')),
'customer_type' => trim((string) $request->get('customer_type', '')),
'warehouse_id' => trim((string) $request->get('warehouse_id', '')),
'date_from' => trim((string) $request->get('date_from', '')),
'date_to' => trim((string) $request->get('date_to', '')),
];
$page = max(1, (int) $request->get('page', 1));
$result = Sale::search($filters, 25, $page);
return $this->view('Sales.Views.sales.index', [
'sales' => $result['data'],
'pagination' => $result['pagination'],
'filters' => $filters,
'statuses' => Sale::getStatuses(),
'customerTypes' => Sale::getCustomerTypes(),
'warehouses' => Warehouse::allActive(),
]);
}
/**
* POS screen for creating a new sale.
*/
public function create(Request $request): Response
{
return $this->view('Sales.Views.pos.index', [
'warehouses' => Warehouse::allActive(),
'packages' => Package::allActive(),
]);
}
public function store(Request $request): Response
{
$warehouseId = (int) $request->post('warehouse_id', 0);
$customerType = trim((string) $request->post('customer_type', 'guest'));
$memberId = (int) $request->post('member_id', 0);
$playerId = (int) $request->post('player_id', 0);
$guestName = trim((string) $request->post('guest_name', ''));
$guestPhone = trim((string) $request->post('guest_phone', ''));
$discount = trim((string) $request->post('discount_amount', '0'));
$discountReason = trim((string) $request->post('discount_reason', ''));
$paymentMethod = trim((string) $request->post('payment_method', 'cash'));
$notes = trim((string) $request->post('notes', ''));
// Parse line items
$itemIds = $request->post('item_ids', []);
$packageIds = $request->post('package_ids', []);
$quantities = $request->post('quantities', []);
$unitPrices = $request->post('unit_prices', []);
$itemNames = $request->post('item_names', []);
$costPrices = $request->post('cost_prices', []);
$taxAmounts = $request->post('tax_amounts', []);
$lines = [];
$count = max(count($itemIds ?: []), count($packageIds ?: []));
for ($i = 0; $i < $count; $i++) {
$qty = (string) ($quantities[$i] ?? '0');
if (bccomp($qty, '0', 3) <= 0) continue;
$lines[] = [
'item_id' => ($itemIds[$i] ?? 0) ? (int) $itemIds[$i] : null,
'package_id' => ($packageIds[$i] ?? 0) ? (int) $packageIds[$i] : null,
'quantity' => $qty,
'unit_price' => (string) ($unitPrices[$i] ?? '0'),
'item_name_ar' => (string) ($itemNames[$i] ?? ''),
'cost_price' => ($costPrices[$i] ?? '') ?: null,
'tax_amount' => (string) ($taxAmounts[$i] ?? '0'),
];
}
$result = SaleService::createSale([
'warehouse_id' => $warehouseId,
'customer_type' => $customerType,
'member_id' => $memberId ?: null,
'player_id' => $playerId ?: null,
'guest_name' => $guestName ?: null,
'guest_phone' => $guestPhone ?: null,
'discount_amount' => $discount,
'discount_reason' => $discountReason ?: null,
'payment_method' => $paymentMethod,
'notes' => $notes ?: null,
], $lines);
if (!$result['success']) {
return $this->redirect('/sales/create')->withError($result['error']);
}
return $this->redirect('/sales/' . $result['sale_id'])->withSuccess('تم إنشاء المبيعة بنجاح — فاتورة: ' . $result['invoice_number']);
}
public function show(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$sale = $db->selectOne(
"SELECT s.*, w.`name_ar` as warehouse_name
FROM `sales` s
JOIN `warehouses` w ON w.`id` = s.`warehouse_id`
WHERE s.`id` = ?",
[(int) $id]
);
if (!$sale) {
return $this->redirect('/sales')->withError('المبيعة غير موجودة');
}
// Customer name
$customerName = $sale['guest_name'] ?? 'زائر';
if ($sale['customer_type'] === 'member' && $sale['member_id']) {
$member = $db->selectOne("SELECT `full_name_ar` FROM `members` WHERE `id` = ?", [(int) $sale['member_id']]);
$customerName = $member['full_name_ar'] ?? '—';
} elseif ($sale['customer_type'] === 'player' && $sale['player_id']) {
$player = $db->selectOne("SELECT `full_name_ar` FROM `players` WHERE `id` = ?", [(int) $sale['player_id']]);
$customerName = $player['full_name_ar'] ?? '—';
}
$items = SaleItem::getForSale((int) $id);
$refunds = SaleRefund::getForSale((int) $id);
return $this->view('Sales.Views.sales.show', [
'sale' => $sale,
'customerName' => $customerName,
'items' => $items,
'refunds' => $refunds,
]);
}
public function void(Request $request, string $id): Response
{
$reason = trim((string) $request->post('reason', ''));
if ($reason === '') {
return $this->redirect('/sales/' . $id)->withError('يجب إدخال سبب الإلغاء');
}
$result = SaleService::voidSale((int) $id, $reason);
if (!$result['success']) {
return $this->redirect('/sales/' . $id)->withError($result['error']);
}
return $this->redirect('/sales/' . $id)->withSuccess('تم إلغاء المبيعة بنجاح');
}
public function refundForm(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$sale = $db->selectOne("SELECT * FROM `sales` WHERE `id` = ?", [(int) $id]);
if (!$sale) {
return $this->redirect('/sales')->withError('المبيعة غير موجودة');
}
$items = SaleItem::getForSale((int) $id);
return $this->view('Sales.Views.sales.refund', [
'sale' => $sale,
'items' => $items,
]);
}
public function refund(Request $request, string $id): Response
{
$amount = trim((string) $request->post('refund_amount', '0'));
$reason = trim((string) $request->post('reason', ''));
$returnToStock = (bool) $request->post('return_to_stock', 0);
if ($reason === '') {
return $this->redirect('/sales/' . $id . '/refund')->withError('يجب إدخال سبب الاسترجاع');
}
// Parse refund items
$saleItemIds = $request->post('sale_item_ids', []);
$refundQtys = $request->post('refund_quantities', []);
$refundItems = [];
foreach ($saleItemIds as $i => $siId) {
$qty = (string) ($refundQtys[$i] ?? '0');
if (bccomp($qty, '0', 3) > 0) {
$refundItems[] = ['sale_item_id' => (int) $siId, 'quantity' => $qty];
}
}
$result = RefundService::processRefund((int) $id, $amount, $reason, $returnToStock, $refundItems);
if (!$result['success']) {
return $this->redirect('/sales/' . $id . '/refund')->withError($result['error']);
}
return $this->redirect('/sales/' . $id)->withSuccess('تم الاسترجاع بنجاح — رقم: ' . $result['refund_number']);
}
public function printInvoice(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$sale = $db->selectOne("SELECT s.*, w.`name_ar` as warehouse_name FROM `sales` s JOIN `warehouses` w ON w.`id` = s.`warehouse_id` WHERE s.`id` = ?", [(int) $id]);
if (!$sale) {
return $this->redirect('/sales')->withError('المبيعة غير موجودة');
}
$items = SaleItem::getForSale((int) $id);
return $this->view('Sales.Views.pos.invoice', [
'sale' => $sale,
'items' => $items,
]);
}
public function memberHistory(Request $request, string $memberId): Response
{
$db = App::getInstance()->db();
$member = $db->selectOne("SELECT `id`, `full_name_ar` FROM `members` WHERE `id` = ?", [(int) $memberId]);
$sales = $db->select(
"SELECT * FROM `sales` WHERE `member_id` = ? ORDER BY `created_at` DESC LIMIT 50",
[(int) $memberId]
);
return $this->view('Sales.Views.sales.member_history', [
'member' => $member,
'sales' => $sales,
]);
}
public function searchItems(Request $request): Response
{
$q = trim((string) $request->get('q', ''));
if (mb_strlen($q) < 2) {
return $this->json(['results' => []]);
}
$db = App::getInstance()->db();
$search = '%' . $q . '%';
$rows = $db->select(
"SELECT `id`, `sku`, `name_ar`, `sale_price_member`, `sale_price_nonmember`, `sale_price_player`, `cost_price`, `tax_rate`, `unit_of_measure`, `tracking_type`
FROM `inventory_items`
WHERE (`name_ar` LIKE ? OR `sku` LIKE ? OR `barcode` LIKE ?)
AND `is_sellable` = 1 AND `is_active` = 1 AND `deleted_at` IS NULL
ORDER BY `name_ar` ASC LIMIT 20",
[$search, $search, $search]
);
return $this->json(['results' => $rows]);
}
public function searchCustomers(Request $request): Response
{
$q = trim((string) $request->get('q', ''));
$type = trim((string) $request->get('type', 'member'));
if (mb_strlen($q) < 2) {
return $this->json(['results' => []]);
}
$db = App::getInstance()->db();
$search = '%' . $q . '%';
if ($type === 'member') {
$rows = $db->select(
"SELECT `id`, `full_name_ar`, `form_number`, `membership_number`
FROM `members`
WHERE (`full_name_ar` LIKE ? OR `form_number` LIKE ? OR `membership_number` LIKE ?)
AND `is_archived` = 0
ORDER BY `full_name_ar` ASC LIMIT 20",
[$search, $search, $search]
);
} else {
$rows = $db->select(
"SELECT `id`, `full_name_ar`, `player_code`
FROM `players`
WHERE (`full_name_ar` LIKE ? OR `player_code` LIKE ?)
ORDER BY `full_name_ar` ASC LIMIT 20",
[$search, $search]
);
}
return $this->json(['results' => $rows]);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Sales\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\Inventory\Models\Warehouse;
class SaleReportController extends Controller
{
public function daily(Request $request): Response
{
$db = App::getInstance()->db();
$date = trim((string) $request->get('date', date('Y-m-d')));
$warehouseId = (int) $request->get('warehouse_id', 0);
$where = 's.`sale_date` = ? AND s.`status` != ?';
$params = [$date, 'voided'];
if ($warehouseId > 0) {
$where .= ' AND s.`warehouse_id` = ?';
$params[] = $warehouseId;
}
$summary = $db->selectOne(
"SELECT COUNT(*) as sale_count,
COALESCE(SUM(s.`total_amount`), 0) as total_revenue,
COALESCE(SUM(s.`discount_amount`), 0) as total_discounts,
COALESCE(SUM(s.`tax_amount`), 0) as total_tax,
COALESCE(SUM(s.`amount_refunded`), 0) as total_refunds
FROM `sales` s
WHERE {$where}",
$params
);
$sales = $db->select(
"SELECT s.*, w.`name_ar` as warehouse_name
FROM `sales` s
JOIN `warehouses` w ON w.`id` = s.`warehouse_id`
WHERE {$where}
ORDER BY s.`created_at` DESC",
$params
);
$byMethod = $db->select(
"SELECT s.`payment_method`, COUNT(*) as cnt, SUM(s.`total_amount`) as total
FROM `sales` s
WHERE {$where}
GROUP BY s.`payment_method`",
$params
);
return $this->view('Sales.Views.reports.daily', [
'summary' => $summary,
'sales' => $sales,
'byMethod' => $byMethod,
'date' => $date,
'warehouses' => Warehouse::allActive(),
'warehouseId' => $warehouseId,
]);
}
public function monthly(Request $request): Response
{
$db = App::getInstance()->db();
$month = trim((string) $request->get('month', date('Y-m')));
$warehouseId = (int) $request->get('warehouse_id', 0);
$dateFrom = $month . '-01';
$dateTo = date('Y-m-t', strtotime($dateFrom));
$where = 's.`sale_date` BETWEEN ? AND ? AND s.`status` != ?';
$params = [$dateFrom, $dateTo, 'voided'];
if ($warehouseId > 0) {
$where .= ' AND s.`warehouse_id` = ?';
$params[] = $warehouseId;
}
$summary = $db->selectOne(
"SELECT COUNT(*) as sale_count,
COALESCE(SUM(s.`total_amount`), 0) as total_revenue,
COALESCE(SUM(s.`discount_amount`), 0) as total_discounts,
COALESCE(SUM(s.`tax_amount`), 0) as total_tax,
COALESCE(SUM(s.`amount_refunded`), 0) as total_refunds
FROM `sales` s
WHERE {$where}",
$params
);
$dailyBreakdown = $db->select(
"SELECT s.`sale_date`, COUNT(*) as cnt,
SUM(s.`total_amount`) as total,
SUM(s.`amount_refunded`) as refunded
FROM `sales` s
WHERE {$where}
GROUP BY s.`sale_date`
ORDER BY s.`sale_date` ASC",
$params
);
return $this->view('Sales.Views.reports.monthly', [
'summary' => $summary,
'dailyBreakdown' => $dailyBreakdown,
'month' => $month,
'warehouses' => Warehouse::allActive(),
'warehouseId' => $warehouseId,
]);
}
public function byItem(Request $request): Response
{
$db = App::getInstance()->db();
$dateFrom = trim((string) $request->get('date_from', date('Y-m-01')));
$dateTo = trim((string) $request->get('date_to', date('Y-m-d')));
$rows = $db->select(
"SELECT si.`item_name_ar`,
COALESCE(i.`sku`, '—') as sku,
SUM(si.`quantity`) as total_qty,
SUM(si.`line_total`) as total_revenue,
SUM(si.`quantity` * COALESCE(si.`cost_price`, 0)) as total_cost,
SUM(si.`line_total`) - SUM(si.`quantity` * COALESCE(si.`cost_price`, 0)) as profit
FROM `sale_items` si
JOIN `sales` s ON s.`id` = si.`sale_id`
LEFT JOIN `inventory_items` i ON i.`id` = si.`item_id`
WHERE s.`sale_date` BETWEEN ? AND ?
AND s.`status` != 'voided'
GROUP BY si.`item_name_ar`, i.`sku`
ORDER BY total_revenue DESC",
[$dateFrom, $dateTo]
);
return $this->view('Sales.Views.reports.by_item', [
'rows' => $rows,
'dateFrom' => $dateFrom,
'dateTo' => $dateTo,
]);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Sales\Models;
use App\Core\Model;
use App\Core\App;
use App\Core\Pagination;
class Package extends Model
{
protected static string $table = 'packages';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = false;
protected static bool $autoTrackAuthor = true;
protected static array $fillable = [
'code',
'name_ar',
'name_en',
'description_ar',
'package_price_member',
'package_price_nonmember',
'items_total_price',
'active_from',
'active_to',
'is_active',
'notes',
];
public static function allActive(): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT * FROM `packages`
WHERE `is_active` = 1
AND (`active_from` IS NULL OR `active_from` <= CURDATE())
AND (`active_to` IS NULL OR `active_to` >= CURDATE())
ORDER BY `name_ar` ASC"
);
}
public static function search(array $filters, int $perPage = 25, int $page = 1): array
{
$query = static::query();
if (!empty($filters['q'])) {
$search = '%' . $filters['q'] . '%';
$query = $query->whereRaw('(`name_ar` LIKE ? OR `code` LIKE ?)', [$search, $search]);
}
if (isset($filters['is_active']) && $filters['is_active'] !== '') {
$query = $query->where('is_active', '=', (int) $filters['is_active']);
}
$query = $query->orderBy('name_ar', 'ASC');
return $query->paginate($perPage, $page);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Sales\Models;
use App\Core\Model;
use App\Core\App;
class PackageItem extends Model
{
protected static string $table = 'package_items';
protected static string $primaryKey = 'id';
protected static bool $timestamps = false;
protected static bool $softDelete = false;
protected static bool $autoTrackAuthor = false;
protected static array $fillable = [
'package_id',
'item_id',
'quantity',
'sort_order',
];
public static function getForPackage(int $packageId): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT pi.*, i.`name_ar` as item_name, i.`sku`, i.`unit_of_measure`,
i.`sale_price_member`, i.`sale_price_nonmember`
FROM `package_items` pi
JOIN `inventory_items` i ON i.`id` = pi.`item_id`
WHERE pi.`package_id` = ?
ORDER BY pi.`sort_order` ASC, i.`name_ar` ASC",
[$packageId]
);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Sales\Models;
use App\Core\Model;
use App\Core\App;
use App\Core\Pagination;
class Sale extends Model
{
protected static string $table = 'sales';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = false;
protected static bool $autoTrackAuthor = true;
protected static array $fillable = [
'invoice_number',
'warehouse_id',
'customer_type',
'member_id',
'player_id',
'guest_name',
'guest_phone',
'subtotal',
'discount_amount',
'discount_reason',
'tax_amount',
'total_amount',
'amount_paid',
'amount_refunded',
'payment_id',
'payment_method',
'status',
'sold_by',
'sale_date',
'notes',
];
public static function getStatuses(): array
{
return [
'draft' => 'مسودة',
'completed' => 'مكتمل',
'partially_refunded' => 'مسترجع جزئيًا',
'fully_refunded' => 'مسترجع بالكامل',
'voided' => 'ملغي',
];
}
public static function getStatusLabel(string $status): string
{
$statuses = self::getStatuses();
return $statuses[$status] ?? $status;
}
public static function getStatusColor(string $status): string
{
return match ($status) {
'draft' => '#6B7280',
'completed' => '#059669',
'partially_refunded' => '#D97706',
'fully_refunded' => '#DC2626',
'voided' => '#DC2626',
default => '#6B7280',
};
}
public static function getCustomerTypes(): array
{
return [
'member' => 'عضو',
'player' => 'لاعب',
'guest' => 'زائر',
];
}
public static function search(array $filters, int $perPage = 25, int $page = 1): array
{
$db = App::getInstance()->db();
$where = '1=1';
$params = [];
if (!empty($filters['q'])) {
$search = '%' . $filters['q'] . '%';
$where .= ' AND (s.`invoice_number` LIKE ? OR s.`guest_name` LIKE ?)';
$params[] = $search;
$params[] = $search;
}
if (!empty($filters['status'])) {
$where .= ' AND s.`status` = ?';
$params[] = $filters['status'];
}
if (!empty($filters['customer_type'])) {
$where .= ' AND s.`customer_type` = ?';
$params[] = $filters['customer_type'];
}
if (!empty($filters['warehouse_id'])) {
$where .= ' AND s.`warehouse_id` = ?';
$params[] = (int) $filters['warehouse_id'];
}
if (!empty($filters['date_from'])) {
$where .= ' AND s.`sale_date` >= ?';
$params[] = $filters['date_from'];
}
if (!empty($filters['date_to'])) {
$where .= ' AND s.`sale_date` <= ?';
$params[] = $filters['date_to'];
}
$countRow = $db->selectOne(
"SELECT COUNT(*) as cnt FROM `sales` s WHERE {$where}",
$params
);
$total = (int) ($countRow['cnt'] ?? 0);
$offset = ($page - 1) * $perPage;
$rows = $db->select(
"SELECT s.*, w.`name_ar` as warehouse_name
FROM `sales` s
JOIN `warehouses` w ON w.`id` = s.`warehouse_id`
WHERE {$where}
ORDER BY s.`created_at` DESC
LIMIT {$perPage} OFFSET {$offset}",
$params
);
return ['data' => $rows, 'pagination' => Pagination::paginate($total, $perPage, $page)];
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Sales\Models;
use App\Core\Model;
use App\Core\App;
class SaleItem extends Model
{
protected static string $table = 'sale_items';
protected static string $primaryKey = 'id';
protected static bool $timestamps = false;
protected static bool $softDelete = false;
protected static bool $autoTrackAuthor = false;
protected static array $fillable = [
'sale_id',
'item_id',
'package_id',
'item_name_ar',
'quantity',
'unit_price',
'discount_amount',
'tax_amount',
'line_total',
'cost_price',
'batch_id',
'is_refunded',
'refunded_quantity',
];
public static function getForSale(int $saleId): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT si.*, i.`sku`, i.`unit_of_measure`
FROM `sale_items` si
LEFT JOIN `inventory_items` i ON i.`id` = si.`item_id`
WHERE si.`sale_id` = ?
ORDER BY si.`id` ASC",
[$saleId]
);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Sales\Models;
use App\Core\Model;
use App\Core\App;
class SaleRefund extends Model
{
protected static string $table = 'sale_refunds';
protected static string $primaryKey = 'id';
protected static bool $timestamps = false;
protected static bool $softDelete = false;
protected static bool $autoTrackAuthor = false;
protected static array $fillable = [
'sale_id',
'refund_number',
'refund_amount',
'reason',
'return_to_stock',
'warehouse_id',
'payment_id',
'refunded_by',
'refund_date',
'notes',
'created_by',
];
public static function getForSale(int $saleId): array
{
return static::query()
->where('sale_id', '=', $saleId)
->orderBy('created_at', 'DESC')
->get();
}
}
<?php
declare(strict_types=1);
return [
// POS / Sales
['GET', '/sales', 'Sales\Controllers\SaleController@index', ['auth'], 'sales.view'],
['GET', '/sales/create', 'Sales\Controllers\SaleController@create', ['auth'], 'sales.create'],
['POST', '/sales', 'Sales\Controllers\SaleController@store', ['auth', 'csrf'], 'sales.create'],
['GET', '/sales/{id:\d+}', 'Sales\Controllers\SaleController@show', ['auth'], 'sales.view'],
['POST', '/sales/{id:\d+}/void', 'Sales\Controllers\SaleController@void', ['auth', 'csrf'], 'sales.void'],
['GET', '/sales/{id:\d+}/refund', 'Sales\Controllers\SaleController@refundForm', ['auth'], 'sales.refund'],
['POST', '/sales/{id:\d+}/refund', 'Sales\Controllers\SaleController@refund', ['auth', 'csrf'], 'sales.refund'],
['GET', '/sales/{id:\d+}/print', 'Sales\Controllers\SaleController@printInvoice', ['auth'], 'sales.view'],
['GET', '/sales/member/{memberId:\d+}', 'Sales\Controllers\SaleController@memberHistory',['auth'], 'sales.view'],
['GET', '/sales/search-items', 'Sales\Controllers\SaleController@searchItems', ['auth'], 'sales.create'],
['GET', '/sales/search-customers', 'Sales\Controllers\SaleController@searchCustomers',['auth'], 'sales.create'],
// Packages
['GET', '/sales/packages', 'Sales\Controllers\PackageController@index', ['auth'], 'package.view'],
['GET', '/sales/packages/create', 'Sales\Controllers\PackageController@create', ['auth'], 'package.manage'],
['POST', '/sales/packages', 'Sales\Controllers\PackageController@store', ['auth', 'csrf'], 'package.manage'],
['GET', '/sales/packages/{id:\d+}', 'Sales\Controllers\PackageController@show', ['auth'], 'package.view'],
['GET', '/sales/packages/{id:\d+}/edit', 'Sales\Controllers\PackageController@edit', ['auth'], 'package.manage'],
['POST', '/sales/packages/{id:\d+}', 'Sales\Controllers\PackageController@update', ['auth', 'csrf'], 'package.manage'],
// Sales Reports
['GET', '/sales/reports/daily', 'Sales\Controllers\SaleReportController@daily', ['auth'], 'report.sales'],
['GET', '/sales/reports/monthly', 'Sales\Controllers\SaleReportController@monthly', ['auth'], 'report.sales'],
['GET', '/sales/reports/by-item', 'Sales\Controllers\SaleReportController@byItem', ['auth'], 'report.sales'],
];
<?php
declare(strict_types=1);
namespace App\Modules\Sales\Services;
use App\Core\App;
use App\Core\Logger;
/**
* Guest payment wrapper — bypasses the member_id validation in PaymentService.
* Creates payment + receipt directly for guest sales.
*/
final class InventoryPaymentService
{
/**
* Process a guest payment (no member_id required).
*/
public static function processGuestPayment(array $data): array
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$amount = $data['amount'] ?? '0.00';
$paymentMethod = $data['payment_method'] ?? 'cash';
$guestName = $data['guest_name'] ?? 'زائر';
$description = $data['description'] ?? 'مبيعات مخزون (زائر)';
if (bccomp((string) $amount, '0.01', 2) < 0) {
return ['success' => false, 'error' => 'المبلغ يجب أن يكون أكبر من صفر'];
}
$db->beginTransaction();
try {
// Generate receipt number (same pattern as PaymentService)
$year = date('Y');
$prefix = 'REC-' . $year . '-';
$last = $db->selectOne(
"SELECT receipt_number FROM receipts WHERE receipt_number LIKE ? ORDER BY id DESC LIMIT 1",
[$prefix . '%']
);
$seq = $last ? ((int) substr($last['receipt_number'], -6)) + 1 : 1;
$receiptNumber = $prefix . str_pad((string) $seq, 6, '0', STR_PAD_LEFT);
// Create payment record (member_id = NULL for guest)
$paymentId = $db->insert('payments', [
'member_id' => null,
'payment_type' => 'inventory_sale',
'amount' => $amount,
'currency' => 'EGP',
'payment_method' => $paymentMethod,
'related_entity_type' => $data['related_entity_type'] ?? null,
'related_entity_id' => $data['related_entity_id'] ?? null,
'notes' => $description . ' — ' . $guestName,
'payment_date' => date('Y-m-d'),
'received_by_employee_id' => $employee ? (int) $employee->id : null,
'is_voided' => 0,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
'created_by' => $employee ? (int) $employee->id : null,
]);
// Create receipt (member_id = NULL — requires Phase_25_021 migration)
$receiptId = $db->insert('receipts', [
'receipt_number' => $receiptNumber,
'member_id' => null,
'payment_id' => $paymentId,
'receipt_type' => 'payment',
'amount' => $amount,
'amount_in_words_ar' => function_exists('number_to_arabic_words') ? number_to_arabic_words((float) $amount) : '',
'description_ar' => $description,
'issued_by_employee_id' => $employee ? (int) $employee->id : null,
'issued_at' => date('Y-m-d H:i:s'),
'is_voided' => 0,
'print_count' => 0,
'created_at' => date('Y-m-d H:i:s'),
]);
$db->update('payments', ['receipt_id' => $receiptId], '`id` = ?', [$paymentId]);
$db->commit();
Logger::info("Guest payment #{$paymentId} processed — receipt: {$receiptNumber}");
return [
'success' => true,
'payment_id' => $paymentId,
'receipt_id' => $receiptId,
'receipt_number' => $receiptNumber,
'amount' => $amount,
];
} catch (\Throwable $e) {
$db->rollBack();
Logger::error("Guest payment failed: " . $e->getMessage());
return ['success' => false, 'error' => 'فشل تسجيل الدفع: ' . $e->getMessage()];
}
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Sales\Services;
use App\Core\App;
final class InvoiceNumberGenerator
{
/**
* Generate invoice number: INV-YYYY-NNNNNN
*/
public static function nextInvoiceNumber(): string
{
return self::generate('INV', 'sales', 'invoice_number');
}
/**
* Generate refund number: REF-YYYY-NNNNNN
*/
public static function nextRefundNumber(): string
{
return self::generate('REF', 'sale_refunds', 'refund_number');
}
private static function generate(string $prefix, string $table, string $column): string
{
$db = App::getInstance()->db();
$year = date('Y');
$pattern = $prefix . '-' . $year . '-';
$last = $db->selectOne(
"SELECT `{$column}` FROM `{$table}` WHERE `{$column}` LIKE ? ORDER BY `id` DESC LIMIT 1",
[$pattern . '%']
);
if ($last) {
$parts = explode('-', $last[$column]);
$seq = (int) end($parts) + 1;
} else {
$seq = 1;
}
return $pattern . str_pad((string) $seq, 6, '0', STR_PAD_LEFT);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Sales\Services;
use App\Core\App;
use App\Core\Logger;
final class PackageService
{
/**
* Create a package with its component items.
*
* @param array $header Keys: code, name_ar, name_en, description_ar, package_price_member,
* package_price_nonmember, active_from, active_to, notes
* @param array $items Each: ['item_id' => int, 'quantity' => string]
* @return int package ID
*/
public static function createPackage(array $header, array $items): int
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
if (empty($items)) {
throw new \RuntimeException('يجب إضافة صنف واحد على الأقل للباقة');
}
// Calculate items total price
$itemsTotalPrice = '0.00';
foreach ($items as $item) {
$itemRow = $db->selectOne(
"SELECT `sale_price_member` FROM `inventory_items` WHERE `id` = ?",
[(int) $item['item_id']]
);
if ($itemRow) {
$linePrice = bcmul((string) ($itemRow['sale_price_member'] ?? '0'), (string) $item['quantity'], 2);
$itemsTotalPrice = bcadd($itemsTotalPrice, $linePrice, 2);
}
}
$db->beginTransaction();
try {
$packageId = $db->insert('packages', [
'code' => $header['code'],
'name_ar' => $header['name_ar'],
'name_en' => $header['name_en'] ?? null,
'description_ar' => $header['description_ar'] ?? null,
'package_price_member' => $header['package_price_member'],
'package_price_nonmember' => $header['package_price_nonmember'],
'items_total_price' => $itemsTotalPrice,
'active_from' => $header['active_from'] ?? null,
'active_to' => $header['active_to'] ?? null,
'is_active' => 1,
'notes' => $header['notes'] ?? null,
'created_by' => $employee ? (int) $employee->id : null,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
foreach ($items as $i => $item) {
$db->insert('package_items', [
'package_id' => $packageId,
'item_id' => (int) $item['item_id'],
'quantity' => $item['quantity'],
'sort_order' => $i,
]);
}
$db->commit();
} catch (\Throwable $e) {
$db->rollBack();
throw $e;
}
Logger::info("Package #{$packageId} ({$header['code']}) created with " . count($items) . " items");
return $packageId;
}
/**
* Update a package and its items.
*/
public static function updatePackage(int $packageId, array $header, array $items): void
{
$db = App::getInstance()->db();
// Recalculate items total price
$itemsTotalPrice = '0.00';
foreach ($items as $item) {
$itemRow = $db->selectOne(
"SELECT `sale_price_member` FROM `inventory_items` WHERE `id` = ?",
[(int) $item['item_id']]
);
if ($itemRow) {
$linePrice = bcmul((string) ($itemRow['sale_price_member'] ?? '0'), (string) $item['quantity'], 2);
$itemsTotalPrice = bcadd($itemsTotalPrice, $linePrice, 2);
}
}
$db->beginTransaction();
try {
$db->update('packages', [
'code' => $header['code'],
'name_ar' => $header['name_ar'],
'name_en' => $header['name_en'] ?? null,
'description_ar' => $header['description_ar'] ?? null,
'package_price_member' => $header['package_price_member'],
'package_price_nonmember' => $header['package_price_nonmember'],
'items_total_price' => $itemsTotalPrice,
'active_from' => $header['active_from'] ?? null,
'active_to' => $header['active_to'] ?? null,
'notes' => $header['notes'] ?? null,
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [$packageId]);
// Replace items
$db->statement("DELETE FROM `package_items` WHERE `package_id` = ?", [$packageId]);
foreach ($items as $i => $item) {
$db->insert('package_items', [
'package_id' => $packageId,
'item_id' => (int) $item['item_id'],
'quantity' => $item['quantity'],
'sort_order' => $i,
]);
}
$db->commit();
} catch (\Throwable $e) {
$db->rollBack();
throw $e;
}
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Sales\Services;
use App\Core\App;
use App\Core\EventBus;
use App\Core\Logger;
use App\Modules\Inventory\Services\StockService;
final class RefundService
{
/**
* Process a refund for a sale.
*
* @param int $saleId
* @param string $amount Refund amount
* @param string $reason Refund reason
* @param bool $returnToStock Whether to return items to stock
* @param array $refundItems Items to refund: [['sale_item_id' => int, 'quantity' => string], ...]
*/
public static function processRefund(int $saleId, string $amount, string $reason, bool $returnToStock, array $refundItems = []): array
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$sale = $db->selectOne("SELECT * FROM `sales` WHERE `id` = ?", [$saleId]);
if (!$sale) {
return ['success' => false, 'error' => 'المبيعة غير موجودة'];
}
if (in_array($sale['status'], ['voided', 'fully_refunded'], true)) {
return ['success' => false, 'error' => 'لا يمكن استرجاع هذه المبيعة'];
}
$maxRefund = bcsub((string) $sale['total_amount'], (string) $sale['amount_refunded'], 2);
if (bccomp($amount, $maxRefund, 2) > 0) {
return ['success' => false, 'error' => 'مبلغ الاسترجاع أكبر من المتبقي — الحد الأقصى: ' . $maxRefund];
}
$refundNumber = InvoiceNumberGenerator::nextRefundNumber();
$warehouseId = (int) $sale['warehouse_id'];
$db->beginTransaction();
try {
$refundId = $db->insert('sale_refunds', [
'sale_id' => $saleId,
'refund_number' => $refundNumber,
'refund_amount' => $amount,
'reason' => $reason,
'return_to_stock' => $returnToStock ? 1 : 0,
'warehouse_id' => $returnToStock ? $warehouseId : null,
'refunded_by' => $employee ? (int) $employee->id : null,
'refund_date' => date('Y-m-d'),
'created_by' => $employee ? (int) $employee->id : null,
'created_at' => date('Y-m-d H:i:s'),
]);
// Return stock if requested
if ($returnToStock && !empty($refundItems)) {
foreach ($refundItems as $ri) {
$saleItem = $db->selectOne("SELECT * FROM `sale_items` WHERE `id` = ?", [(int) $ri['sale_item_id']]);
if (!$saleItem || !$saleItem['item_id']) continue;
$refQty = (string) $ri['quantity'];
StockService::moveStock([
'item_id' => (int) $saleItem['item_id'],
'warehouse_id' => $warehouseId,
'movement_type' => 'return_in',
'direction' => 'in',
'quantity' => $refQty,
'reference_type' => 'sale_refunds',
'reference_id' => $refundId,
'notes' => 'مرتجع مبيعات — ' . $refundNumber,
]);
// Update sale item refunded tracking
$newRefQty = bcadd((string) $saleItem['refunded_quantity'], $refQty, 3);
$isFullyRefunded = bccomp($newRefQty, (string) $saleItem['quantity'], 3) >= 0 ? 1 : 0;
$db->update('sale_items', [
'refunded_quantity' => $newRefQty,
'is_refunded' => $isFullyRefunded,
], '`id` = ?', [(int) $saleItem['id']]);
}
}
// Update sale totals
$newRefundedTotal = bcadd((string) $sale['amount_refunded'], $amount, 2);
$isFullyRefunded = bccomp($newRefundedTotal, (string) $sale['total_amount'], 2) >= 0;
$db->update('sales', [
'amount_refunded' => $newRefundedTotal,
'status' => $isFullyRefunded ? 'fully_refunded' : 'partially_refunded',
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [$saleId]);
$db->commit();
} catch (\Throwable $e) {
$db->rollBack();
return ['success' => false, 'error' => 'فشل الاسترجاع: ' . $e->getMessage()];
}
EventBus::dispatch('sale.refunded', [
'sale_id' => $saleId,
'refund_id' => $refundId,
'refund_number' => $refundNumber,
'amount' => $amount,
]);
Logger::info("Refund #{$refundId} ({$refundNumber}) for sale #{$saleId} — amount: {$amount}");
return [
'success' => true,
'refund_id' => $refundId,
'refund_number' => $refundNumber,
];
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Sales\Services;
use App\Core\App;
use App\Core\EventBus;
use App\Core\Logger;
use App\Modules\Inventory\Services\StockService;
use App\Modules\Inventory\Services\BatchService;
use App\Modules\Payments\Services\PaymentService;
final class SaleService
{
/**
* Create a complete sale.
*
* @param array $header Keys: warehouse_id, customer_type, member_id, player_id, guest_name, guest_phone,
* discount_amount, discount_reason, payment_method, notes
* @param array $lines Each: ['item_id' => ?int, 'package_id' => ?int, 'quantity' => string, 'unit_price' => string,
* 'item_name_ar' => string, 'cost_price' => ?string, 'tax_amount' => ?string]
* @return array ['success' => bool, 'sale_id' => int, 'invoice_number' => string, ...]
*/
public static function createSale(array $header, array $lines): array
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
if (empty($lines)) {
return ['success' => false, 'error' => 'يجب إضافة صنف واحد على الأقل'];
}
$warehouseId = (int) ($header['warehouse_id'] ?? 0);
$customerType = (string) ($header['customer_type'] ?? 'guest');
$memberId = ($header['member_id'] ?? null) ? (int) $header['member_id'] : null;
$playerId = ($header['player_id'] ?? null) ? (int) $header['player_id'] : null;
if ($warehouseId <= 0) {
return ['success' => false, 'error' => 'يجب تحديد المخزن'];
}
$invoiceNumber = InvoiceNumberGenerator::nextInvoiceNumber();
$discountAmount = (string) ($header['discount_amount'] ?? '0.00');
$paymentMethod = $header['payment_method'] ?? 'cash';
// Calculate totals
$subtotal = '0.00';
$taxTotal = '0.00';
foreach ($lines as $line) {
$lineTotal = bcmul((string) $line['quantity'], (string) $line['unit_price'], 2);
$subtotal = bcadd($subtotal, $lineTotal, 2);
$taxTotal = bcadd($taxTotal, (string) ($line['tax_amount'] ?? '0.00'), 2);
}
$totalAmount = bcsub(bcadd($subtotal, $taxTotal, 2), $discountAmount, 2);
if (bccomp($totalAmount, '0.00', 2) <= 0) {
return ['success' => false, 'error' => 'إجمالي المبيعة يجب أن يكون أكبر من صفر'];
}
$db->beginTransaction();
try {
$saleId = $db->insert('sales', [
'invoice_number' => $invoiceNumber,
'warehouse_id' => $warehouseId,
'customer_type' => $customerType,
'member_id' => $memberId,
'player_id' => $playerId,
'guest_name' => $header['guest_name'] ?? null,
'guest_phone' => $header['guest_phone'] ?? null,
'subtotal' => $subtotal,
'discount_amount' => $discountAmount,
'discount_reason' => $header['discount_reason'] ?? null,
'tax_amount' => $taxTotal,
'total_amount' => $totalAmount,
'amount_paid' => $totalAmount,
'amount_refunded' => '0.00',
'payment_method' => $paymentMethod,
'status' => 'completed',
'sold_by' => $employee ? (int) $employee->id : null,
'sale_date' => date('Y-m-d'),
'notes' => $header['notes'] ?? null,
'created_by' => $employee ? (int) $employee->id : null,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
// Process each line item
foreach ($lines as $line) {
$lineQty = (string) $line['quantity'];
$linePrice = (string) $line['unit_price'];
$lineTotal = bcmul($lineQty, $linePrice, 2);
$lineTax = (string) ($line['tax_amount'] ?? '0.00');
$batchId = null;
$itemId = isset($line['item_id']) ? (int) $line['item_id'] : null;
// Deduct stock for items (not packages — packages are expanded below)
if ($itemId) {
$item = $db->selectOne("SELECT `tracking_type` FROM `inventory_items` WHERE `id` = ?", [$itemId]);
// FEFO for expiry items
if ($item && $item['tracking_type'] === 'expiry') {
$allocations = BatchService::allocateBatch($itemId, $warehouseId, $lineQty);
foreach ($allocations as $alloc) {
StockService::moveStock([
'item_id' => $itemId,
'warehouse_id' => $warehouseId,
'movement_type' => 'sale_out',
'direction' => 'out',
'quantity' => $alloc['quantity'],
'batch_id' => $alloc['batch_id'],
'reference_type' => 'sales',
'reference_id' => $saleId,
]);
$batchId = $alloc['batch_id']; // last batch for record
}
} else {
StockService::moveStock([
'item_id' => $itemId,
'warehouse_id' => $warehouseId,
'movement_type' => 'sale_out',
'direction' => 'out',
'quantity' => $lineQty,
'reference_type' => 'sales',
'reference_id' => $saleId,
]);
}
}
// If it's a package, expand and deduct component items
$packageId = isset($line['package_id']) ? (int) $line['package_id'] : null;
if ($packageId) {
$components = $db->select(
"SELECT pi.`item_id`, pi.`quantity`, i.`tracking_type`
FROM `package_items` pi
JOIN `inventory_items` i ON i.`id` = pi.`item_id`
WHERE pi.`package_id` = ?",
[$packageId]
);
foreach ($components as $comp) {
$compQty = bcmul((string) $comp['quantity'], $lineQty, 3);
if ($comp['tracking_type'] === 'expiry') {
$allocs = BatchService::allocateBatch((int) $comp['item_id'], $warehouseId, $compQty);
foreach ($allocs as $a) {
StockService::moveStock([
'item_id' => (int) $comp['item_id'],
'warehouse_id' => $warehouseId,
'movement_type' => 'sale_out',
'direction' => 'out',
'quantity' => $a['quantity'],
'batch_id' => $a['batch_id'],
'reference_type' => 'sales',
'reference_id' => $saleId,
]);
}
} else {
StockService::moveStock([
'item_id' => (int) $comp['item_id'],
'warehouse_id' => $warehouseId,
'movement_type' => 'sale_out',
'direction' => 'out',
'quantity' => $compQty,
'reference_type' => 'sales',
'reference_id' => $saleId,
]);
}
}
}
$db->insert('sale_items', [
'sale_id' => $saleId,
'item_id' => $itemId,
'package_id' => $packageId,
'item_name_ar' => $line['item_name_ar'],
'quantity' => $lineQty,
'unit_price' => $linePrice,
'discount_amount' => '0.00',
'tax_amount' => $lineTax,
'line_total' => bcadd($lineTotal, $lineTax, 2),
'cost_price' => $line['cost_price'] ?? null,
'batch_id' => $batchId,
]);
}
// Process payment
$paymentResult = self::processPayment($saleId, $customerType, $memberId, $playerId, $totalAmount, $paymentMethod, $invoiceNumber, $header);
if ($paymentResult && isset($paymentResult['payment_id'])) {
$db->update('sales', [
'payment_id' => (int) $paymentResult['payment_id'],
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [$saleId]);
}
$db->commit();
} catch (\Throwable $e) {
$db->rollBack();
Logger::error("Sale failed: " . $e->getMessage());
return ['success' => false, 'error' => 'فشل إنشاء المبيعة: ' . $e->getMessage()];
}
EventBus::dispatch('sale.completed', [
'sale_id' => $saleId,
'invoice_number' => $invoiceNumber,
'customer_type' => $customerType,
'total_amount' => $totalAmount,
]);
Logger::info("Sale #{$saleId} ({$invoiceNumber}) completed — total: {$totalAmount}");
return [
'success' => true,
'sale_id' => $saleId,
'invoice_number' => $invoiceNumber,
'total_amount' => $totalAmount,
];
}
/**
* Void a sale — reverses stock but does not refund payment.
*/
public static function voidSale(int $saleId, string $reason): array
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$sale = $db->selectOne("SELECT * FROM `sales` WHERE `id` = ?", [$saleId]);
if (!$sale) {
return ['success' => false, 'error' => 'المبيعة غير موجودة'];
}
if ($sale['status'] === 'voided') {
return ['success' => false, 'error' => 'المبيعة ملغاة بالفعل'];
}
$db->beginTransaction();
try {
// Return stock for each line item
$items = $db->select("SELECT * FROM `sale_items` WHERE `sale_id` = ?", [$saleId]);
foreach ($items as $item) {
if ($item['item_id']) {
StockService::moveStock([
'item_id' => (int) $item['item_id'],
'warehouse_id' => (int) $sale['warehouse_id'],
'movement_type' => 'return_in',
'direction' => 'in',
'quantity' => (string) $item['quantity'],
'reference_type' => 'sales',
'reference_id' => $saleId,
'notes' => 'إلغاء مبيعة — ' . $reason,
]);
}
}
$db->update('sales', [
'status' => 'voided',
'notes' => ($sale['notes'] ? $sale['notes'] . "\n" : '') . 'إلغاء: ' . $reason,
'updated_at' => date('Y-m-d H:i:s'),
'updated_by' => $employee ? (int) $employee->id : null,
], '`id` = ?', [$saleId]);
// Void the payment if exists
if ($sale['payment_id']) {
PaymentService::voidPayment((int) $sale['payment_id'], 'إلغاء مبيعة ' . $sale['invoice_number']);
}
$db->commit();
} catch (\Throwable $e) {
$db->rollBack();
return ['success' => false, 'error' => 'فشل الإلغاء: ' . $e->getMessage()];
}
EventBus::dispatch('sale.voided', [
'sale_id' => $saleId,
'reason' => $reason,
]);
return ['success' => true];
}
private static function processPayment(int $saleId, string $customerType, ?int $memberId, ?int $playerId, string $amount, string $method, string $invoiceNumber, array $header): ?array
{
if ($customerType === 'member' && $memberId) {
$result = PaymentService::processPayment([
'member_id' => $memberId,
'amount' => $amount,
'payment_type' => 'inventory_sale',
'payment_method' => $method,
'related_entity_type' => 'sales',
'related_entity_id' => $saleId,
'description' => 'مبيعات مخزون — فاتورة ' . $invoiceNumber,
]);
return $result['success'] ? $result : null;
}
if ($customerType === 'player' && $playerId) {
$result = PaymentService::processPayment([
'member_id' => 0,
'player_id' => $playerId,
'amount' => $amount,
'payment_type' => 'inventory_sale',
'payment_method' => $method,
'related_entity_type' => 'sales',
'related_entity_id' => $saleId,
'description' => 'مبيعات مخزون — فاتورة ' . $invoiceNumber,
]);
return $result['success'] ? $result : null;
}
// Guest — use InventoryPaymentService
$result = InventoryPaymentService::processGuestPayment([
'amount' => $amount,
'payment_method' => $method,
'guest_name' => $header['guest_name'] ?? 'زائر',
'related_entity_type' => 'sales',
'related_entity_id' => $saleId,
'description' => 'مبيعات مخزون (زائر) — فاتورة ' . $invoiceNumber,
]);
return $result['success'] ? $result : null;
}
}
<?php
$isEdit = $package !== null;
$__template->layout('Layout.main');
?>
<?php $__template->section('title'); ?><?= $isEdit ? 'تعديل الباقة' : 'إنشاء باقة جديدة' ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<?php if ($isEdit): ?>
<a href="/sales/packages/<?= (int) $package->id ?>" class="btn btn-outline"><i data-lucide="eye" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> عرض الباقة</a>
<?php endif; ?>
<a href="/sales/packages" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة للقائمة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<form method="POST" action="<?= $isEdit ? '/sales/packages/' . (int) $package->id : '/sales/packages' ?>" id="packageForm">
<?= csrf_field() ?>
<!-- Header Fields -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="gift" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">بيانات الباقة</h3>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:20px;">
<div class="form-group">
<label class="form-label">كود الباقة <span style="color:#DC2626;">*</span></label>
<input type="text" name="code" value="<?= e(old('code', $package->code ?? '')) ?>" class="form-input" required placeholder="مثال: PKG-001" style="direction:ltr;text-align:left;text-transform:uppercase;">
</div>
<div class="form-group">
<label class="form-label">الاسم بالعربي <span style="color:#DC2626;">*</span></label>
<input type="text" name="name_ar" value="<?= e(old('name_ar', $package->name_ar ?? '')) ?>" class="form-input" required placeholder="اسم الباقة بالعربي">
</div>
<div class="form-group">
<label class="form-label">الاسم بالإنجليزي</label>
<input type="text" name="name_en" value="<?= e(old('name_en', $package->name_en ?? '')) ?>" class="form-input" placeholder="Package name in English" style="direction:ltr;text-align:left;">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:20px;margin-top:15px;">
<div class="form-group">
<label class="form-label">سعر الباقة (عضو) <span style="color:#DC2626;">*</span></label>
<input type="number" name="package_price_member" value="<?= e(old('package_price_member', $package->package_price_member ?? '0.00')) ?>" class="form-input" step="0.01" min="0" required style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">سعر الباقة (غير عضو) <span style="color:#DC2626;">*</span></label>
<input type="number" name="package_price_nonmember" value="<?= e(old('package_price_nonmember', $package->package_price_nonmember ?? '0.00')) ?>" class="form-input" step="0.01" min="0" required style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">فعّال من</label>
<input type="date" name="active_from" value="<?= e(old('active_from', $package->active_from ?? '')) ?>" class="form-input">
</div>
<div class="form-group">
<label class="form-label">فعّال حتى</label>
<input type="date" name="active_to" value="<?= e(old('active_to', $package->active_to ?? '')) ?>" class="form-input">
</div>
</div>
<div class="form-group" style="margin-top:15px;">
<label class="form-label">ملاحظات</label>
<textarea name="notes" class="form-input" rows="2" placeholder="ملاحظات إضافية..."><?= e(old('notes', $package->notes ?? '')) ?></textarea>
</div>
</div>
</div>
<!-- Package Items (Dynamic) -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;justify-content:space-between;">
<div style="display:flex;align-items:center;gap:8px;">
<i data-lucide="package" style="width:18px;height:18px;color:#059669;"></i>
<h3 style="margin:0;color:#059669;font-size:15px;">أصناف الباقة</h3>
</div>
<button type="button" onclick="addItemRow()" class="btn btn-sm btn-primary" style="font-size:12px;padding:6px 14px;">
<i data-lucide="plus" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> إضافة صنف
</button>
</div>
<div class="table-responsive">
<table class="data-table" id="itemsTable">
<thead>
<tr>
<th>الصنف <span style="color:#DC2626;">*</span></th>
<th style="width:120px;">الكمية <span style="color:#DC2626;">*</span></th>
<th style="width:130px;">سعر الوحدة (عضو)</th>
<th style="width:130px;">الإجمالي</th>
<th style="width:50px;"></th>
</tr>
</thead>
<tbody id="itemsBody">
<!-- JS will populate -->
</tbody>
<tfoot>
<tr style="background:#F9FAFB;">
<td colspan="3" style="font-weight:700;padding:12px 15px;">إجمالي قيمة الأصناف</td>
<td id="itemsTotalDisplay" style="font-weight:800;font-size:16px;color:#059669;direction:ltr;text-align:left;padding:12px 15px;">0.00</td>
<td></td>
</tr>
</tfoot>
</table>
</div>
</div>
<!-- Submit Buttons -->
<div style="display:flex;gap:10px;justify-content:flex-start;">
<button type="submit" class="btn btn-primary" style="padding:12px 30px;font-size:15px;">
<i data-lucide="check" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i>
<?= $isEdit ? 'حفظ التعديلات' : 'إنشاء الباقة' ?>
</button>
<a href="/sales/packages" class="btn btn-outline" style="padding:12px 30px;font-size:15px;">إلغاء</a>
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
initItems();
});
var availableItems = <?= json_encode(array_map(function($i) {
return [
'id' => (int) $i['id'],
'name' => $i['name_ar'] ?? '',
'sku' => $i['sku'] ?? '',
'price' => (float) ($i['sale_price_member'] ?? 0),
];
}, $items), JSON_UNESCAPED_UNICODE) ?>;
var rowIndex = 0;
function initItems() {
<?php if ($isEdit && !empty($packageItems)): ?>
var existingItems = <?= json_encode(array_map(function($pi) {
return [
'item_id' => (int) ($pi['item_id'] ?? 0),
'quantity' => (float) ($pi['quantity'] ?? 1),
];
}, $packageItems), JSON_UNESCAPED_UNICODE) ?>;
existingItems.forEach(function(ei) {
addItemRow(ei.item_id, ei.quantity);
});
<?php else: ?>
addItemRow();
<?php endif; ?>
}
function addItemRow(selectedItemId, selectedQty) {
selectedItemId = selectedItemId || '';
selectedQty = selectedQty || 1;
var tbody = document.getElementById('itemsBody');
var idx = rowIndex++;
var options = '<option value="">-- اختر صنف --</option>';
availableItems.forEach(function(item) {
var sel = item.id == selectedItemId ? ' selected' : '';
options += '<option value="' + item.id + '" data-price="' + item.price + '"' + sel + '>' + escapeHtml(item.name) + ' (' + escapeHtml(item.sku) + ')</option>';
});
var tr = document.createElement('tr');
tr.id = 'itemRow_' + idx;
tr.innerHTML =
'<td><select name="item_ids[]" class="form-select item-select" data-idx="' + idx + '" onchange="onItemChange(' + idx + ')" required>' + options + '</select></td>' +
'<td><input type="number" name="quantities[]" class="form-input item-qty" data-idx="' + idx + '" value="' + selectedQty + '" min="1" step="1" onchange="calcRowTotal(' + idx + ')" style="text-align:center;"></td>' +
'<td><span class="item-unit-price" data-idx="' + idx + '" style="font-weight:600;direction:ltr;display:inline-block;">0.00</span></td>' +
'<td><span class="item-line-total" data-idx="' + idx + '" style="font-weight:700;direction:ltr;display:inline-block;">0.00</span></td>' +
'<td><button type="button" onclick="removeItemRow(' + idx + ')" style="background:none;border:none;color:#DC2626;cursor:pointer;padding:4px;"><i data-lucide="trash-2" style="width:14px;height:14px;"></i></button></td>';
tbody.appendChild(tr);
if (typeof lucide !== 'undefined') lucide.createIcons();
onItemChange(idx);
}
function removeItemRow(idx) {
var row = document.getElementById('itemRow_' + idx);
if (row) row.remove();
calcGrandTotal();
}
function onItemChange(idx) {
var sel = document.querySelector('.item-select[data-idx="' + idx + '"]');
var opt = sel.options[sel.selectedIndex];
var price = parseFloat(opt.getAttribute('data-price')) || 0;
document.querySelector('.item-unit-price[data-idx="' + idx + '"]').textContent = price.toFixed(2);
calcRowTotal(idx);
}
function calcRowTotal(idx) {
var sel = document.querySelector('.item-select[data-idx="' + idx + '"]');
var opt = sel.options[sel.selectedIndex];
var price = parseFloat(opt.getAttribute('data-price')) || 0;
var qty = parseInt(document.querySelector('.item-qty[data-idx="' + idx + '"]').value) || 0;
var total = price * qty;
document.querySelector('.item-line-total[data-idx="' + idx + '"]').textContent = total.toFixed(2);
calcGrandTotal();
}
function calcGrandTotal() {
var sum = 0;
document.querySelectorAll('.item-line-total').forEach(function(el) {
sum += parseFloat(el.textContent) || 0;
});
document.getElementById('itemsTotalDisplay').textContent = sum.toFixed(2);
}
function escapeHtml(str) {
var div = document.createElement('div');
div.appendChild(document.createTextNode(str || ''));
return div.innerHTML;
}
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>الباقات<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/sales/packages/create" class="btn btn-primary"><i data-lucide="plus" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> باقة جديدة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Search & Filters -->
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/sales/packages" style="display:flex;gap:10px;flex-wrap:wrap;align-items:end;">
<div style="flex:1;min-width:200px;">
<label class="form-label" style="font-size:12px;">بحث</label>
<input type="text" name="q" value="<?= e($filters['q'] ?? '') ?>" placeholder="اسم الباقة أو الكود..." class="form-input">
</div>
<div style="min-width:140px;">
<label class="form-label" style="font-size:12px;">الحالة</label>
<select name="is_active" class="form-select">
<option value="">الكل</option>
<option value="1" <?= ($filters['is_active'] ?? '') === '1' ? 'selected' : '' ?>>فعّالة</option>
<option value="0" <?= ($filters['is_active'] ?? '') === '0' ? 'selected' : '' ?>>معطّلة</option>
</select>
</div>
<button type="submit" class="btn btn-outline"><i data-lucide="search" style="width:16px;height:16px;vertical-align:middle;"></i> بحث</button>
<a href="/sales/packages" class="btn btn-sm btn-outline" style="color:#6B7280;">مسح</a>
</form>
</div>
<!-- Packages Table -->
<?php if (!empty($packages)): ?>
<div class="card">
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>الكود</th>
<th>الاسم</th>
<th>سعر العضو</th>
<th>سعر غير العضو</th>
<th>التوفير</th>
<th>الحالة</th>
<th>إجراءات</th>
</tr>
</thead>
<tbody>
<?php foreach ($packages as $pkg): ?>
<?php
$memberPrice = (float) ($pkg['package_price_member'] ?? 0);
$itemsTotal = (float) ($pkg['items_total_price'] ?? 0);
$saving = $itemsTotal > 0 ? $itemsTotal - $memberPrice : 0;
$savingPct = $itemsTotal > 0 ? round(($saving / $itemsTotal) * 100) : 0;
?>
<tr>
<td style="font-family:monospace;font-weight:600;direction:ltr;text-align:left;"><?= e($pkg['code'] ?? '') ?></td>
<td>
<a href="/sales/packages/<?= (int) $pkg['id'] ?>" style="color:#0D7377;font-weight:600;text-decoration:none;">
<?= e($pkg['name_ar'] ?? '') ?>
</a>
<?php if (!empty($pkg['name_en'])): ?>
<div style="font-size:11px;color:#9CA3AF;"><?= e($pkg['name_en']) ?></div>
<?php endif; ?>
</td>
<td style="font-weight:700;direction:ltr;text-align:left;color:#059669;"><?= money($pkg['package_price_member'] ?? 0) ?></td>
<td style="font-weight:600;direction:ltr;text-align:left;"><?= money($pkg['package_price_nonmember'] ?? 0) ?></td>
<td>
<?php if ($saving > 0): ?>
<div style="font-weight:700;color:#059669;direction:ltr;text-align:left;"><?= money($saving) ?></div>
<div style="font-size:11px;color:#059669;">(<?= $savingPct ?>%)</div>
<?php else: ?>
<span style="color:#9CA3AF;"></span>
<?php endif; ?>
</td>
<td>
<?php if ((int) ($pkg['is_active'] ?? 0)): ?>
<span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;background:#ECFDF5;color:#059669;">فعّالة</span>
<?php else: ?>
<span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;background:#FEE2E2;color:#DC2626;">معطّلة</span>
<?php endif; ?>
</td>
<td>
<div style="display:flex;gap:6px;">
<a href="/sales/packages/<?= (int) $pkg['id'] ?>" class="btn btn-sm btn-outline" style="font-size:12px;padding:4px 10px;">
<i data-lucide="eye" style="width:13px;height:13px;vertical-align:middle;"></i> عرض
</a>
<a href="/sales/packages/<?= (int) $pkg['id'] ?>/edit" class="btn btn-sm btn-outline" style="font-size:12px;padding:4px 10px;">
<i data-lucide="edit-3" style="width:13px;height:13px;vertical-align:middle;"></i> تعديل
</a>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php if (isset($pagination) && ($pagination['last_page'] ?? 1) > 1): ?>
<div style="padding:15px;">
<?php $__template->include('Shared.Components.pagination', ['pagination' => $pagination]); ?>
</div>
<?php endif; ?>
<?php else: ?>
<div class="card" style="padding:60px 20px;text-align:center;">
<div style="margin-bottom:15px;">
<i data-lucide="gift" style="width:48px;height:48px;color:#D1D5DB;"></i>
</div>
<h3 style="color:#6B7280;margin:0 0 8px;">لا توجد باقات</h3>
<p style="color:#9CA3AF;font-size:14px;margin:0 0 20px;">
<?php if (!empty($filters['q']) || ($filters['is_active'] ?? '') !== ''): ?>
لا توجد نتائج مطابقة لبحثك. جرب تغيير معايير البحث.
<?php else: ?>
ابدأ بإنشاء باقة جديدة لتجميع الأصناف بسعر مخفّض.
<?php endif; ?>
</p>
<a href="/sales/packages/create" class="btn btn-primary"><i data-lucide="plus" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> باقة جديدة</a>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?><?= e($package->name_ar) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/sales/packages/<?= (int) $package->id ?>/edit" class="btn btn-outline"><i data-lucide="edit-3" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> تعديل</a>
<a href="/sales/packages" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة للقائمة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$memberPrice = (float) ($package->package_price_member ?? 0);
$nonmemberPrice = (float) ($package->package_price_nonmember ?? 0);
$itemsTotal = (float) ($package->items_total_price ?? 0);
$saving = $itemsTotal > 0 ? $itemsTotal - $memberPrice : 0;
$savingPct = $itemsTotal > 0 ? round(($saving / $itemsTotal) * 100) : 0;
?>
<!-- Package Info Card -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="gift" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">بيانات الباقة</h3>
<div style="margin-right:auto;">
<?php if ((int) $package->is_active): ?>
<span style="display:inline-block;padding:3px 12px;border-radius:10px;font-size:12px;font-weight:600;background:#ECFDF5;color:#059669;">فعّالة</span>
<?php else: ?>
<span style="display:inline-block;padding:3px 12px;border-radius:10px;font-size:12px;font-weight:600;background:#FEE2E2;color:#DC2626;">معطّلة</span>
<?php endif; ?>
</div>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(200px, 1fr));gap:20px;">
<div>
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">كود الباقة</div>
<div style="font-weight:700;font-family:monospace;font-size:15px;direction:ltr;text-align:left;"><?= e($package->code ?? '') ?></div>
</div>
<div>
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">الاسم بالعربي</div>
<div style="font-weight:700;font-size:15px;"><?= e($package->name_ar ?? '') ?></div>
</div>
<div>
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">الاسم بالإنجليزي</div>
<div style="font-weight:600;direction:ltr;text-align:left;"><?= e($package->name_en ?? '') ?: '—' ?></div>
</div>
</div>
<!-- Pricing Section -->
<div style="margin-top:20px;padding-top:15px;border-top:1px solid #F3F4F6;">
<div style="font-size:13px;font-weight:600;color:#059669;margin-bottom:12px;display:flex;align-items:center;gap:6px;">
<i data-lucide="banknote" style="width:16px;height:16px;"></i> الأسعار
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(180px, 1fr));gap:15px;">
<div style="padding:12px;background:#F0FDF4;border-radius:8px;">
<div style="font-size:11px;color:#6B7280;">سعر الباقة (عضو)</div>
<div style="font-weight:700;font-size:16px;color:#059669;direction:ltr;text-align:left;"><?= money($memberPrice) ?></div>
</div>
<div style="padding:12px;background:#F9FAFB;border-radius:8px;">
<div style="font-size:11px;color:#6B7280;">سعر الباقة (غير عضو)</div>
<div style="font-weight:700;font-size:16px;color:#1A1A2E;direction:ltr;text-align:left;"><?= money($nonmemberPrice) ?></div>
</div>
<div style="padding:12px;background:#F9FAFB;border-radius:8px;">
<div style="font-size:11px;color:#6B7280;">إجمالي أسعار الأصناف</div>
<div style="font-weight:700;font-size:16px;color:#1A1A2E;direction:ltr;text-align:left;"><?= money($itemsTotal) ?></div>
</div>
<?php if ($saving > 0): ?>
<div style="padding:12px;background:#ECFDF5;border-radius:8px;border:1px solid #BBF7D0;">
<div style="font-size:11px;color:#059669;">التوفير</div>
<div style="font-weight:800;font-size:16px;color:#059669;direction:ltr;text-align:left;"><?= money($saving) ?> <span style="font-size:12px;">(<?= $savingPct ?>%)</span></div>
</div>
<?php endif; ?>
</div>
</div>
<!-- Validity -->
<?php if ($package->active_from || $package->active_to): ?>
<div style="margin-top:20px;padding-top:15px;border-top:1px solid #F3F4F6;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;">
<div>
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">فعّال من</div>
<div style="font-weight:600;"><?= e($package->active_from ?? '—') ?></div>
</div>
<div>
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">فعّال حتى</div>
<div style="font-weight:600;"><?= e($package->active_to ?? '—') ?></div>
</div>
</div>
</div>
<?php endif; ?>
<?php if (!empty($package->notes)): ?>
<div style="margin-top:20px;padding-top:15px;border-top:1px solid #F3F4F6;">
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">ملاحظات</div>
<div style="font-size:14px;color:#4B5563;line-height:1.7;"><?= e($package->notes) ?></div>
</div>
<?php endif; ?>
</div>
</div>
<!-- Package Items -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="package" style="width:18px;height:18px;color:#059669;"></i>
<h3 style="margin:0;color:#059669;font-size:15px;">أصناف الباقة</h3>
<span style="background:#059669;color:#fff;padding:2px 8px;border-radius:10px;font-size:12px;font-weight:600;margin-right:auto;"><?= count($packageItems) ?> صنف</span>
</div>
<?php if (!empty($packageItems)): ?>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>#</th>
<th>الصنف</th>
<th>SKU</th>
<th>الكمية</th>
<th>سعر الوحدة (عضو)</th>
<th>الإجمالي</th>
</tr>
</thead>
<tbody>
<?php
$calcTotal = 0;
foreach ($packageItems as $i => $pi):
$qty = (float) ($pi['quantity'] ?? 0);
$unitPrice = (float) ($pi['sale_price_member'] ?? 0);
$lineTotal = $qty * $unitPrice;
$calcTotal += $lineTotal;
?>
<tr>
<td style="color:#6B7280;"><?= $i + 1 ?></td>
<td style="font-weight:600;"><?= e($pi['item_name'] ?? $pi['item_name_ar'] ?? '') ?></td>
<td style="font-family:monospace;font-size:12px;color:#6B7280;direction:ltr;text-align:left;"><?= e($pi['sku'] ?? '') ?></td>
<td style="font-weight:600;"><?= number_format($qty, 0) ?></td>
<td style="direction:ltr;text-align:left;"><?= money($unitPrice) ?></td>
<td style="direction:ltr;text-align:left;font-weight:700;"><?= money($lineTotal) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
<tfoot>
<tr style="background:#F0FDF4;font-weight:700;">
<td colspan="5" style="padding:12px 15px;">إجمالي قيمة الأصناف</td>
<td style="padding:12px 15px;direction:ltr;text-align:left;font-size:16px;color:#059669;"><?= money($calcTotal) ?></td>
</tr>
</tfoot>
</table>
</div>
<?php else: ?>
<div style="padding:40px 20px;text-align:center;color:#6B7280;">
<i data-lucide="package-x" style="width:36px;height:36px;color:#D1D5DB;margin-bottom:8px;"></i>
<p style="margin:0;">لا توجد أصناف مسجّلة في هذه الباقة</p>
</div>
<?php endif; ?>
</div>
<!-- Metadata -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="info" style="width:18px;height:18px;color:#6B7280;"></i>
<h3 style="margin:0;color:#6B7280;font-size:15px;">معلومات النظام</h3>
</div>
<div style="padding:15px 20px;">
<table style="width:100%;font-size:13px;">
<tr>
<td style="padding:6px 0;color:#6B7280;width:30%;">رقم السجل</td>
<td style="padding:6px 0;font-weight:600;">#<?= (int) $package->id ?></td>
</tr>
<?php if ($package->created_at): ?>
<tr>
<td style="padding:6px 0;color:#6B7280;">تاريخ الإنشاء</td>
<td style="padding:6px 0;"><?= e($package->created_at) ?></td>
</tr>
<?php endif; ?>
<?php if ($package->updated_at): ?>
<tr>
<td style="padding:6px 0;color:#6B7280;">آخر تحديث</td>
<td style="padding:6px 0;"><?= e($package->updated_at) ?></td>
</tr>
<?php endif; ?>
</table>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>نقطة البيع<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/sales" class="btn btn-outline"><i data-lucide="list" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> المبيعات</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<form method="POST" action="/sales" id="posForm">
<?= csrf_field() ?>
<div style="display:grid;grid-template-columns:1fr 400px;gap:20px;align-items:start;">
<!-- Left Side: Items Search & Packages -->
<div>
<!-- Item Search -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="search" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">بحث الأصناف</h3>
</div>
<div style="padding:15px 20px;">
<input type="text" id="itemSearchInput" placeholder="ابحث بالاسم، SKU، أو الباركود..." class="form-input" style="font-size:15px;padding:12px;" autocomplete="off">
</div>
<div id="itemResults" style="max-height:400px;overflow-y:auto;display:none;">
<!-- AJAX results will appear here -->
</div>
</div>
<!-- Packages Section -->
<?php if (!empty($packages)): ?>
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="gift" style="width:18px;height:18px;color:#D97706;"></i>
<h3 style="margin:0;color:#D97706;font-size:15px;">الباقات المتاحة</h3>
</div>
<div style="padding:15px;display:grid;grid-template-columns:repeat(auto-fill, minmax(200px, 1fr));gap:12px;">
<?php foreach ($packages as $pkg): ?>
<div style="border:1px solid #E5E7EB;border-radius:10px;padding:14px;cursor:pointer;transition:all 0.2s;"
class="package-card"
onclick="addPackageToCart(<?= (int) $pkg['id'] ?>, '<?= e(addslashes($pkg['name_ar'])) ?>', '<?= e($pkg['package_price_member']) ?>', '<?= e($pkg['package_price_nonmember']) ?>')"
onmouseover="this.style.borderColor='#0D7377';this.style.background='#F0FDFA'"
onmouseout="this.style.borderColor='#E5E7EB';this.style.background=''">
<div style="font-weight:700;color:#1A1A2E;margin-bottom:6px;font-size:14px;"><?= e($pkg['name_ar']) ?></div>
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">كود: <?= e($pkg['code']) ?></div>
<div style="display:flex;justify-content:space-between;font-size:13px;margin-top:8px;">
<span style="color:#059669;font-weight:600;">عضو: <?= money($pkg['package_price_member']) ?></span>
</div>
<div style="font-size:12px;color:#6B7280;margin-top:2px;">غير عضو: <?= money($pkg['package_price_nonmember']) ?></div>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
</div>
<!-- Right Side: Cart & Checkout -->
<div>
<!-- Customer Section -->
<div class="card" style="margin-bottom:15px;">
<div style="padding:12px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="user" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:14px;">العميل</h3>
</div>
<div style="padding:15px;">
<!-- Customer Type Tabs -->
<div style="display:flex;gap:0;margin-bottom:15px;border:1px solid #E5E7EB;border-radius:8px;overflow:hidden;">
<button type="button" class="customer-tab active" data-type="member" onclick="switchCustomerType('member')"
style="flex:1;padding:10px;border:none;cursor:pointer;font-weight:600;font-size:13px;font-family:inherit;background:#0D7377;color:#fff;transition:all 0.2s;">
عضو
</button>
<button type="button" class="customer-tab" data-type="player" onclick="switchCustomerType('player')"
style="flex:1;padding:10px;border:none;border-right:1px solid #E5E7EB;border-left:1px solid #E5E7EB;cursor:pointer;font-weight:600;font-size:13px;font-family:inherit;background:#fff;color:#6B7280;transition:all 0.2s;">
لاعب
</button>
<button type="button" class="customer-tab" data-type="guest" onclick="switchCustomerType('guest')"
style="flex:1;padding:10px;border:none;cursor:pointer;font-weight:600;font-size:13px;font-family:inherit;background:#fff;color:#6B7280;transition:all 0.2s;">
زائر
</button>
</div>
<input type="hidden" name="customer_type" id="customerType" value="member">
<input type="hidden" name="member_id" id="memberId" value="">
<input type="hidden" name="player_id" id="playerId" value="">
<!-- Member/Player Search -->
<div id="customerSearchSection">
<div style="position:relative;">
<input type="text" id="customerSearchInput" placeholder="ابحث بالاسم أو الرقم..." class="form-input" autocomplete="off">
<div id="customerResults" style="position:absolute;top:100%;right:0;left:0;z-index:10;background:#fff;border:1px solid #E5E7EB;border-radius:0 0 8px 8px;max-height:200px;overflow-y:auto;display:none;box-shadow:0 4px 12px rgba(0,0,0,0.1);"></div>
</div>
<div id="selectedCustomer" style="display:none;margin-top:10px;padding:10px;background:#F0FDF4;border-radius:8px;border:1px solid #BBF7D0;">
<div style="display:flex;justify-content:space-between;align-items:center;">
<span id="selectedCustomerName" style="font-weight:600;color:#059669;"></span>
<button type="button" onclick="clearCustomer()" style="background:none;border:none;color:#DC2626;cursor:pointer;font-size:12px;font-family:inherit;">
<i data-lucide="x" style="width:14px;height:14px;"></i>
</button>
</div>
</div>
</div>
<!-- Guest Fields -->
<div id="guestSection" style="display:none;">
<div style="display:grid;gap:10px;">
<input type="text" name="guest_name" id="guestName" placeholder="اسم الزائر" class="form-input">
<input type="text" name="guest_phone" id="guestPhone" placeholder="رقم الهاتف" class="form-input" style="direction:ltr;text-align:left;">
</div>
</div>
</div>
</div>
<!-- Warehouse & Payment -->
<div class="card" style="margin-bottom:15px;">
<div style="padding:15px;">
<div style="display:grid;gap:10px;">
<div>
<label class="form-label" style="font-size:12px;">المخزن <span style="color:#DC2626;">*</span></label>
<select name="warehouse_id" class="form-select" required>
<option value="">اختر المخزن</option>
<?php foreach ($warehouses as $wh): ?>
<option value="<?= (int) $wh['id'] ?>"><?= e($wh['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label" style="font-size:12px;">طريقة الدفع</label>
<select name="payment_method" class="form-select">
<option value="cash">نقدي</option>
<option value="visa">فيزا</option>
<option value="transfer">تحويل بنكي</option>
</select>
</div>
</div>
</div>
</div>
<!-- Cart -->
<div class="card" style="margin-bottom:15px;">
<div style="padding:12px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;justify-content:space-between;">
<div style="display:flex;align-items:center;gap:8px;">
<i data-lucide="shopping-cart" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:14px;">السلة</h3>
</div>
<span id="cartCount" style="background:#0D7377;color:#fff;padding:2px 8px;border-radius:10px;font-size:12px;font-weight:600;">0</span>
</div>
<div id="cartContainer">
<div id="emptyCart" style="padding:40px 20px;text-align:center;color:#9CA3AF;">
<i data-lucide="shopping-bag" style="width:36px;height:36px;color:#D1D5DB;margin-bottom:8px;"></i>
<p style="margin:0;font-size:13px;">السلة فارغة</p>
</div>
<table id="cartTable" class="data-table" style="display:none;">
<thead>
<tr>
<th style="font-size:12px;">الصنف</th>
<th style="font-size:12px;width:60px;">الكمية</th>
<th style="font-size:12px;width:80px;">السعر</th>
<th style="font-size:12px;width:80px;">الإجمالي</th>
<th style="width:30px;"></th>
</tr>
</thead>
<tbody id="cartBody"></tbody>
</table>
</div>
<!-- Hidden inputs container for form submission -->
<div id="hiddenInputs"></div>
</div>
<!-- Discount -->
<div class="card" style="margin-bottom:15px;">
<div style="padding:15px;">
<div style="display:grid;grid-template-columns:1fr 2fr;gap:10px;">
<div>
<label class="form-label" style="font-size:12px;">الخصم</label>
<input type="number" name="discount_amount" id="discountAmount" value="0" class="form-input" step="0.01" min="0" style="direction:ltr;text-align:left;" onchange="updateTotals()">
</div>
<div>
<label class="form-label" style="font-size:12px;">سبب الخصم</label>
<input type="text" name="discount_reason" placeholder="اختياري" class="form-input">
</div>
</div>
</div>
</div>
<!-- Totals -->
<div class="card" style="margin-bottom:15px;overflow:hidden;">
<div style="padding:15px;">
<div style="display:flex;justify-content:space-between;padding:6px 0;font-size:14px;color:#6B7280;">
<span>المجموع الفرعي</span>
<span id="subtotalDisplay" style="font-weight:600;direction:ltr;">0.00</span>
</div>
<div style="display:flex;justify-content:space-between;padding:6px 0;font-size:14px;color:#DC2626;">
<span>الخصم</span>
<span id="discountDisplay" style="font-weight:600;direction:ltr;">- 0.00</span>
</div>
<div style="display:flex;justify-content:space-between;padding:6px 0;font-size:14px;color:#D97706;">
<span>الضريبة</span>
<span id="taxDisplay" style="font-weight:600;direction:ltr;">0.00</span>
</div>
<div style="border-top:2px solid #E5E7EB;margin-top:8px;padding-top:10px;display:flex;justify-content:space-between;font-size:20px;font-weight:800;color:#0D7377;">
<span>الإجمالي</span>
<span id="totalDisplay" style="direction:ltr;">0.00</span>
</div>
</div>
</div>
<!-- Notes -->
<div style="margin-bottom:15px;">
<textarea name="notes" placeholder="ملاحظات..." class="form-input" rows="2" style="font-size:13px;"></textarea>
</div>
<!-- Submit -->
<button type="submit" id="submitBtn" class="btn btn-primary" style="width:100%;padding:16px;font-size:18px;font-weight:700;border-radius:12px;" disabled>
<i data-lucide="credit-card" style="width:20px;height:20px;vertical-align:middle;margin-left:6px;"></i>
إتمام البيع
</button>
</div>
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
});
var cart = [];
var currentCustomerType = 'member';
var searchTimeout = null;
// --- Item Search ---
document.getElementById('itemSearchInput').addEventListener('input', function() {
var q = this.value.trim();
clearTimeout(searchTimeout);
if (q.length < 2) {
document.getElementById('itemResults').style.display = 'none';
return;
}
searchTimeout = setTimeout(function() { searchItems(q); }, 300);
});
function searchItems(q) {
fetch('/sales/search-items?q=' + encodeURIComponent(q))
.then(function(r) { return r.json(); })
.then(function(data) {
var container = document.getElementById('itemResults');
if (!data.results || data.results.length === 0) {
container.innerHTML = '<div style="padding:20px;text-align:center;color:#9CA3AF;font-size:13px;">لا توجد نتائج</div>';
container.style.display = 'block';
return;
}
var html = '';
data.results.forEach(function(item) {
var priceLabel = currentCustomerType === 'member' ? item.sale_price_member :
(currentCustomerType === 'player' && item.sale_price_player ? item.sale_price_player : item.sale_price_nonmember);
html += '<div style="padding:12px 20px;border-bottom:1px solid #F3F4F6;cursor:pointer;display:flex;justify-content:space-between;align-items:center;transition:background 0.1s;" ' +
'onmouseover="this.style.background=\'#F0FDFA\'" onmouseout="this.style.background=\'\'" ' +
'onclick="addItemToCart(' + item.id + ', \'' + escapeHtml(item.name_ar) + '\', \'' + priceLabel + '\', \'' + (item.cost_price || '0') + '\', \'' + (item.tax_rate || '0') + '\')">' +
'<div><div style="font-weight:600;font-size:14px;">' + escapeHtml(item.name_ar) + '</div>' +
'<div style="font-size:11px;color:#9CA3AF;font-family:monospace;">' + escapeHtml(item.sku || '') + '</div></div>' +
'<div style="font-weight:700;color:#059669;direction:ltr;">' + parseFloat(priceLabel || 0).toFixed(2) + '</div></div>';
});
container.innerHTML = html;
container.style.display = 'block';
});
}
function addItemToCart(itemId, name, price, costPrice, taxRate) {
var existing = cart.find(function(c) { return c.item_id == itemId && !c.package_id; });
if (existing) {
existing.quantity++;
} else {
cart.push({
item_id: itemId,
package_id: '',
name: name,
quantity: 1,
unit_price: parseFloat(price) || 0,
cost_price: parseFloat(costPrice) || 0,
tax_rate: parseFloat(taxRate) || 0
});
}
renderCart();
document.getElementById('itemSearchInput').value = '';
document.getElementById('itemResults').style.display = 'none';
}
function addPackageToCart(pkgId, name, priceMember, priceNonmember) {
var price = (currentCustomerType === 'member') ? parseFloat(priceMember) : parseFloat(priceNonmember);
var existing = cart.find(function(c) { return c.package_id == pkgId; });
if (existing) {
existing.quantity++;
} else {
cart.push({
item_id: '',
package_id: pkgId,
name: name,
quantity: 1,
unit_price: price || 0,
cost_price: 0,
tax_rate: 0
});
}
renderCart();
}
function removeFromCart(index) {
cart.splice(index, 1);
renderCart();
}
function updateQty(index, val) {
var qty = parseInt(val) || 1;
if (qty < 1) qty = 1;
cart[index].quantity = qty;
renderCart();
}
function renderCart() {
var tbody = document.getElementById('cartBody');
var emptyCart = document.getElementById('emptyCart');
var cartTable = document.getElementById('cartTable');
var hiddenInputs = document.getElementById('hiddenInputs');
if (cart.length === 0) {
emptyCart.style.display = 'block';
cartTable.style.display = 'none';
document.getElementById('submitBtn').disabled = true;
} else {
emptyCart.style.display = 'none';
cartTable.style.display = 'table';
document.getElementById('submitBtn').disabled = false;
}
var html = '';
var hiddenHtml = '';
cart.forEach(function(item, i) {
var lineTotal = item.quantity * item.unit_price;
var taxAmount = lineTotal * (item.tax_rate / 100);
html += '<tr>' +
'<td style="font-size:13px;font-weight:600;">' + escapeHtml(item.name) + (item.package_id ? ' <span style="font-size:10px;color:#D97706;background:#FFF7ED;padding:1px 6px;border-radius:6px;">باقة</span>' : '') + '</td>' +
'<td><input type="number" value="' + item.quantity + '" min="1" style="width:50px;padding:4px;text-align:center;border:1px solid #E5E7EB;border-radius:6px;font-size:13px;" onchange="updateQty(' + i + ', this.value)"></td>' +
'<td style="font-size:13px;direction:ltr;text-align:left;">' + item.unit_price.toFixed(2) + '</td>' +
'<td style="font-size:13px;font-weight:700;direction:ltr;text-align:left;">' + lineTotal.toFixed(2) + '</td>' +
'<td><button type="button" onclick="removeFromCart(' + i + ')" style="background:none;border:none;color:#DC2626;cursor:pointer;padding:4px;"><i data-lucide="trash-2" style="width:14px;height:14px;"></i></button></td>' +
'</tr>';
hiddenHtml += '<input type="hidden" name="item_ids[]" value="' + (item.item_id || '') + '">' +
'<input type="hidden" name="package_ids[]" value="' + (item.package_id || '') + '">' +
'<input type="hidden" name="quantities[]" value="' + item.quantity + '">' +
'<input type="hidden" name="unit_prices[]" value="' + item.unit_price.toFixed(2) + '">' +
'<input type="hidden" name="item_names[]" value="' + escapeHtml(item.name) + '">' +
'<input type="hidden" name="cost_prices[]" value="' + item.cost_price.toFixed(2) + '">' +
'<input type="hidden" name="tax_amounts[]" value="' + (item.quantity * item.unit_price * (item.tax_rate / 100)).toFixed(2) + '">';
});
tbody.innerHTML = html;
hiddenInputs.innerHTML = hiddenHtml;
document.getElementById('cartCount').textContent = cart.length;
updateTotals();
if (typeof lucide !== 'undefined') lucide.createIcons();
}
function updateTotals() {
var subtotal = 0;
var totalTax = 0;
cart.forEach(function(item) {
var lineTotal = item.quantity * item.unit_price;
subtotal += lineTotal;
totalTax += lineTotal * (item.tax_rate / 100);
});
var discount = parseFloat(document.getElementById('discountAmount').value) || 0;
var total = subtotal - discount + totalTax;
if (total < 0) total = 0;
document.getElementById('subtotalDisplay').textContent = subtotal.toFixed(2);
document.getElementById('discountDisplay').textContent = '- ' + discount.toFixed(2);
document.getElementById('taxDisplay').textContent = totalTax.toFixed(2);
document.getElementById('totalDisplay').textContent = total.toFixed(2);
}
// --- Customer ---
function switchCustomerType(type) {
currentCustomerType = type;
document.getElementById('customerType').value = type;
document.querySelectorAll('.customer-tab').forEach(function(btn) {
if (btn.getAttribute('data-type') === type) {
btn.style.background = '#0D7377';
btn.style.color = '#fff';
} else {
btn.style.background = '#fff';
btn.style.color = '#6B7280';
}
});
if (type === 'guest') {
document.getElementById('customerSearchSection').style.display = 'none';
document.getElementById('guestSection').style.display = 'block';
} else {
document.getElementById('customerSearchSection').style.display = 'block';
document.getElementById('guestSection').style.display = 'none';
}
clearCustomer();
}
var customerSearchTimeout = null;
document.getElementById('customerSearchInput').addEventListener('input', function() {
var q = this.value.trim();
clearTimeout(customerSearchTimeout);
if (q.length < 2) {
document.getElementById('customerResults').style.display = 'none';
return;
}
customerSearchTimeout = setTimeout(function() { searchCustomers(q); }, 300);
});
function searchCustomers(q) {
var type = currentCustomerType;
fetch('/sales/search-customers?q=' + encodeURIComponent(q) + '&type=' + encodeURIComponent(type))
.then(function(r) { return r.json(); })
.then(function(data) {
var container = document.getElementById('customerResults');
if (!data.results || data.results.length === 0) {
container.innerHTML = '<div style="padding:15px;text-align:center;color:#9CA3AF;font-size:13px;">لا توجد نتائج</div>';
container.style.display = 'block';
return;
}
var html = '';
data.results.forEach(function(c) {
var subtitle = type === 'member' ? (c.membership_number || c.form_number || '') : (c.player_code || '');
html += '<div style="padding:10px 15px;border-bottom:1px solid #F3F4F6;cursor:pointer;transition:background 0.1s;" ' +
'onmouseover="this.style.background=\'#F0FDF4\'" onmouseout="this.style.background=\'\'" ' +
'onclick="selectCustomer(' + c.id + ', \'' + escapeHtml(c.full_name_ar) + '\')">' +
'<div style="font-weight:600;font-size:13px;">' + escapeHtml(c.full_name_ar) + '</div>' +
(subtitle ? '<div style="font-size:11px;color:#9CA3AF;">' + escapeHtml(subtitle) + '</div>' : '') +
'</div>';
});
container.innerHTML = html;
container.style.display = 'block';
});
}
function selectCustomer(id, name) {
if (currentCustomerType === 'member') {
document.getElementById('memberId').value = id;
document.getElementById('playerId').value = '';
} else {
document.getElementById('playerId').value = id;
document.getElementById('memberId').value = '';
}
document.getElementById('selectedCustomerName').textContent = name;
document.getElementById('selectedCustomer').style.display = 'block';
document.getElementById('customerResults').style.display = 'none';
document.getElementById('customerSearchInput').value = '';
if (typeof lucide !== 'undefined') lucide.createIcons();
}
function clearCustomer() {
document.getElementById('memberId').value = '';
document.getElementById('playerId').value = '';
document.getElementById('selectedCustomer').style.display = 'none';
document.getElementById('customerSearchInput').value = '';
document.getElementById('customerResults').style.display = 'none';
}
function escapeHtml(str) {
var div = document.createElement('div');
div.appendChild(document.createTextNode(str || ''));
return div.innerHTML;
}
</script>
<?php $__template->endSection(); ?>
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>فاتورة <?= e($sale['invoice_number'] ?? '') ?></title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', Tahoma, Arial, sans-serif;
direction: rtl;
color: #1A1A2E;
background: #fff;
padding: 20px;
font-size: 13px;
line-height: 1.6;
}
.invoice-container { max-width: 800px; margin: 0 auto; }
.header { text-align: center; margin-bottom: 25px; padding-bottom: 15px; border-bottom: 2px solid #0D7377; }
.header h1 { font-size: 22px; color: #0D7377; margin-bottom: 4px; }
.header .invoice-number { font-size: 16px; font-weight: 700; color: #1A1A2E; margin-top: 8px; }
.header .invoice-date { font-size: 13px; color: #6B7280; }
.info-row { display: flex; justify-content: space-between; margin-bottom: 20px; gap: 20px; }
.info-block { flex: 1; }
.info-block h3 { font-size: 13px; color: #0D7377; margin-bottom: 6px; border-bottom: 1px solid #E5E7EB; padding-bottom: 4px; }
.info-block p { font-size: 13px; margin-bottom: 3px; }
.info-block p span { color: #6B7280; }
table { width: 100%; border-collapse: collapse; margin-bottom: 20px; }
thead th {
background: #0D7377;
color: #fff;
padding: 10px 12px;
font-size: 13px;
font-weight: 600;
text-align: right;
}
tbody td {
padding: 10px 12px;
border-bottom: 1px solid #E5E7EB;
font-size: 13px;
}
tbody tr:nth-child(even) { background: #F9FAFB; }
.totals-section { display: flex; justify-content: flex-end; margin-bottom: 25px; }
.totals-table { width: 280px; }
.totals-table .row { display: flex; justify-content: space-between; padding: 6px 0; font-size: 13px; }
.totals-table .row.total {
border-top: 2px solid #0D7377;
margin-top: 6px;
padding-top: 10px;
font-size: 18px;
font-weight: 800;
color: #0D7377;
}
.footer { text-align: center; padding-top: 20px; border-top: 1px dashed #D1D5DB; color: #6B7280; font-size: 12px; }
@media print {
body { padding: 0; }
@page { margin: 15mm; }
}
</style>
</head>
<body>
<div class="invoice-container">
<!-- Header -->
<div class="header">
<h1>النادي</h1>
<div class="invoice-number">فاتورة رقم: <?= e($sale['invoice_number'] ?? '') ?></div>
<div class="invoice-date"><?= e($sale['sale_date'] ?? $sale['created_at'] ?? '') ?></div>
</div>
<!-- Customer & Sale Info -->
<div class="info-row">
<div class="info-block">
<h3>بيانات العميل</h3>
<?php
$customerTypeLabels = ['member' => 'عضو', 'player' => 'لاعب', 'guest' => 'زائر'];
$cType = $sale['customer_type'] ?? 'guest';
?>
<p><span>النوع:</span> <?= e($customerTypeLabels[$cType] ?? $cType) ?></p>
<?php if ($cType === 'guest'): ?>
<p><span>الاسم:</span> <?= e($sale['guest_name'] ?? '—') ?></p>
<?php if (!empty($sale['guest_phone'])): ?>
<p><span>الهاتف:</span> <?= e($sale['guest_phone']) ?></p>
<?php endif; ?>
<?php else: ?>
<p><span>الاسم:</span> <?= e($sale['guest_name'] ?? '—') ?></p>
<?php endif; ?>
</div>
<div class="info-block">
<h3>بيانات الفاتورة</h3>
<p><span>المخزن:</span> <?= e($sale['warehouse_name'] ?? '—') ?></p>
<p><span>طريقة الدفع:</span> <?= e(match($sale['payment_method'] ?? '') { 'cash' => 'نقدي', 'visa' => 'فيزا', 'transfer' => 'تحويل بنكي', default => $sale['payment_method'] ?? '—' }) ?></p>
<p><span>الحالة:</span> <?= e(\App\Modules\Sales\Models\Sale::getStatusLabel($sale['status'] ?? 'draft')) ?></p>
</div>
</div>
<!-- Items Table -->
<table>
<thead>
<tr>
<th style="width:40px;">#</th>
<th>الصنف</th>
<th style="width:80px;">الكمية</th>
<th style="width:100px;">السعر</th>
<th style="width:110px;">الإجمالي</th>
</tr>
</thead>
<tbody>
<?php foreach ($items as $i => $item): ?>
<tr>
<td><?= $i + 1 ?></td>
<td style="font-weight:600;"><?= e($item['item_name_ar'] ?? '') ?></td>
<td style="text-align:center;"><?= e($item['quantity'] ?? '0') ?></td>
<td style="direction:ltr;text-align:left;"><?= money($item['unit_price'] ?? 0) ?></td>
<td style="direction:ltr;text-align:left;font-weight:700;"><?= money($item['line_total'] ?? 0) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<!-- Totals -->
<div class="totals-section">
<div class="totals-table">
<div class="row">
<span style="color:#6B7280;">المجموع الفرعي</span>
<span style="font-weight:600;direction:ltr;"><?= money($sale['subtotal'] ?? 0) ?></span>
</div>
<?php if ((float) ($sale['discount_amount'] ?? 0) > 0): ?>
<div class="row">
<span style="color:#DC2626;">الخصم</span>
<span style="font-weight:600;color:#DC2626;direction:ltr;">- <?= money($sale['discount_amount'] ?? 0) ?></span>
</div>
<?php endif; ?>
<?php if ((float) ($sale['tax_amount'] ?? 0) > 0): ?>
<div class="row">
<span style="color:#D97706;">الضريبة</span>
<span style="font-weight:600;direction:ltr;"><?= money($sale['tax_amount'] ?? 0) ?></span>
</div>
<?php endif; ?>
<div class="row total">
<span>الإجمالي</span>
<span style="direction:ltr;"><?= money($sale['total_amount'] ?? 0) ?></span>
</div>
</div>
</div>
<!-- Footer -->
<div class="footer">
<p>شكرًا لتعاملكم معنا</p>
<p style="margin-top:4px;">تم الطباعة بتاريخ: <?= date('Y-m-d H:i') ?></p>
</div>
</div>
<script>
window.addEventListener('load', function() {
window.print();
});
</script>
</body>
</html>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تقرير المبيعات حسب الصنف<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<button onclick="window.print()" class="btn btn-outline"><i data-lucide="printer" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> طباعة</button>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Filters -->
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/sales/reports/by-item" style="display:flex;gap:10px;flex-wrap:wrap;align-items:end;">
<div style="min-width:150px;">
<label class="form-label" style="font-size:12px;">من تاريخ</label>
<input type="date" name="date_from" value="<?= e($dateFrom ?? '') ?>" class="form-input">
</div>
<div style="min-width:150px;">
<label class="form-label" style="font-size:12px;">إلى تاريخ</label>
<input type="date" name="date_to" value="<?= e($dateTo ?? '') ?>" class="form-input">
</div>
<button type="submit" class="btn btn-primary"><i data-lucide="search" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> عرض التقرير</button>
<a href="/sales/reports/by-item" class="btn btn-outline" style="color:#6B7280;">مسح</a>
</form>
</div>
<?php if (!empty($rows)): ?>
<?php
$totalQty = 0;
$totalRevenue = '0.00';
$totalCost = '0.00';
$totalProfit = '0.00';
foreach ($rows as $r) {
$totalQty += (float) ($r['total_qty'] ?? 0);
$totalRevenue = bcadd($totalRevenue, (string) ($r['total_revenue'] ?? 0), 2);
$totalCost = bcadd($totalCost, (string) ($r['total_cost'] ?? 0), 2);
$totalProfit = bcadd($totalProfit, (string) ($r['profit'] ?? 0), 2);
}
?>
<!-- Summary Cards -->
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:15px;margin-bottom:20px;">
<div class="card" style="padding:18px;text-align:center;background:#F0FDF4;border:1px solid #BBF7D0;">
<div style="font-size:26px;font-weight:700;color:#059669;"><?= number_format($totalQty) ?></div>
<div style="color:#6B7280;font-size:13px;">إجمالي الكمية</div>
</div>
<div class="card" style="padding:18px;text-align:center;background:#ECFDF5;border:1px solid #A7F3D0;">
<div style="font-size:26px;font-weight:700;color:#059669;"><?= money($totalRevenue) ?></div>
<div style="color:#6B7280;font-size:13px;">الإيرادات</div>
</div>
<div class="card" style="padding:18px;text-align:center;background:#FEF2F2;border:1px solid #FECACA;">
<div style="font-size:26px;font-weight:700;color:#DC2626;"><?= money($totalCost) ?></div>
<div style="color:#6B7280;font-size:13px;">التكلفة</div>
</div>
<div class="card" style="padding:18px;text-align:center;background:#EFF6FF;border:1px solid #BFDBFE;">
<div style="font-size:26px;font-weight:700;color:#2563EB;"><?= money($totalProfit) ?></div>
<div style="color:#6B7280;font-size:13px;">الربح</div>
</div>
</div>
<!-- Items Table -->
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="package" style="width:20px;height:20px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;">تفاصيل المبيعات حسب الصنف</h3>
</div>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>الصنف</th>
<th>SKU</th>
<th>الكمية المباعة</th>
<th>الإيرادات</th>
<th>التكلفة</th>
<th>الربح</th>
</tr>
</thead>
<tbody>
<?php foreach ($rows as $row): ?>
<?php
$profit = (float) ($row['profit'] ?? 0);
$profitColor = $profit >= 0 ? '#059669' : '#DC2626';
?>
<tr>
<td style="font-weight:600;"><?= e($row['item_name_ar'] ?? '') ?></td>
<td style="direction:ltr;text-align:right;font-family:monospace;font-size:13px;"><?= e($row['sku'] ?? '—') ?></td>
<td style="font-weight:700;"><?= number_format((float) ($row['total_qty'] ?? 0)) ?></td>
<td style="font-weight:700;color:#059669;direction:ltr;text-align:right;"><?= money($row['total_revenue'] ?? 0) ?></td>
<td style="color:#DC2626;direction:ltr;text-align:right;"><?= money($row['total_cost'] ?? 0) ?></td>
<td style="font-weight:700;color:<?= $profitColor ?>;direction:ltr;text-align:right;"><?= money($row['profit'] ?? 0) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
<tfoot>
<tr style="background:#F0FDF4;font-weight:700;">
<td colspan="2" style="padding:12px 15px;">الإجمالي</td>
<td style="padding:12px 15px;"><?= number_format($totalQty) ?></td>
<td style="padding:12px 15px;direction:ltr;text-align:right;color:#059669;"><?= money($totalRevenue) ?></td>
<td style="padding:12px 15px;direction:ltr;text-align:right;color:#DC2626;"><?= money($totalCost) ?></td>
<td style="padding:12px 15px;direction:ltr;text-align:right;font-size:16px;color:<?= (float) $totalProfit >= 0 ? '#059669' : '#DC2626' ?>;"><?= money($totalProfit) ?></td>
</tr>
</tfoot>
</table>
</div>
</div>
<?php else: ?>
<div class="card" style="padding:60px 20px;text-align:center;">
<div style="margin-bottom:15px;">
<i data-lucide="package-search" style="width:48px;height:48px;color:#D1D5DB;"></i>
</div>
<h3 style="color:#6B7280;margin:0 0 8px;">لا توجد بيانات</h3>
<p style="color:#9CA3AF;font-size:14px;margin:0;">اختر الفترة الزمنية لعرض تقرير المبيعات حسب الصنف</p>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تقرير المبيعات اليومي — <?= e($date ?? '') ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<button onclick="window.print()" class="btn btn-outline"><i data-lucide="printer" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> طباعة</button>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Filters -->
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/sales/reports/daily" style="display:flex;gap:10px;flex-wrap:wrap;align-items:end;">
<div style="min-width:160px;">
<label class="form-label" style="font-size:12px;">التاريخ</label>
<input type="date" name="date" value="<?= e($date ?? '') ?>" class="form-input">
</div>
<div style="min-width:160px;">
<label class="form-label" style="font-size:12px;">المخزن</label>
<select name="warehouse_id" class="form-select">
<option value="">كل المخازن</option>
<?php foreach ($warehouses as $wh): ?>
<option value="<?= (int) $wh['id'] ?>" <?= $warehouseId == $wh['id'] ? 'selected' : '' ?>><?= e($wh['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<button type="submit" class="btn btn-primary"><i data-lucide="search" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> عرض التقرير</button>
</form>
</div>
<!-- Summary Cards -->
<div style="display:grid;grid-template-columns:repeat(5,1fr);gap:15px;margin-bottom:20px;">
<div class="card" style="padding:18px;text-align:center;background:#F0FDF4;border:1px solid #BBF7D0;">
<div style="font-size:26px;font-weight:700;color:#059669;"><?= (int) ($summary['sale_count'] ?? 0) ?></div>
<div style="color:#6B7280;font-size:13px;">عدد المبيعات</div>
</div>
<div class="card" style="padding:18px;text-align:center;background:#ECFDF5;border:1px solid #A7F3D0;">
<div style="font-size:26px;font-weight:700;color:#059669;"><?= money($summary['total_revenue'] ?? 0) ?></div>
<div style="color:#6B7280;font-size:13px;">الإيرادات</div>
</div>
<div class="card" style="padding:18px;text-align:center;background:#FFFBEB;border:1px solid #FDE68A;">
<div style="font-size:26px;font-weight:700;color:#D97706;"><?= money($summary['total_discounts'] ?? 0) ?></div>
<div style="color:#6B7280;font-size:13px;">الخصومات</div>
</div>
<div class="card" style="padding:18px;text-align:center;background:#EFF6FF;border:1px solid #BFDBFE;">
<div style="font-size:26px;font-weight:700;color:#2563EB;"><?= money($summary['total_tax'] ?? 0) ?></div>
<div style="color:#6B7280;font-size:13px;">الضريبة</div>
</div>
<div class="card" style="padding:18px;text-align:center;background:#FEF2F2;border:1px solid #FECACA;">
<div style="font-size:26px;font-weight:700;color:#DC2626;"><?= money($summary['total_refunds'] ?? 0) ?></div>
<div style="color:#6B7280;font-size:13px;">المرتجعات</div>
</div>
</div>
<!-- Payment Method Breakdown -->
<?php if (!empty($byMethod)): ?>
<div class="card" style="margin-bottom:20px;padding:20px;">
<h4 style="color:#0D7377;margin:0 0 12px;font-size:14px;display:flex;align-items:center;gap:6px;">
<i data-lucide="credit-card" style="width:18px;height:18px;"></i> حسب طريقة الدفع
</h4>
<?php
$methodLabels = [
'cash' => 'نقدي',
'card' => 'بطاقة',
'bank' => 'تحويل بنكي',
'wallet' => 'محفظة',
'installment' => 'تقسيط',
];
?>
<?php foreach ($byMethod as $m): ?>
<div style="display:flex;justify-content:space-between;padding:6px 0;font-size:13px;border-bottom:1px solid #F3F4F6;">
<span><?= e($methodLabels[$m['payment_method'] ?? ''] ?? ($m['payment_method'] ?? '—')) ?></span>
<strong><?= money($m['total'] ?? 0) ?> <small style="color:#9CA3AF;">(<?= (int) ($m['cnt'] ?? 0) ?>)</small></strong>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<!-- Sales List -->
<?php if (!empty($sales)): ?>
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="receipt" style="width:20px;height:20px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;">قائمة المبيعات</h3>
</div>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>#</th>
<th>رقم الفاتورة</th>
<th>العميل</th>
<th>الإجمالي</th>
<th>الخصم</th>
<th>الضريبة</th>
<th>الصافي</th>
<th>طريقة الدفع</th>
<th>الوقت</th>
</tr>
</thead>
<tbody>
<?php foreach ($sales as $i => $sale): ?>
<tr>
<td><?= $i + 1 ?></td>
<td style="font-family:monospace;font-weight:600;"><?= e($sale['invoice_number'] ?? '—') ?></td>
<td><?= e($sale['customer_name'] ?? 'عميل نقدي') ?></td>
<td style="font-weight:600;direction:ltr;text-align:right;"><?= money($sale['subtotal'] ?? 0) ?></td>
<td style="color:#D97706;direction:ltr;text-align:right;"><?= money($sale['discount'] ?? 0) ?></td>
<td style="color:#2563EB;direction:ltr;text-align:right;"><?= money($sale['tax'] ?? 0) ?></td>
<td style="font-weight:700;color:#059669;direction:ltr;text-align:right;"><?= money($sale['grand_total'] ?? 0) ?></td>
<td style="font-size:13px;"><?= e($methodLabels[$sale['payment_method'] ?? ''] ?? ($sale['payment_method'] ?? '—')) ?></td>
<td style="font-size:13px;color:#6B7280;white-space:nowrap;"><?= e($sale['created_at'] ?? '—') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php else: ?>
<div class="card" style="padding:60px 20px;text-align:center;">
<div style="margin-bottom:15px;">
<i data-lucide="shopping-cart" style="width:48px;height:48px;color:#D1D5DB;"></i>
</div>
<h3 style="color:#6B7280;margin:0 0 8px;">لا توجد مبيعات</h3>
<p style="color:#9CA3AF;font-size:14px;margin:0;">لم يتم تسجيل أي مبيعات في هذا التاريخ</p>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تقرير المبيعات الشهري<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<button onclick="window.print()" class="btn btn-outline"><i data-lucide="printer" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> طباعة</button>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Filters -->
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/sales/reports/monthly" style="display:flex;gap:10px;flex-wrap:wrap;align-items:end;">
<div style="min-width:200px;">
<label class="form-label" style="font-size:12px;">الشهر</label>
<input type="month" name="month" value="<?= e($month ?? '') ?>" class="form-input">
</div>
<div style="min-width:160px;">
<label class="form-label" style="font-size:12px;">المخزن</label>
<select name="warehouse_id" class="form-select">
<option value="">كل المخازن</option>
<?php foreach ($warehouses as $wh): ?>
<option value="<?= (int) $wh['id'] ?>" <?= $warehouseId == $wh['id'] ? 'selected' : '' ?>><?= e($wh['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<button type="submit" class="btn btn-primary"><i data-lucide="search" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> عرض التقرير</button>
</form>
</div>
<!-- Summary Cards -->
<div style="display:grid;grid-template-columns:repeat(5,1fr);gap:15px;margin-bottom:20px;">
<div class="card" style="padding:18px;text-align:center;background:#F0FDF4;border:1px solid #BBF7D0;">
<div style="font-size:26px;font-weight:700;color:#059669;"><?= (int) ($summary['sale_count'] ?? 0) ?></div>
<div style="color:#6B7280;font-size:13px;">عدد المبيعات</div>
</div>
<div class="card" style="padding:18px;text-align:center;background:#ECFDF5;border:1px solid #A7F3D0;">
<div style="font-size:26px;font-weight:700;color:#059669;"><?= money($summary['total_revenue'] ?? 0) ?></div>
<div style="color:#6B7280;font-size:13px;">الإيرادات</div>
</div>
<div class="card" style="padding:18px;text-align:center;background:#FFFBEB;border:1px solid #FDE68A;">
<div style="font-size:26px;font-weight:700;color:#D97706;"><?= money($summary['total_discounts'] ?? 0) ?></div>
<div style="color:#6B7280;font-size:13px;">الخصومات</div>
</div>
<div class="card" style="padding:18px;text-align:center;background:#EFF6FF;border:1px solid #BFDBFE;">
<div style="font-size:26px;font-weight:700;color:#2563EB;"><?= money($summary['total_tax'] ?? 0) ?></div>
<div style="color:#6B7280;font-size:13px;">الضريبة</div>
</div>
<div class="card" style="padding:18px;text-align:center;background:#FEF2F2;border:1px solid #FECACA;">
<div style="font-size:26px;font-weight:700;color:#DC2626;"><?= money($summary['total_refunds'] ?? 0) ?></div>
<div style="color:#6B7280;font-size:13px;">المرتجعات</div>
</div>
</div>
<!-- Daily Breakdown Table -->
<?php if (!empty($dailyBreakdown)): ?>
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="calendar-days" style="width:20px;height:20px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;">التفصيل اليومي</h3>
</div>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>التاريخ</th>
<th>عدد المبيعات</th>
<th>الإيرادات</th>
<th>المرتجعات</th>
<th>الصافي</th>
</tr>
</thead>
<tbody>
<?php
$grandTotal = '0.00';
$grandRefunded = '0.00';
$grandNet = '0.00';
$grandCount = 0;
?>
<?php foreach ($dailyBreakdown as $day): ?>
<?php
$dayTotal = (string) ($day['total'] ?? 0);
$dayRefunded = (string) ($day['refunded'] ?? 0);
$dayNet = bcsub($dayTotal, $dayRefunded, 2);
$grandTotal = bcadd($grandTotal, $dayTotal, 2);
$grandRefunded = bcadd($grandRefunded, $dayRefunded, 2);
$grandNet = bcadd($grandNet, $dayNet, 2);
$grandCount += (int) ($day['cnt'] ?? 0);
?>
<tr>
<td style="font-weight:600;white-space:nowrap;"><?= e($day['sale_date'] ?? '—') ?></td>
<td style="font-weight:600;"><?= (int) ($day['cnt'] ?? 0) ?></td>
<td style="font-weight:700;color:#059669;direction:ltr;text-align:right;"><?= money($dayTotal) ?></td>
<td style="color:#DC2626;direction:ltr;text-align:right;"><?= money($dayRefunded) ?></td>
<td style="font-weight:700;color:#0D7377;direction:ltr;text-align:right;"><?= money($dayNet) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
<tfoot>
<tr style="background:#F0FDF4;font-weight:700;">
<td style="padding:12px 15px;">الإجمالي</td>
<td style="padding:12px 15px;"><?= $grandCount ?></td>
<td style="padding:12px 15px;direction:ltr;text-align:right;color:#059669;"><?= money($grandTotal) ?></td>
<td style="padding:12px 15px;direction:ltr;text-align:right;color:#DC2626;"><?= money($grandRefunded) ?></td>
<td style="padding:12px 15px;direction:ltr;text-align:right;font-size:16px;color:#0D7377;"><?= money($grandNet) ?></td>
</tr>
</tfoot>
</table>
</div>
</div>
<?php else: ?>
<div class="card" style="padding:60px 20px;text-align:center;">
<div style="margin-bottom:15px;">
<i data-lucide="calendar" style="width:48px;height:48px;color:#D1D5DB;"></i>
</div>
<h3 style="color:#6B7280;margin:0 0 8px;">لا توجد بيانات</h3>
<p style="color:#9CA3AF;font-size:14px;margin:0;">اختر الشهر لعرض تقرير المبيعات الشهري</p>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>المبيعات<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/sales/create" class="btn btn-primary"><i data-lucide="plus" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> مبيعة جديدة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$statusColors = [
'draft' => ['bg' => '#F3F4F6', 'color' => '#6B7280'],
'completed' => ['bg' => '#ECFDF5', 'color' => '#059669'],
'partially_refunded' => ['bg' => '#FFF7ED', 'color' => '#D97706'],
'fully_refunded' => ['bg' => '#FEE2E2', 'color' => '#DC2626'],
'voided' => ['bg' => '#FEE2E2', 'color' => '#DC2626'],
];
$customerTypeLabels = ['member' => 'عضو', 'player' => 'لاعب', 'guest' => 'زائر'];
?>
<!-- Search & Filters -->
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/sales" style="display:flex;gap:10px;flex-wrap:wrap;align-items:end;">
<div style="flex:1;min-width:180px;">
<label class="form-label" style="font-size:12px;">بحث</label>
<input type="text" name="q" value="<?= e($filters['q'] ?? '') ?>" placeholder="رقم الفاتورة، اسم العميل..." class="form-input">
</div>
<div style="min-width:140px;">
<label class="form-label" style="font-size:12px;">الحالة</label>
<select name="status" class="form-select">
<option value="">الكل</option>
<?php foreach ($statuses as $key => $label): ?>
<option value="<?= e($key) ?>" <?= ($filters['status'] ?? '') === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="min-width:130px;">
<label class="form-label" style="font-size:12px;">نوع العميل</label>
<select name="customer_type" class="form-select">
<option value="">الكل</option>
<?php foreach ($customerTypes as $key => $label): ?>
<option value="<?= e($key) ?>" <?= ($filters['customer_type'] ?? '') === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="min-width:140px;">
<label class="form-label" style="font-size:12px;">المخزن</label>
<select name="warehouse_id" class="form-select">
<option value="">الكل</option>
<?php foreach ($warehouses as $wh): ?>
<option value="<?= (int) $wh['id'] ?>" <?= ($filters['warehouse_id'] ?? '') == $wh['id'] ? 'selected' : '' ?>><?= e($wh['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="min-width:140px;">
<label class="form-label" style="font-size:12px;">من تاريخ</label>
<input type="date" name="date_from" value="<?= e($filters['date_from'] ?? '') ?>" class="form-input">
</div>
<div style="min-width:140px;">
<label class="form-label" style="font-size:12px;">إلى تاريخ</label>
<input type="date" name="date_to" value="<?= e($filters['date_to'] ?? '') ?>" class="form-input">
</div>
<button type="submit" class="btn btn-outline"><i data-lucide="search" style="width:16px;height:16px;vertical-align:middle;"></i> بحث</button>
<a href="/sales" class="btn btn-sm btn-outline" style="color:#6B7280;">مسح</a>
</form>
</div>
<!-- Sales Table -->
<?php if (!empty($sales)): ?>
<div class="card">
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>رقم الفاتورة</th>
<th>نوع العميل</th>
<th>المخزن</th>
<th>الإجمالي</th>
<th>الحالة</th>
<th>التاريخ</th>
<th>إجراءات</th>
</tr>
</thead>
<tbody>
<?php foreach ($sales as $sale): ?>
<?php
$st = $sale['status'] ?? 'draft';
$stColors = $statusColors[$st] ?? $statusColors['draft'];
$stLabel = $statuses[$st] ?? $st;
$ct = $sale['customer_type'] ?? 'guest';
$ctLabel = $customerTypeLabels[$ct] ?? $ct;
?>
<tr>
<td>
<a href="/sales/<?= (int) $sale['id'] ?>" style="color:#0D7377;font-weight:700;text-decoration:none;font-family:monospace;">
<?= e($sale['invoice_number'] ?? '') ?>
</a>
</td>
<td>
<span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;background:#F3F4F6;color:#6B7280;">
<?= e($ctLabel) ?>
</span>
</td>
<td style="font-size:13px;"><?= e($sale['warehouse_name'] ?? '—') ?></td>
<td style="font-weight:700;direction:ltr;text-align:left;"><?= money($sale['total_amount'] ?? 0) ?></td>
<td>
<span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;background:<?= $stColors['bg'] ?>;color:<?= $stColors['color'] ?>;">
<?= e($stLabel) ?>
</span>
</td>
<td style="font-size:13px;white-space:nowrap;"><?= e($sale['sale_date'] ?? '') ?></td>
<td>
<div style="display:flex;gap:6px;">
<a href="/sales/<?= (int) $sale['id'] ?>" class="btn btn-sm btn-outline" style="font-size:12px;padding:4px 10px;">
<i data-lucide="eye" style="width:13px;height:13px;vertical-align:middle;"></i> عرض
</a>
<a href="/sales/<?= (int) $sale['id'] ?>/print" target="_blank" class="btn btn-sm btn-outline" style="font-size:12px;padding:4px 10px;">
<i data-lucide="printer" style="width:13px;height:13px;vertical-align:middle;"></i>
</a>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php if (isset($pagination) && ($pagination['last_page'] ?? 1) > 1): ?>
<div style="padding:15px;">
<?php $__template->include('Shared.Components.pagination', ['pagination' => $pagination]); ?>
</div>
<?php endif; ?>
<?php else: ?>
<div class="card" style="padding:60px 20px;text-align:center;">
<div style="margin-bottom:15px;">
<i data-lucide="receipt" style="width:48px;height:48px;color:#D1D5DB;"></i>
</div>
<h3 style="color:#6B7280;margin:0 0 8px;">لا توجد مبيعات</h3>
<p style="color:#9CA3AF;font-size:14px;margin:0 0 20px;">
<?php if (!empty($filters['q']) || !empty($filters['status']) || !empty($filters['customer_type']) || !empty($filters['warehouse_id']) || !empty($filters['date_from']) || !empty($filters['date_to'])): ?>
لا توجد نتائج مطابقة لبحثك. جرب تغيير معايير البحث.
<?php else: ?>
ابدأ بإنشاء مبيعة جديدة من نقطة البيع.
<?php endif; ?>
</p>
<a href="/sales/create" class="btn btn-primary"><i data-lucide="plus" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> مبيعة جديدة</a>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>استرجاع مبيعة <?= e($sale['invoice_number'] ?? '') ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/sales/<?= (int) $sale['id'] ?>" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة للفاتورة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Sale Summary -->
<div class="card" style="margin-bottom:20px;background:linear-gradient(135deg, #FFF7ED10, #FFF7ED30);">
<div style="padding:15px 20px;display:flex;align-items:center;gap:15px;flex-wrap:wrap;">
<div>
<div style="font-size:12px;color:#6B7280;">رقم الفاتورة</div>
<div style="font-weight:700;font-family:monospace;color:#0D7377;font-size:16px;"><?= e($sale['invoice_number'] ?? '') ?></div>
</div>
<div style="width:1px;height:35px;background:#E5E7EB;"></div>
<div>
<div style="font-size:12px;color:#6B7280;">الإجمالي</div>
<div style="font-weight:700;font-size:16px;color:#059669;direction:ltr;text-align:left;"><?= money($sale['total_amount'] ?? 0) ?></div>
</div>
<?php if ((float) ($sale['amount_refunded'] ?? 0) > 0): ?>
<div style="width:1px;height:35px;background:#E5E7EB;"></div>
<div>
<div style="font-size:12px;color:#6B7280;">المسترجع سابقًا</div>
<div style="font-weight:700;font-size:16px;color:#DC2626;direction:ltr;text-align:left;"><?= money($sale['amount_refunded'] ?? 0) ?></div>
</div>
<?php endif; ?>
</div>
</div>
<form method="POST" action="/sales/<?= (int) $sale['id'] ?>/refund" id="refundForm">
<?= csrf_field() ?>
<!-- Items to Refund -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="package" style="width:18px;height:18px;color:#D97706;"></i>
<h3 style="margin:0;color:#D97706;font-size:15px;">اختر الأصناف المراد استرجاعها</h3>
</div>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th style="width:40px;">
<input type="checkbox" id="selectAll" onchange="toggleAll()" style="width:16px;height:16px;accent-color:#0D7377;">
</th>
<th>الصنف</th>
<th>الكمية المباعة</th>
<th>المسترجع سابقًا</th>
<th>المتاح للاسترجاع</th>
<th style="width:100px;">كمية الاسترجاع</th>
<th>سعر الوحدة</th>
<th>إجمالي الاسترجاع</th>
</tr>
</thead>
<tbody>
<?php foreach ($items as $i => $item): ?>
<?php
$qty = (float) ($item['quantity'] ?? 0);
$refundedQty = (float) ($item['refunded_quantity'] ?? 0);
$available = $qty - $refundedQty;
$unitPrice = (float) ($item['unit_price'] ?? 0);
?>
<tr id="itemRow_<?= $i ?>" style="<?= $available <= 0 ? 'opacity:0.4;' : '' ?>">
<td>
<?php if ($available > 0): ?>
<input type="checkbox" class="item-check" data-index="<?= $i ?>" onchange="recalcRefund()" style="width:16px;height:16px;accent-color:#0D7377;">
<input type="hidden" name="sale_item_ids[]" value="<?= (int) $item['id'] ?>" disabled>
<?php endif; ?>
</td>
<td style="font-weight:600;">
<?= e($item['item_name_ar'] ?? '') ?>
<?php if (!empty($item['sku'])): ?>
<div style="font-size:11px;color:#9CA3AF;font-family:monospace;"><?= e($item['sku']) ?></div>
<?php endif; ?>
</td>
<td style="font-weight:600;"><?= number_format($qty, 0) ?></td>
<td style="color:#D97706;font-weight:600;"><?= number_format($refundedQty, 0) ?></td>
<td style="font-weight:700;color:<?= $available > 0 ? '#059669' : '#DC2626' ?>;"><?= number_format($available, 0) ?></td>
<td>
<?php if ($available > 0): ?>
<input type="number" name="refund_quantities[]" class="refund-qty form-input" data-index="<?= $i ?>"
value="0" min="0" max="<?= (int) $available ?>" step="1"
style="width:80px;padding:6px;text-align:center;font-size:13px;" onchange="recalcRefund()" disabled>
<?php else: ?>
<span style="color:#9CA3AF;"></span>
<?php endif; ?>
</td>
<td style="direction:ltr;text-align:left;"><?= money($unitPrice) ?></td>
<td>
<span class="line-refund-total" data-index="<?= $i ?>" style="font-weight:700;direction:ltr;display:inline-block;">0.00</span>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<!-- Refund Details -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="settings" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">تفاصيل الاسترجاع</h3>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 2fr;gap:20px;">
<div class="form-group">
<label class="form-label">مبلغ الاسترجاع <span style="color:#DC2626;">*</span></label>
<input type="number" name="refund_amount" id="refundAmountInput" class="form-input" step="0.01" min="0" value="0" required style="direction:ltr;text-align:left;font-size:16px;font-weight:700;">
<div style="font-size:11px;color:#9CA3AF;margin-top:4px;">يتم حسابه تلقائيًا، ويمكنك تعديله يدويًا</div>
</div>
<div class="form-group">
<label class="form-label">سبب الاسترجاع <span style="color:#DC2626;">*</span></label>
<textarea name="reason" class="form-input" rows="2" required placeholder="أدخل سبب الاسترجاع..."><?= e(old('reason', '')) ?></textarea>
</div>
</div>
<div style="margin-top:15px;">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;padding:12px 16px;border:1px solid #E5E7EB;border-radius:8px;width:fit-content;">
<input type="hidden" name="return_to_stock" value="0">
<input type="checkbox" name="return_to_stock" value="1" checked style="width:18px;height:18px;accent-color:#0D7377;">
<div>
<div style="font-size:13px;font-weight:600;color:#1A1A2E;">إرجاع الكميات للمخزون</div>
<div style="font-size:11px;color:#9CA3AF;">سيتم إرجاع الكميات المسترجعة إلى المخزن تلقائيًا</div>
</div>
</label>
</div>
</div>
</div>
<!-- Submit -->
<div style="display:flex;gap:10px;justify-content:flex-start;">
<button type="submit" class="btn" style="background:#D97706;color:#fff;padding:12px 30px;font-size:15px;border:none;border-radius:8px;cursor:pointer;font-weight:600;font-family:inherit;">
<i data-lucide="rotate-ccw" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i>
تأكيد الاسترجاع
</button>
<a href="/sales/<?= (int) $sale['id'] ?>" class="btn btn-outline" style="padding:12px 30px;font-size:15px;">إلغاء</a>
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
});
var itemPrices = {};
<?php foreach ($items as $i => $item): ?>
<?php $available = (float) ($item['quantity'] ?? 0) - (float) ($item['refunded_quantity'] ?? 0); ?>
<?php if ($available > 0): ?>
itemPrices[<?= $i ?>] = <?= (float) ($item['unit_price'] ?? 0) ?>;
<?php endif; ?>
<?php endforeach; ?>
function toggleAll() {
var checked = document.getElementById('selectAll').checked;
document.querySelectorAll('.item-check').forEach(function(cb) {
cb.checked = checked;
toggleItemRow(cb);
});
recalcRefund();
}
document.querySelectorAll('.item-check').forEach(function(cb) {
cb.addEventListener('change', function() {
toggleItemRow(this);
recalcRefund();
});
});
function toggleItemRow(cb) {
var index = cb.getAttribute('data-index');
var row = document.getElementById('itemRow_' + index);
var qtyInput = row.querySelector('.refund-qty');
var hiddenInput = row.querySelector('input[name="sale_item_ids[]"]');
if (cb.checked) {
qtyInput.disabled = false;
hiddenInput.disabled = false;
if (parseInt(qtyInput.value) === 0) {
qtyInput.value = qtyInput.getAttribute('max');
}
} else {
qtyInput.disabled = true;
hiddenInput.disabled = true;
qtyInput.value = 0;
}
}
function recalcRefund() {
var totalRefund = 0;
document.querySelectorAll('.item-check').forEach(function(cb) {
var index = cb.getAttribute('data-index');
var qtyInput = document.querySelector('.refund-qty[data-index="' + index + '"]');
var lineDisplay = document.querySelector('.line-refund-total[data-index="' + index + '"]');
if (cb.checked && qtyInput) {
var qty = parseInt(qtyInput.value) || 0;
var lineTotal = qty * (itemPrices[index] || 0);
lineDisplay.textContent = lineTotal.toFixed(2);
totalRefund += lineTotal;
} else if (lineDisplay) {
lineDisplay.textContent = '0.00';
}
});
document.getElementById('refundAmountInput').value = totalRefund.toFixed(2);
}
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>فاتورة <?= e($sale['invoice_number'] ?? '') ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/sales/<?= (int) $sale['id'] ?>/print" target="_blank" class="btn btn-outline"><i data-lucide="printer" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> طباعة</a>
<?php if (($sale['status'] ?? '') === 'completed'): ?>
<button type="button" onclick="document.getElementById('voidSection').style.display='block'" class="btn btn-outline" style="color:#DC2626;border-color:#DC2626;">
<i data-lucide="ban" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> إلغاء
</button>
<a href="/sales/<?= (int) $sale['id'] ?>/refund" class="btn btn-outline" style="color:#D97706;border-color:#D97706;">
<i data-lucide="rotate-ccw" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> استرجاع
</a>
<?php endif; ?>
<?php if (($sale['status'] ?? '') === 'partially_refunded'): ?>
<a href="/sales/<?= (int) $sale['id'] ?>/refund" class="btn btn-outline" style="color:#D97706;border-color:#D97706;">
<i data-lucide="rotate-ccw" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> استرجاع إضافي
</a>
<?php endif; ?>
<a href="/sales" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة للقائمة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$statusColors = [
'draft' => ['bg' => '#F3F4F6', 'color' => '#6B7280'],
'completed' => ['bg' => '#ECFDF5', 'color' => '#059669'],
'partially_refunded' => ['bg' => '#FFF7ED', 'color' => '#D97706'],
'fully_refunded' => ['bg' => '#FEE2E2', 'color' => '#DC2626'],
'voided' => ['bg' => '#FEE2E2', 'color' => '#DC2626'],
];
$statusLabels = \App\Modules\Sales\Models\Sale::getStatuses();
$customerTypeLabels = ['member' => 'عضو', 'player' => 'لاعب', 'guest' => 'زائر'];
$paymentMethodLabels = ['cash' => 'نقدي', 'visa' => 'فيزا', 'transfer' => 'تحويل بنكي'];
$st = $sale['status'] ?? 'draft';
$stColors = $statusColors[$st] ?? $statusColors['draft'];
?>
<!-- Sale Info Card -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="file-text" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">بيانات الفاتورة</h3>
<div style="margin-right:auto;">
<span style="display:inline-block;padding:3px 12px;border-radius:10px;font-size:12px;font-weight:600;background:<?= $stColors['bg'] ?>;color:<?= $stColors['color'] ?>;">
<?= e($statusLabels[$st] ?? $st) ?>
</span>
</div>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(200px, 1fr));gap:20px;">
<div>
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">رقم الفاتورة</div>
<div style="font-weight:700;font-family:monospace;font-size:16px;color:#0D7377;"><?= e($sale['invoice_number'] ?? '') ?></div>
</div>
<div>
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">التاريخ</div>
<div style="font-weight:600;"><?= e($sale['sale_date'] ?? $sale['created_at'] ?? '') ?></div>
</div>
<div>
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">العميل</div>
<div style="font-weight:600;">
<span style="display:inline-block;padding:2px 8px;border-radius:8px;font-size:11px;font-weight:600;background:#F3F4F6;color:#6B7280;margin-left:6px;">
<?= e($customerTypeLabels[$sale['customer_type'] ?? 'guest'] ?? '') ?>
</span>
<?= e($customerName ?? '—') ?>
</div>
</div>
<div>
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">المخزن</div>
<div style="font-weight:600;"><?= e($sale['warehouse_name'] ?? '—') ?></div>
</div>
<div>
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">طريقة الدفع</div>
<div style="font-weight:600;"><?= e($paymentMethodLabels[$sale['payment_method'] ?? ''] ?? ($sale['payment_method'] ?? '—')) ?></div>
</div>
<div>
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">الحالة</div>
<div>
<span style="display:inline-block;padding:3px 12px;border-radius:10px;font-size:12px;font-weight:600;background:<?= $stColors['bg'] ?>;color:<?= $stColors['color'] ?>;">
<?= e($statusLabels[$st] ?? $st) ?>
</span>
</div>
</div>
</div>
<?php if (!empty($sale['notes'])): ?>
<div style="margin-top:15px;padding-top:12px;border-top:1px solid #F3F4F6;">
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">ملاحظات</div>
<div style="font-size:14px;color:#4B5563;"><?= e($sale['notes']) ?></div>
</div>
<?php endif; ?>
</div>
</div>
<!-- Items Table -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="package" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">الأصناف</h3>
</div>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>الصنف</th>
<th>الكمية</th>
<th>السعر</th>
<th>الضريبة</th>
<th>الإجمالي</th>
</tr>
</thead>
<tbody>
<?php foreach ($items as $item): ?>
<tr>
<td style="font-weight:600;">
<?= e($item['item_name_ar'] ?? '') ?>
<?php if (!empty($item['sku'])): ?>
<div style="font-size:11px;color:#9CA3AF;font-family:monospace;"><?= e($item['sku']) ?></div>
<?php endif; ?>
<?php if (!empty($item['package_id'])): ?>
<span style="display:inline-block;font-size:10px;color:#D97706;background:#FFF7ED;padding:1px 6px;border-radius:6px;margin-top:2px;">باقة</span>
<?php endif; ?>
</td>
<td style="font-weight:600;"><?= e($item['quantity'] ?? '0') ?></td>
<td style="direction:ltr;text-align:left;"><?= money($item['unit_price'] ?? 0) ?></td>
<td style="direction:ltr;text-align:left;color:#D97706;"><?= money($item['tax_amount'] ?? 0) ?></td>
<td style="direction:ltr;text-align:left;font-weight:700;"><?= money($item['line_total'] ?? 0) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<!-- Totals Card -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:20px;">
<div style="max-width:320px;margin-right:auto;">
<div style="display:flex;justify-content:space-between;padding:8px 0;font-size:14px;color:#6B7280;">
<span>المجموع الفرعي</span>
<span style="font-weight:600;direction:ltr;"><?= money($sale['subtotal'] ?? 0) ?></span>
</div>
<?php if ((float) ($sale['discount_amount'] ?? 0) > 0): ?>
<div style="display:flex;justify-content:space-between;padding:8px 0;font-size:14px;color:#DC2626;">
<span>الخصم <?= !empty($sale['discount_reason']) ? '(' . e($sale['discount_reason']) . ')' : '' ?></span>
<span style="font-weight:600;direction:ltr;">- <?= money($sale['discount_amount']) ?></span>
</div>
<?php endif; ?>
<?php if ((float) ($sale['tax_amount'] ?? 0) > 0): ?>
<div style="display:flex;justify-content:space-between;padding:8px 0;font-size:14px;color:#D97706;">
<span>الضريبة</span>
<span style="font-weight:600;direction:ltr;"><?= money($sale['tax_amount']) ?></span>
</div>
<?php endif; ?>
<div style="display:flex;justify-content:space-between;padding:12px 0;margin-top:8px;border-top:2px solid #0D7377;font-size:20px;font-weight:800;color:#0D7377;">
<span>الإجمالي</span>
<span style="direction:ltr;"><?= money($sale['total_amount'] ?? 0) ?></span>
</div>
<?php if ((float) ($sale['amount_refunded'] ?? 0) > 0): ?>
<div style="display:flex;justify-content:space-between;padding:8px 0;font-size:14px;color:#DC2626;">
<span>المبلغ المسترجع</span>
<span style="font-weight:600;direction:ltr;">- <?= money($sale['amount_refunded']) ?></span>
</div>
<div style="display:flex;justify-content:space-between;padding:8px 0;font-size:16px;font-weight:700;color:#059669;">
<span>الصافي</span>
<span style="direction:ltr;"><?= money(bcsub((string) ($sale['total_amount'] ?? '0'), (string) ($sale['amount_refunded'] ?? '0'), 2)) ?></span>
</div>
<?php endif; ?>
</div>
</div>
</div>
<!-- Void Section (Hidden) -->
<?php if (($sale['status'] ?? '') === 'completed'): ?>
<div id="voidSection" class="card" style="margin-bottom:20px;display:none;border:2px solid #DC2626;">
<div style="padding:15px 20px;border-bottom:1px solid #FEE2E2;background:#FEF2F2;display:flex;align-items:center;gap:8px;">
<i data-lucide="alert-triangle" style="width:18px;height:18px;color:#DC2626;"></i>
<h3 style="margin:0;color:#DC2626;font-size:15px;">إلغاء الفاتورة</h3>
</div>
<div style="padding:20px;">
<p style="color:#6B7280;font-size:14px;margin-bottom:15px;">هل أنت متأكد من إلغاء هذه الفاتورة؟ سيتم إرجاع الكميات للمخزون تلقائيًا.</p>
<form method="POST" action="/sales/<?= (int) $sale['id'] ?>/void">
<?= csrf_field() ?>
<div class="form-group" style="margin-bottom:15px;">
<label class="form-label">سبب الإلغاء <span style="color:#DC2626;">*</span></label>
<textarea name="reason" class="form-input" rows="3" required placeholder="أدخل سبب إلغاء الفاتورة..."></textarea>
</div>
<div style="display:flex;gap:10px;">
<button type="submit" class="btn" style="background:#DC2626;color:#fff;padding:10px 25px;border:none;border-radius:8px;cursor:pointer;font-weight:600;font-family:inherit;">
<i data-lucide="ban" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> تأكيد الإلغاء
</button>
<button type="button" onclick="document.getElementById('voidSection').style.display='none'" class="btn btn-outline" style="padding:10px 25px;">
تراجع
</button>
</div>
</form>
</div>
</div>
<?php endif; ?>
<!-- Refunds List -->
<?php if (!empty($refunds)): ?>
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="rotate-ccw" style="width:18px;height:18px;color:#D97706;"></i>
<h3 style="margin:0;color:#D97706;font-size:15px;">المرتجعات</h3>
</div>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>رقم المرتجع</th>
<th>المبلغ</th>
<th>السبب</th>
<th>إعادة للمخزون</th>
<th>التاريخ</th>
</tr>
</thead>
<tbody>
<?php foreach ($refunds as $refund): ?>
<tr>
<td style="font-weight:600;font-family:monospace;"><?= e($refund['refund_number'] ?? '') ?></td>
<td style="font-weight:700;color:#DC2626;direction:ltr;text-align:left;"><?= money($refund['refund_amount'] ?? 0) ?></td>
<td style="font-size:13px;color:#6B7280;"><?= e($refund['reason'] ?? '—') ?></td>
<td>
<?php if ((int) ($refund['return_to_stock'] ?? 0)): ?>
<span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;background:#ECFDF5;color:#059669;">نعم</span>
<?php else: ?>
<span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;background:#F3F4F6;color:#6B7280;">لا</span>
<?php endif; ?>
</td>
<td style="font-size:13px;white-space:nowrap;"><?= e($refund['created_at'] ?? '') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php
declare(strict_types=1);
use App\Core\Registries\MenuRegistry;
use App\Core\Registries\PermissionRegistry;
// ────────────────────────────────────────────────────────────
// Sales — Permissions
// ────────────────────────────────────────────────────────────
PermissionRegistry::register('sales', [
'sales.view' => ['ar' => 'عرض المبيعات', 'en' => 'View Sales'],
'sales.create' => ['ar' => 'إنشاء عملية بيع', 'en' => 'Create Sale'],
'sales.void' => ['ar' => 'إلغاء عملية بيع', 'en' => 'Void Sale'],
'sales.refund' => ['ar' => 'استرجاع مبيعات', 'en' => 'Refund Sale'],
'package.view' => ['ar' => 'عرض الباقات', 'en' => 'View Packages'],
'package.manage'=> ['ar' => 'إدارة الباقات', 'en' => 'Manage Packages'],
'report.sales' => ['ar' => 'تقارير المبيعات', 'en' => 'Sales Reports'],
]);
// ────────────────────────────────────────────────────────────
// Sales — Sidebar menu
// ────────────────────────────────────────────────────────────
MenuRegistry::register('sales', [
'label_ar' => 'المبيعات',
'label_en' => 'Sales & POS',
'icon' => 'shopping-cart',
'route' => '/sales',
'permission' => 'sales.view',
'parent' => null,
'order' => 710,
'children' => [
['label_ar' => 'نقطة البيع', 'label_en' => 'POS', 'route' => '/sales/create', 'permission' => 'sales.create', 'order' => 1],
['label_ar' => 'كل المبيعات', 'label_en' => 'All Sales', 'route' => '/sales', 'permission' => 'sales.view', 'order' => 2],
['label_ar' => 'الباقات', 'label_en' => 'Packages', 'route' => '/sales/packages', 'permission' => 'package.view', 'order' => 3],
['label_ar' => 'تقرير يومي', 'label_en' => 'Daily Report', 'route' => '/sales/reports/daily', 'permission' => 'report.sales', 'order' => 4],
['label_ar' => 'تقرير شهري', 'label_en' => 'Monthly Report', 'route' => '/sales/reports/monthly', 'permission' => 'report.sales', 'order' => 5],
],
]);
<?php
declare(strict_types=1);
namespace CronJobs;
use App\Core\Database;
use App\Core\Logger;
/**
* Daily: flags expired batches and alerts near-expiry items.
*/
class ExpiryAlertJob
{
private Database $db;
public function __construct(Database $db) { $this->db = $db; }
public function shouldRun(): bool
{
return true; // Runs daily
}
public function run(): array
{
// Bootstrap the app for service access
$app = \App\Core\App::getInstance();
$expired = \App\Modules\Inventory\Services\BatchService::flagExpiredBatches();
$nearExpiry = \App\Modules\Inventory\Services\BatchService::alertNearExpiry(30);
Logger::info("Expiry alert job: {$expired} expired, {$nearExpiry} near-expiry batches flagged");
return [
'expired_flagged' => $expired,
'near_expiry_alerted' => $nearExpiry,
];
}
}
<?php
declare(strict_types=1);
namespace CronJobs;
use App\Core\Database;
use App\Core\Logger;
use App\Core\EventBus;
/**
* Daily: alerts items below minimum stock level.
*/
class LowStockAlertJob
{
private Database $db;
public function __construct(Database $db) { $this->db = $db; }
public function shouldRun(): bool
{
return true; // Runs daily
}
public function run(): array
{
$lowStockItems = \App\Modules\Inventory\Models\ItemWarehouseStock::getLowStockItems();
$count = count($lowStockItems);
foreach ($lowStockItems as $item) {
EventBus::dispatch('inventory.low_stock_alert', [
'item_id' => (int) $item['item_id'],
'warehouse_id' => (int) $item['warehouse_id'],
'item_name' => $item['name_ar'] ?? '',
'quantity' => $item['quantity'],
'min_level' => $item['min_level'],
]);
}
if ($count > 0) {
Logger::info("Low stock alert: {$count} item(s) below minimum level");
}
return ['low_stock_items' => $count];
}
}
<?php
declare(strict_types=1);
namespace CronJobs;
use App\Core\Database;
use App\Core\Logger;
/**
* Monthly: runs depreciation for all active assets.
* Runs on the 1st of each month.
*/
class MonthlyDepreciationJob
{
private Database $db;
public function __construct(Database $db) { $this->db = $db; }
public function shouldRun(): bool
{
return (int) date('j') === 1;
}
public function run(): array
{
$month = date('Y-m');
$count = \App\Modules\Inventory\Services\DepreciationService::runMonthlyDepreciation($month);
Logger::info("Monthly depreciation for {$month}: {$count} asset(s) processed");
return [
'period_month' => $month,
'assets_processed' => $count,
];
}
}
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `warehouses` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`code` VARCHAR(20) NOT NULL,
`name_ar` VARCHAR(200) NOT NULL,
`name_en` VARCHAR(200) NULL,
`warehouse_type` VARCHAR(30) NOT NULL DEFAULT 'general' COMMENT 'general, sports_shop, cafeteria, gym, medical, maintenance',
`location_ar` VARCHAR(300) NULL,
`keeper_employee_id` BIGINT UNSIGNED NULL,
`capacity_units` INT UNSIGNED NULL,
`branch_id` BIGINT UNSIGNED NULL,
`phone` VARCHAR(30) NULL,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`notes` TEXT NULL,
`is_archived` TINYINT(1) NOT NULL DEFAULT 0,
`archived_at` TIMESTAMP NULL DEFAULT NULL,
`archived_by` BIGINT UNSIGNED NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
`updated_by` BIGINT UNSIGNED NULL,
UNIQUE KEY `uq_warehouses_code` (`code`),
INDEX `idx_warehouses_type` (`warehouse_type`),
INDEX `idx_warehouses_branch` (`branch_id`),
INDEX `idx_warehouses_keeper` (`keeper_employee_id`),
INDEX `idx_warehouses_active` (`is_active`),
CONSTRAINT `fk_warehouses_keeper` FOREIGN KEY (`keeper_employee_id`) REFERENCES `employees` (`id`) ON DELETE SET NULL,
CONSTRAINT `fk_warehouses_branch` FOREIGN KEY (`branch_id`) REFERENCES `branches` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `warehouses`",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `item_categories` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`parent_id` BIGINT UNSIGNED NULL,
`code` VARCHAR(30) NOT NULL,
`name_ar` VARCHAR(200) NOT NULL,
`name_en` VARCHAR(200) NULL,
`description_ar` TEXT NULL,
`sort_order` INT NOT NULL DEFAULT 0,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`is_archived` TINYINT(1) NOT NULL DEFAULT 0,
`archived_at` TIMESTAMP NULL DEFAULT NULL,
`archived_by` BIGINT UNSIGNED NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
`updated_by` BIGINT UNSIGNED NULL,
UNIQUE KEY `uq_item_categories_code` (`code`),
INDEX `idx_item_categories_parent` (`parent_id`),
INDEX `idx_item_categories_active` (`is_active`),
CONSTRAINT `fk_item_categories_parent` FOREIGN KEY (`parent_id`) REFERENCES `item_categories` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `item_categories`",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `inventory_items` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`category_id` BIGINT UNSIGNED NULL,
`sku` VARCHAR(50) NOT NULL,
`barcode` VARCHAR(100) NULL,
`name_ar` VARCHAR(300) NOT NULL,
`name_en` VARCHAR(300) NULL,
`description_ar` TEXT NULL,
`unit_of_measure` VARCHAR(30) NOT NULL DEFAULT 'piece' COMMENT 'piece, kg, liter, box, pack, meter, set',
`tracking_type` VARCHAR(20) NOT NULL DEFAULT 'standard' COMMENT 'standard, expiry, asset',
`cost_price` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`sale_price_member` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`sale_price_nonmember` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`sale_price_player` DECIMAL(15,2) NULL,
`tax_rate` DECIMAL(5,2) NOT NULL DEFAULT 0.00,
`is_sellable` TINYINT(1) NOT NULL DEFAULT 1,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`image_document_id` BIGINT UNSIGNED NULL,
`notes` TEXT NULL,
`is_archived` TINYINT(1) NOT NULL DEFAULT 0,
`archived_at` TIMESTAMP NULL DEFAULT NULL,
`archived_by` BIGINT UNSIGNED NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
`updated_by` BIGINT UNSIGNED NULL,
UNIQUE KEY `uq_inventory_items_sku` (`sku`),
INDEX `idx_inventory_items_category` (`category_id`),
INDEX `idx_inventory_items_barcode` (`barcode`),
INDEX `idx_inventory_items_tracking` (`tracking_type`),
INDEX `idx_inventory_items_active` (`is_active`),
INDEX `idx_inventory_items_sellable` (`is_sellable`),
CONSTRAINT `fk_inventory_items_category` FOREIGN KEY (`category_id`) REFERENCES `item_categories` (`id`) ON DELETE SET NULL,
CONSTRAINT `fk_inventory_items_image` FOREIGN KEY (`image_document_id`) REFERENCES `documents` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `inventory_items`",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `item_warehouse_stock` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`item_id` BIGINT UNSIGNED NOT NULL,
`warehouse_id` BIGINT UNSIGNED NOT NULL,
`quantity` DECIMAL(15,3) NOT NULL DEFAULT 0.000,
`min_level` DECIMAL(15,3) NULL,
`max_level` DECIMAL(15,3) NULL,
`last_counted_at` TIMESTAMP NULL,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY `uq_item_warehouse_stock_item_warehouse` (`item_id`, `warehouse_id`),
INDEX `idx_item_warehouse_stock_warehouse` (`warehouse_id`),
INDEX `idx_item_warehouse_stock_quantity` (`quantity`),
CONSTRAINT `fk_item_warehouse_stock_item` FOREIGN KEY (`item_id`) REFERENCES `inventory_items` (`id`) ON DELETE CASCADE,
CONSTRAINT `fk_item_warehouse_stock_warehouse` FOREIGN KEY (`warehouse_id`) REFERENCES `warehouses` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `item_warehouse_stock`",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `stock_movements` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`warehouse_id` BIGINT UNSIGNED NOT NULL,
`item_id` BIGINT UNSIGNED NOT NULL,
`movement_type` VARCHAR(30) NOT NULL COMMENT 'purchase_in, transfer_in, return_in, adjustment_in, opening_balance, sale_out, transfer_out, damage_out, expired_out, consumed_out, adjustment_out',
`direction` ENUM('in','out') NOT NULL,
`quantity` DECIMAL(15,3) NOT NULL,
`unit_cost` DECIMAL(15,2) NULL,
`total_cost` DECIMAL(15,2) NULL,
`batch_id` BIGINT UNSIGNED NULL,
`reference_type` VARCHAR(50) NULL COMMENT 'purchase_orders, sales, stock_transfers, stock_audits',
`reference_id` BIGINT UNSIGNED NULL,
`transfer_id` BIGINT UNSIGNED NULL,
`notes` TEXT NULL,
`movement_date` DATE NOT NULL,
`branch_id` BIGINT UNSIGNED NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
INDEX `idx_stock_movements_warehouse` (`warehouse_id`),
INDEX `idx_stock_movements_item` (`item_id`),
INDEX `idx_stock_movements_type` (`movement_type`),
INDEX `idx_stock_movements_direction` (`direction`),
INDEX `idx_stock_movements_date` (`movement_date`),
INDEX `idx_stock_movements_batch` (`batch_id`),
INDEX `idx_stock_movements_reference` (`reference_type`, `reference_id`),
INDEX `idx_stock_movements_transfer` (`transfer_id`),
INDEX `idx_stock_movements_item_date` (`item_id`, `movement_date`),
INDEX `idx_stock_movements_warehouse_date` (`warehouse_id`, `movement_date`),
CONSTRAINT `fk_stock_movements_warehouse` FOREIGN KEY (`warehouse_id`) REFERENCES `warehouses` (`id`) ON DELETE RESTRICT,
CONSTRAINT `fk_stock_movements_item` FOREIGN KEY (`item_id`) REFERENCES `inventory_items` (`id`) ON DELETE RESTRICT,
CONSTRAINT `chk_stock_movements_quantity` CHECK (`quantity` > 0)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `stock_movements`",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `item_batches` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`item_id` BIGINT UNSIGNED NOT NULL,
`warehouse_id` BIGINT UNSIGNED NOT NULL,
`batch_number` VARCHAR(100) NOT NULL,
`expiry_date` DATE NOT NULL,
`quantity` DECIMAL(15,3) NOT NULL DEFAULT 0.000,
`initial_quantity` DECIMAL(15,3) NOT NULL,
`unit_cost` DECIMAL(15,2) NULL,
`received_date` DATE NOT NULL,
`status` VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT 'active, expired, depleted',
`notes` TEXT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
INDEX `idx_item_batches_item` (`item_id`),
INDEX `idx_item_batches_warehouse` (`warehouse_id`),
INDEX `idx_item_batches_expiry` (`expiry_date`),
INDEX `idx_item_batches_status` (`status`),
INDEX `idx_item_batches_item_warehouse` (`item_id`, `warehouse_id`),
INDEX `idx_item_batches_batch_number` (`batch_number`),
CONSTRAINT `fk_item_batches_item` FOREIGN KEY (`item_id`) REFERENCES `inventory_items` (`id`) ON DELETE CASCADE,
CONSTRAINT `fk_item_batches_warehouse` FOREIGN KEY (`warehouse_id`) REFERENCES `warehouses` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `item_batches`",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `asset_register` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`item_id` BIGINT UNSIGNED NOT NULL,
`warehouse_id` BIGINT UNSIGNED NOT NULL,
`asset_tag` VARCHAR(50) NOT NULL,
`serial_number` VARCHAR(100) NULL,
`purchase_date` DATE NOT NULL,
`purchase_cost` DECIMAL(15,2) NOT NULL,
`useful_life_months` INT UNSIGNED NOT NULL DEFAULT 60,
`salvage_value` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`depreciation_method` VARCHAR(30) NOT NULL DEFAULT 'straight_line' COMMENT 'straight_line, declining_balance',
`declining_rate` DECIMAL(5,2) NULL,
`accumulated_depreciation` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`book_value` DECIMAL(15,2) NOT NULL,
`status` VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT 'active, disposed, transferred, under_maintenance',
`disposed_at` DATE NULL,
`disposal_value` DECIMAL(15,2) NULL,
`disposal_reason` TEXT NULL,
`condition_notes` TEXT NULL,
`last_depreciation_date` DATE NULL,
`branch_id` BIGINT UNSIGNED NULL,
`is_archived` TINYINT(1) NOT NULL DEFAULT 0,
`archived_at` TIMESTAMP NULL DEFAULT NULL,
`archived_by` BIGINT UNSIGNED NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
`updated_by` BIGINT UNSIGNED NULL,
UNIQUE KEY `uq_asset_register_asset_tag` (`asset_tag`),
INDEX `idx_asset_register_item` (`item_id`),
INDEX `idx_asset_register_warehouse` (`warehouse_id`),
INDEX `idx_asset_register_status` (`status`),
INDEX `idx_asset_register_purchase_date` (`purchase_date`),
INDEX `idx_asset_register_method` (`depreciation_method`),
CONSTRAINT `fk_asset_register_item` FOREIGN KEY (`item_id`) REFERENCES `inventory_items` (`id`) ON DELETE RESTRICT,
CONSTRAINT `fk_asset_register_warehouse` FOREIGN KEY (`warehouse_id`) REFERENCES `warehouses` (`id`) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `asset_register`",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `depreciation_entries` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`asset_id` BIGINT UNSIGNED NOT NULL,
`period_month` VARCHAR(7) NOT NULL COMMENT 'YYYY-MM',
`depreciation_amount` DECIMAL(15,2) NOT NULL,
`accumulated_total` DECIMAL(15,2) NOT NULL,
`book_value_after` DECIMAL(15,2) NOT NULL,
`method_used` VARCHAR(30) NOT NULL,
`is_manual` TINYINT(1) NOT NULL DEFAULT 0,
`notes` TEXT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
UNIQUE KEY `uq_dep_entries_asset_period` (`asset_id`, `period_month`),
INDEX `idx_dep_entries_period` (`period_month`),
CONSTRAINT `fk_dep_entries_asset` FOREIGN KEY (`asset_id`) REFERENCES `asset_register`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `depreciation_entries`",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `stock_transfers` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`transfer_number` VARCHAR(50) NOT NULL,
`from_warehouse_id` BIGINT UNSIGNED NOT NULL,
`to_warehouse_id` BIGINT UNSIGNED NOT NULL,
`status` VARCHAR(20) NOT NULL DEFAULT 'draft' COMMENT 'draft, pending_approval, approved, in_transit, received, cancelled',
`requested_by` BIGINT UNSIGNED NULL,
`approved_by` BIGINT UNSIGNED NULL,
`approved_at` TIMESTAMP NULL DEFAULT NULL,
`received_by` BIGINT UNSIGNED NULL,
`received_at` TIMESTAMP NULL DEFAULT NULL,
`transfer_date` DATE NOT NULL,
`notes` TEXT NULL,
`branch_id` BIGINT UNSIGNED NULL,
`is_archived` TINYINT(1) NOT NULL DEFAULT 0,
`archived_at` TIMESTAMP NULL DEFAULT NULL,
`archived_by` BIGINT UNSIGNED NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
`updated_by` BIGINT UNSIGNED NULL,
UNIQUE KEY `uq_stock_transfers_number` (`transfer_number`),
INDEX `idx_stock_transfers_from` (`from_warehouse_id`),
INDEX `idx_stock_transfers_to` (`to_warehouse_id`),
INDEX `idx_stock_transfers_status` (`status`),
INDEX `idx_stock_transfers_date` (`transfer_date`),
CONSTRAINT `fk_st_from_warehouse` FOREIGN KEY (`from_warehouse_id`) REFERENCES `warehouses`(`id`) ON DELETE RESTRICT,
CONSTRAINT `fk_st_to_warehouse` FOREIGN KEY (`to_warehouse_id`) REFERENCES `warehouses`(`id`) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `stock_transfers`",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `stock_transfer_items` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`transfer_id` BIGINT UNSIGNED NOT NULL,
`item_id` BIGINT UNSIGNED NOT NULL,
`quantity` DECIMAL(15,3) NOT NULL,
`batch_id` BIGINT UNSIGNED NULL,
`notes` TEXT NULL,
INDEX `idx_sti_transfer` (`transfer_id`),
INDEX `idx_sti_item` (`item_id`),
CONSTRAINT `fk_sti_transfer` FOREIGN KEY (`transfer_id`) REFERENCES `stock_transfers`(`id`) ON DELETE CASCADE,
CONSTRAINT `fk_sti_item` FOREIGN KEY (`item_id`) REFERENCES `inventory_items`(`id`) ON DELETE RESTRICT,
CONSTRAINT `fk_sti_batch` FOREIGN KEY (`batch_id`) REFERENCES `item_batches`(`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `stock_transfer_items`",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `suppliers` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`code` VARCHAR(30) NOT NULL,
`name_ar` VARCHAR(300) NOT NULL,
`name_en` VARCHAR(300) NULL,
`contact_person` VARCHAR(200) NULL,
`phone` VARCHAR(30) NULL,
`email` VARCHAR(200) NULL,
`address` TEXT NULL,
`tax_number` VARCHAR(50) NULL,
`commercial_register` VARCHAR(50) NULL,
`payment_terms` VARCHAR(100) NULL,
`rating` TINYINT UNSIGNED NULL COMMENT '1-5',
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`notes` TEXT NULL,
`is_archived` TINYINT(1) NOT NULL DEFAULT 0,
`archived_at` TIMESTAMP NULL DEFAULT NULL,
`archived_by` BIGINT UNSIGNED NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
`updated_by` BIGINT UNSIGNED NULL,
UNIQUE KEY `uq_suppliers_code` (`code`),
INDEX `idx_suppliers_active` (`is_active`),
INDEX `idx_suppliers_name` (`name_ar`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `suppliers`",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `purchase_orders` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`po_number` VARCHAR(50) NOT NULL,
`supplier_id` BIGINT UNSIGNED NOT NULL,
`warehouse_id` BIGINT UNSIGNED NOT NULL,
`status` VARCHAR(20) NOT NULL DEFAULT 'draft' COMMENT 'draft, submitted, approved, partially_received, received, cancelled',
`order_date` DATE NOT NULL,
`expected_delivery` DATE NULL,
`subtotal` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`tax_amount` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`discount_amount` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`total_amount` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`currency` VARCHAR(3) NOT NULL DEFAULT 'EGP',
`payment_terms` VARCHAR(100) NULL,
`submitted_by` BIGINT UNSIGNED NULL,
`submitted_at` TIMESTAMP NULL DEFAULT NULL,
`approved_by` BIGINT UNSIGNED NULL,
`approved_at` TIMESTAMP NULL DEFAULT NULL,
`notes` TEXT NULL,
`branch_id` BIGINT UNSIGNED NULL,
`is_archived` TINYINT(1) NOT NULL DEFAULT 0,
`archived_at` TIMESTAMP NULL DEFAULT NULL,
`archived_by` BIGINT UNSIGNED NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
`updated_by` BIGINT UNSIGNED NULL,
UNIQUE KEY `uq_purchase_orders_number` (`po_number`),
INDEX `idx_po_supplier` (`supplier_id`),
INDEX `idx_po_warehouse` (`warehouse_id`),
INDEX `idx_po_status` (`status`),
INDEX `idx_po_date` (`order_date`),
CONSTRAINT `fk_po_supplier` FOREIGN KEY (`supplier_id`) REFERENCES `suppliers`(`id`) ON DELETE RESTRICT,
CONSTRAINT `fk_po_warehouse` FOREIGN KEY (`warehouse_id`) REFERENCES `warehouses`(`id`) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `purchase_orders`",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `purchase_order_items` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`purchase_order_id` BIGINT UNSIGNED NOT NULL,
`item_id` BIGINT UNSIGNED NOT NULL,
`quantity_ordered` DECIMAL(15,3) NOT NULL,
`quantity_received` DECIMAL(15,3) NOT NULL DEFAULT 0.000,
`unit_cost` DECIMAL(15,2) NOT NULL,
`tax_rate` DECIMAL(5,2) NOT NULL DEFAULT 0.00,
`line_total` DECIMAL(15,2) NOT NULL,
`notes` TEXT NULL,
INDEX `idx_poi_po` (`purchase_order_id`),
INDEX `idx_poi_item` (`item_id`),
CONSTRAINT `fk_poi_po` FOREIGN KEY (`purchase_order_id`) REFERENCES `purchase_orders`(`id`) ON DELETE CASCADE,
CONSTRAINT `fk_poi_item` FOREIGN KEY (`item_id`) REFERENCES `inventory_items`(`id`) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `purchase_order_items`",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `stock_audits` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`audit_number` VARCHAR(50) NOT NULL,
`warehouse_id` BIGINT UNSIGNED NOT NULL,
`audit_type` VARCHAR(20) NOT NULL COMMENT 'full, partial, spot_check',
`category_id` BIGINT UNSIGNED NULL,
`status` VARCHAR(20) NOT NULL DEFAULT 'draft' COMMENT 'draft, in_progress, pending_approval, approved, cancelled',
`audit_date` DATE NOT NULL,
`started_at` TIMESTAMP NULL DEFAULT NULL,
`completed_at` TIMESTAMP NULL DEFAULT NULL,
`approved_by` BIGINT UNSIGNED NULL,
`approved_at` TIMESTAMP NULL DEFAULT NULL,
`total_items` INT UNSIGNED NOT NULL DEFAULT 0,
`items_counted` INT UNSIGNED NOT NULL DEFAULT 0,
`variance_count` INT UNSIGNED NOT NULL DEFAULT 0,
`notes` TEXT NULL,
`branch_id` BIGINT UNSIGNED NULL,
`is_archived` TINYINT(1) NOT NULL DEFAULT 0,
`archived_at` TIMESTAMP NULL DEFAULT NULL,
`archived_by` BIGINT UNSIGNED NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
`updated_by` BIGINT UNSIGNED NULL,
UNIQUE KEY `uq_stock_audits_number` (`audit_number`),
INDEX `idx_sa_warehouse` (`warehouse_id`),
INDEX `idx_sa_status` (`status`),
INDEX `idx_sa_type` (`audit_type`),
INDEX `idx_sa_date` (`audit_date`),
CONSTRAINT `fk_sa_warehouse` FOREIGN KEY (`warehouse_id`) REFERENCES `warehouses`(`id`) ON DELETE RESTRICT,
CONSTRAINT `fk_sa_category` FOREIGN KEY (`category_id`) REFERENCES `item_categories`(`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `stock_audits`",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `stock_audit_items` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`audit_id` BIGINT UNSIGNED NOT NULL,
`item_id` BIGINT UNSIGNED NOT NULL,
`batch_id` BIGINT UNSIGNED NULL,
`system_quantity` DECIMAL(15,3) NOT NULL,
`physical_quantity` DECIMAL(15,3) NULL,
`variance` DECIMAL(15,3) NULL,
`variance_cost` DECIMAL(15,2) NULL,
`status` VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT 'pending, counted, approved, skipped',
`counted_by` BIGINT UNSIGNED NULL,
`counted_at` TIMESTAMP NULL DEFAULT NULL,
`notes` TEXT NULL,
INDEX `idx_stock_audit_items_audit` (`audit_id`),
INDEX `idx_stock_audit_items_item` (`item_id`),
INDEX `idx_stock_audit_items_status` (`status`),
CONSTRAINT `fk_stock_audit_items_audit` FOREIGN KEY (`audit_id`) REFERENCES `stock_audits`(`id`) ON DELETE CASCADE,
CONSTRAINT `fk_stock_audit_items_item` FOREIGN KEY (`item_id`) REFERENCES `inventory_items`(`id`) ON DELETE RESTRICT,
CONSTRAINT `fk_stock_audit_items_batch` FOREIGN KEY (`batch_id`) REFERENCES `item_batches`(`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `stock_audit_items`",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `packages` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`code` VARCHAR(30) NOT NULL,
`name_ar` VARCHAR(200) NOT NULL,
`name_en` VARCHAR(200) NULL,
`description_ar` TEXT NULL,
`package_price_member` DECIMAL(15,2) NOT NULL,
`package_price_nonmember` DECIMAL(15,2) NOT NULL,
`items_total_price` DECIMAL(15,2) NOT NULL DEFAULT 0.00 COMMENT 'Sum of individual item prices',
`active_from` DATE NULL,
`active_to` DATE NULL,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`image_document_id` BIGINT UNSIGNED NULL,
`notes` TEXT NULL,
`is_archived` TINYINT(1) NOT NULL DEFAULT 0,
`archived_at` TIMESTAMP NULL DEFAULT NULL,
`archived_by` BIGINT UNSIGNED NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
`updated_by` BIGINT UNSIGNED NULL,
UNIQUE KEY `uq_packages_code` (`code`),
INDEX `idx_packages_active_dates` (`is_active`, `active_from`, `active_to`),
CONSTRAINT `fk_packages_image_document` FOREIGN KEY (`image_document_id`) REFERENCES `documents`(`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `packages`",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `package_items` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`package_id` BIGINT UNSIGNED NOT NULL,
`item_id` BIGINT UNSIGNED NOT NULL,
`quantity` DECIMAL(15,3) NOT NULL DEFAULT 1.000,
`sort_order` INT NOT NULL DEFAULT 0,
INDEX `idx_package_items_package` (`package_id`),
INDEX `idx_package_items_item` (`item_id`),
UNIQUE KEY `uq_package_items_package_item` (`package_id`, `item_id`),
CONSTRAINT `fk_package_items_package` FOREIGN KEY (`package_id`) REFERENCES `packages`(`id`) ON DELETE CASCADE,
CONSTRAINT `fk_package_items_item` FOREIGN KEY (`item_id`) REFERENCES `inventory_items`(`id`) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `package_items`",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `sales` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`invoice_number` VARCHAR(50) NOT NULL,
`warehouse_id` BIGINT UNSIGNED NOT NULL,
`customer_type` VARCHAR(20) NOT NULL COMMENT 'member, player, guest',
`member_id` BIGINT UNSIGNED NULL,
`player_id` BIGINT UNSIGNED NULL,
`guest_name` VARCHAR(200) NULL,
`guest_phone` VARCHAR(30) NULL,
`subtotal` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`discount_amount` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`discount_reason` VARCHAR(200) NULL,
`tax_amount` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`total_amount` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`amount_paid` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`amount_refunded` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`payment_id` BIGINT UNSIGNED NULL,
`payment_method` VARCHAR(30) NULL,
`status` VARCHAR(30) NOT NULL DEFAULT 'draft' COMMENT 'draft, completed, partially_refunded, fully_refunded, voided',
`sold_by` BIGINT UNSIGNED NULL,
`sale_date` DATE NOT NULL,
`notes` TEXT NULL,
`branch_id` BIGINT UNSIGNED NULL,
`is_archived` TINYINT(1) NOT NULL DEFAULT 0,
`archived_at` TIMESTAMP NULL DEFAULT NULL,
`archived_by` BIGINT UNSIGNED NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
`updated_by` BIGINT UNSIGNED NULL,
UNIQUE KEY `uq_sales_invoice_number` (`invoice_number`),
INDEX `idx_sales_warehouse` (`warehouse_id`),
INDEX `idx_sales_customer_type` (`customer_type`),
INDEX `idx_sales_member` (`member_id`),
INDEX `idx_sales_player` (`player_id`),
INDEX `idx_sales_status` (`status`),
INDEX `idx_sales_date` (`sale_date`),
INDEX `idx_sales_payment` (`payment_id`),
INDEX `idx_sales_sold_by` (`sold_by`),
CONSTRAINT `fk_sales_warehouse` FOREIGN KEY (`warehouse_id`) REFERENCES `warehouses`(`id`) ON DELETE RESTRICT,
CONSTRAINT `fk_sales_member` FOREIGN KEY (`member_id`) REFERENCES `members`(`id`) ON DELETE SET NULL,
CONSTRAINT `fk_sales_player` FOREIGN KEY (`player_id`) REFERENCES `players`(`id`) ON DELETE SET NULL,
CONSTRAINT `fk_sales_payment` FOREIGN KEY (`payment_id`) REFERENCES `payments`(`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `sales`",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `sale_items` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`sale_id` BIGINT UNSIGNED NOT NULL,
`item_id` BIGINT UNSIGNED NULL COMMENT 'NULL if package line',
`package_id` BIGINT UNSIGNED NULL COMMENT 'NULL if single item line',
`item_name_ar` VARCHAR(300) NOT NULL COMMENT 'Snapshot at time of sale',
`quantity` DECIMAL(15,3) NOT NULL,
`unit_price` DECIMAL(15,2) NOT NULL,
`discount_amount` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`tax_amount` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`line_total` DECIMAL(15,2) NOT NULL,
`cost_price` DECIMAL(15,2) NULL COMMENT 'Snapshot for profit calc',
`batch_id` BIGINT UNSIGNED NULL,
`is_refunded` TINYINT(1) NOT NULL DEFAULT 0,
`refunded_quantity` DECIMAL(15,3) NOT NULL DEFAULT 0.000,
INDEX `idx_sale_items_sale` (`sale_id`),
INDEX `idx_sale_items_item` (`item_id`),
INDEX `idx_sale_items_package` (`package_id`),
INDEX `idx_sale_items_batch` (`batch_id`),
CONSTRAINT `fk_sale_items_sale` FOREIGN KEY (`sale_id`) REFERENCES `sales`(`id`) ON DELETE CASCADE,
CONSTRAINT `fk_sale_items_item` FOREIGN KEY (`item_id`) REFERENCES `inventory_items`(`id`) ON DELETE SET NULL,
CONSTRAINT `fk_sale_items_package` FOREIGN KEY (`package_id`) REFERENCES `packages`(`id`) ON DELETE SET NULL,
CONSTRAINT `fk_sale_items_batch` FOREIGN KEY (`batch_id`) REFERENCES `item_batches`(`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `sale_items`",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `sale_refunds` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`sale_id` BIGINT UNSIGNED NOT NULL,
`refund_number` VARCHAR(50) NOT NULL,
`refund_amount` DECIMAL(15,2) NOT NULL,
`reason` TEXT NOT NULL,
`return_to_stock` TINYINT(1) NOT NULL DEFAULT 1,
`warehouse_id` BIGINT UNSIGNED NULL,
`payment_id` BIGINT UNSIGNED NULL,
`refunded_by` BIGINT UNSIGNED NULL,
`refund_date` DATE NOT NULL,
`notes` TEXT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
UNIQUE KEY `uq_sale_refunds_refund_number` (`refund_number`),
INDEX `idx_sale_refunds_sale` (`sale_id`),
INDEX `idx_sale_refunds_date` (`refund_date`),
CONSTRAINT `fk_sale_refunds_sale` FOREIGN KEY (`sale_id`) REFERENCES `sales`(`id`) ON DELETE RESTRICT,
CONSTRAINT `fk_sale_refunds_warehouse` FOREIGN KEY (`warehouse_id`) REFERENCES `warehouses`(`id`) ON DELETE SET NULL,
CONSTRAINT `fk_sale_refunds_payment` FOREIGN KEY (`payment_id`) REFERENCES `payments`(`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `sale_refunds`",
];
<?php
declare(strict_types=1);
return [
'up' => "
ALTER TABLE `receipts`
DROP FOREIGN KEY `fk_receipts_member`;
ALTER TABLE `receipts`
MODIFY COLUMN `member_id` BIGINT UNSIGNED NULL;
ALTER TABLE `receipts`
ADD CONSTRAINT `fk_receipts_member` FOREIGN KEY (`member_id`) REFERENCES `members`(`id`) ON DELETE SET NULL
",
'down' => "
ALTER TABLE `receipts`
DROP FOREIGN KEY `fk_receipts_member`;
ALTER TABLE `receipts`
MODIFY COLUMN `member_id` BIGINT UNSIGNED NOT NULL;
ALTER TABLE `receipts`
ADD CONSTRAINT `fk_receipts_member` FOREIGN KEY (`member_id`) REFERENCES `members`(`id`)
",
];
<?php
declare(strict_types=1);
use App\Core\Database;
return function (Database $db): void {
$ts = date('Y-m-d H:i:s');
// ── Helpers ──────────────────────────────────────────────────
$ensureCategory = function (string $code, string $nameAr, ?int $parentId, int $sortOrder) use ($db, $ts): void {
$existing = $db->selectOne("SELECT id FROM item_categories WHERE code = ?", [$code]);
if ($existing) {
return;
}
$db->insert('item_categories', [
'code' => $code,
'name_ar' => $nameAr,
'parent_id' => $parentId,
'sort_order' => $sortOrder,
'is_active' => 1,
'created_at' => $ts,
'updated_at' => $ts,
]);
};
$catId = function (string $code) use ($db): ?int {
$row = $db->selectOne("SELECT id FROM item_categories WHERE code = ?", [$code]);
return $row ? (int) ($row->id ?? $row['id']) : null;
};
// ── Root categories ─────────────────────────────────────────
$ensureCategory('sports_equipment', 'معدات رياضية', null, 1);
$ensureCategory('food_beverages', 'مأكولات ومشروبات', null, 2);
$ensureCategory('sportswear', 'ملابس رياضية', null, 3);
$ensureCategory('medical_supplies', 'مستلزمات طبية', null, 4);
$ensureCategory('furniture', 'أثاث ومفروشات', null, 5);
$ensureCategory('stationery', 'قرطاسية', null, 6);
$ensureCategory('maintenance', 'صيانة', null, 7);
$ensureCategory('other', 'أخرى', null, 8);
// ── Child categories — Sports Equipment ─────────────────────
$sportsId = $catId('sports_equipment');
$ensureCategory('gym_machines', 'أجهزة جيم', $sportsId, 1);
$ensureCategory('balls', 'كرات', $sportsId, 2);
$ensureCategory('training_tools', 'أدوات تدريب', $sportsId, 3);
// ── Child categories — Food & Beverages ─────────────────────
$foodId = $catId('food_beverages');
$ensureCategory('beverages', 'مشروبات', $foodId, 1);
$ensureCategory('snacks', 'وجبات خفيفة', $foodId, 2);
$ensureCategory('supplements', 'مكملات غذائية', $foodId, 3);
// ── Child categories — Sportswear ───────────────────────────
$sportswearId = $catId('sportswear');
$ensureCategory('uniforms', 'يونيفورم', $sportswearId, 1);
};
<?php
declare(strict_types=1);
use App\Core\Database;
return function (Database $db): void {
$ts = date('Y-m-d H:i:s');
$ensureRule = function (array $rule) use ($db, $ts): void {
$existing = $db->selectOne(
"SELECT id FROM business_rules WHERE rule_code = ? AND branch_id IS NULL",
[$rule['rule_code']]
);
if ($existing) {
return;
}
$db->insert('business_rules', array_merge($rule, [
'branch_id' => null,
'effective_from' => date('Y-m-d'),
'is_active' => 1,
'version' => 1,
'created_at' => $ts,
'updated_at' => $ts,
]));
};
// ── Inventory & Sales Business Rules ────────────────────────
$ensureRule([
'rule_code' => 'INV_LOW_STOCK_ALERT_DAYS',
'category' => 'inventory',
'name_ar' => 'عدد أيام تنبيه نقص المخزون',
'name_en' => 'Low Stock Alert Days',
'data_type' => 'number',
'current_value_json' => '{"days":1}',
'parameters_json' => '{"days":"integer"}',
]);
$ensureRule([
'rule_code' => 'INV_EXPIRY_ALERT_DAYS',
'category' => 'inventory',
'name_ar' => 'أيام التنبيه قبل انتهاء الصلاحية',
'name_en' => 'Expiry Alert Days',
'data_type' => 'number',
'current_value_json' => '{"days":30}',
'parameters_json' => '{"days":"integer"}',
]);
$ensureRule([
'rule_code' => 'INV_TRANSFER_REQUIRES_APPROVAL',
'category' => 'inventory',
'name_ar' => 'النقل بين المخازن يتطلب اعتماد',
'name_en' => 'Stock Transfer Requires Approval',
'data_type' => 'boolean',
'current_value_json' => '{"value":true}',
'parameters_json' => '{"value":"boolean"}',
]);
$ensureRule([
'rule_code' => 'INV_PO_REQUIRES_APPROVAL',
'category' => 'inventory',
'name_ar' => 'أوامر الشراء تتطلب اعتماد',
'name_en' => 'Purchase Orders Require Approval',
'data_type' => 'boolean',
'current_value_json' => '{"value":true}',
'parameters_json' => '{"value":"boolean"}',
]);
$ensureRule([
'rule_code' => 'INV_DEFAULT_TAX_RATE',
'category' => 'inventory',
'name_ar' => 'نسبة ضريبة القيمة المضافة',
'name_en' => 'Default VAT Rate',
'data_type' => 'number',
'current_value_json' => '{"percentage":"14.00"}',
'parameters_json' => '{"percentage":"decimal"}',
]);
$ensureRule([
'rule_code' => 'INV_DEPRECIATION_RUN_DAY',
'category' => 'inventory',
'name_ar' => 'يوم تشغيل الإهلاك الشهري',
'name_en' => 'Monthly Depreciation Run Day',
'data_type' => 'number',
'current_value_json' => '{"day":1}',
'parameters_json' => '{"day":"integer"}',
]);
$ensureRule([
'rule_code' => 'INV_MEMBER_DISCOUNT_PERCENTAGE',
'category' => 'inventory',
'name_ar' => 'نسبة خصم الأعضاء على المبيعات',
'name_en' => 'Member Sales Discount Percentage',
'data_type' => 'number',
'current_value_json' => '{"percentage":"0.00"}',
'parameters_json' => '{"percentage":"decimal"}',
]);
$ensureRule([
'rule_code' => 'INV_GUEST_SALE_ALLOWED',
'category' => 'inventory',
'name_ar' => 'السماح بالبيع للزوار',
'name_en' => 'Allow Guest Sales',
'data_type' => 'boolean',
'current_value_json' => '{"value":true}',
'parameters_json' => '{"value":"boolean"}',
]);
$ensureRule([
'rule_code' => 'INV_VOID_SALE_WINDOW_HOURS',
'category' => 'inventory',
'name_ar' => 'ساعات مهلة إلغاء عملية البيع',
'name_en' => 'Void Sale Window Hours',
'data_type' => 'number',
'current_value_json' => '{"hours":24}',
'parameters_json' => '{"hours":"integer"}',
]);
};
<?php
declare(strict_types=1);
use App\Core\Database;
return function (Database $db): void {
$ts = date('Y-m-d H:i:s');
$warehouses = [
['code' => 'WH-MAIN', 'name_ar' => 'المخزن الرئيسي', 'warehouse_type' => 'general'],
['code' => 'WH-SHOP', 'name_ar' => 'المحل الرياضي', 'warehouse_type' => 'sports_shop'],
['code' => 'WH-CAFE', 'name_ar' => 'مخزن الكافيتريا', 'warehouse_type' => 'cafeteria'],
['code' => 'WH-GYM', 'name_ar' => 'مخزن الجيم', 'warehouse_type' => 'gym'],
['code' => 'WH-MED', 'name_ar' => 'مخزن المستلزمات الطبية', 'warehouse_type' => 'medical'],
];
foreach ($warehouses as $wh) {
$existing = $db->selectOne("SELECT id FROM warehouses WHERE code = ?", [$wh['code']]);
if ($existing) {
continue;
}
$db->insert('warehouses', [
'code' => $wh['code'],
'name_ar' => $wh['name_ar'],
'warehouse_type' => $wh['warehouse_type'],
'is_active' => 1,
'created_at' => $ts,
'updated_at' => $ts,
]);
}
};
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