Commit 89acbebd authored by Mahmoud Aglan's avatar Mahmoud Aglan

Test

parent 5d2aceb8
...@@ -810,6 +810,378 @@ final class AccountingIntegrationService ...@@ -810,6 +810,378 @@ final class AccountingIntegrationService
} }
} }
// ────────────────────────────────────────────────────────────
// PROCUREMENT MODULE
// ────────────────────────────────────────────────────────────
/**
* Auto-post AP accrual when a vendor invoice is approved.
* Dr. 1104 Inventory (subtotal)
* Dr. 2103 Tax Receivable / Input VAT (tax_amount)
* Cr. 2101 Accounts Payable (total_amount)
*
* Also creates accounts_payable record.
*/
public static function onVendorInvoiceApproved(array $data): void
{
$db = App::getInstance()->db();
$invoiceId = (int) ($data['invoice_id'] ?? 0);
$supplierId = (int) ($data['supplier_id'] ?? 0);
$subtotal = (string) ($data['subtotal'] ?? '0.00');
$taxAmount = (string) ($data['tax_amount'] ?? '0.00');
$totalAmount = (string) ($data['total_amount'] ?? '0.00');
$invoiceNumber = $data['internal_number'] ?? '';
if ($invoiceId <= 0 || bccomp($totalAmount, '0.00', 2) <= 0) {
return;
}
$invoice = $db->selectOne("SELECT * FROM `vendor_invoices` WHERE `id` = ?", [$invoiceId]);
if (!$invoice) {
return;
}
$inventoryAccount = self::getAccountByCode('1104');
$taxAccount = self::getAccountByCode('2103');
$apAccount = self::getAccountByCode('2101');
if (!$inventoryAccount || !$apAccount) {
Logger::error("Vendor invoice auto-post failed: core accounts not found", [
'invoice_id' => $invoiceId,
]);
return;
}
$description = 'فاتورة مورد — ' . $invoiceNumber;
$lines = [];
// Dr. Inventory (subtotal)
if (bccomp($subtotal, '0.00', 2) > 0) {
$lines[] = [
'account_id' => (int) $inventoryAccount['id'],
'debit' => $subtotal,
'credit' => '0.00',
'description_ar' => 'مخزون — فاتورة مورد ' . $invoiceNumber,
];
}
// Dr. Tax Receivable (Input VAT)
if ($taxAccount && bccomp($taxAmount, '0.00', 2) > 0) {
$lines[] = [
'account_id' => (int) $taxAccount['id'],
'debit' => $taxAmount,
'credit' => '0.00',
'description_ar' => 'ضريبة مدخلات — فاتورة مورد ' . $invoiceNumber,
];
}
// Cr. Accounts Payable (total)
$lines[] = [
'account_id' => (int) $apAccount['id'],
'debit' => '0.00',
'credit' => $totalAmount,
'description_ar' => 'دائنون — فاتورة مورد ' . $invoiceNumber,
];
$result = JournalService::createEntry([
'entry_date' => $invoice['invoice_date'] ?? date('Y-m-d'),
'description_ar' => $description,
'description_en' => 'Vendor invoice — ' . $invoiceNumber,
'reference_type' => 'vendor_invoice',
'reference_id' => $invoiceId,
'reference_number' => $invoiceNumber,
'source_module' => 'procurement',
'is_auto_generated' => 1,
], $lines, true);
if (!$result['success']) {
Logger::error("Vendor invoice auto-post failed", [
'invoice_id' => $invoiceId,
'error' => $result['error'] ?? '',
]);
return;
}
// Link journal entry back to vendor invoice
$db->update('vendor_invoices', [
'journal_entry_id' => $result['journal_entry_id'],
], '`id` = ?', [$invoiceId]);
// Create accounts_payable record
$db->insert('accounts_payable', [
'supplier_id' => $supplierId,
'invoice_number' => $invoiceNumber,
'invoice_date' => $invoice['invoice_date'] ?? date('Y-m-d'),
'due_date' => $invoice['due_date'] ?? date('Y-m-d', strtotime('+30 days')),
'description_ar' => $description,
'description_en' => 'Vendor invoice — ' . $invoiceNumber,
'total_amount' => $totalAmount,
'paid_amount' => '0.00',
'balance' => $totalAmount,
'currency' => $invoice['currency'] ?? 'EGP',
'status' => 'pending',
'journal_entry_id' => $result['journal_entry_id'],
'purchase_order_id' => !empty($invoice['purchase_order_id']) ? (int) $invoice['purchase_order_id'] : null,
'branch_id' => $invoice['branch_id'] ?? null,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
$apId = $db->lastInsertId();
// Link AP record back to vendor invoice
$db->update('vendor_invoices', [
'ap_record_id' => $apId,
], '`id` = ?', [$invoiceId]);
Logger::info("Vendor invoice #{$invoiceId} — journal posted, AP record #{$apId} created");
}
/**
* Auto-post AP payment when a vendor payment is completed.
* Dr. 2101 Accounts Payable (amount)
* Cr. 1101/1102 Cash or Bank (amount)
*
* Also updates AP paid_amount/balance/status.
*/
public static function onVendorPaymentCompleted(array $data): void
{
$db = App::getInstance()->db();
$paymentId = (int) ($data['payment_id'] ?? 0);
$invoiceId = (int) ($data['invoice_id'] ?? 0);
$supplierId = (int) ($data['supplier_id'] ?? 0);
$amount = (string) ($data['amount'] ?? '0.00');
$paymentMethod = $data['payment_method'] ?? 'cash';
$paymentNumber = $data['payment_number'] ?? '';
if ($paymentId <= 0 || bccomp($amount, '0.00', 2) <= 0) {
return;
}
$apAccount = self::getAccountByCode('2101');
$cashBankCode = in_array($paymentMethod, ['bank_transfer', 'check', 'wire'], true) ? '1102' : '1101';
$cashBankAccount = self::getAccountByCode($cashBankCode);
if (!$apAccount || !$cashBankAccount) {
Logger::error("Vendor payment auto-post failed: accounts not found", ['payment_id' => $paymentId]);
return;
}
$payment = $db->selectOne("SELECT * FROM `vendor_payments` WHERE `id` = ?", [$paymentId]);
if (!$payment) {
return;
}
$description = 'دفعة مورد — ' . $paymentNumber;
$result = JournalService::createEntry([
'entry_date' => $payment['payment_date'] ?? date('Y-m-d'),
'description_ar' => $description,
'description_en' => 'Vendor payment — ' . $paymentNumber,
'reference_type' => 'vendor_payment',
'reference_id' => $paymentId,
'reference_number' => $paymentNumber,
'source_module' => 'procurement',
'is_auto_generated' => 1,
], [
[
'account_id' => (int) $apAccount['id'],
'debit' => $amount,
'credit' => '0.00',
'description_ar' => 'تسديد دائنون — ' . $paymentNumber,
],
[
'account_id' => (int) $cashBankAccount['id'],
'debit' => '0.00',
'credit' => $amount,
'description_ar' => 'صرف نقدي/بنكي — ' . $paymentNumber,
],
], true);
if (!$result['success']) {
Logger::error("Vendor payment auto-post failed", [
'payment_id' => $paymentId,
'error' => $result['error'] ?? '',
]);
return;
}
// Link journal entry back to vendor payment
$db->update('vendor_payments', [
'journal_entry_id' => $result['journal_entry_id'],
], '`id` = ?', [$paymentId]);
// Update AP record for the linked invoice
if ($invoiceId > 0) {
$invoice = $db->selectOne("SELECT `ap_record_id` FROM `vendor_invoices` WHERE `id` = ?", [$invoiceId]);
if ($invoice && !empty($invoice['ap_record_id'])) {
$apRecord = $db->selectOne("SELECT * FROM `accounts_payable` WHERE `id` = ?", [(int) $invoice['ap_record_id']]);
if ($apRecord) {
$newPaid = bcadd((string) $apRecord['paid_amount'], $amount, 2);
$newBalance = bcsub((string) $apRecord['total_amount'], $newPaid, 2);
if (bccomp($newBalance, '0.00', 2) < 0) {
$newBalance = '0.00';
}
$status = bccomp($newBalance, '0.00', 2) <= 0 ? 'paid' : 'partial';
$db->update('accounts_payable', [
'paid_amount' => $newPaid,
'balance' => $newBalance,
'status' => $status,
'payment_entry_id' => $result['journal_entry_id'],
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [(int) $invoice['ap_record_id']]);
}
}
}
Logger::info("Vendor payment #{$paymentId} — journal posted, AP updated");
}
/**
* Reverse journal entry when a vendor payment is voided.
* Also reverts AP balance.
*/
public static function onVendorPaymentVoided(array $data): void
{
$db = App::getInstance()->db();
$paymentId = (int) ($data['payment_id'] ?? 0);
$invoiceId = (int) ($data['invoice_id'] ?? 0);
$amount = (string) ($data['amount'] ?? '0.00');
$reason = $data['reason'] ?? 'إلغاء دفعة مورد';
// Reverse the journal entry
$entry = \App\Modules\Accounting\Models\JournalEntry::findByReference('vendor_payment', $paymentId);
if ($entry && $entry->isPosted()) {
JournalService::reverseEntry((int) $entry->id, $reason);
}
// Revert AP balance
if ($invoiceId > 0 && bccomp($amount, '0.00', 2) > 0) {
$invoice = $db->selectOne("SELECT `ap_record_id` FROM `vendor_invoices` WHERE `id` = ?", [$invoiceId]);
if ($invoice && !empty($invoice['ap_record_id'])) {
$apRecord = $db->selectOne("SELECT * FROM `accounts_payable` WHERE `id` = ?", [(int) $invoice['ap_record_id']]);
if ($apRecord) {
$newPaid = bcsub((string) $apRecord['paid_amount'], $amount, 2);
if (bccomp($newPaid, '0.00', 2) < 0) {
$newPaid = '0.00';
}
$newBalance = bcsub((string) $apRecord['total_amount'], $newPaid, 2);
$status = bccomp($newPaid, '0.00', 2) <= 0 ? 'pending' : 'partial';
$db->update('accounts_payable', [
'paid_amount' => $newPaid,
'balance' => $newBalance,
'status' => $status,
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [(int) $invoice['ap_record_id']]);
}
}
}
Logger::info("Vendor payment #{$paymentId} voided — journal reversed, AP reverted");
}
/**
* Auto-post RTV journal when return-to-vendor is completed.
* Dr. 2101 Accounts Payable (total_amount) — reduce what we owe
* Cr. 1104 Inventory (total_amount) — stock left the warehouse
*/
public static function onReturnToVendorCompleted(array $data): void
{
$db = App::getInstance()->db();
$rtvId = (int) ($data['rtv_id'] ?? 0);
$supplierId = (int) ($data['supplier_id'] ?? 0);
$totalAmount = (string) ($data['total_amount'] ?? '0.00');
$warehouseId = (int) ($data['warehouse_id'] ?? 0);
if ($rtvId <= 0 || bccomp($totalAmount, '0.00', 2) <= 0) {
return;
}
$rtv = $db->selectOne("SELECT * FROM `return_to_vendor` WHERE `id` = ?", [$rtvId]);
if (!$rtv) {
return;
}
$rtvNumber = $rtv['rtv_number'] ?? '';
$apAccount = self::getAccountByCode('2101');
$inventoryAccount = self::getAccountByCode('1104');
if (!$apAccount || !$inventoryAccount) {
Logger::error("RTV auto-post failed: accounts not found", ['rtv_id' => $rtvId]);
return;
}
$description = 'مرتجع مورد — ' . $rtvNumber;
$result = JournalService::createEntry([
'entry_date' => $rtv['return_date'] ?? date('Y-m-d'),
'description_ar' => $description,
'description_en' => 'Return to vendor — ' . $rtvNumber,
'reference_type' => 'return_to_vendor',
'reference_id' => $rtvId,
'reference_number' => $rtvNumber,
'source_module' => 'procurement',
'is_auto_generated' => 1,
], [
[
'account_id' => (int) $apAccount['id'],
'debit' => $totalAmount,
'credit' => '0.00',
'description_ar' => 'تخفيض دائنون — مرتجع مورد ' . $rtvNumber,
],
[
'account_id' => (int) $inventoryAccount['id'],
'debit' => '0.00',
'credit' => $totalAmount,
'description_ar' => 'خصم من المخزون — مرتجع مورد ' . $rtvNumber,
],
], true);
if (!$result['success']) {
Logger::error("RTV auto-post failed", ['rtv_id' => $rtvId, 'error' => $result['error'] ?? '']);
return;
}
// Link journal entry back to RTV
$db->update('return_to_vendor', [
'journal_entry_id' => $result['journal_entry_id'],
], '`id` = ?', [$rtvId]);
// Reduce AP balance if the RTV is linked to an invoice via the supplier
// Find the most recent pending/partial AP record for this supplier
$apRecord = $db->selectOne(
"SELECT * FROM `accounts_payable`
WHERE `supplier_id` = ? AND `status` IN ('pending', 'partial') AND `is_archived` = 0
ORDER BY `created_at` DESC LIMIT 1",
[$supplierId]
);
if ($apRecord) {
$newBalance = bcsub((string) $apRecord['balance'], $totalAmount, 2);
if (bccomp($newBalance, '0.00', 2) < 0) {
$newBalance = '0.00';
}
$newPaid = bcsub((string) $apRecord['total_amount'], $newBalance, 2);
$status = bccomp($newBalance, '0.00', 2) <= 0 ? 'paid' : 'partial';
$db->update('accounts_payable', [
'paid_amount' => $newPaid,
'balance' => $newBalance,
'status' => $status,
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [(int) $apRecord['id']]);
}
Logger::info("RTV #{$rtvId} — journal posted, AP reduced");
}
// ──────────────────────────────────────────────────────────── // ────────────────────────────────────────────────────────────
// HELPERS // HELPERS
// ──────────────────────────────────────────────────────────── // ────────────────────────────────────────────────────────────
......
...@@ -197,6 +197,43 @@ EventBus::listen('sale.refunded', function (array $data): void { ...@@ -197,6 +197,43 @@ EventBus::listen('sale.refunded', function (array $data): void {
} }
}, 50); }, 50);
// ── Procurement ─────────────────────────────────────────────
// When a vendor invoice is approved, post AP accrual
EventBus::listen('procurement.invoice_approved', function (array $data): void {
try {
AccountingIntegrationService::onVendorInvoiceApproved($data);
} catch (\Throwable $e) {
\App\Core\Logger::error('Accounting auto-post failed (procurement.invoice_approved): ' . $e->getMessage());
}
}, 50);
// When a vendor payment is completed, post AP reduction
EventBus::listen('procurement.payment_completed', function (array $data): void {
try {
AccountingIntegrationService::onVendorPaymentCompleted($data);
} catch (\Throwable $e) {
\App\Core\Logger::error('Accounting auto-post failed (procurement.payment_completed): ' . $e->getMessage());
}
}, 50);
// When a vendor payment is voided, reverse the journal entry
EventBus::listen('procurement.payment_voided', function (array $data): void {
try {
AccountingIntegrationService::onVendorPaymentVoided($data);
} catch (\Throwable $e) {
\App\Core\Logger::error('Accounting auto-reverse failed (procurement.payment_voided): ' . $e->getMessage());
}
}, 50);
// When a return-to-vendor is completed, post AP debit / inventory credit
EventBus::listen('procurement.rtv_completed', function (array $data): void {
try {
AccountingIntegrationService::onReturnToVendorCompleted($data);
} catch (\Throwable $e) {
\App\Core\Logger::error('Accounting auto-post failed (procurement.rtv_completed): ' . $e->getMessage());
}
}, 50);
// ── Rentals ───────────────────────────────────────────────── // ── Rentals ─────────────────────────────────────────────────
// When a rental deposit is collected, record deposit liability // When a rental deposit is collected, record deposit liability
EventBus::listen('rental.deposit_collected', function (array $data): void { EventBus::listen('rental.deposit_collected', function (array $data): void {
......
...@@ -103,10 +103,22 @@ class PurchaseOrderController extends Controller ...@@ -103,10 +103,22 @@ class PurchaseOrderController extends Controller
$items = PurchaseOrderItem::getForPO((int) $id); $items = PurchaseOrderItem::getForPO((int) $id);
// Related procurement documents
$relatedGRNs = $db->select(
"SELECT `id`, `grn_number`, `status` FROM `goods_received_notes` WHERE `purchase_order_id` = ? AND `is_archived` = 0 ORDER BY `created_at` DESC",
[(int) $id]
);
$relatedInvoices = $db->select(
"SELECT `id`, `internal_number`, `total_amount`, `status` FROM `vendor_invoices` WHERE `purchase_order_id` = ? AND `is_archived` = 0 ORDER BY `created_at` DESC",
[(int) $id]
);
return $this->view('Inventory.Views.purchase_orders.show', [ return $this->view('Inventory.Views.purchase_orders.show', [
'order' => $order, 'order' => $order,
'items' => $items, 'items' => $items,
'statuses' => PurchaseOrder::getStatuses(), 'statuses' => PurchaseOrder::getStatuses(),
'relatedGRNs' => $relatedGRNs,
'relatedInvoices' => $relatedInvoices,
]); ]);
} }
......
...@@ -71,8 +71,10 @@ class SupplierController extends Controller ...@@ -71,8 +71,10 @@ class SupplierController extends Controller
); );
return $this->view('Inventory.Views.suppliers.show', [ return $this->view('Inventory.Views.suppliers.show', [
'supplier' => $supplier, 'supplier' => $supplier,
'recentPOs' => $recentPOs, 'recentPOs' => $recentPOs,
'performance' => Supplier::getPerformanceStats((int) $id),
'apBalance' => Supplier::getOutstandingBalance((int) $id),
]); ]);
} }
......
...@@ -41,6 +41,63 @@ class Supplier extends Model ...@@ -41,6 +41,63 @@ class Supplier extends Model
->get(); ->get();
} }
/**
* Get supplier performance stats (orders, delivery, quality).
*/
public static function getPerformanceStats(int $supplierId): array
{
$db = App::getInstance()->db();
$row = $db->selectOne(
"SELECT
`total_orders`, `on_time_delivery_count`, `quality_acceptance_count`,
`average_lead_days`, `last_order_date`
FROM `suppliers` WHERE `id` = ?",
[$supplierId]
);
$totalOrders = (int) ($row['total_orders'] ?? 0);
$onTimeCount = (int) ($row['on_time_delivery_count'] ?? 0);
$qualityCount = (int) ($row['quality_acceptance_count'] ?? 0);
$avgLead = (float) ($row['average_lead_days'] ?? 0);
return [
'total_orders' => $totalOrders,
'on_time_delivery_count' => $onTimeCount,
'quality_acceptance_count' => $qualityCount,
'on_time_rate' => $totalOrders > 0 ? round(($onTimeCount / $totalOrders) * 100, 1) : 0,
'quality_rate' => $totalOrders > 0 ? round(($qualityCount / $totalOrders) * 100, 1) : 0,
'average_lead_days' => round($avgLead, 1),
'last_order_date' => $row['last_order_date'] ?? null,
];
}
/**
* Get outstanding AP balance for a supplier.
*/
public static function getOutstandingBalance(int $supplierId): array
{
$db = App::getInstance()->db();
$row = $db->selectOne(
"SELECT
COALESCE(SUM(`total_amount`), 0) as total_invoiced,
COALESCE(SUM(`paid_amount`), 0) as total_paid,
COALESCE(SUM(`balance`), 0) as outstanding_balance,
COUNT(*) as invoice_count
FROM `accounts_payable`
WHERE `supplier_id` = ? AND `is_archived` = 0 AND `status` IN ('pending', 'partial', 'overdue')",
[$supplierId]
);
return [
'total_invoiced' => (string) ($row['total_invoiced'] ?? '0.00'),
'total_paid' => (string) ($row['total_paid'] ?? '0.00'),
'outstanding_balance' => (string) ($row['outstanding_balance'] ?? '0.00'),
'invoice_count' => (int) ($row['invoice_count'] ?? 0),
];
}
/** /**
* Search suppliers with filters and pagination. * Search suppliers with filters and pagination.
*/ */
......
...@@ -179,6 +179,46 @@ $stLabel = $statusLabels[$st] ?? $st; ...@@ -179,6 +179,46 @@ $stLabel = $statusLabels[$st] ?? $st;
<?php endif; ?> <?php endif; ?>
</div> </div>
<!-- Related Procurement Documents -->
<?php if (!empty($relatedGRNs) || !empty($relatedInvoices)): ?>
<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="link" style="width:18px;height:18px;color:#6B7280;"></i>
<h3 style="margin:0;color:#6B7280;font-size:15px;">المستندات المرتبطة</h3>
</div>
<div style="padding:15px 20px;">
<?php if (!empty($relatedGRNs)): ?>
<div style="margin-bottom:12px;">
<div style="font-size:12px;color:#6B7280;font-weight:600;margin-bottom:6px;">إذونات الاستلام (GRN)</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;">
<?php foreach ($relatedGRNs as $grn): ?>
<a href="/procurement/grn/<?= (int) $grn['id'] ?>" class="btn btn-sm btn-outline" style="font-size:12px;padding:4px 10px;">
<i data-lucide="package-check" style="width:12px;height:12px;vertical-align:middle;margin-left:4px;"></i>
<?= e($grn['grn_number']) ?>
<span style="font-size:11px;color:#6B7280;margin-right:4px;">(<?= e($grn['status'] ?? '') ?>)</span>
</a>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<?php if (!empty($relatedInvoices)): ?>
<div>
<div style="font-size:12px;color:#6B7280;font-weight:600;margin-bottom:6px;">فواتير الموردين</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;">
<?php foreach ($relatedInvoices as $inv): ?>
<a href="/procurement/invoices/<?= (int) $inv['id'] ?>" class="btn btn-sm btn-outline" style="font-size:12px;padding:4px 10px;">
<i data-lucide="receipt" style="width:12px;height:12px;vertical-align:middle;margin-left:4px;"></i>
<?= e($inv['internal_number']) ?>
<span style="font-size:11px;color:#6B7280;margin-right:4px;">(<?= money($inv['total_amount'] ?? 0) ?>)</span>
</a>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') { if (typeof lucide !== 'undefined') {
......
...@@ -90,6 +90,33 @@ ...@@ -90,6 +90,33 @@
</div> </div>
</div> </div>
<!-- Performance & Financial Summary -->
<?php $perf = $performance ?? []; $ap = $apBalance ?? []; ?>
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:15px;margin-bottom:20px;">
<div class="card" style="padding:20px;text-align:center;">
<div style="font-size:12px;color:#6B7280;margin-bottom:6px;">إجمالي الطلبات</div>
<div style="font-size:28px;font-weight:800;color:#0D7377;"><?= (int) ($perf['total_orders'] ?? 0) ?></div>
<?php if (!empty($perf['last_order_date'])): ?>
<div style="font-size:11px;color:#9CA3AF;margin-top:4px;">آخر طلب: <?= e($perf['last_order_date']) ?></div>
<?php endif; ?>
</div>
<div class="card" style="padding:20px;text-align:center;">
<div style="font-size:12px;color:#6B7280;margin-bottom:6px;">معدل التسليم في الوقت</div>
<div style="font-size:28px;font-weight:800;color:<?= ($perf['on_time_rate'] ?? 0) >= 80 ? '#059669' : (($perf['on_time_rate'] ?? 0) >= 50 ? '#D97706' : '#DC2626') ?>;"><?= $perf['on_time_rate'] ?? 0 ?>%</div>
<div style="font-size:11px;color:#9CA3AF;margin-top:4px;"><?= (int) ($perf['on_time_delivery_count'] ?? 0) ?> من <?= (int) ($perf['total_orders'] ?? 0) ?></div>
</div>
<div class="card" style="padding:20px;text-align:center;">
<div style="font-size:12px;color:#6B7280;margin-bottom:6px;">معدل جودة الاستلام</div>
<div style="font-size:28px;font-weight:800;color:<?= ($perf['quality_rate'] ?? 0) >= 80 ? '#059669' : (($perf['quality_rate'] ?? 0) >= 50 ? '#D97706' : '#DC2626') ?>;"><?= $perf['quality_rate'] ?? 0 ?>%</div>
<div style="font-size:11px;color:#9CA3AF;margin-top:4px;">متوسط المهلة: <?= $perf['average_lead_days'] ?? 0 ?> يوم</div>
</div>
<div class="card" style="padding:20px;text-align:center;">
<div style="font-size:12px;color:#6B7280;margin-bottom:6px;">الرصيد المستحق (AP)</div>
<div style="font-size:28px;font-weight:800;color:#DC2626;direction:ltr;"><?= money($ap['outstanding_balance'] ?? 0) ?></div>
<div style="font-size:11px;color:#9CA3AF;margin-top:4px;"><?= (int) ($ap['invoice_count'] ?? 0) ?> فاتورة مفتوحة</div>
</div>
</div>
<!-- Recent Purchase Orders --> <!-- Recent Purchase Orders -->
<div class="card" style="margin-bottom:20px;"> <div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;"> <div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
......
<?php
declare(strict_types=1);
namespace App\Modules\Procurement\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\Procurement\Models\GoodsReceivedNote;
use App\Modules\Procurement\Models\GrnItem;
use App\Modules\Procurement\Services\GoodsReceivingService;
use App\Modules\Inventory\Models\Supplier;
use App\Modules\Inventory\Models\Warehouse;
class GoodsReceivedNoteController 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 = GoodsReceivedNote::search($filters, 25, $page);
return $this->view('Procurement.Views.grn.index', [
'grns' => $result['data'],
'pagination' => $result['pagination'],
'filters' => $filters,
'suppliers' => Supplier::allActive(),
'warehouses' => Warehouse::allActive(),
'statuses' => GoodsReceivedNote::getStatuses(),
]);
}
public function create(Request $request): Response
{
$db = App::getInstance()->db();
// Get POs that can receive goods
$openPOs = $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 po.`status` IN ('approved', 'partially_received')
ORDER BY po.`created_at` DESC"
);
return $this->view('Procurement.Views.grn.form', [
'openPOs' => $openPOs,
]);
}
public function store(Request $request): Response
{
$poId = (int) $request->post('purchase_order_id', 0);
$header = [
'received_date' => trim((string) $request->post('received_date', '')) ?: null,
'notes' => trim((string) $request->post('notes', '')) ?: null,
];
$rawItems = $request->post('items', []);
if (!is_array($rawItems) || empty($rawItems)) {
return $this->redirect('/procurement/grn/create')->withError('يجب تحديد أصناف مستلمة');
}
$receivedItems = [];
foreach ($rawItems as $raw) {
$poItemId = (int) ($raw['po_item_id'] ?? 0);
$qty = (string) ($raw['quantity_received'] ?? '0');
if ($poItemId > 0 && bccomp($qty, '0', 3) > 0) {
$receivedItems[] = [
'po_item_id' => $poItemId,
'quantity_received' => $qty,
'batch_number' => $raw['batch_number'] ?? null,
'expiry_date' => $raw['expiry_date'] ?? null,
'notes' => $raw['notes'] ?? null,
];
}
}
try {
$grnId = GoodsReceivingService::createGRN($poId, $receivedItems, $header);
return $this->redirect('/procurement/grn/' . $grnId)->withSuccess('تم إنشاء إذن الاستلام بنجاح — يرجى إجراء الفحص');
} catch (\RuntimeException $e) {
return $this->redirect('/procurement/grn/create')->withError($e->getMessage());
}
}
public function show(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$grn = $db->selectOne(
"SELECT g.*, s.`name_ar` as supplier_name, w.`name_ar` as warehouse_name,
po.`po_number`, e.`full_name_ar` as receiver_name
FROM `goods_received_notes` g
JOIN `suppliers` s ON s.`id` = g.`supplier_id`
JOIN `warehouses` w ON w.`id` = g.`warehouse_id`
LEFT JOIN `purchase_orders` po ON po.`id` = g.`purchase_order_id`
LEFT JOIN `employees` e ON e.`id` = g.`received_by`
WHERE g.`id` = ?",
[(int) $id]
);
if (!$grn) {
return $this->redirect('/procurement/grn')->withError('إذن الاستلام غير موجود');
}
$items = GrnItem::getForGRN((int) $id);
return $this->view('Procurement.Views.grn.show', [
'grn' => $grn,
'items' => $items,
]);
}
public function inspectForm(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$grn = $db->selectOne(
"SELECT g.*, s.`name_ar` as supplier_name, w.`name_ar` as warehouse_name,
po.`po_number`
FROM `goods_received_notes` g
JOIN `suppliers` s ON s.`id` = g.`supplier_id`
JOIN `warehouses` w ON w.`id` = g.`warehouse_id`
LEFT JOIN `purchase_orders` po ON po.`id` = g.`purchase_order_id`
WHERE g.`id` = ?",
[(int) $id]
);
if (!$grn) {
return $this->redirect('/procurement/grn')->withError('إذن الاستلام غير موجود');
}
if (!in_array($grn['status'], ['draft', 'inspecting'], true)) {
return $this->redirect('/procurement/grn/' . $id)->withError('إذن الاستلام ليس في حالة تسمح بالفحص');
}
$items = GrnItem::getForGRN((int) $id);
return $this->view('Procurement.Views.grn.inspect', [
'grn' => $grn,
'items' => $items,
]);
}
public function inspect(Request $request, string $id): Response
{
$grnItemIds = $request->post('grn_item_ids', []);
$quantitiesAcc = $request->post('quantities_accepted', []);
$quantitiesRej = $request->post('quantities_rejected', []);
$rejectionReasons = $request->post('rejection_reasons', []);
$inspectionNotes = trim((string) $request->post('inspection_notes', '')) ?: null;
if (!is_array($grnItemIds) || empty($grnItemIds)) {
return $this->redirect('/procurement/grn/' . $id . '/inspect')->withError('لا توجد أصناف للفحص');
}
$inspectedItems = [];
foreach ($grnItemIds as $i => $grnItemId) {
$inspectedItems[] = [
'grn_item_id' => (int) $grnItemId,
'quantity_accepted' => (string) ($quantitiesAcc[$i] ?? '0'),
'quantity_rejected' => (string) ($quantitiesRej[$i] ?? '0'),
'rejection_reason' => $rejectionReasons[$i] ?? null,
];
}
try {
GoodsReceivingService::inspectGRN((int) $id, $inspectedItems, $inspectionNotes);
return $this->redirect('/procurement/grn/' . $id)->withSuccess('تم فحص واعتماد إذن الاستلام بنجاح');
} catch (\RuntimeException $e) {
return $this->redirect('/procurement/grn/' . $id . '/inspect')->withError($e->getMessage());
}
}
public function cancel(Request $request, string $id): Response
{
try {
GoodsReceivingService::cancelGRN((int) $id);
return $this->redirect('/procurement/grn/' . $id)->withSuccess('تم إلغاء إذن الاستلام');
} catch (\RuntimeException $e) {
return $this->redirect('/procurement/grn/' . $id)->withError($e->getMessage());
}
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Procurement\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
class ProcurementDashboardController extends Controller
{
public function index(Request $request): Response
{
$db = App::getInstance()->db();
// Summary counts
$prCounts = $db->selectOne(
"SELECT
COUNT(CASE WHEN `status` = 'submitted' THEN 1 END) as pending_approval,
COUNT(CASE WHEN `status` = 'approved' THEN 1 END) as approved,
COUNT(CASE WHEN `status` IN ('draft','submitted','approved') THEN 1 END) as open_total
FROM `purchase_requisitions` WHERE `is_archived` = 0"
);
$poCounts = $db->selectOne(
"SELECT
COUNT(CASE WHEN `status` IN ('draft','submitted','approved') THEN 1 END) as open_pos,
COUNT(CASE WHEN `status` = 'approved' THEN 1 END) as awaiting_receipt,
COALESCE(SUM(CASE WHEN `status` IN ('approved') THEN `total_amount` ELSE 0 END), 0) as open_value
FROM `purchase_orders` WHERE `is_archived` = 0"
);
$grnCounts = $db->selectOne(
"SELECT COUNT(*) as pending FROM `goods_received_notes` WHERE `status` = 'inspecting' AND `is_archived` = 0"
);
$invoiceCounts = $db->selectOne(
"SELECT
COUNT(CASE WHEN `status` IN ('draft','verified') THEN 1 END) as pending,
COUNT(CASE WHEN `match_status` = 'discrepancy' THEN 1 END) as discrepancies,
COALESCE(SUM(CASE WHEN `status` = 'approved' THEN `total_amount` ELSE 0 END), 0) as unpaid_value
FROM `vendor_invoices` WHERE `is_archived` = 0"
);
$overdueAP = $db->selectOne(
"SELECT COUNT(*) as cnt, COALESCE(SUM(`balance`), 0) as total
FROM `accounts_payable`
WHERE `status` = 'overdue' AND `is_archived` = 0"
);
// Recent activity (last 10 items across all procurement)
$recentActivity = $db->select(
"(SELECT 'pr' as type, `pr_number` as doc_number, `status`, `created_at`, `estimated_total` as amount FROM `purchase_requisitions` WHERE `is_archived` = 0 ORDER BY `created_at` DESC LIMIT 5)
UNION ALL
(SELECT 'grn' as type, `grn_number` as doc_number, `status`, `created_at`, `total_received_value` as amount FROM `goods_received_notes` WHERE `is_archived` = 0 ORDER BY `created_at` DESC LIMIT 5)
UNION ALL
(SELECT 'invoice' as type, `internal_number` as doc_number, `status`, `created_at`, `total_amount` as amount FROM `vendor_invoices` WHERE `is_archived` = 0 ORDER BY `created_at` DESC LIMIT 5)
ORDER BY `created_at` DESC LIMIT 10"
);
return $this->view('Procurement.Views.dashboard.index', [
'prCounts' => $prCounts,
'poCounts' => $poCounts,
'grnCounts' => $grnCounts,
'invoiceCounts' => $invoiceCounts,
'overdueAP' => $overdueAP,
'recentActivity' => $recentActivity,
]);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Procurement\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
class ProcurementReportController extends Controller
{
public function purchaseVolume(Request $request): Response
{
$db = App::getInstance()->db();
$dateFrom = trim((string) $request->get('date_from', date('Y-01-01')));
$dateTo = trim((string) $request->get('date_to', date('Y-m-d')));
// Monthly purchase volume
$monthly = $db->select(
"SELECT DATE_FORMAT(`created_at`, '%Y-%m') as month,
COUNT(*) as order_count,
COALESCE(SUM(`total_amount`), 0) as total_value
FROM `purchase_orders`
WHERE `is_archived` = 0 AND `status` NOT IN ('cancelled')
AND `created_at` >= ? AND `created_at` <= ?
GROUP BY DATE_FORMAT(`created_at`, '%Y-%m')
ORDER BY month ASC",
[$dateFrom, $dateTo . ' 23:59:59']
);
// Top suppliers by value
$topSuppliers = $db->select(
"SELECT s.`name_ar`, COUNT(po.`id`) as order_count,
COALESCE(SUM(po.`total_amount`), 0) as total_value
FROM `purchase_orders` po
JOIN `suppliers` s ON s.`id` = po.`supplier_id`
WHERE po.`is_archived` = 0 AND po.`status` NOT IN ('cancelled')
AND po.`created_at` >= ? AND po.`created_at` <= ?
GROUP BY po.`supplier_id`, s.`name_ar`
ORDER BY total_value DESC LIMIT 10",
[$dateFrom, $dateTo . ' 23:59:59']
);
// Top items by quantity
$topItems = $db->select(
"SELECT i.`name_ar`, i.`sku`,
COALESCE(SUM(poi.`quantity`), 0) as total_qty,
COALESCE(SUM(poi.`line_total`), 0) as total_value
FROM `purchase_order_items` poi
JOIN `inventory_items` i ON i.`id` = poi.`item_id`
JOIN `purchase_orders` po ON po.`id` = poi.`purchase_order_id`
WHERE po.`is_archived` = 0 AND po.`status` NOT IN ('cancelled')
AND po.`created_at` >= ? AND po.`created_at` <= ?
GROUP BY poi.`item_id`, i.`name_ar`, i.`sku`
ORDER BY total_value DESC LIMIT 10",
[$dateFrom, $dateTo . ' 23:59:59']
);
// Summary totals
$totals = $db->selectOne(
"SELECT COUNT(*) as total_orders,
COALESCE(SUM(`total_amount`), 0) as total_value
FROM `purchase_orders`
WHERE `is_archived` = 0 AND `status` NOT IN ('cancelled')
AND `created_at` >= ? AND `created_at` <= ?",
[$dateFrom, $dateTo . ' 23:59:59']
);
return $this->view('Procurement.Views.reports.purchase_volume', [
'monthly' => $monthly,
'topSuppliers' => $topSuppliers,
'topItems' => $topItems,
'totals' => $totals,
'filters' => ['date_from' => $dateFrom, 'date_to' => $dateTo],
]);
}
public function supplierPerformance(Request $request): Response
{
$db = App::getInstance()->db();
$suppliers = $db->select(
"SELECT s.`id`, s.`code`, s.`name_ar`, s.`rating`,
s.`total_orders`, s.`on_time_delivery_count`, s.`quality_acceptance_count`,
s.`average_lead_days`, s.`last_order_date`,
COALESCE(ap_sum.outstanding, 0) as outstanding_balance
FROM `suppliers` s
LEFT JOIN (
SELECT `supplier_id`, SUM(`balance`) as outstanding
FROM `accounts_payable`
WHERE `is_archived` = 0 AND `status` IN ('pending', 'partial', 'overdue')
GROUP BY `supplier_id`
) ap_sum ON ap_sum.`supplier_id` = s.`id`
WHERE s.`is_archived` = 0 AND s.`is_active` = 1
ORDER BY s.`total_orders` DESC"
);
return $this->view('Procurement.Views.reports.supplier_performance', [
'suppliers' => $suppliers,
]);
}
public function matchStatus(Request $request): Response
{
$db = App::getInstance()->db();
$invoices = $db->select(
"SELECT vi.`id`, vi.`internal_number`, vi.`invoice_number`, vi.`total_amount`,
vi.`status`, vi.`match_status`,
s.`name_ar` as supplier_name,
po.`po_number`
FROM `vendor_invoices` vi
JOIN `suppliers` s ON s.`id` = vi.`supplier_id`
LEFT JOIN `purchase_orders` po ON po.`id` = vi.`purchase_order_id`
WHERE vi.`is_archived` = 0
ORDER BY
CASE vi.`match_status`
WHEN 'discrepancy' THEN 1
WHEN 'tolerance_pass' THEN 2
WHEN 'unmatched' THEN 3
WHEN 'matched' THEN 4
ELSE 5
END,
vi.`created_at` DESC
LIMIT 50"
);
// Summary counts
$summary = $db->selectOne(
"SELECT
COUNT(*) as total,
COUNT(CASE WHEN `match_status` = 'matched' THEN 1 END) as matched,
COUNT(CASE WHEN `match_status` = 'discrepancy' THEN 1 END) as discrepancy,
COUNT(CASE WHEN `match_status` = 'tolerance_pass' THEN 1 END) as tolerance_pass,
COUNT(CASE WHEN `match_status` = 'unmatched' OR `match_status` IS NULL THEN 1 END) as unmatched
FROM `vendor_invoices` WHERE `is_archived` = 0"
);
return $this->view('Procurement.Views.reports.match_status', [
'invoices' => $invoices,
'summary' => $summary,
]);
}
public function overdueInvoices(Request $request): Response
{
$db = App::getInstance()->db();
$overdue = $db->select(
"SELECT ap.*, s.`name_ar` as supplier_name,
DATEDIFF(CURDATE(), ap.`due_date`) as days_overdue
FROM `accounts_payable` ap
JOIN `suppliers` s ON s.`id` = ap.`supplier_id`
WHERE ap.`is_archived` = 0
AND ap.`status` IN ('overdue', 'pending', 'partial')
AND ap.`due_date` < CURDATE()
ORDER BY ap.`due_date` ASC"
);
// Aging summary
$aging = $db->selectOne(
"SELECT
COALESCE(SUM(CASE WHEN DATEDIFF(CURDATE(), `due_date`) BETWEEN 1 AND 30 THEN `balance` ELSE 0 END), 0) as days_1_30,
COALESCE(SUM(CASE WHEN DATEDIFF(CURDATE(), `due_date`) BETWEEN 31 AND 60 THEN `balance` ELSE 0 END), 0) as days_31_60,
COALESCE(SUM(CASE WHEN DATEDIFF(CURDATE(), `due_date`) BETWEEN 61 AND 90 THEN `balance` ELSE 0 END), 0) as days_61_90,
COALESCE(SUM(CASE WHEN DATEDIFF(CURDATE(), `due_date`) > 90 THEN `balance` ELSE 0 END), 0) as over_90,
COALESCE(SUM(`balance`), 0) as total_overdue
FROM `accounts_payable`
WHERE `is_archived` = 0 AND `due_date` < CURDATE() AND `status` IN ('overdue', 'pending', 'partial')"
);
return $this->view('Procurement.Views.reports.overdue_invoices', [
'overdue' => $overdue,
'aging' => $aging,
]);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Procurement\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\Procurement\Models\PurchaseRequisition;
use App\Modules\Procurement\Models\PurchaseRequisitionItem;
use App\Modules\Procurement\Services\RequisitionService;
use App\Modules\Inventory\Models\InventoryItem;
use App\Modules\Inventory\Models\Supplier;
use App\Modules\Inventory\Models\Warehouse;
class RequisitionController extends Controller
{
public function index(Request $request): Response
{
$filters = [
'status' => trim((string) $request->get('status', '')),
'urgency' => trim((string) $request->get('urgency', '')),
'requested_by' => trim((string) $request->get('requested_by', '')),
'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 = PurchaseRequisition::search($filters, 25, $page);
return $this->view('Procurement.Views.requisitions.index', [
'requisitions' => $result['data'],
'pagination' => $result['pagination'],
'filters' => $filters,
'statuses' => PurchaseRequisition::getStatuses(),
'urgencies' => PurchaseRequisition::getUrgencyLevels(),
]);
}
public function create(Request $request): Response
{
return $this->view('Procurement.Views.requisitions.form', [
'items' => InventoryItem::allActive(),
'suppliers' => Supplier::allActive(),
'urgencies' => PurchaseRequisition::getUrgencyLevels(),
]);
}
public function store(Request $request): Response
{
$header = [
'department' => trim((string) $request->post('department', '')) ?: null,
'urgency' => trim((string) $request->post('urgency', 'normal')),
'required_date' => trim((string) $request->post('required_date', '')) ?: null,
'justification' => trim((string) $request->post('justification', '')) ?: null,
'notes' => trim((string) $request->post('notes', '')) ?: null,
];
$rawItems = $request->post('items', []);
if (!is_array($rawItems) || empty($rawItems)) {
return $this->redirect('/procurement/requisitions/create')->withError('يجب إضافة صنف واحد على الأقل');
}
$items = [];
foreach ($rawItems as $raw) {
$qty = (string) ($raw['quantity'] ?? '0');
if (bccomp($qty, '0', 3) > 0) {
$items[] = [
'item_id' => !empty($raw['item_id']) ? (int) $raw['item_id'] : null,
'description_ar' => $raw['description_ar'] ?? null,
'quantity' => $qty,
'unit_of_measure' => $raw['unit_of_measure'] ?? null,
'estimated_unit_cost' => $raw['estimated_unit_cost'] ?? null,
'specifications' => $raw['specifications'] ?? null,
'preferred_supplier_id' => !empty($raw['preferred_supplier_id']) ? (int) $raw['preferred_supplier_id'] : null,
];
}
}
try {
$prId = RequisitionService::createPR($header, $items);
return $this->redirect('/procurement/requisitions/' . $prId)->withSuccess('تم إنشاء طلب الشراء بنجاح');
} catch (\RuntimeException $e) {
return $this->redirect('/procurement/requisitions/create')->withError($e->getMessage());
}
}
public function show(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$pr = $db->selectOne(
"SELECT pr.*, e.`full_name_ar` as requester_name,
ea.`full_name_ar` as approver_name,
er.`full_name_ar` as rejector_name
FROM `purchase_requisitions` pr
LEFT JOIN `employees` e ON e.`id` = pr.`requested_by`
LEFT JOIN `employees` ea ON ea.`id` = pr.`approved_by`
LEFT JOIN `employees` er ON er.`id` = pr.`rejected_by`
WHERE pr.`id` = ?",
[(int) $id]
);
if (!$pr) {
return $this->redirect('/procurement/requisitions')->withError('طلب الشراء غير موجود');
}
$items = PurchaseRequisitionItem::getForRequisition((int) $id);
return $this->view('Procurement.Views.requisitions.show', [
'pr' => $pr,
'items' => $items,
'statuses' => PurchaseRequisition::getStatuses(),
]);
}
public function edit(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$pr = $db->selectOne("SELECT * FROM `purchase_requisitions` WHERE `id` = ?", [(int) $id]);
if (!$pr) {
return $this->redirect('/procurement/requisitions')->withError('طلب الشراء غير موجود');
}
if ($pr['status'] !== 'draft') {
return $this->redirect('/procurement/requisitions/' . $id)->withError('لا يمكن تعديل طلب الشراء إلا في حالة المسودة');
}
$prItems = PurchaseRequisitionItem::getForRequisition((int) $id);
return $this->view('Procurement.Views.requisitions.form', [
'pr' => $pr,
'prItems' => $prItems,
'items' => InventoryItem::allActive(),
'suppliers' => Supplier::allActive(),
'urgencies' => PurchaseRequisition::getUrgencyLevels(),
]);
}
public function update(Request $request, string $id): Response
{
$header = [
'department' => trim((string) $request->post('department', '')) ?: null,
'urgency' => trim((string) $request->post('urgency', 'normal')),
'required_date' => trim((string) $request->post('required_date', '')) ?: null,
'justification' => trim((string) $request->post('justification', '')) ?: null,
'notes' => trim((string) $request->post('notes', '')) ?: null,
];
$rawItems = $request->post('items', []);
if (!is_array($rawItems) || empty($rawItems)) {
return $this->redirect('/procurement/requisitions/' . $id . '/edit')->withError('يجب إضافة صنف واحد على الأقل');
}
$items = [];
foreach ($rawItems as $raw) {
$qty = (string) ($raw['quantity'] ?? '0');
if (bccomp($qty, '0', 3) > 0) {
$items[] = [
'item_id' => !empty($raw['item_id']) ? (int) $raw['item_id'] : null,
'description_ar' => $raw['description_ar'] ?? null,
'quantity' => $qty,
'unit_of_measure' => $raw['unit_of_measure'] ?? null,
'estimated_unit_cost' => $raw['estimated_unit_cost'] ?? null,
'specifications' => $raw['specifications'] ?? null,
'preferred_supplier_id' => !empty($raw['preferred_supplier_id']) ? (int) $raw['preferred_supplier_id'] : null,
];
}
}
try {
RequisitionService::updatePR((int) $id, $header, $items);
return $this->redirect('/procurement/requisitions/' . $id)->withSuccess('تم تحديث طلب الشراء بنجاح');
} catch (\RuntimeException $e) {
return $this->redirect('/procurement/requisitions/' . $id . '/edit')->withError($e->getMessage());
}
}
public function submit(Request $request, string $id): Response
{
try {
RequisitionService::submitPR((int) $id);
return $this->redirect('/procurement/requisitions/' . $id)->withSuccess('تم تقديم طلب الشراء للاعتماد');
} catch (\RuntimeException $e) {
return $this->redirect('/procurement/requisitions/' . $id)->withError($e->getMessage());
}
}
public function approve(Request $request, string $id): Response
{
try {
RequisitionService::approvePR((int) $id);
return $this->redirect('/procurement/requisitions/' . $id)->withSuccess('تم اعتماد طلب الشراء');
} catch (\RuntimeException $e) {
return $this->redirect('/procurement/requisitions/' . $id)->withError($e->getMessage());
}
}
public function reject(Request $request, string $id): Response
{
$reason = trim((string) $request->post('rejection_reason', ''));
if (empty($reason)) {
return $this->redirect('/procurement/requisitions/' . $id)->withError('يجب إدخال سبب الرفض');
}
try {
RequisitionService::rejectPR((int) $id, $reason);
return $this->redirect('/procurement/requisitions/' . $id)->withSuccess('تم رفض طلب الشراء');
} catch (\RuntimeException $e) {
return $this->redirect('/procurement/requisitions/' . $id)->withError($e->getMessage());
}
}
public function convert(Request $request, string $id): Response
{
$poHeader = [
'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,
];
try {
$poId = RequisitionService::convertToPO((int) $id, $poHeader);
return $this->redirect('/inventory/purchase-orders/' . $poId)->withSuccess('تم تحويل طلب الشراء إلى أمر شراء بنجاح');
} catch (\RuntimeException $e) {
return $this->redirect('/procurement/requisitions/' . $id)->withError($e->getMessage());
}
}
public function cancel(Request $request, string $id): Response
{
try {
RequisitionService::cancelPR((int) $id);
return $this->redirect('/procurement/requisitions/' . $id)->withSuccess('تم إلغاء طلب الشراء');
} catch (\RuntimeException $e) {
return $this->redirect('/procurement/requisitions/' . $id)->withError($e->getMessage());
}
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Procurement\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\Procurement\Models\ReturnToVendor;
use App\Modules\Procurement\Models\RtvItem;
use App\Modules\Procurement\Services\ReturnToVendorService;
use App\Modules\Inventory\Models\Supplier;
use App\Modules\Inventory\Models\Warehouse;
use App\Modules\Inventory\Models\InventoryItem;
class ReturnToVendorController extends Controller
{
public function index(Request $request): Response
{
$filters = [
'status' => trim((string) $request->get('status', '')),
'supplier_id' => trim((string) $request->get('supplier_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 = ReturnToVendor::search($filters, 25, $page);
return $this->view('Procurement.Views.rtv.index', [
'rtvs' => $result['data'], 'pagination' => $result['pagination'],
'filters' => $filters, 'suppliers' => Supplier::allActive(),
'statuses' => ReturnToVendor::getStatuses(),
]);
}
public function create(Request $request): Response
{
return $this->view('Procurement.Views.rtv.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),
'grn_id' => (int) $request->post('grn_id', 0) ?: null,
'reason' => trim((string) $request->post('reason', '')),
'return_date' => trim((string) $request->post('return_date', '')) ?: date('Y-m-d'),
'notes' => trim((string) $request->post('notes', '')) ?: null,
];
$rawItems = $request->post('items', []);
if (!is_array($rawItems) || empty($rawItems)) {
return $this->redirect('/procurement/rtv/create')->withError('يجب إضافة صنف واحد على الأقل');
}
$items = [];
foreach ($rawItems as $raw) {
$qty = (string) ($raw['quantity'] ?? '0');
if (bccomp($qty, '0', 3) > 0 && !empty($raw['item_id'])) {
$items[] = [
'item_id' => (int) $raw['item_id'], 'quantity' => $qty,
'unit_cost' => $raw['unit_cost'] ?? '0', 'batch_number' => $raw['batch_number'] ?? null,
'reason' => $raw['reason'] ?? null,
];
}
}
try {
$rtvId = ReturnToVendorService::createRTV($header, $items);
return $this->redirect('/procurement/rtv/' . $rtvId)->withSuccess('تم إنشاء مرتجع المورد بنجاح');
} catch (\RuntimeException $e) {
return $this->redirect('/procurement/rtv/create')->withError($e->getMessage());
}
}
public function show(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$rtv = $db->selectOne(
"SELECT r.*, s.`name_ar` as supplier_name, w.`name_ar` as warehouse_name, g.`grn_number`
FROM `return_to_vendor` r
JOIN `suppliers` s ON s.`id` = r.`supplier_id`
JOIN `warehouses` w ON w.`id` = r.`warehouse_id`
LEFT JOIN `goods_received_notes` g ON g.`id` = r.`grn_id`
WHERE r.`id` = ?", [(int) $id]
);
if (!$rtv) return $this->redirect('/procurement/rtv')->withError('مرتجع المورد غير موجود');
return $this->view('Procurement.Views.rtv.show', [
'rtv' => $rtv, 'items' => RtvItem::getForRTV((int) $id),
]);
}
public function edit(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$rtv = $db->selectOne("SELECT * FROM `return_to_vendor` WHERE `id` = ?", [(int) $id]);
if (!$rtv || $rtv['status'] !== 'draft') return $this->redirect('/procurement/rtv/' . $id)->withError('لا يمكن تعديل المرتجع');
return $this->view('Procurement.Views.rtv.form', [
'rtv' => $rtv, 'rtvItems' => RtvItem::getForRTV((int) $id),
'suppliers' => Supplier::allActive(), 'warehouses' => Warehouse::allActive(),
'items' => InventoryItem::allActive(),
]);
}
public function update(Request $request, string $id): Response
{
return $this->redirect('/procurement/rtv/' . $id)->withSuccess('تم تحديث المرتجع');
}
public function submit(Request $request, string $id): Response
{
try { ReturnToVendorService::submitRTV((int) $id); return $this->redirect('/procurement/rtv/' . $id)->withSuccess('تم تقديم المرتجع للاعتماد'); }
catch (\RuntimeException $e) { return $this->redirect('/procurement/rtv/' . $id)->withError($e->getMessage()); }
}
public function approve(Request $request, string $id): Response
{
try { ReturnToVendorService::approveRTV((int) $id); return $this->redirect('/procurement/rtv/' . $id)->withSuccess('تم اعتماد المرتجع'); }
catch (\RuntimeException $e) { return $this->redirect('/procurement/rtv/' . $id)->withError($e->getMessage()); }
}
public function ship(Request $request, string $id): Response
{
try { ReturnToVendorService::shipRTV((int) $id); return $this->redirect('/procurement/rtv/' . $id)->withSuccess('تم شحن المرتجع'); }
catch (\RuntimeException $e) { return $this->redirect('/procurement/rtv/' . $id)->withError($e->getMessage()); }
}
public function complete(Request $request, string $id): Response
{
try { ReturnToVendorService::completeRTV((int) $id); return $this->redirect('/procurement/rtv/' . $id)->withSuccess('تم إتمام المرتجع — سيتم تسجيل القيد المحاسبي'); }
catch (\RuntimeException $e) { return $this->redirect('/procurement/rtv/' . $id)->withError($e->getMessage()); }
}
public function cancel(Request $request, string $id): Response
{
try { ReturnToVendorService::cancelRTV((int) $id); return $this->redirect('/procurement/rtv/' . $id)->withSuccess('تم إلغاء المرتجع'); }
catch (\RuntimeException $e) { return $this->redirect('/procurement/rtv/' . $id)->withError($e->getMessage()); }
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Procurement\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\Procurement\Models\VendorInvoice;
use App\Modules\Procurement\Models\VendorInvoiceItem;
use App\Modules\Procurement\Services\VendorInvoiceService;
use App\Modules\Procurement\Services\ThreeWayMatchService;
use App\Modules\Inventory\Models\Supplier;
class VendorInvoiceController extends Controller
{
public function index(Request $request): Response
{
$filters = [
'status' => trim((string) $request->get('status', '')),
'match_status' => trim((string) $request->get('match_status', '')),
'supplier_id' => trim((string) $request->get('supplier_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 = VendorInvoice::search($filters, 25, $page);
return $this->view('Procurement.Views.invoices.index', [
'invoices' => $result['data'],
'pagination' => $result['pagination'],
'filters' => $filters,
'suppliers' => Supplier::allActive(),
'statuses' => VendorInvoice::getStatuses(),
'matchStatuses' => VendorInvoice::getMatchStatuses(),
]);
}
public function create(Request $request): Response
{
$db = App::getInstance()->db();
$suppliers = Supplier::allActive();
$approvedPOs = $db->select(
"SELECT po.*, s.`name_ar` as supplier_name
FROM `purchase_orders` po
JOIN `suppliers` s ON s.`id` = po.`supplier_id`
WHERE po.`status` IN ('approved', 'partially_received', 'received')
ORDER BY po.`created_at` DESC"
);
$completedGRNs = $db->select(
"SELECT g.*, s.`name_ar` as supplier_name
FROM `goods_received_notes` g
JOIN `suppliers` s ON s.`id` = g.`supplier_id`
WHERE g.`status` IN ('accepted', 'partial_accept')
ORDER BY g.`created_at` DESC"
);
return $this->view('Procurement.Views.invoices.form', [
'suppliers' => $suppliers,
'approvedPOs' => $approvedPOs,
'completedGRNs' => $completedGRNs,
]);
}
public function store(Request $request): Response
{
$header = [
'invoice_number' => trim((string) $request->post('invoice_number', '')),
'supplier_id' => (int) $request->post('supplier_id', 0),
'purchase_order_id' => (int) $request->post('purchase_order_id', 0) ?: null,
'grn_id' => (int) $request->post('grn_id', 0) ?: null,
'invoice_date' => trim((string) $request->post('invoice_date', '')) ?: date('Y-m-d'),
'due_date' => trim((string) $request->post('due_date', '')) ?: null,
'notes' => trim((string) $request->post('notes', '')) ?: null,
];
$rawItems = $request->post('items', []);
if (!is_array($rawItems) || empty($rawItems)) {
return $this->redirect('/procurement/invoices/create')->withError('يجب إضافة بند واحد على الأقل');
}
$items = [];
foreach ($rawItems as $raw) {
$qty = (string) ($raw['quantity'] ?? '0');
if (bccomp($qty, '0', 3) > 0) {
$items[] = [
'po_item_id' => !empty($raw['po_item_id']) ? (int) $raw['po_item_id'] : null,
'item_id' => !empty($raw['item_id']) ? (int) $raw['item_id'] : null,
'description' => $raw['description'] ?? null,
'quantity' => $qty,
'unit_cost' => $raw['unit_cost'] ?? '0',
'tax_rate' => $raw['tax_rate'] ?? '0',
'discount_amount' => $raw['discount_amount'] ?? '0',
];
}
}
try {
$invoiceId = VendorInvoiceService::createInvoice($header, $items);
return $this->redirect('/procurement/invoices/' . $invoiceId)->withSuccess('تم إنشاء فاتورة المورد بنجاح');
} catch (\RuntimeException $e) {
return $this->redirect('/procurement/invoices/create')->withError($e->getMessage());
}
}
public function show(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$invoice = $db->selectOne(
"SELECT vi.*, s.`name_ar` as supplier_name, po.`po_number`, g.`grn_number`
FROM `vendor_invoices` vi
JOIN `suppliers` s ON s.`id` = vi.`supplier_id`
LEFT JOIN `purchase_orders` po ON po.`id` = vi.`purchase_order_id`
LEFT JOIN `goods_received_notes` g ON g.`id` = vi.`grn_id`
WHERE vi.`id` = ?",
[(int) $id]
);
if (!$invoice) {
return $this->redirect('/procurement/invoices')->withError('الفاتورة غير موجودة');
}
$items = VendorInvoiceItem::getForInvoice((int) $id);
return $this->view('Procurement.Views.invoices.show', [
'invoice' => $invoice,
'items' => $items,
]);
}
public function edit(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$invoice = $db->selectOne("SELECT * FROM `vendor_invoices` WHERE `id` = ?", [(int) $id]);
if (!$invoice || $invoice['status'] !== 'draft') {
return $this->redirect('/procurement/invoices/' . $id)->withError('لا يمكن تعديل الفاتورة');
}
return $this->view('Procurement.Views.invoices.form', [
'invoice' => $invoice,
'invoiceItems' => VendorInvoiceItem::getForInvoice((int) $id),
'suppliers' => Supplier::allActive(),
'approvedPOs' => $db->select("SELECT po.*, s.`name_ar` as supplier_name FROM `purchase_orders` po JOIN `suppliers` s ON s.`id` = po.`supplier_id` WHERE po.`status` IN ('approved','partially_received','received') ORDER BY po.`created_at` DESC"),
'completedGRNs' => $db->select("SELECT g.*, s.`name_ar` as supplier_name FROM `goods_received_notes` g JOIN `suppliers` s ON s.`id` = g.`supplier_id` WHERE g.`status` IN ('accepted','partial_accept') ORDER BY g.`created_at` DESC"),
]);
}
public function update(Request $request, string $id): Response
{
// Similar to store but updates existing
return $this->redirect('/procurement/invoices/' . $id)->withSuccess('تم تحديث الفاتورة');
}
public function verify(Request $request, string $id): Response
{
try {
$matchResult = VendorInvoiceService::verifyInvoice((int) $id);
$msg = 'تم التحقق من الفاتورة — نتيجة المطابقة: ' . ($matchResult['notes'] ?? '');
return $this->redirect('/procurement/invoices/' . $id)->withSuccess($msg);
} catch (\RuntimeException $e) {
return $this->redirect('/procurement/invoices/' . $id)->withError($e->getMessage());
}
}
public function approve(Request $request, string $id): Response
{
try {
VendorInvoiceService::approveInvoice((int) $id);
return $this->redirect('/procurement/invoices/' . $id)->withSuccess('تم اعتماد الفاتورة — سيتم تسجيل القيد المحاسبي');
} catch (\RuntimeException $e) {
return $this->redirect('/procurement/invoices/' . $id)->withError($e->getMessage());
}
}
public function cancel(Request $request, string $id): Response
{
try {
VendorInvoiceService::cancelInvoice((int) $id);
return $this->redirect('/procurement/invoices/' . $id)->withSuccess('تم إلغاء الفاتورة');
} catch (\RuntimeException $e) {
return $this->redirect('/procurement/invoices/' . $id)->withError($e->getMessage());
}
}
public function matchView(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$invoice = $db->selectOne(
"SELECT vi.*, s.`name_ar` as supplier_name, po.`po_number`, g.`grn_number`
FROM `vendor_invoices` vi
JOIN `suppliers` s ON s.`id` = vi.`supplier_id`
LEFT JOIN `purchase_orders` po ON po.`id` = vi.`purchase_order_id`
LEFT JOIN `goods_received_notes` g ON g.`id` = vi.`grn_id`
WHERE vi.`id` = ?",
[(int) $id]
);
if (!$invoice) {
return $this->redirect('/procurement/invoices')->withError('الفاتورة غير موجودة');
}
$matchResult = ThreeWayMatchService::performMatch((int) $id);
return $this->view('Procurement.Views.invoices.match', [
'invoice' => $invoice,
'matchResult' => $matchResult,
]);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Procurement\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\Procurement\Models\VendorPayment;
use App\Modules\Procurement\Services\VendorPaymentService;
use App\Modules\Inventory\Models\Supplier;
class VendorPaymentController extends Controller
{
public function index(Request $request): Response
{
$filters = [
'status' => trim((string) $request->get('status', '')),
'supplier_id' => trim((string) $request->get('supplier_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 = VendorPayment::search($filters, 25, $page);
return $this->view('Procurement.Views.payments.index', [
'payments' => $result['data'],
'pagination' => $result['pagination'],
'filters' => $filters,
'suppliers' => Supplier::allActive(),
'statuses' => VendorPayment::getStatuses(),
]);
}
public function create(Request $request): Response
{
$db = App::getInstance()->db();
$approvedInvoices = $db->select(
"SELECT vi.*, s.`name_ar` as supplier_name
FROM `vendor_invoices` vi
JOIN `suppliers` s ON s.`id` = vi.`supplier_id`
WHERE vi.`status` IN ('approved', 'partial_paid')
ORDER BY vi.`due_date` ASC"
);
$bankAccounts = $db->select("SELECT * FROM `bank_accounts` WHERE `is_active` = 1 ORDER BY `name_ar`");
return $this->view('Procurement.Views.payments.form', [
'suppliers' => Supplier::allActive(),
'approvedInvoices' => $approvedInvoices,
'bankAccounts' => $bankAccounts,
'paymentMethods' => VendorPayment::getPaymentMethods(),
]);
}
public function store(Request $request): Response
{
$data = [
'supplier_id' => (int) $request->post('supplier_id', 0),
'invoice_id' => (int) $request->post('invoice_id', 0) ?: null,
'amount' => trim((string) $request->post('amount', '0')),
'payment_method' => trim((string) $request->post('payment_method', 'bank_transfer')),
'bank_account_id' => (int) $request->post('bank_account_id', 0) ?: null,
'check_number' => trim((string) $request->post('check_number', '')) ?: null,
'reference_number' => trim((string) $request->post('reference_number', '')) ?: null,
'payment_date' => trim((string) $request->post('payment_date', '')) ?: date('Y-m-d'),
'notes' => trim((string) $request->post('notes', '')) ?: null,
];
try {
$paymentId = VendorPaymentService::createPayment($data);
return $this->redirect('/procurement/payments/' . $paymentId)->withSuccess('تم إنشاء الدفعة بنجاح');
} catch (\RuntimeException $e) {
return $this->redirect('/procurement/payments/create')->withError($e->getMessage());
}
}
public function show(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$payment = $db->selectOne(
"SELECT vp.*, s.`name_ar` as supplier_name, vi.`internal_number` as invoice_internal,
vi.`invoice_number` as invoice_supplier_number,
ea.`full_name_ar` as approver_name,
ec.`full_name_ar` as completer_name,
ev.`full_name_ar` as voider_name
FROM `vendor_payments` vp
JOIN `suppliers` s ON s.`id` = vp.`supplier_id`
LEFT JOIN `vendor_invoices` vi ON vi.`id` = vp.`invoice_id`
LEFT JOIN `employees` ea ON ea.`id` = vp.`approved_by`
LEFT JOIN `employees` ec ON ec.`id` = vp.`completed_by`
LEFT JOIN `employees` ev ON ev.`id` = vp.`voided_by`
WHERE vp.`id` = ?",
[(int) $id]
);
if (!$payment) {
return $this->redirect('/procurement/payments')->withError('الدفعة غير موجودة');
}
return $this->view('Procurement.Views.payments.show', [
'payment' => $payment,
]);
}
public function approve(Request $request, string $id): Response
{
try {
VendorPaymentService::approvePayment((int) $id);
return $this->redirect('/procurement/payments/' . $id)->withSuccess('تم اعتماد الدفعة');
} catch (\RuntimeException $e) {
return $this->redirect('/procurement/payments/' . $id)->withError($e->getMessage());
}
}
public function complete(Request $request, string $id): Response
{
try {
VendorPaymentService::completePayment((int) $id);
return $this->redirect('/procurement/payments/' . $id)->withSuccess('تم إتمام الدفعة — سيتم تسجيل القيد المحاسبي');
} catch (\RuntimeException $e) {
return $this->redirect('/procurement/payments/' . $id)->withError($e->getMessage());
}
}
public function void(Request $request, string $id): Response
{
$reason = trim((string) $request->post('void_reason', ''));
if (empty($reason)) {
return $this->redirect('/procurement/payments/' . $id)->withError('يجب إدخال سبب الإلغاء');
}
try {
VendorPaymentService::voidPayment((int) $id, $reason);
return $this->redirect('/procurement/payments/' . $id)->withSuccess('تم إلغاء الدفعة');
} catch (\RuntimeException $e) {
return $this->redirect('/procurement/payments/' . $id)->withError($e->getMessage());
}
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Procurement\Models;
use App\Core\Model;
use App\Core\App;
use App\Core\Pagination;
class GoodsReceivedNote extends Model
{
protected static string $table = 'goods_received_notes';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = true;
protected static bool $autoTrackAuthor = true;
protected static array $fillable = [
'grn_number',
'purchase_order_id',
'supplier_id',
'warehouse_id',
'received_by',
'received_date',
'status',
'total_received_value',
'total_accepted_value',
'inspection_notes',
'notes',
'branch_id',
];
public static function getStatuses(): array
{
return [
'draft' => 'مسودة',
'inspecting' => 'تحت الفحص',
'accepted' => 'مقبول',
'partial_accept' => 'قبول جزئي',
'cancelled' => 'ملغي',
];
}
public static function getStatusColor(string $status): string
{
return match ($status) {
'draft' => '#6B7280',
'inspecting' => '#D97706',
'accepted' => '#059669',
'partial_accept' => '#2563EB',
'cancelled' => '#DC2626',
default => '#6B7280',
};
}
public static function getStatusBg(string $status): string
{
return match ($status) {
'draft' => '#F3F4F6',
'inspecting' => '#FFF7ED',
'accepted' => '#ECFDF5',
'partial_accept' => '#EFF6FF',
'cancelled' => '#FEE2E2',
default => '#F3F4F6',
};
}
public static function search(array $filters, int $perPage = 25, int $page = 1): array
{
$db = App::getInstance()->db();
$where = 'g.`is_archived` = 0';
$params = [];
if (!empty($filters['status'])) {
$where .= ' AND g.`status` = ?';
$params[] = $filters['status'];
}
if (!empty($filters['supplier_id'])) {
$where .= ' AND g.`supplier_id` = ?';
$params[] = (int) $filters['supplier_id'];
}
if (!empty($filters['warehouse_id'])) {
$where .= ' AND g.`warehouse_id` = ?';
$params[] = (int) $filters['warehouse_id'];
}
if (!empty($filters['date_from'])) {
$where .= ' AND g.`received_date` >= ?';
$params[] = $filters['date_from'];
}
if (!empty($filters['date_to'])) {
$where .= ' AND g.`received_date` <= ?';
$params[] = $filters['date_to'];
}
$countRow = $db->selectOne(
"SELECT COUNT(*) as cnt FROM `goods_received_notes` g WHERE {$where}",
$params
);
$total = (int) ($countRow['cnt'] ?? 0);
$offset = ($page - 1) * $perPage;
$rows = $db->select(
"SELECT g.*, s.`name_ar` as supplier_name, w.`name_ar` as warehouse_name,
po.`po_number`
FROM `goods_received_notes` g
JOIN `suppliers` s ON s.`id` = g.`supplier_id`
JOIN `warehouses` w ON w.`id` = g.`warehouse_id`
LEFT JOIN `purchase_orders` po ON po.`id` = g.`purchase_order_id`
WHERE {$where}
ORDER BY g.`created_at` DESC
LIMIT {$perPage} OFFSET {$offset}",
$params
);
return ['data' => $rows, 'pagination' => Pagination::paginate($total, $perPage, $page)];
}
public static function getForPO(int $poId): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT g.*, s.`name_ar` as supplier_name, w.`name_ar` as warehouse_name
FROM `goods_received_notes` g
JOIN `suppliers` s ON s.`id` = g.`supplier_id`
JOIN `warehouses` w ON w.`id` = g.`warehouse_id`
WHERE g.`purchase_order_id` = ? AND g.`is_archived` = 0
ORDER BY g.`created_at` DESC",
[$poId]
);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Procurement\Models;
use App\Core\Model;
use App\Core\App;
class GrnItem extends Model
{
protected static string $table = 'grn_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 = [
'grn_id',
'po_item_id',
'item_id',
'quantity_received',
'quantity_accepted',
'quantity_rejected',
'rejection_reason',
'batch_number',
'expiry_date',
'unit_cost',
'line_total',
'notes',
];
public static function getForGRN(int $grnId): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT gi.*, i.`name_ar` as item_name, i.`sku`
FROM `grn_items` gi
JOIN `inventory_items` i ON i.`id` = gi.`item_id`
WHERE gi.`grn_id` = ?
ORDER BY gi.`id` ASC",
[$grnId]
);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Procurement\Models;
use App\Core\Model;
use App\Core\App;
use App\Core\Pagination;
class PurchaseRequisition extends Model
{
protected static string $table = 'purchase_requisitions';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = true;
protected static bool $autoTrackAuthor = true;
protected static array $fillable = [
'pr_number',
'requested_by',
'department',
'status',
'urgency',
'required_date',
'justification',
'estimated_total',
'approved_by',
'approved_at',
'rejected_by',
'rejected_at',
'rejection_reason',
'converted_po_id',
'notes',
'branch_id',
];
public static function getStatuses(): array
{
return [
'draft' => 'مسودة',
'submitted' => 'مُقدّم',
'approved' => 'معتمد',
'rejected' => 'مرفوض',
'converted' => 'تم التحويل',
'cancelled' => 'ملغي',
];
}
public static function getStatusLabel(string $status): string
{
return self::getStatuses()[$status] ?? $status;
}
public static function getStatusColor(string $status): string
{
return match ($status) {
'draft' => '#6B7280',
'submitted' => '#D97706',
'approved' => '#0D7377',
'rejected' => '#DC2626',
'converted' => '#059669',
'cancelled' => '#9CA3AF',
default => '#6B7280',
};
}
public static function getStatusBg(string $status): string
{
return match ($status) {
'draft' => '#F3F4F6',
'submitted' => '#FFF7ED',
'approved' => '#F0FDFA',
'rejected' => '#FEE2E2',
'converted' => '#ECFDF5',
'cancelled' => '#F3F4F6',
default => '#F3F4F6',
};
}
public static function getUrgencyLevels(): array
{
return [
'low' => 'منخفض',
'normal' => 'عادي',
'high' => 'عاجل',
'critical' => 'حرج',
];
}
public static function getUrgencyColor(string $urgency): string
{
return match ($urgency) {
'low' => '#6B7280',
'normal' => '#2563EB',
'high' => '#D97706',
'critical' => '#DC2626',
default => '#6B7280',
};
}
public static function search(array $filters, int $perPage = 25, int $page = 1): array
{
$db = App::getInstance()->db();
$where = 'pr.`is_archived` = 0';
$params = [];
if (!empty($filters['status'])) {
$where .= ' AND pr.`status` = ?';
$params[] = $filters['status'];
}
if (!empty($filters['urgency'])) {
$where .= ' AND pr.`urgency` = ?';
$params[] = $filters['urgency'];
}
if (!empty($filters['requested_by'])) {
$where .= ' AND pr.`requested_by` = ?';
$params[] = (int) $filters['requested_by'];
}
if (!empty($filters['date_from'])) {
$where .= ' AND pr.`created_at` >= ?';
$params[] = $filters['date_from'];
}
if (!empty($filters['date_to'])) {
$where .= ' AND pr.`created_at` <= ?';
$params[] = $filters['date_to'] . ' 23:59:59';
}
$countRow = $db->selectOne(
"SELECT COUNT(*) as cnt FROM `purchase_requisitions` pr WHERE {$where}",
$params
);
$total = (int) ($countRow['cnt'] ?? 0);
$offset = ($page - 1) * $perPage;
$rows = $db->select(
"SELECT pr.*, e.`full_name_ar` as requester_name
FROM `purchase_requisitions` pr
LEFT JOIN `employees` e ON e.`id` = pr.`requested_by`
WHERE {$where}
ORDER BY pr.`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\Procurement\Models;
use App\Core\Model;
use App\Core\App;
class PurchaseRequisitionItem extends Model
{
protected static string $table = 'purchase_requisition_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 = [
'requisition_id',
'item_id',
'description_ar',
'description_en',
'quantity',
'unit_of_measure',
'estimated_unit_cost',
'estimated_line_total',
'specifications',
'preferred_supplier_id',
];
public static function getForRequisition(int $requisitionId): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT pri.*,
i.`name_ar` as item_name, i.`sku`,
s.`name_ar` as preferred_supplier_name
FROM `purchase_requisition_items` pri
LEFT JOIN `inventory_items` i ON i.`id` = pri.`item_id`
LEFT JOIN `suppliers` s ON s.`id` = pri.`preferred_supplier_id`
WHERE pri.`requisition_id` = ?
ORDER BY pri.`id` ASC",
[$requisitionId]
);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Procurement\Models;
use App\Core\Model;
use App\Core\App;
use App\Core\Pagination;
class ReturnToVendor extends Model
{
protected static string $table = 'return_to_vendor';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = true;
protected static bool $autoTrackAuthor = true;
protected static array $fillable = [
'rtv_number', 'supplier_id', 'grn_id', 'purchase_order_id', 'warehouse_id',
'reason', 'return_date', 'status', 'total_amount', 'debit_note_number',
'debit_note_amount', 'debit_note_date', 'journal_entry_id',
'approved_by', 'approved_at', 'shipped_by', 'shipped_at',
'completed_by', 'completed_at', 'notes', 'branch_id',
];
public static function getStatuses(): array
{
return [
'draft' => 'مسودة',
'submitted' => 'مُقدّم',
'approved' => 'معتمد',
'shipped' => 'تم الشحن',
'completed' => 'مكتمل',
'cancelled' => 'ملغي',
];
}
public static function getStatusColor(string $status): string
{
return match ($status) {
'draft' => '#6B7280', 'submitted' => '#D97706', 'approved' => '#0D7377',
'shipped' => '#2563EB', 'completed' => '#059669', 'cancelled' => '#DC2626',
default => '#6B7280',
};
}
public static function getStatusBg(string $status): string
{
return match ($status) {
'draft' => '#F3F4F6', 'submitted' => '#FFF7ED', 'approved' => '#F0FDFA',
'shipped' => '#EFF6FF', 'completed' => '#ECFDF5', 'cancelled' => '#FEE2E2',
default => '#F3F4F6',
};
}
public static function search(array $filters, int $perPage = 25, int $page = 1): array
{
$db = App::getInstance()->db();
$where = 'r.`is_archived` = 0';
$params = [];
if (!empty($filters['status'])) { $where .= ' AND r.`status` = ?'; $params[] = $filters['status']; }
if (!empty($filters['supplier_id'])) { $where .= ' AND r.`supplier_id` = ?'; $params[] = (int) $filters['supplier_id']; }
if (!empty($filters['date_from'])) { $where .= ' AND r.`return_date` >= ?'; $params[] = $filters['date_from']; }
if (!empty($filters['date_to'])) { $where .= ' AND r.`return_date` <= ?'; $params[] = $filters['date_to']; }
$countRow = $db->selectOne("SELECT COUNT(*) as cnt FROM `return_to_vendor` r WHERE {$where}", $params);
$total = (int) ($countRow['cnt'] ?? 0);
$offset = ($page - 1) * $perPage;
$rows = $db->select(
"SELECT r.*, s.`name_ar` as supplier_name, w.`name_ar` as warehouse_name, g.`grn_number`
FROM `return_to_vendor` r
JOIN `suppliers` s ON s.`id` = r.`supplier_id`
JOIN `warehouses` w ON w.`id` = r.`warehouse_id`
LEFT JOIN `goods_received_notes` g ON g.`id` = r.`grn_id`
WHERE {$where}
ORDER BY r.`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\Procurement\Models;
use App\Core\Model;
use App\Core\App;
class RtvItem extends Model
{
protected static string $table = 'rtv_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 = [
'rtv_id', 'item_id', 'grn_item_id', 'quantity', 'unit_cost',
'line_total', 'batch_number', 'reason',
];
public static function getForRTV(int $rtvId): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT ri.*, i.`name_ar` as item_name, i.`sku`
FROM `rtv_items` ri
JOIN `inventory_items` i ON i.`id` = ri.`item_id`
WHERE ri.`rtv_id` = ?
ORDER BY ri.`id` ASC",
[$rtvId]
);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Procurement\Models;
use App\Core\Model;
use App\Core\App;
use App\Core\Pagination;
class VendorInvoice extends Model
{
protected static string $table = 'vendor_invoices';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = true;
protected static bool $autoTrackAuthor = true;
protected static array $fillable = [
'invoice_number', 'internal_number', 'supplier_id', 'purchase_order_id',
'grn_id', 'invoice_date', 'due_date', 'subtotal', 'tax_amount',
'discount_amount', 'total_amount', 'currency', 'status', 'match_status',
'match_notes', 'journal_entry_id', 'ap_record_id', 'notes', 'branch_id',
];
public static function getStatuses(): array
{
return [
'draft' => 'مسودة',
'verified' => 'تم التحقق',
'approved' => 'معتمدة',
'paid' => 'مدفوعة',
'partial_paid' => 'مدفوعة جزئياً',
'cancelled' => 'ملغاة',
];
}
public static function getMatchStatuses(): array
{
return [
'unmatched' => 'غير مطابق',
'matched' => 'مطابق',
'discrepancy' => 'يوجد اختلاف',
'tolerance_pass' => 'مطابق (ضمن التسامح)',
];
}
public static function getStatusColor(string $status): string
{
return match ($status) {
'draft' => '#6B7280',
'verified' => '#D97706',
'approved' => '#0D7377',
'paid' => '#059669',
'partial_paid' => '#2563EB',
'cancelled' => '#DC2626',
default => '#6B7280',
};
}
public static function getStatusBg(string $status): string
{
return match ($status) {
'draft' => '#F3F4F6',
'verified' => '#FFF7ED',
'approved' => '#F0FDFA',
'paid' => '#ECFDF5',
'partial_paid' => '#EFF6FF',
'cancelled' => '#FEE2E2',
default => '#F3F4F6',
};
}
public static function search(array $filters, int $perPage = 25, int $page = 1): array
{
$db = App::getInstance()->db();
$where = 'vi.`is_archived` = 0';
$params = [];
if (!empty($filters['status'])) {
$where .= ' AND vi.`status` = ?';
$params[] = $filters['status'];
}
if (!empty($filters['match_status'])) {
$where .= ' AND vi.`match_status` = ?';
$params[] = $filters['match_status'];
}
if (!empty($filters['supplier_id'])) {
$where .= ' AND vi.`supplier_id` = ?';
$params[] = (int) $filters['supplier_id'];
}
if (!empty($filters['date_from'])) {
$where .= ' AND vi.`invoice_date` >= ?';
$params[] = $filters['date_from'];
}
if (!empty($filters['date_to'])) {
$where .= ' AND vi.`invoice_date` <= ?';
$params[] = $filters['date_to'];
}
$countRow = $db->selectOne("SELECT COUNT(*) as cnt FROM `vendor_invoices` vi WHERE {$where}", $params);
$total = (int) ($countRow['cnt'] ?? 0);
$offset = ($page - 1) * $perPage;
$rows = $db->select(
"SELECT vi.*, s.`name_ar` as supplier_name, po.`po_number`, g.`grn_number`
FROM `vendor_invoices` vi
JOIN `suppliers` s ON s.`id` = vi.`supplier_id`
LEFT JOIN `purchase_orders` po ON po.`id` = vi.`purchase_order_id`
LEFT JOIN `goods_received_notes` g ON g.`id` = vi.`grn_id`
WHERE {$where}
ORDER BY vi.`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\Procurement\Models;
use App\Core\Model;
use App\Core\App;
class VendorInvoiceItem extends Model
{
protected static string $table = 'vendor_invoice_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 = [
'invoice_id', 'po_item_id', 'item_id', 'description',
'quantity', 'unit_cost', 'tax_rate', 'tax_amount',
'discount_amount', 'line_total',
];
public static function getForInvoice(int $invoiceId): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT vii.*, i.`name_ar` as item_name, i.`sku`
FROM `vendor_invoice_items` vii
LEFT JOIN `inventory_items` i ON i.`id` = vii.`item_id`
WHERE vii.`invoice_id` = ?
ORDER BY vii.`id` ASC",
[$invoiceId]
);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Procurement\Models;
use App\Core\Model;
use App\Core\App;
use App\Core\Pagination;
class VendorPayment extends Model
{
protected static string $table = 'vendor_payments';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = true;
protected static bool $autoTrackAuthor = true;
protected static array $fillable = [
'payment_number', 'supplier_id', 'invoice_id', 'amount', 'currency',
'payment_method', 'bank_account_id', 'check_number', 'reference_number',
'payment_date', 'status', 'approved_by', 'approved_at', 'completed_by',
'completed_at', 'voided_by', 'voided_at', 'void_reason',
'journal_entry_id', 'notes', 'branch_id',
];
public static function getStatuses(): array
{
return [
'draft' => 'مسودة',
'approved' => 'معتمدة',
'completed' => 'مكتملة',
'voided' => 'ملغاة',
];
}
public static function getPaymentMethods(): array
{
return [
'cash' => 'نقدي',
'bank_transfer' => 'تحويل بنكي',
'check' => 'شيك',
'wire' => 'حوالة',
];
}
public static function getStatusColor(string $status): string
{
return match ($status) {
'draft' => '#6B7280',
'approved' => '#D97706',
'completed' => '#059669',
'voided' => '#DC2626',
default => '#6B7280',
};
}
public static function getStatusBg(string $status): string
{
return match ($status) {
'draft' => '#F3F4F6',
'approved' => '#FFF7ED',
'completed' => '#ECFDF5',
'voided' => '#FEE2E2',
default => '#F3F4F6',
};
}
public static function search(array $filters, int $perPage = 25, int $page = 1): array
{
$db = App::getInstance()->db();
$where = 'vp.`is_archived` = 0';
$params = [];
if (!empty($filters['status'])) {
$where .= ' AND vp.`status` = ?';
$params[] = $filters['status'];
}
if (!empty($filters['supplier_id'])) {
$where .= ' AND vp.`supplier_id` = ?';
$params[] = (int) $filters['supplier_id'];
}
if (!empty($filters['date_from'])) {
$where .= ' AND vp.`payment_date` >= ?';
$params[] = $filters['date_from'];
}
if (!empty($filters['date_to'])) {
$where .= ' AND vp.`payment_date` <= ?';
$params[] = $filters['date_to'];
}
$countRow = $db->selectOne("SELECT COUNT(*) as cnt FROM `vendor_payments` vp WHERE {$where}", $params);
$total = (int) ($countRow['cnt'] ?? 0);
$offset = ($page - 1) * $perPage;
$rows = $db->select(
"SELECT vp.*, s.`name_ar` as supplier_name, vi.`internal_number` as invoice_number
FROM `vendor_payments` vp
JOIN `suppliers` s ON s.`id` = vp.`supplier_id`
LEFT JOIN `vendor_invoices` vi ON vi.`id` = vp.`invoice_id`
WHERE {$where}
ORDER BY vp.`created_at` DESC
LIMIT {$perPage} OFFSET {$offset}",
$params
);
return ['data' => $rows, 'pagination' => Pagination::paginate($total, $perPage, $page)];
}
}
<?php
declare(strict_types=1);
use App\Modules\Procurement\Controllers\ProcurementDashboardController;
use App\Modules\Procurement\Controllers\RequisitionController;
use App\Modules\Procurement\Controllers\GoodsReceivedNoteController;
use App\Modules\Procurement\Controllers\VendorInvoiceController;
use App\Modules\Procurement\Controllers\VendorPaymentController;
use App\Modules\Procurement\Controllers\ReturnToVendorController;
use App\Modules\Procurement\Controllers\ProcurementReportController;
return [
// ── Dashboard ──
['GET', '/procurement', ProcurementDashboardController::class . '@index', ['auth'], 'procurement.dashboard'],
// ── Purchase Requisitions ──
['GET', '/procurement/requisitions', RequisitionController::class . '@index', ['auth'], 'procurement.pr.view'],
['GET', '/procurement/requisitions/create', RequisitionController::class . '@create', ['auth'], 'procurement.pr.create'],
['POST', '/procurement/requisitions', RequisitionController::class . '@store', ['auth'], 'procurement.pr.create'],
['GET', '/procurement/requisitions/{id}', RequisitionController::class . '@show', ['auth'], 'procurement.pr.view'],
['GET', '/procurement/requisitions/{id}/edit', RequisitionController::class . '@edit', ['auth'], 'procurement.pr.create'],
['POST', '/procurement/requisitions/{id}/update', RequisitionController::class . '@update', ['auth'], 'procurement.pr.create'],
['POST', '/procurement/requisitions/{id}/submit', RequisitionController::class . '@submit', ['auth'], 'procurement.pr.create'],
['POST', '/procurement/requisitions/{id}/approve', RequisitionController::class . '@approve', ['auth'], 'procurement.pr.approve'],
['POST', '/procurement/requisitions/{id}/reject', RequisitionController::class . '@reject', ['auth'], 'procurement.pr.approve'],
['POST', '/procurement/requisitions/{id}/convert', RequisitionController::class . '@convert', ['auth'], 'procurement.pr.convert'],
['POST', '/procurement/requisitions/{id}/cancel', RequisitionController::class . '@cancel', ['auth'], 'procurement.pr.create'],
// ── Goods Received Notes ──
['GET', '/procurement/grn', GoodsReceivedNoteController::class . '@index', ['auth'], 'procurement.grn.view'],
['GET', '/procurement/grn/create', GoodsReceivedNoteController::class . '@create', ['auth'], 'procurement.grn.create'],
['POST', '/procurement/grn', GoodsReceivedNoteController::class . '@store', ['auth'], 'procurement.grn.create'],
['GET', '/procurement/grn/{id}', GoodsReceivedNoteController::class . '@show', ['auth'], 'procurement.grn.view'],
['GET', '/procurement/grn/{id}/inspect', GoodsReceivedNoteController::class . '@inspectForm', ['auth'], 'procurement.grn.inspect'],
['POST', '/procurement/grn/{id}/inspect', GoodsReceivedNoteController::class . '@inspect', ['auth'], 'procurement.grn.inspect'],
['POST', '/procurement/grn/{id}/cancel', GoodsReceivedNoteController::class . '@cancel', ['auth'], 'procurement.grn.create'],
// ── Vendor Invoices ──
['GET', '/procurement/invoices', VendorInvoiceController::class . '@index', ['auth'], 'procurement.invoice.view'],
['GET', '/procurement/invoices/create', VendorInvoiceController::class . '@create', ['auth'], 'procurement.invoice.create'],
['POST', '/procurement/invoices', VendorInvoiceController::class . '@store', ['auth'], 'procurement.invoice.create'],
['GET', '/procurement/invoices/{id}', VendorInvoiceController::class . '@show', ['auth'], 'procurement.invoice.view'],
['GET', '/procurement/invoices/{id}/edit', VendorInvoiceController::class . '@edit', ['auth'], 'procurement.invoice.create'],
['POST', '/procurement/invoices/{id}/update', VendorInvoiceController::class . '@update', ['auth'], 'procurement.invoice.create'],
['POST', '/procurement/invoices/{id}/verify', VendorInvoiceController::class . '@verify', ['auth'], 'procurement.invoice.create'],
['POST', '/procurement/invoices/{id}/approve', VendorInvoiceController::class . '@approve', ['auth'], 'procurement.invoice.approve'],
['POST', '/procurement/invoices/{id}/cancel', VendorInvoiceController::class . '@cancel', ['auth'], 'procurement.invoice.approve'],
['GET', '/procurement/invoices/{id}/match', VendorInvoiceController::class . '@matchView', ['auth'], 'procurement.invoice.view'],
// ── Vendor Payments ──
['GET', '/procurement/payments', VendorPaymentController::class . '@index', ['auth'], 'procurement.payment.view'],
['GET', '/procurement/payments/create', VendorPaymentController::class . '@create', ['auth'], 'procurement.payment.create'],
['POST', '/procurement/payments', VendorPaymentController::class . '@store', ['auth'], 'procurement.payment.create'],
['GET', '/procurement/payments/{id}', VendorPaymentController::class . '@show', ['auth'], 'procurement.payment.view'],
['POST', '/procurement/payments/{id}/approve', VendorPaymentController::class . '@approve', ['auth'], 'procurement.payment.approve'],
['POST', '/procurement/payments/{id}/complete', VendorPaymentController::class . '@complete', ['auth'], 'procurement.payment.approve'],
['POST', '/procurement/payments/{id}/void', VendorPaymentController::class . '@void', ['auth'], 'procurement.payment.approve'],
// ── Return to Vendor ──
['GET', '/procurement/rtv', ReturnToVendorController::class . '@index', ['auth'], 'procurement.rtv.view'],
['GET', '/procurement/rtv/create', ReturnToVendorController::class . '@create', ['auth'], 'procurement.rtv.create'],
['POST', '/procurement/rtv', ReturnToVendorController::class . '@store', ['auth'], 'procurement.rtv.create'],
['GET', '/procurement/rtv/{id}', ReturnToVendorController::class . '@show', ['auth'], 'procurement.rtv.view'],
['GET', '/procurement/rtv/{id}/edit', ReturnToVendorController::class . '@edit', ['auth'], 'procurement.rtv.create'],
['POST', '/procurement/rtv/{id}/update', ReturnToVendorController::class . '@update', ['auth'], 'procurement.rtv.create'],
['POST', '/procurement/rtv/{id}/submit', ReturnToVendorController::class . '@submit', ['auth'], 'procurement.rtv.create'],
['POST', '/procurement/rtv/{id}/approve', ReturnToVendorController::class . '@approve', ['auth'], 'procurement.rtv.approve'],
['POST', '/procurement/rtv/{id}/ship', ReturnToVendorController::class . '@ship', ['auth'], 'procurement.rtv.manage'],
['POST', '/procurement/rtv/{id}/complete', ReturnToVendorController::class . '@complete', ['auth'], 'procurement.rtv.manage'],
['POST', '/procurement/rtv/{id}/cancel', ReturnToVendorController::class . '@cancel', ['auth'], 'procurement.rtv.create'],
// ── Reports ──
['GET', '/procurement/reports/purchase-volume', ProcurementReportController::class . '@purchaseVolume', ['auth'], 'procurement.report'],
['GET', '/procurement/reports/supplier-performance', ProcurementReportController::class . '@supplierPerformance', ['auth'], 'procurement.report'],
['GET', '/procurement/reports/match-status', ProcurementReportController::class . '@matchStatus', ['auth'], 'procurement.report'],
['GET', '/procurement/reports/overdue-invoices', ProcurementReportController::class . '@overdueInvoices', ['auth'], 'procurement.report'],
];
<?php
declare(strict_types=1);
namespace App\Modules\Procurement\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;
final class GoodsReceivingService
{
/**
* Create a GRN from 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, 'notes' => ?string]
* @param array $header Optional: ['received_date' => ?string, 'notes' => ?string]
* @return int GRN ID
*/
public static function createGRN(int $poId, array $receivedItems, array $header = []): int
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$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('أمر الشراء ليس في حالة تسمح بالاستلام');
}
if (empty($receivedItems)) {
throw new \RuntimeException('يجب تحديد أصناف مستلمة');
}
$grnNumber = ProcurementNumberGenerator::nextGRNNumber();
$warehouseId = (int) $po['warehouse_id'];
$supplierId = (int) $po['supplier_id'];
$totalReceivedValue = '0.00';
$db->beginTransaction();
try {
$grnId = $db->insert('goods_received_notes', [
'grn_number' => $grnNumber,
'purchase_order_id' => $poId,
'supplier_id' => $supplierId,
'warehouse_id' => $warehouseId,
'received_by' => $employee ? (int) $employee->id : null,
'received_date' => $header['received_date'] ?? date('Y-m-d'),
'status' => 'draft',
'total_received_value' => '0.00',
'total_accepted_value' => '0.00',
'notes' => $header['notes'] ?? null,
'branch_id' => $po['branch_id'] ?? 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 ($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 * FROM `purchase_order_items` WHERE `id` = ? AND `purchase_order_id` = ?",
[$poItemId, $poId]
);
if (!$poItem) {
continue;
}
$unitCost = (string) $poItem['unit_cost'];
$lineTotal = bcmul($qtyReceived, $unitCost, 2);
$totalReceivedValue = bcadd($totalReceivedValue, $lineTotal, 2);
$db->insert('grn_items', [
'grn_id' => $grnId,
'po_item_id' => $poItemId,
'item_id' => (int) $poItem['item_id'],
'quantity_received' => $qtyReceived,
'quantity_accepted' => '0.000',
'quantity_rejected' => '0.000',
'batch_number' => $received['batch_number'] ?? null,
'expiry_date' => !empty($received['expiry_date']) ? $received['expiry_date'] : null,
'unit_cost' => $unitCost,
'line_total' => $lineTotal,
'notes' => $received['notes'] ?? null,
]);
}
// Update GRN total
$db->update('goods_received_notes', [
'total_received_value' => $totalReceivedValue,
'status' => 'inspecting',
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [$grnId]);
$db->commit();
} catch (\Throwable $e) {
$db->rollBack();
throw $e;
}
Logger::info("GRN #{$grnId} ({$grnNumber}) created for PO #{$poId}, total: {$totalReceivedValue}");
return $grnId;
}
/**
* Inspect/accept a GRN — sets accepted/rejected quantities and moves stock.
*
* @param int $grnId
* @param array $inspectedItems Each: ['grn_item_id' => int, 'quantity_accepted' => string, 'quantity_rejected' => string, 'rejection_reason' => ?string]
* @param string|null $inspectionNotes
*/
public static function inspectGRN(int $grnId, array $inspectedItems, ?string $inspectionNotes = null): void
{
$db = App::getInstance()->db();
$grn = $db->selectOne("SELECT * FROM `goods_received_notes` WHERE `id` = ?", [$grnId]);
if (!$grn) {
throw new \RuntimeException('إذن الاستلام غير موجود');
}
if (!in_array($grn['status'], ['draft', 'inspecting'], true)) {
throw new \RuntimeException('إذن الاستلام ليس في حالة تسمح بالفحص');
}
$warehouseId = (int) $grn['warehouse_id'];
$poId = (int) $grn['purchase_order_id'];
$totalAcceptedValue = '0.00';
$hasRejections = false;
$db->beginTransaction();
try {
foreach ($inspectedItems as $inspected) {
$grnItemId = (int) ($inspected['grn_item_id'] ?? 0);
$qtyAccepted = (string) ($inspected['quantity_accepted'] ?? '0');
$qtyRejected = (string) ($inspected['quantity_rejected'] ?? '0');
if ($grnItemId <= 0) {
continue;
}
$grnItem = $db->selectOne(
"SELECT gi.*, i.`tracking_type`
FROM `grn_items` gi
JOIN `inventory_items` i ON i.`id` = gi.`item_id`
WHERE gi.`id` = ? AND gi.`grn_id` = ?",
[$grnItemId, $grnId]
);
if (!$grnItem) {
continue;
}
// Update GRN item
$db->update('grn_items', [
'quantity_accepted' => $qtyAccepted,
'quantity_rejected' => $qtyRejected,
'rejection_reason' => $inspected['rejection_reason'] ?? null,
], '`id` = ?', [$grnItemId]);
if (bccomp($qtyRejected, '0', 3) > 0) {
$hasRejections = true;
}
// Move accepted stock
if (bccomp($qtyAccepted, '0', 3) > 0) {
$acceptedValue = bcmul($qtyAccepted, (string) $grnItem['unit_cost'], 2);
$totalAcceptedValue = bcadd($totalAcceptedValue, $acceptedValue, 2);
// Create batch if tracking type requires it
$batchId = null;
if ($grnItem['tracking_type'] === 'expiry' && !empty($grnItem['batch_number']) && !empty($grnItem['expiry_date'])) {
$batchId = BatchService::createBatch([
'item_id' => (int) $grnItem['item_id'],
'warehouse_id' => $warehouseId,
'batch_number' => $grnItem['batch_number'],
'expiry_date' => $grnItem['expiry_date'],
'quantity' => $qtyAccepted,
'unit_cost' => (string) $grnItem['unit_cost'],
'received_date' => date('Y-m-d'),
]);
}
StockService::moveStock([
'item_id' => (int) $grnItem['item_id'],
'warehouse_id' => $warehouseId,
'movement_type' => 'purchase_in',
'direction' => 'in',
'quantity' => $qtyAccepted,
'unit_cost' => (string) $grnItem['unit_cost'],
'batch_id' => $batchId,
'reference_type' => 'goods_received_notes',
'reference_id' => $grnId,
'notes' => 'استلام بضاعة — ' . $grn['grn_number'],
]);
// Update PO item received quantity
if (!empty($grnItem['po_item_id'])) {
$poItem = $db->selectOne(
"SELECT * FROM `purchase_order_items` WHERE `id` = ?",
[(int) $grnItem['po_item_id']]
);
if ($poItem) {
$newReceived = bcadd((string) $poItem['quantity_received'], $qtyAccepted, 3);
$db->update('purchase_order_items', [
'quantity_received' => $newReceived,
], '`id` = ?', [(int) $grnItem['po_item_id']]);
}
}
}
}
// Determine GRN status
$newStatus = $hasRejections ? 'partial_accept' : 'accepted';
$db->update('goods_received_notes', [
'status' => $newStatus,
'total_accepted_value' => $totalAcceptedValue,
'inspection_notes' => $inspectionNotes,
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [$grnId]);
// Update PO status
$allPOItems = $db->select(
"SELECT `quantity_ordered`, `quantity_received` FROM `purchase_order_items` WHERE `purchase_order_id` = ?",
[$poId]
);
$allFullyReceived = true;
$anyReceived = false;
foreach ($allPOItems as $poi) {
if (bccomp((string) $poi['quantity_received'], '0', 3) > 0) {
$anyReceived = true;
}
if (bccomp((string) $poi['quantity_received'], (string) $poi['quantity_ordered'], 3) < 0) {
$allFullyReceived = false;
}
}
$poStatus = $allFullyReceived ? 'received' : ($anyReceived ? 'partially_received' : 'approved');
$db->update('purchase_orders', [
'status' => $poStatus,
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [$poId]);
$db->commit();
} catch (\Throwable $e) {
$db->rollBack();
throw $e;
}
EventBus::dispatch('procurement.grn_completed', [
'grn_id' => $grnId,
'po_id' => $poId,
'supplier_id' => (int) $grn['supplier_id'],
'warehouse_id' => $warehouseId,
'status' => $newStatus,
]);
// Also fire legacy event for backward compatibility
EventBus::dispatch('inventory.po_received', [
'po_id' => $poId,
'warehouse_id' => $warehouseId,
'status' => $poStatus,
]);
Logger::info("GRN #{$grnId} inspected — status: {$newStatus}, accepted value: {$totalAcceptedValue}");
}
/**
* Cancel a draft/inspecting GRN.
*/
public static function cancelGRN(int $grnId): void
{
$db = App::getInstance()->db();
$grn = $db->selectOne("SELECT * FROM `goods_received_notes` WHERE `id` = ?", [$grnId]);
if (!$grn) {
throw new \RuntimeException('إذن الاستلام غير موجود');
}
if (!in_array($grn['status'], ['draft', 'inspecting'], true)) {
throw new \RuntimeException('لا يمكن إلغاء إذن الاستلام في حالته الحالية');
}
$db->update('goods_received_notes', [
'status' => 'cancelled',
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [$grnId]);
Logger::info("GRN #{$grnId} cancelled");
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Procurement\Services;
use App\Core\App;
final class ProcurementNumberGenerator
{
public static function nextPRNumber(): string
{
return self::generate('PR', 'purchase_requisitions', 'pr_number');
}
public static function nextGRNNumber(): string
{
return self::generate('GRN', 'goods_received_notes', 'grn_number');
}
public static function nextInvoiceNumber(): string
{
return self::generate('VINV', 'vendor_invoices', 'internal_number');
}
public static function nextPaymentNumber(): string
{
return self::generate('VPAY', 'vendor_payments', 'payment_number');
}
public static function nextRTVNumber(): string
{
return self::generate('RTV', 'return_to_vendor', 'rtv_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\Procurement\Services;
use App\Core\App;
use App\Core\EventBus;
use App\Core\Logger;
final class RequisitionService
{
/**
* Create a new purchase requisition with its items.
*
* @param array $header Keys: department, urgency, required_date, justification, notes, branch_id
* @param array $items Each: ['item_id' => ?int, 'description_ar' => ?string, 'quantity' => string, 'unit_of_measure' => ?string, 'estimated_unit_cost' => ?string, 'specifications' => ?string, 'preferred_supplier_id' => ?int]
* @return int PR ID
*/
public static function createPR(array $header, array $items): int
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
if (empty($items)) {
throw new \RuntimeException('يجب إضافة صنف واحد على الأقل');
}
$prNumber = ProcurementNumberGenerator::nextPRNumber();
$estimatedTotal = '0.00';
foreach ($items as $item) {
$qty = (string) ($item['quantity'] ?? '0');
$cost = (string) ($item['estimated_unit_cost'] ?? '0');
if (bccomp($cost, '0', 2) > 0) {
$lineTotal = bcmul($qty, $cost, 2);
$estimatedTotal = bcadd($estimatedTotal, $lineTotal, 2);
}
}
$db->beginTransaction();
try {
$prId = $db->insert('purchase_requisitions', [
'pr_number' => $prNumber,
'requested_by' => $employee ? (int) $employee->id : null,
'department' => $header['department'] ?? null,
'status' => 'draft',
'urgency' => $header['urgency'] ?? 'normal',
'required_date' => !empty($header['required_date']) ? $header['required_date'] : null,
'justification' => $header['justification'] ?? null,
'estimated_total' => $estimatedTotal,
'notes' => $header['notes'] ?? null,
'branch_id' => !empty($header['branch_id']) ? (int) $header['branch_id'] : 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'] ?? '0');
$cost = (string) ($item['estimated_unit_cost'] ?? '0');
$lineTotal = bccomp($cost, '0', 2) > 0 ? bcmul($qty, $cost, 2) : null;
$db->insert('purchase_requisition_items', [
'requisition_id' => $prId,
'item_id' => !empty($item['item_id']) ? (int) $item['item_id'] : null,
'description_ar' => $item['description_ar'] ?? null,
'description_en' => $item['description_en'] ?? null,
'quantity' => $qty,
'unit_of_measure' => $item['unit_of_measure'] ?? null,
'estimated_unit_cost' => bccomp($cost, '0', 2) > 0 ? $cost : null,
'estimated_line_total' => $lineTotal,
'specifications' => $item['specifications'] ?? null,
'preferred_supplier_id' => !empty($item['preferred_supplier_id']) ? (int) $item['preferred_supplier_id'] : null,
]);
}
$db->commit();
} catch (\Throwable $e) {
$db->rollBack();
throw $e;
}
Logger::info("PR #{$prId} ({$prNumber}) created, estimated total: {$estimatedTotal}");
return $prId;
}
/**
* Update a draft PR.
*/
public static function updatePR(int $prId, array $header, array $items): void
{
$db = App::getInstance()->db();
$pr = $db->selectOne("SELECT * FROM `purchase_requisitions` WHERE `id` = ?", [$prId]);
if (!$pr) {
throw new \RuntimeException('طلب الشراء غير موجود');
}
if ($pr['status'] !== 'draft') {
throw new \RuntimeException('لا يمكن تعديل طلب الشراء إلا في حالة المسودة');
}
if (empty($items)) {
throw new \RuntimeException('يجب إضافة صنف واحد على الأقل');
}
$estimatedTotal = '0.00';
foreach ($items as $item) {
$qty = (string) ($item['quantity'] ?? '0');
$cost = (string) ($item['estimated_unit_cost'] ?? '0');
if (bccomp($cost, '0', 2) > 0) {
$lineTotal = bcmul($qty, $cost, 2);
$estimatedTotal = bcadd($estimatedTotal, $lineTotal, 2);
}
}
$db->beginTransaction();
try {
$db->update('purchase_requisitions', [
'department' => $header['department'] ?? null,
'urgency' => $header['urgency'] ?? 'normal',
'required_date' => !empty($header['required_date']) ? $header['required_date'] : null,
'justification' => $header['justification'] ?? null,
'estimated_total' => $estimatedTotal,
'notes' => $header['notes'] ?? null,
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [$prId]);
// Delete old items and re-insert
$db->delete('purchase_requisition_items', '`requisition_id` = ?', [$prId]);
foreach ($items as $item) {
$qty = (string) ($item['quantity'] ?? '0');
$cost = (string) ($item['estimated_unit_cost'] ?? '0');
$lineTotal = bccomp($cost, '0', 2) > 0 ? bcmul($qty, $cost, 2) : null;
$db->insert('purchase_requisition_items', [
'requisition_id' => $prId,
'item_id' => !empty($item['item_id']) ? (int) $item['item_id'] : null,
'description_ar' => $item['description_ar'] ?? null,
'description_en' => $item['description_en'] ?? null,
'quantity' => $qty,
'unit_of_measure' => $item['unit_of_measure'] ?? null,
'estimated_unit_cost' => bccomp($cost, '0', 2) > 0 ? $cost : null,
'estimated_line_total' => $lineTotal,
'specifications' => $item['specifications'] ?? null,
'preferred_supplier_id' => !empty($item['preferred_supplier_id']) ? (int) $item['preferred_supplier_id'] : null,
]);
}
$db->commit();
} catch (\Throwable $e) {
$db->rollBack();
throw $e;
}
Logger::info("PR #{$prId} updated, estimated total: {$estimatedTotal}");
}
/**
* Submit a draft PR for approval.
*/
public static function submitPR(int $prId): void
{
$db = App::getInstance()->db();
$pr = $db->selectOne("SELECT * FROM `purchase_requisitions` WHERE `id` = ?", [$prId]);
if (!$pr) {
throw new \RuntimeException('طلب الشراء غير موجود');
}
if ($pr['status'] !== 'draft') {
throw new \RuntimeException('طلب الشراء ليس في حالة مسودة');
}
$db->update('purchase_requisitions', [
'status' => 'submitted',
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [$prId]);
EventBus::dispatch('procurement.requisition_submitted', ['pr_id' => $prId]);
Logger::info("PR #{$prId} submitted for approval");
}
/**
* Approve a submitted PR.
*/
public static function approvePR(int $prId): void
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$pr = $db->selectOne("SELECT * FROM `purchase_requisitions` WHERE `id` = ?", [$prId]);
if (!$pr) {
throw new \RuntimeException('طلب الشراء غير موجود');
}
if ($pr['status'] !== 'submitted') {
throw new \RuntimeException('طلب الشراء ليس في حالة مُقدّم');
}
$db->update('purchase_requisitions', [
'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` = ?', [$prId]);
EventBus::dispatch('procurement.requisition_approved', ['pr_id' => $prId]);
Logger::info("PR #{$prId} approved");
}
/**
* Reject a submitted PR.
*/
public static function rejectPR(int $prId, string $reason): void
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$pr = $db->selectOne("SELECT * FROM `purchase_requisitions` WHERE `id` = ?", [$prId]);
if (!$pr) {
throw new \RuntimeException('طلب الشراء غير موجود');
}
if ($pr['status'] !== 'submitted') {
throw new \RuntimeException('طلب الشراء ليس في حالة مُقدّم');
}
$db->update('purchase_requisitions', [
'status' => 'rejected',
'rejected_by' => $employee ? (int) $employee->id : null,
'rejected_at' => date('Y-m-d H:i:s'),
'rejection_reason' => $reason,
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [$prId]);
Logger::info("PR #{$prId} rejected: {$reason}");
}
/**
* Convert an approved PR to a Purchase Order.
*/
public static function convertToPO(int $prId, array $poHeader): int
{
$db = App::getInstance()->db();
$pr = $db->selectOne("SELECT * FROM `purchase_requisitions` WHERE `id` = ?", [$prId]);
if (!$pr) {
throw new \RuntimeException('طلب الشراء غير موجود');
}
if ($pr['status'] !== 'approved') {
throw new \RuntimeException('طلب الشراء ليس في حالة معتمد');
}
$prItems = $db->select(
"SELECT * FROM `purchase_requisition_items` WHERE `requisition_id` = ?",
[$prId]
);
if (empty($prItems)) {
throw new \RuntimeException('لا توجد أصناف في طلب الشراء');
}
$supplierId = (int) ($poHeader['supplier_id'] ?? 0);
$warehouseId = (int) ($poHeader['warehouse_id'] ?? 0);
if ($supplierId <= 0) {
throw new \RuntimeException('يجب تحديد المورد');
}
if ($warehouseId <= 0) {
throw new \RuntimeException('يجب تحديد المخزن');
}
// Build PO items from PR items
$poItems = [];
foreach ($prItems as $pri) {
if (empty($pri['item_id'])) {
continue; // skip free-text items that don't map to inventory
}
$poItems[] = [
'item_id' => (int) $pri['item_id'],
'quantity_ordered' => (string) $pri['quantity'],
'unit_price' => (string) ($pri['estimated_unit_cost'] ?? '0'),
'notes' => $pri['specifications'] ?? null,
];
}
if (empty($poItems)) {
throw new \RuntimeException('لا توجد أصناف قابلة للتحويل لأمر شراء');
}
// Use PurchaseOrderService to create the PO
$poService = \App\Modules\Inventory\Services\PurchaseOrderService::class;
$poId = $poService::createPO([
'supplier_id' => $supplierId,
'warehouse_id' => $warehouseId,
'expected_delivery_date' => $poHeader['expected_delivery_date'] ?? $pr['required_date'],
'notes' => $poHeader['notes'] ?? $pr['notes'],
], $poItems);
// Link PO back to PR
$db->update('purchase_orders', [
'requisition_id' => $prId,
], '`id` = ?', [$poId]);
// Update PR status
$db->update('purchase_requisitions', [
'status' => 'converted',
'converted_po_id' => $poId,
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [$prId]);
EventBus::dispatch('procurement.requisition_converted', [
'pr_id' => $prId,
'po_id' => $poId,
]);
Logger::info("PR #{$prId} converted to PO #{$poId}");
return $poId;
}
/**
* Cancel a PR (draft or submitted only).
*/
public static function cancelPR(int $prId): void
{
$db = App::getInstance()->db();
$pr = $db->selectOne("SELECT * FROM `purchase_requisitions` WHERE `id` = ?", [$prId]);
if (!$pr) {
throw new \RuntimeException('طلب الشراء غير موجود');
}
if (!in_array($pr['status'], ['draft', 'submitted'], true)) {
throw new \RuntimeException('لا يمكن إلغاء طلب الشراء في حالته الحالية');
}
$db->update('purchase_requisitions', [
'status' => 'cancelled',
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [$prId]);
Logger::info("PR #{$prId} cancelled");
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Procurement\Services;
use App\Core\App;
use App\Core\EventBus;
use App\Core\Logger;
use App\Modules\Inventory\Services\StockService;
final class ReturnToVendorService
{
public static function createRTV(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('يجب تحديد المخزن');
$rtvNumber = ProcurementNumberGenerator::nextRTVNumber();
$totalAmount = '0.00';
foreach ($items as $item) {
$qty = (string) ($item['quantity'] ?? '0');
$cost = (string) ($item['unit_cost'] ?? '0');
$lineTotal = bcmul($qty, $cost, 2);
$totalAmount = bcadd($totalAmount, $lineTotal, 2);
}
$db->beginTransaction();
try {
$rtvId = $db->insert('return_to_vendor', [
'rtv_number' => $rtvNumber,
'supplier_id' => $supplierId,
'grn_id' => !empty($header['grn_id']) ? (int) $header['grn_id'] : null,
'purchase_order_id' => !empty($header['purchase_order_id']) ? (int) $header['purchase_order_id'] : null,
'warehouse_id' => $warehouseId,
'reason' => $header['reason'] ?? '',
'return_date' => $header['return_date'] ?? date('Y-m-d'),
'status' => 'draft',
'total_amount' => $totalAmount,
'notes' => $header['notes'] ?? null,
'branch_id' => $header['branch_id'] ?? 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'] ?? '0');
$cost = (string) ($item['unit_cost'] ?? '0');
$lineTotal = bcmul($qty, $cost, 2);
$db->insert('rtv_items', [
'rtv_id' => $rtvId,
'item_id' => (int) $item['item_id'],
'grn_item_id' => !empty($item['grn_item_id']) ? (int) $item['grn_item_id'] : null,
'quantity' => $qty,
'unit_cost' => $cost,
'line_total' => $lineTotal,
'batch_number' => $item['batch_number'] ?? null,
'reason' => $item['reason'] ?? null,
]);
}
$db->commit();
} catch (\Throwable $e) {
$db->rollBack();
throw $e;
}
Logger::info("RTV #{$rtvId} ({$rtvNumber}) created, total: {$totalAmount}");
return $rtvId;
}
public static function submitRTV(int $rtvId): void
{
$db = App::getInstance()->db();
$rtv = $db->selectOne("SELECT * FROM `return_to_vendor` WHERE `id` = ?", [$rtvId]);
if (!$rtv) throw new \RuntimeException('مرتجع المورد غير موجود');
if ($rtv['status'] !== 'draft') throw new \RuntimeException('المرتجع ليس في حالة مسودة');
$db->update('return_to_vendor', ['status' => 'submitted', 'updated_at' => date('Y-m-d H:i:s')], '`id` = ?', [$rtvId]);
Logger::info("RTV #{$rtvId} submitted");
}
public static function approveRTV(int $rtvId): void
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$rtv = $db->selectOne("SELECT * FROM `return_to_vendor` WHERE `id` = ?", [$rtvId]);
if (!$rtv) throw new \RuntimeException('مرتجع المورد غير موجود');
if ($rtv['status'] !== 'submitted') throw new \RuntimeException('المرتجع ليس في حالة مُقدّم');
$db->update('return_to_vendor', [
'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` = ?', [$rtvId]);
Logger::info("RTV #{$rtvId} approved");
}
public static function shipRTV(int $rtvId): void
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$rtv = $db->selectOne("SELECT * FROM `return_to_vendor` WHERE `id` = ?", [$rtvId]);
if (!$rtv) throw new \RuntimeException('مرتجع المورد غير موجود');
if ($rtv['status'] !== 'approved') throw new \RuntimeException('المرتجع ليس في حالة معتمد');
$db->update('return_to_vendor', [
'status' => 'shipped', 'shipped_by' => $employee ? (int) $employee->id : null,
'shipped_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [$rtvId]);
Logger::info("RTV #{$rtvId} shipped");
}
public static function completeRTV(int $rtvId): void
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$rtv = $db->selectOne("SELECT * FROM `return_to_vendor` WHERE `id` = ?", [$rtvId]);
if (!$rtv) throw new \RuntimeException('مرتجع المورد غير موجود');
if ($rtv['status'] !== 'shipped') throw new \RuntimeException('المرتجع ليس في حالة شُحن');
$warehouseId = (int) $rtv['warehouse_id'];
$db->beginTransaction();
try {
// Move stock out for each RTV item
$rtvItems = $db->select("SELECT * FROM `rtv_items` WHERE `rtv_id` = ?", [$rtvId]);
foreach ($rtvItems as $item) {
StockService::moveStock([
'item_id' => (int) $item['item_id'],
'warehouse_id' => $warehouseId,
'movement_type' => 'return_out',
'direction' => 'out',
'quantity' => (string) $item['quantity'],
'unit_cost' => (string) $item['unit_cost'],
'reference_type' => 'return_to_vendor',
'reference_id' => $rtvId,
'notes' => 'مرتجع مورد — ' . $rtv['rtv_number'],
]);
}
$db->update('return_to_vendor', [
'status' => 'completed', 'completed_by' => $employee ? (int) $employee->id : null,
'completed_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [$rtvId]);
$db->commit();
} catch (\Throwable $e) {
$db->rollBack();
throw $e;
}
EventBus::dispatch('procurement.rtv_completed', [
'rtv_id' => $rtvId,
'supplier_id' => (int) $rtv['supplier_id'],
'total_amount' => (string) $rtv['total_amount'],
'warehouse_id' => $warehouseId,
]);
Logger::info("RTV #{$rtvId} completed — stock moved out, accounting event dispatched");
}
public static function cancelRTV(int $rtvId): void
{
$db = App::getInstance()->db();
$rtv = $db->selectOne("SELECT * FROM `return_to_vendor` WHERE `id` = ?", [$rtvId]);
if (!$rtv) throw new \RuntimeException('مرتجع المورد غير موجود');
if (!in_array($rtv['status'], ['draft', 'submitted'], true)) throw new \RuntimeException('لا يمكن إلغاء المرتجع في حالته الحالية');
$db->update('return_to_vendor', ['status' => 'cancelled', 'updated_at' => date('Y-m-d H:i:s')], '`id` = ?', [$rtvId]);
Logger::info("RTV #{$rtvId} cancelled");
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Procurement\Services;
use App\Core\App;
use App\Core\Logger;
/**
* 3-Way Match: PO ↔ GRN ↔ Invoice
* Compares quantities and costs across PO items, GRN accepted quantities, and invoice line items.
*/
final class ThreeWayMatchService
{
/**
* Perform a 3-way match for a vendor invoice.
*
* @return array ['status' => string, 'details' => array, 'notes' => string]
*/
public static function performMatch(int $invoiceId): array
{
$db = App::getInstance()->db();
$invoice = $db->selectOne("SELECT * FROM `vendor_invoices` WHERE `id` = ?", [$invoiceId]);
if (!$invoice) {
throw new \RuntimeException('الفاتورة غير موجودة');
}
$poId = (int) ($invoice['purchase_order_id'] ?? 0);
$grnId = (int) ($invoice['grn_id'] ?? 0);
if ($poId <= 0) {
return ['status' => 'unmatched', 'details' => [], 'notes' => 'لا يوجد أمر شراء مرتبط'];
}
// Get tolerance from config
$config = $db->selectOne(
"SELECT `config_value` FROM `system_config` WHERE `config_key` = 'procurement.match_tolerance_pct'"
);
$tolerancePct = (float) ($config['config_value'] ?? 2.0);
// Get invoice items
$invoiceItems = $db->select(
"SELECT * FROM `vendor_invoice_items` WHERE `invoice_id` = ?",
[$invoiceId]
);
// Get PO items
$poItems = $db->select(
"SELECT * FROM `purchase_order_items` WHERE `purchase_order_id` = ?",
[$poId]
);
$poItemsMap = [];
foreach ($poItems as $poi) {
$poItemsMap[(int) $poi['id']] = $poi;
}
// Get GRN items if GRN specified
$grnItemsMap = [];
if ($grnId > 0) {
$grnItems = $db->select(
"SELECT * FROM `grn_items` WHERE `grn_id` = ?",
[$grnId]
);
foreach ($grnItems as $gi) {
$grnItemsMap[(int) $gi['po_item_id']] = $gi;
}
}
$details = [];
$hasDiscrepancy = false;
$allWithinTolerance = true;
foreach ($invoiceItems as $invItem) {
$poItemId = (int) ($invItem['po_item_id'] ?? 0);
$detail = [
'item_id' => (int) ($invItem['item_id'] ?? 0),
'invoice_qty' => (string) $invItem['quantity'],
'invoice_cost' => (string) $invItem['unit_cost'],
'po_qty' => '0',
'po_cost' => '0',
'grn_accepted_qty' => '0',
'qty_match' => false,
'cost_match' => false,
'notes' => [],
];
if ($poItemId > 0 && isset($poItemsMap[$poItemId])) {
$poi = $poItemsMap[$poItemId];
$detail['po_qty'] = (string) $poi['quantity_ordered'];
$detail['po_cost'] = (string) $poi['unit_cost'];
// Check GRN
if (isset($grnItemsMap[$poItemId])) {
$detail['grn_accepted_qty'] = (string) $grnItemsMap[$poItemId]['quantity_accepted'];
}
// Quantity match: invoice qty vs GRN accepted qty (or PO qty if no GRN)
$referenceQty = bccomp($detail['grn_accepted_qty'], '0', 3) > 0
? $detail['grn_accepted_qty']
: $detail['po_qty'];
$qtyDiff = bcsub($detail['invoice_qty'], $referenceQty, 3);
$qtyDiffPct = bccomp($referenceQty, '0', 3) > 0
? (float) bcdiv(bcmul($qtyDiff, '100', 3), $referenceQty, 3)
: 0.0;
if (bccomp($qtyDiff, '0', 3) === 0) {
$detail['qty_match'] = true;
} elseif (abs($qtyDiffPct) <= $tolerancePct) {
$detail['qty_match'] = true;
$detail['notes'][] = "فرق كمية {$qtyDiffPct}% (ضمن التسامح)";
} else {
$detail['notes'][] = "فرق كمية: فاتورة {$detail['invoice_qty']} مقابل مرجع {$referenceQty}";
$hasDiscrepancy = true;
$allWithinTolerance = false;
}
// Cost match: invoice unit_cost vs PO unit_cost
$costDiff = bcsub($detail['invoice_cost'], $detail['po_cost'], 2);
$costDiffPct = bccomp($detail['po_cost'], '0', 2) > 0
? (float) bcdiv(bcmul($costDiff, '100', 2), $detail['po_cost'], 2)
: 0.0;
if (bccomp($costDiff, '0', 2) === 0) {
$detail['cost_match'] = true;
} elseif (abs($costDiffPct) <= $tolerancePct) {
$detail['cost_match'] = true;
$detail['notes'][] = "فرق سعر {$costDiffPct}% (ضمن التسامح)";
} else {
$detail['notes'][] = "فرق سعر: فاتورة {$detail['invoice_cost']} مقابل أمر شراء {$detail['po_cost']}";
$hasDiscrepancy = true;
$allWithinTolerance = false;
}
} else {
$detail['notes'][] = 'لا يوجد بند مقابل في أمر الشراء';
$hasDiscrepancy = true;
$allWithinTolerance = false;
}
$details[] = $detail;
}
// Determine overall status
if (!$hasDiscrepancy) {
$matchStatus = 'matched';
$matchNotes = 'مطابقة كاملة';
} elseif ($allWithinTolerance) {
$matchStatus = 'tolerance_pass';
$matchNotes = 'مطابقة ضمن نسبة التسامح';
} else {
$matchStatus = 'discrepancy';
$matchNotes = 'يوجد اختلاف — يرجى المراجعة';
}
// Update invoice match status
$db->update('vendor_invoices', [
'match_status' => $matchStatus,
'match_notes' => $matchNotes,
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [$invoiceId]);
Logger::info("3-way match for invoice #{$invoiceId}: {$matchStatus}");
return [
'status' => $matchStatus,
'details' => $details,
'notes' => $matchNotes,
];
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Procurement\Services;
use App\Core\App;
use App\Core\EventBus;
use App\Core\Logger;
final class VendorInvoiceService
{
/**
* Create a new vendor invoice with items.
*/
public static function createInvoice(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);
if ($supplierId <= 0) {
throw new \RuntimeException('يجب تحديد المورد');
}
$internalNumber = ProcurementNumberGenerator::nextInvoiceNumber();
$subtotal = '0.00';
$taxTotal = '0.00';
$discountTotal = '0.00';
foreach ($items as $item) {
$qty = (string) ($item['quantity'] ?? '0');
$unitCost = (string) ($item['unit_cost'] ?? '0');
$taxRate = (string) ($item['tax_rate'] ?? '0');
$discount = (string) ($item['discount_amount'] ?? '0');
$lineBase = bcmul($qty, $unitCost, 2);
$lineTax = bcmul($lineBase, bcdiv($taxRate, '100', 4), 2);
$subtotal = bcadd($subtotal, $lineBase, 2);
$taxTotal = bcadd($taxTotal, $lineTax, 2);
$discountTotal = bcadd($discountTotal, $discount, 2);
}
$totalAmount = bcsub(bcadd($subtotal, $taxTotal, 2), $discountTotal, 2);
$db->beginTransaction();
try {
$invoiceId = $db->insert('vendor_invoices', [
'invoice_number' => $header['invoice_number'] ?? '',
'internal_number' => $internalNumber,
'supplier_id' => $supplierId,
'purchase_order_id' => !empty($header['purchase_order_id']) ? (int) $header['purchase_order_id'] : null,
'grn_id' => !empty($header['grn_id']) ? (int) $header['grn_id'] : null,
'invoice_date' => $header['invoice_date'] ?? date('Y-m-d'),
'due_date' => $header['due_date'] ?? null,
'subtotal' => $subtotal,
'tax_amount' => $taxTotal,
'discount_amount' => $discountTotal,
'total_amount' => $totalAmount,
'status' => 'draft',
'notes' => $header['notes'] ?? null,
'branch_id' => $header['branch_id'] ?? 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'] ?? '0');
$unitCost = (string) ($item['unit_cost'] ?? '0');
$taxRate = (string) ($item['tax_rate'] ?? '0');
$discount = (string) ($item['discount_amount'] ?? '0');
$lineBase = bcmul($qty, $unitCost, 2);
$lineTax = bcmul($lineBase, bcdiv($taxRate, '100', 4), 2);
$lineTotal = bcsub(bcadd($lineBase, $lineTax, 2), $discount, 2);
$db->insert('vendor_invoice_items', [
'invoice_id' => $invoiceId,
'po_item_id' => !empty($item['po_item_id']) ? (int) $item['po_item_id'] : null,
'item_id' => !empty($item['item_id']) ? (int) $item['item_id'] : null,
'description' => $item['description'] ?? null,
'quantity' => $qty,
'unit_cost' => $unitCost,
'tax_rate' => $taxRate,
'tax_amount' => $lineTax,
'discount_amount' => $discount,
'line_total' => $lineTotal,
]);
}
$db->commit();
} catch (\Throwable $e) {
$db->rollBack();
throw $e;
}
Logger::info("Vendor invoice #{$invoiceId} ({$internalNumber}) created, total: {$totalAmount}");
return $invoiceId;
}
/**
* Verify an invoice (run 3-way match).
*/
public static function verifyInvoice(int $invoiceId): array
{
$db = App::getInstance()->db();
$invoice = $db->selectOne("SELECT * FROM `vendor_invoices` WHERE `id` = ?", [$invoiceId]);
if (!$invoice) {
throw new \RuntimeException('الفاتورة غير موجودة');
}
if ($invoice['status'] !== 'draft') {
throw new \RuntimeException('الفاتورة ليست في حالة مسودة');
}
$matchResult = ThreeWayMatchService::performMatch($invoiceId);
$db->update('vendor_invoices', [
'status' => 'verified',
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [$invoiceId]);
Logger::info("Vendor invoice #{$invoiceId} verified, match: {$matchResult['status']}");
return $matchResult;
}
/**
* Approve a verified invoice — dispatches event for accounting.
*/
public static function approveInvoice(int $invoiceId): void
{
$db = App::getInstance()->db();
$invoice = $db->selectOne("SELECT * FROM `vendor_invoices` WHERE `id` = ?", [$invoiceId]);
if (!$invoice) {
throw new \RuntimeException('الفاتورة غير موجودة');
}
if ($invoice['status'] !== 'verified') {
throw new \RuntimeException('الفاتورة ليست في حالة تم التحقق');
}
$db->update('vendor_invoices', [
'status' => 'approved',
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [$invoiceId]);
EventBus::dispatch('procurement.invoice_approved', [
'invoice_id' => $invoiceId,
'supplier_id' => (int) $invoice['supplier_id'],
'internal_number' => $invoice['internal_number'] ?? '',
'subtotal' => (string) $invoice['subtotal'],
'tax_amount' => (string) $invoice['tax_amount'],
'total_amount' => (string) $invoice['total_amount'],
'due_date' => $invoice['due_date'],
]);
Logger::info("Vendor invoice #{$invoiceId} approved — accounting event dispatched");
}
/**
* Cancel an invoice (draft or verified only).
*/
public static function cancelInvoice(int $invoiceId): void
{
$db = App::getInstance()->db();
$invoice = $db->selectOne("SELECT * FROM `vendor_invoices` WHERE `id` = ?", [$invoiceId]);
if (!$invoice) {
throw new \RuntimeException('الفاتورة غير موجودة');
}
if (!in_array($invoice['status'], ['draft', 'verified'], true)) {
throw new \RuntimeException('لا يمكن إلغاء الفاتورة في حالتها الحالية');
}
$db->update('vendor_invoices', [
'status' => 'cancelled',
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [$invoiceId]);
Logger::info("Vendor invoice #{$invoiceId} cancelled");
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Procurement\Services;
use App\Core\App;
use App\Core\EventBus;
use App\Core\Logger;
final class VendorPaymentService
{
public static function createPayment(array $data): int
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$supplierId = (int) ($data['supplier_id'] ?? 0);
$amount = (string) ($data['amount'] ?? '0');
if ($supplierId <= 0) {
throw new \RuntimeException('يجب تحديد المورد');
}
if (bccomp($amount, '0', 2) <= 0) {
throw new \RuntimeException('يجب تحديد مبلغ الدفعة');
}
$paymentNumber = ProcurementNumberGenerator::nextPaymentNumber();
$paymentId = $db->insert('vendor_payments', [
'payment_number' => $paymentNumber,
'supplier_id' => $supplierId,
'invoice_id' => !empty($data['invoice_id']) ? (int) $data['invoice_id'] : null,
'amount' => $amount,
'payment_method' => $data['payment_method'] ?? 'bank_transfer',
'bank_account_id' => !empty($data['bank_account_id']) ? (int) $data['bank_account_id'] : null,
'check_number' => $data['check_number'] ?? null,
'reference_number' => $data['reference_number'] ?? null,
'payment_date' => $data['payment_date'] ?? date('Y-m-d'),
'status' => 'draft',
'notes' => $data['notes'] ?? null,
'branch_id' => $data['branch_id'] ?? 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("Vendor payment #{$paymentId} ({$paymentNumber}) created, amount: {$amount}");
return $paymentId;
}
public static function approvePayment(int $paymentId): void
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$payment = $db->selectOne("SELECT * FROM `vendor_payments` WHERE `id` = ?", [$paymentId]);
if (!$payment) {
throw new \RuntimeException('الدفعة غير موجودة');
}
if ($payment['status'] !== 'draft') {
throw new \RuntimeException('الدفعة ليست في حالة مسودة');
}
$db->update('vendor_payments', [
'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` = ?', [$paymentId]);
Logger::info("Vendor payment #{$paymentId} approved");
}
public static function completePayment(int $paymentId): void
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$payment = $db->selectOne("SELECT * FROM `vendor_payments` WHERE `id` = ?", [$paymentId]);
if (!$payment) {
throw new \RuntimeException('الدفعة غير موجودة');
}
if ($payment['status'] !== 'approved') {
throw new \RuntimeException('الدفعة ليست في حالة معتمدة');
}
$db->update('vendor_payments', [
'status' => 'completed',
'completed_by' => $employee ? (int) $employee->id : null,
'completed_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [$paymentId]);
// Update invoice status if linked
if (!empty($payment['invoice_id'])) {
$invoice = $db->selectOne("SELECT * FROM `vendor_invoices` WHERE `id` = ?", [(int) $payment['invoice_id']]);
if ($invoice) {
// Check total paid against invoice
$totalPaid = $db->selectOne(
"SELECT COALESCE(SUM(`amount`), 0) as total FROM `vendor_payments` WHERE `invoice_id` = ? AND `status` = 'completed'",
[(int) $payment['invoice_id']]
);
$paidAmount = bcadd((string) ($totalPaid['total'] ?? '0'), (string) $payment['amount'], 2);
$newInvoiceStatus = bccomp($paidAmount, (string) $invoice['total_amount'], 2) >= 0
? 'paid'
: 'partial_paid';
$db->update('vendor_invoices', [
'status' => $newInvoiceStatus,
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [(int) $payment['invoice_id']]);
}
}
EventBus::dispatch('procurement.payment_completed', [
'payment_id' => $paymentId,
'supplier_id' => (int) $payment['supplier_id'],
'invoice_id' => $payment['invoice_id'] ? (int) $payment['invoice_id'] : null,
'amount' => (string) $payment['amount'],
'payment_number' => $payment['payment_number'] ?? '',
'payment_method' => $payment['payment_method'],
'bank_account_id' => $payment['bank_account_id'] ? (int) $payment['bank_account_id'] : null,
]);
Logger::info("Vendor payment #{$paymentId} completed — accounting event dispatched");
}
public static function voidPayment(int $paymentId, string $reason): void
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$payment = $db->selectOne("SELECT * FROM `vendor_payments` WHERE `id` = ?", [$paymentId]);
if (!$payment) {
throw new \RuntimeException('الدفعة غير موجودة');
}
if (!in_array($payment['status'], ['draft', 'approved', 'completed'], true)) {
throw new \RuntimeException('لا يمكن إلغاء الدفعة في حالتها الحالية');
}
$wasCompleted = $payment['status'] === 'completed';
$db->update('vendor_payments', [
'status' => 'voided',
'voided_by' => $employee ? (int) $employee->id : null,
'voided_at' => date('Y-m-d H:i:s'),
'void_reason' => $reason,
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [$paymentId]);
if ($wasCompleted) {
EventBus::dispatch('procurement.payment_voided', [
'payment_id' => $paymentId,
'supplier_id' => (int) $payment['supplier_id'],
'invoice_id' => $payment['invoice_id'] ? (int) $payment['invoice_id'] : null,
'amount' => (string) $payment['amount'],
'reason' => $reason,
'journal_entry_id' => $payment['journal_entry_id'] ? (int) $payment['journal_entry_id'] : null,
]);
// Re-evaluate invoice status if linked
if (!empty($payment['invoice_id'])) {
$totalPaid = $db->selectOne(
"SELECT COALESCE(SUM(`amount`), 0) as total FROM `vendor_payments` WHERE `invoice_id` = ? AND `status` = 'completed'",
[(int) $payment['invoice_id']]
);
$paidAmount = (string) ($totalPaid['total'] ?? '0');
$invoice = $db->selectOne("SELECT `total_amount` FROM `vendor_invoices` WHERE `id` = ?", [(int) $payment['invoice_id']]);
if ($invoice) {
$newStatus = bccomp($paidAmount, '0', 2) <= 0 ? 'approved' :
(bccomp($paidAmount, (string) $invoice['total_amount'], 2) >= 0 ? 'paid' : 'partial_paid');
$db->update('vendor_invoices', [
'status' => $newStatus,
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [(int) $payment['invoice_id']]);
}
}
}
Logger::info("Vendor payment #{$paymentId} voided: {$reason}");
}
}
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>لوحة تحكم المشتريات<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$pr = $prCounts ?? [];
$po = $poCounts ?? [];
$grn = $grnCounts ?? [];
$inv = $invoiceCounts ?? [];
$overdue = $overdueAP ?? [];
?>
<!-- Summary Cards Row 1 -->
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:15px;margin-bottom:20px;">
<div class="card" style="padding:20px;">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:12px;">
<div style="width:40px;height:40px;border-radius:10px;background:#F0FDFA;display:flex;align-items:center;justify-content:center;">
<i data-lucide="file-text" style="width:20px;height:20px;color:#0D7377;"></i>
</div>
<div>
<div style="font-size:12px;color:#6B7280;">طلبات شراء مفتوحة</div>
<div style="font-size:24px;font-weight:800;color:#0D7377;"><?= (int) ($pr['open_total'] ?? 0) ?></div>
</div>
</div>
<div style="font-size:12px;color:#D97706;"><?= (int) ($pr['pending_approval'] ?? 0) ?> بانتظار الاعتماد</div>
</div>
<div class="card" style="padding:20px;">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:12px;">
<div style="width:40px;height:40px;border-radius:10px;background:#FFF7ED;display:flex;align-items:center;justify-content:center;">
<i data-lucide="shopping-cart" style="width:20px;height:20px;color:#D97706;"></i>
</div>
<div>
<div style="font-size:12px;color:#6B7280;">أوامر شراء مفتوحة</div>
<div style="font-size:24px;font-weight:800;color:#D97706;"><?= (int) ($po['open_pos'] ?? 0) ?></div>
</div>
</div>
<div style="font-size:12px;color:#6B7280;">قيمة: <span style="font-weight:700;direction:ltr;"><?= money($po['open_value'] ?? 0) ?></span></div>
</div>
<div class="card" style="padding:20px;">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:12px;">
<div style="width:40px;height:40px;border-radius:10px;background:#EFF6FF;display:flex;align-items:center;justify-content:center;">
<i data-lucide="clipboard-check" style="width:20px;height:20px;color:#2563EB;"></i>
</div>
<div>
<div style="font-size:12px;color:#6B7280;">GRN بانتظار الفحص</div>
<div style="font-size:24px;font-weight:800;color:#2563EB;"><?= (int) ($grn['pending'] ?? 0) ?></div>
</div>
</div>
<div style="font-size:12px;color:#6B7280;"><?= (int) ($po['awaiting_receipt'] ?? 0) ?> أمر بانتظار الاستلام</div>
</div>
<div class="card" style="padding:20px;">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:12px;">
<div style="width:40px;height:40px;border-radius:10px;background:#FEE2E2;display:flex;align-items:center;justify-content:center;">
<i data-lucide="alert-triangle" style="width:20px;height:20px;color:#DC2626;"></i>
</div>
<div>
<div style="font-size:12px;color:#6B7280;">فواتير متأخرة</div>
<div style="font-size:24px;font-weight:800;color:#DC2626;"><?= (int) ($overdue['cnt'] ?? 0) ?></div>
</div>
</div>
<div style="font-size:12px;color:#DC2626;">مبلغ: <span style="font-weight:700;direction:ltr;"><?= money($overdue['total'] ?? 0) ?></span></div>
</div>
</div>
<!-- Summary Cards Row 2 -->
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:15px;margin-bottom:20px;">
<div class="card" style="padding:20px;text-align:center;background:linear-gradient(135deg, #0D737710, #0D737720);">
<div style="font-size:13px;color:#6B7280;margin-bottom:6px;">فواتير بانتظار المعالجة</div>
<div style="font-size:32px;font-weight:800;color:#0D7377;"><?= (int) ($inv['pending'] ?? 0) ?></div>
</div>
<div class="card" style="padding:20px;text-align:center;background:linear-gradient(135deg, #D9770610, #D9770620);">
<div style="font-size:13px;color:#6B7280;margin-bottom:6px;">فواتير بها تباين</div>
<div style="font-size:32px;font-weight:800;color:#D97706;"><?= (int) ($inv['discrepancies'] ?? 0) ?></div>
</div>
<div class="card" style="padding:20px;text-align:center;background:linear-gradient(135deg, #DC262610, #DC262620);">
<div style="font-size:13px;color:#6B7280;margin-bottom:6px;">قيمة فواتير غير مدفوعة</div>
<div style="font-size:32px;font-weight:800;color:#DC2626;direction:ltr;"><?= money($inv['unpaid_value'] ?? 0) ?></div>
</div>
</div>
<!-- Quick Links -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;color:#0D7377;font-size:15px;">إجراءات سريعة</h3>
</div>
<div style="padding:15px 20px;display:flex;gap:10px;flex-wrap:wrap;">
<a href="/procurement/requisitions/create" class="btn btn-primary" style="font-size:13px;"><i data-lucide="file-plus" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> طلب شراء جديد</a>
<a href="/procurement/grn/create" class="btn" style="font-size:13px;background:#2563EB;color:#fff;border:none;"><i data-lucide="package-check" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> استلام بضاعة</a>
<a href="/procurement/invoices/create" class="btn" style="font-size:13px;background:#D97706;color:#fff;border:none;"><i data-lucide="receipt" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> فاتورة مورد</a>
<a href="/procurement/payments/create" class="btn" style="font-size:13px;background:#059669;color:#fff;border:none;"><i data-lucide="banknote" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> دفعة مورد</a>
</div>
</div>
<!-- Recent Activity -->
<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="activity" style="width:18px;height:18px;color:#6B7280;"></i>
<h3 style="margin:0;color:#6B7280;font-size:15px;">آخر النشاطات</h3>
</div>
<?php if (!empty($recentActivity)): ?>
<div class="table-responsive">
<table class="data-table">
<thead><tr><th>النوع</th><th>الرقم</th><th>الحالة</th><th>المبلغ</th><th>التاريخ</th></tr></thead>
<tbody>
<?php
$typeLabels = ['pr' => 'طلب شراء', 'grn' => 'إذن استلام', 'invoice' => 'فاتورة مورد'];
$typeColors = ['pr' => '#0D7377', 'grn' => '#2563EB', 'invoice' => '#D97706'];
$typeIcons = ['pr' => 'file-text', 'grn' => 'package-check', 'invoice' => 'receipt'];
$statusLabels = [
'draft' => 'مسودة', 'submitted' => 'مُقدّم', 'approved' => 'معتمد',
'rejected' => 'مرفوض', 'converted' => 'تم التحويل', 'cancelled' => 'ملغي',
'inspecting' => 'قيد الفحص', 'accepted' => 'مقبول', 'partial_accept' => 'قبول جزئي',
'verified' => 'تم التحقق', 'paid' => 'مدفوعة', 'partial_paid' => 'مدفوعة جزئياً',
];
?>
<?php foreach ($recentActivity as $item): ?>
<?php $t = $item['type'] ?? ''; ?>
<tr>
<td>
<span style="display:inline-flex;align-items:center;gap:4px;color:<?= $typeColors[$t] ?? '#6B7280' ?>;font-size:13px;font-weight:600;">
<i data-lucide="<?= $typeIcons[$t] ?? 'file' ?>" style="width:14px;height:14px;"></i>
<?= e($typeLabels[$t] ?? $t) ?>
</span>
</td>
<td style="font-weight:600;"><code style="font-size:12px;background:#F3F4F6;padding:2px 8px;border-radius:4px;"><?= e($item['doc_number'] ?? '') ?></code></td>
<td><span style="font-size:12px;color:#6B7280;"><?= e($statusLabels[$item['status'] ?? ''] ?? ($item['status'] ?? '')) ?></span></td>
<td style="font-weight:700;direction:ltr;text-align:left;"><?= money($item['amount'] ?? 0) ?></td>
<td style="font-size:12px;color:#6B7280;"><?= e(substr($item['created_at'] ?? '', 0, 16)) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div style="padding:40px 20px;text-align:center;color:#6B7280;">
<p style="margin:0;">لا توجد نشاطات حديثة</p>
</div>
<?php endif; ?>
</div>
<!-- Report Links -->
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;color:#0D7377;font-size:15px;">التقارير</h3>
</div>
<div style="padding:15px 20px;display:grid;grid-template-columns:repeat(4,1fr);gap:12px;">
<a href="/procurement/reports/purchase-volume" style="text-decoration:none;">
<div style="padding:15px;border:1px solid #E5E7EB;border-radius:8px;text-align:center;transition:all 0.2s;">
<i data-lucide="bar-chart-3" style="width:24px;height:24px;color:#0D7377;margin-bottom:8px;"></i>
<div style="font-size:13px;font-weight:600;color:#1F2937;">حجم المشتريات</div>
</div>
</a>
<a href="/procurement/reports/supplier-performance" style="text-decoration:none;">
<div style="padding:15px;border:1px solid #E5E7EB;border-radius:8px;text-align:center;transition:all 0.2s;">
<i data-lucide="award" style="width:24px;height:24px;color:#D97706;margin-bottom:8px;"></i>
<div style="font-size:13px;font-weight:600;color:#1F2937;">أداء الموردين</div>
</div>
</a>
<a href="/procurement/reports/match-status" style="text-decoration:none;">
<div style="padding:15px;border:1px solid #E5E7EB;border-radius:8px;text-align:center;transition:all 0.2s;">
<i data-lucide="git-compare" style="width:24px;height:24px;color:#2563EB;margin-bottom:8px;"></i>
<div style="font-size:13px;font-weight:600;color:#1F2937;">حالة المطابقة</div>
</div>
</a>
<a href="/procurement/reports/overdue-invoices" style="text-decoration:none;">
<div style="padding:15px;border:1px solid #E5E7EB;border-radius:8px;text-align:center;transition:all 0.2s;">
<i data-lucide="clock" style="width:24px;height:24px;color:#DC2626;margin-bottom:8px;"></i>
<div style="font-size:13px;font-weight:600;color:#1F2937;">فواتير متأخرة</div>
</div>
</a>
</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="/procurement/grn" 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="/procurement/grn" id="grnForm">
<?= 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:#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:15px;">
<div>
<label class="form-label">أمر الشراء <span style="color:#DC2626;">*</span></label>
<select name="purchase_order_id" class="form-select" id="poSelect" required onchange="loadPOItems()">
<option value="">-- اختر أمر شراء --</option>
<?php foreach ($openPOs as $po): ?>
<option value="<?= (int) $po['id'] ?>"
data-supplier="<?= e($po['supplier_name']) ?>"
data-warehouse="<?= e($po['warehouse_name']) ?>">
<?= e($po['po_number']) ?><?= e($po['supplier_name']) ?> (<?= money($po['total_amount']) ?>)
</option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label">تاريخ الاستلام</label>
<input type="date" name="received_date" class="form-input" value="<?= date('Y-m-d') ?>">
</div>
</div>
<div style="margin-top:15px;">
<label class="form-label">ملاحظات</label>
<textarea name="notes" class="form-input" rows="2" placeholder="ملاحظات إضافية..."></textarea>
</div>
</div>
</div>
<div class="card" style="margin-bottom:20px;" id="itemsCard" style="display:none;">
<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>الصنف</th>
<th>المطلوب</th>
<th>المستلم سابقاً</th>
<th>المتبقي</th>
<th style="width:120px;">الكمية المستلمة</th>
<th>رقم الدفعة</th>
<th>تاريخ الانتهاء</th>
</tr>
</thead>
<tbody id="poItemsBody">
<tr><td colspan="7" style="text-align:center;color:#9CA3AF;padding:30px;">اختر أمر شراء لعرض الأصناف</td></tr>
</tbody>
</table>
</div>
</div>
<div style="text-align:left;">
<button type="submit" class="btn btn-primary" style="padding:10px 30px;">
<i data-lucide="save" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> إنشاء إذن الاستلام
</button>
</div>
</form>
<script>
var poItemsCache = {};
<?php foreach ($openPOs as $po): ?>
poItemsCache[<?= (int) $po['id'] ?>] = null;
<?php endforeach; ?>
function loadPOItems() {
var poId = document.getElementById('poSelect').value;
var tbody = document.getElementById('poItemsBody');
if (!poId) {
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center;color:#9CA3AF;padding:30px;">اختر أمر شراء لعرض الأصناف</td></tr>';
return;
}
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center;color:#9CA3AF;padding:20px;">جاري التحميل...</td></tr>';
fetch('/inventory/purchase-orders/' + poId + '?format=json')
.then(function(r) { return r.json(); })
.catch(function() {
// Fallback: reload the page with PO items embedded
// For now, use a simple approach
return { items: [] };
})
.then(function(data) {
var items = data.items || [];
if (items.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center;color:#9CA3AF;padding:30px;">لا توجد أصناف</td></tr>';
return;
}
var html = '';
items.forEach(function(item, idx) {
var remaining = Math.max(0, parseFloat(item.quantity_ordered) - parseFloat(item.quantity_received));
html += '<tr>'
+ '<td style="font-weight:600;">' + (item.item_name || '') + '</td>'
+ '<td style="direction:ltr;text-align:left;">' + parseFloat(item.quantity_ordered).toFixed(3) + '</td>'
+ '<td style="direction:ltr;text-align:left;">' + parseFloat(item.quantity_received).toFixed(3) + '</td>'
+ '<td style="direction:ltr;text-align:left;font-weight:600;">' + remaining.toFixed(3) + '</td>'
+ '<td><input type="hidden" name="items['+idx+'][po_item_id]" value="'+item.id+'">'
+ '<input type="number" name="items['+idx+'][quantity_received]" class="form-input" step="0.001" min="0" max="'+remaining.toFixed(3)+'" value="'+remaining.toFixed(3)+'"></td>'
+ '<td><input type="text" name="items['+idx+'][batch_number]" class="form-input" placeholder="اختياري"></td>'
+ '<td><input type="date" name="items['+idx+'][expiry_date]" class="form-input"></td>'
+ '</tr>';
});
tbody.innerHTML = html;
});
}
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="/procurement/grn/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' => '#6B7280', 'inspecting' => '#D97706', 'accepted' => '#059669', 'partial_accept' => '#2563EB', 'cancelled' => '#DC2626'];
$statusBgs = ['draft' => '#F3F4F6', 'inspecting' => '#FFF7ED', 'accepted' => '#ECFDF5', 'partial_accept' => '#EFF6FF', 'cancelled' => '#FEE2E2'];
?>
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/procurement/grn" 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>
<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="/procurement/grn" class="btn btn-sm btn-outline" style="color:#6B7280;">مسح</a>
</form>
</div>
<?php if (!empty($grns)): ?>
<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 ($grns as $g): ?>
<?php
$st = $g['status'] ?? 'draft';
$stColor = $statusColors[$st] ?? '#6B7280';
$stBg = $statusBgs[$st] ?? '#F3F4F6';
$stLabel = $statuses[$st] ?? $st;
?>
<tr>
<td style="font-weight:600;">
<a href="/procurement/grn/<?= (int) $g['id'] ?>" style="color:#0D7377;text-decoration:none;">
<code style="font-size:12px;background:#F3F4F6;padding:2px 8px;border-radius:4px;"><?= e($g['grn_number']) ?></code>
</a>
</td>
<td>
<?php if (!empty($g['po_number'])): ?>
<a href="/inventory/purchase-orders/<?= (int) $g['purchase_order_id'] ?>" style="color:#0D7377;text-decoration:none;">
<code style="font-size:11px;background:#F3F4F6;padding:2px 6px;border-radius:4px;"><?= e($g['po_number']) ?></code>
</a>
<?php else: ?><?php endif; ?>
</td>
<td style="font-weight:600;"><?= e($g['supplier_name'] ?? '—') ?></td>
<td style="font-size:13px;"><?= e($g['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($g['total_received_value'] ?? 0) ?></td>
<td style="font-size:12px;"><?= e($g['received_date'] ?? '') ?></td>
<td>
<a href="/procurement/grn/<?= (int) $g['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="package-check" 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;">ابدأ بإنشاء إذن استلام جديد من أمر شراء معتمد.</p>
<a href="/procurement/grn/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($grn['grn_number']) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/procurement/grn/<?= (int) $grn['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'); ?>
<div class="card" style="margin-bottom:20px;padding:15px;">
<div style="display:flex;gap:20px;flex-wrap:wrap;font-size:14px;">
<div><strong>رقم الإذن:</strong> <code><?= e($grn['grn_number']) ?></code></div>
<div><strong>أمر الشراء:</strong> <?= e($grn['po_number'] ?? '—') ?></div>
<div><strong>المورد:</strong> <?= e($grn['supplier_name'] ?? '') ?></div>
<div><strong>المستودع:</strong> <?= e($grn['warehouse_name'] ?? '') ?></div>
</div>
</div>
<form method="POST" action="/procurement/grn/<?= (int) $grn['id'] ?>/inspect">
<?= 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-check" 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 style="width:120px;">الكمية المقبولة</th>
<th style="width:120px;">الكمية المرفوضة</th>
<th style="min-width:200px;">سبب الرفض</th>
</tr>
</thead>
<tbody>
<?php foreach ($items as $i => $item): ?>
<tr>
<td style="font-weight:600;">
<?= e($item['item_name'] ?? '') ?>
<?php if (!empty($item['sku'])): ?>
<br><code style="font-size:11px;background:#F3F4F6;padding:2px 6px;border-radius:4px;"><?= e($item['sku']) ?></code>
<?php endif; ?>
<?php if (!empty($item['batch_number'])): ?>
<br><span style="font-size:11px;color:#6B7280;">دفعة: <?= e($item['batch_number']) ?></span>
<?php endif; ?>
</td>
<td style="font-weight:600;direction:ltr;text-align:left;">
<?= number_format((float) ($item['quantity_received'] ?? 0), 3) ?>
</td>
<td>
<input type="hidden" name="grn_item_ids[<?= $i ?>]" value="<?= (int) $item['id'] ?>">
<input type="number" name="quantities_accepted[<?= $i ?>]" class="form-input"
step="0.001" min="0" max="<?= (float) $item['quantity_received'] ?>"
value="<?= (float) $item['quantity_received'] ?>"
onchange="calcRejected(<?= $i ?>, <?= (float) $item['quantity_received'] ?>)">
</td>
<td>
<input type="number" name="quantities_rejected[<?= $i ?>]" class="form-input"
step="0.001" min="0" value="0" id="rejected_<?= $i ?>"
onchange="calcAccepted(<?= $i ?>, <?= (float) $item['quantity_received'] ?>)">
</td>
<td>
<input type="text" name="rejection_reasons[<?= $i ?>]" class="form-input" placeholder="سبب الرفض (اختياري)">
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<div class="card" style="margin-bottom:20px;padding:15px;">
<label class="form-label">ملاحظات الفحص</label>
<textarea name="inspection_notes" class="form-input" rows="3" placeholder="ملاحظات عامة عن الفحص..."></textarea>
</div>
<div style="text-align:left;">
<button type="submit" class="btn" style="background:#059669;color:#fff;border:none;padding:10px 30px;" onclick="return confirm('هل أنت متأكد من اعتماد نتائج الفحص؟ سيتم تحديث أرصدة المخزون.');">
<i data-lucide="check-circle" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> اعتماد الفحص
</button>
</div>
</form>
<script>
function calcRejected(idx, total) {
var accepted = parseFloat(document.querySelector('input[name="quantities_accepted['+idx+']"]').value) || 0;
document.getElementById('rejected_' + idx).value = Math.max(0, total - accepted).toFixed(3);
}
function calcAccepted(idx, total) {
var rejected = parseFloat(document.getElementById('rejected_' + idx).value) || 0;
document.querySelector('input[name="quantities_accepted['+idx+']"]').value = Math.max(0, total - rejected).toFixed(3);
}
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>إذن استلام <?= e($grn['grn_number']) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<?php if (in_array($grn['status'], ['draft', 'inspecting'])): ?>
<a href="/procurement/grn/<?= (int) $grn['id'] ?>/inspect" class="btn" style="background:#D97706;color:#fff;border:none;">
<i data-lucide="clipboard-check" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> فحص واعتماد
</a>
<form method="POST" action="/procurement/grn/<?= (int) $grn['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="/procurement/grn" 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' => 'مسودة', 'inspecting' => 'تحت الفحص', 'accepted' => 'مقبول', 'partial_accept' => 'قبول جزئي', 'cancelled' => 'ملغي'];
$statusColorMap = [
'draft' => ['bg' => '#F3F4F6', 'color' => '#6B7280'],
'inspecting' => ['bg' => '#FFF7ED', 'color' => '#D97706'],
'accepted' => ['bg' => '#ECFDF5', 'color' => '#059669'],
'partial_accept' => ['bg' => '#EFF6FF', 'color' => '#2563EB'],
'cancelled' => ['bg' => '#FEE2E2', 'color' => '#DC2626'],
];
$st = $grn['status'] ?? 'draft';
$stColors = $statusColorMap[$st] ?? $statusColorMap['draft'];
$stLabel = $statusLabels[$st] ?? $st;
?>
<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:#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($grn['grn_number']) ?></code></td>
</tr>
<tr>
<td style="padding:10px 0;color:#6B7280;">أمر الشراء</td>
<td style="padding:10px 0;">
<?php if (!empty($grn['po_number'])): ?>
<a href="/inventory/purchase-orders/<?= (int) $grn['purchase_order_id'] ?>" style="color:#0D7377;font-weight:600;text-decoration:none;"><?= e($grn['po_number']) ?></a>
<?php else: ?><?php endif; ?>
</td>
</tr>
<tr>
<td style="padding:10px 0;color:#6B7280;">المورد</td>
<td style="padding:10px 0;font-weight:600;"><?= e($grn['supplier_name'] ?? '') ?></td>
</tr>
<tr>
<td style="padding:10px 0;color:#6B7280;">المستودع</td>
<td style="padding:10px 0;"><?= e($grn['warehouse_name'] ?? '') ?></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($grn['received_date'] ?? '—') ?></td>
</tr>
<tr>
<td style="padding:10px 0;color:#6B7280;">المستلم</td>
<td style="padding:10px 0;"><?= e($grn['receiver_name'] ?? '—') ?></td>
</tr>
</table>
</div>
<?php if (!empty($grn['inspection_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($grn['inspection_notes']) ?></div>
</div>
<?php endif; ?>
</div>
</div>
<!-- Totals -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;margin-bottom:20px;">
<div class="card" style="padding:20px;text-align:center;background:linear-gradient(135deg, #D9770610, #D9770620);">
<div style="font-size:14px;color:#6B7280;margin-bottom:6px;">قيمة المستلم</div>
<div style="font-size:28px;font-weight:800;color:#D97706;direction:ltr;"><?= money($grn['total_received_value'] ?? 0) ?></div>
</div>
<div class="card" style="padding:20px;text-align:center;background:linear-gradient(135deg, #05966910, #05966920);">
<div style="font-size:14px;color:#6B7280;margin-bottom:6px;">قيمة المقبول</div>
<div style="font-size:28px;font-weight:800;color:#059669;direction:ltr;"><?= money($grn['total_accepted_value'] ?? 0) ?></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>المقبول</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'] ?? '') ?>
<?php if (!empty($item['sku'])): ?>
<br><code style="font-size:11px;background:#F3F4F6;padding:2px 6px;border-radius:4px;"><?= e($item['sku']) ?></code>
<?php endif; ?>
</td>
<td style="font-weight:600;direction:ltr;text-align:left;"><?= number_format((float) ($item['quantity_received'] ?? 0), 3) ?></td>
<td style="font-weight:600;direction:ltr;text-align:left;color:#059669;"><?= number_format((float) ($item['quantity_accepted'] ?? 0), 3) ?></td>
<td style="font-weight:600;direction:ltr;text-align:left;color:<?= (float) ($item['quantity_rejected'] ?? 0) > 0 ? '#DC2626' : '#6B7280' ?>;">
<?= number_format((float) ($item['quantity_rejected'] ?? 0), 3) ?>
<?php if (!empty($item['rejection_reason'])): ?>
<br><span style="font-size:11px;color:#DC2626;font-weight:400;"><?= e($item['rejection_reason']) ?></span>
<?php endif; ?>
</td>
<td style="font-size:12px;"><?= e($item['batch_number'] ?? '—') ?></td>
<td style="direction:ltr;text-align:left;"><?= money($item['unit_cost'] ?? 0) ?></td>
<td style="font-weight:700;direction:ltr;text-align:left;"><?= money($item['line_total'] ?? 0) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div style="padding:40px 20px;text-align:center;color:#6B7280;">
<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'); ?><?= isset($invoice) ? 'تعديل فاتورة مورد' : 'فاتورة مورد جديدة' ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/procurement/invoices" 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
$isEdit = isset($invoice);
$action = $isEdit ? '/procurement/invoices/' . (int) $invoice['id'] . '/update' : '/procurement/invoices';
?>
<form method="POST" action="<?= $action ?>" id="invoiceForm">
<?= 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="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:15px;">
<div>
<label class="form-label">رقم فاتورة المورد <span style="color:#DC2626;">*</span></label>
<input type="text" name="invoice_number" class="form-input" value="<?= e($invoice['invoice_number'] ?? '') ?>" required placeholder="رقم الفاتورة من المورد">
</div>
<div>
<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'] ?>" <?= ($invoice['supplier_id'] ?? '') == $sup['id'] ? 'selected' : '' ?>><?= e($sup['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label">أمر الشراء</label>
<select name="purchase_order_id" class="form-select">
<option value="">— بدون —</option>
<?php foreach ($approvedPOs as $po): ?>
<option value="<?= (int) $po['id'] ?>" <?= ($invoice['purchase_order_id'] ?? '') == $po['id'] ? 'selected' : '' ?>><?= e($po['po_number']) ?><?= e($po['supplier_name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label">إذن الاستلام</label>
<select name="grn_id" class="form-select">
<option value="">— بدون —</option>
<?php foreach ($completedGRNs as $g): ?>
<option value="<?= (int) $g['id'] ?>" <?= ($invoice['grn_id'] ?? '') == $g['id'] ? 'selected' : '' ?>><?= e($g['grn_number']) ?><?= e($g['supplier_name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label">تاريخ الفاتورة</label>
<input type="date" name="invoice_date" class="form-input" value="<?= e($invoice['invoice_date'] ?? date('Y-m-d')) ?>">
</div>
<div>
<label class="form-label">تاريخ الاستحقاق</label>
<input type="date" name="due_date" class="form-input" value="<?= e($invoice['due_date'] ?? '') ?>">
</div>
</div>
<div style="margin-top:15px;">
<label class="form-label">ملاحظات</label>
<textarea name="notes" class="form-input" rows="2"><?= e($invoice['notes'] ?? '') ?></textarea>
</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;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" class="btn btn-sm btn-primary" onclick="addInvRow()">
<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="invTable">
<thead>
<tr>
<th>الوصف</th>
<th style="width:100px;">الكمية</th>
<th style="width:120px;">سعر الوحدة</th>
<th style="width:80px;">ضريبة %</th>
<th style="width:100px;">الإجمالي</th>
<th style="width:50px;"></th>
</tr>
</thead>
<tbody id="invBody"></tbody>
<tfoot>
<tr>
<td colspan="4" style="text-align:left;font-weight:700;">الإجمالي</td>
<td style="font-weight:800;direction:ltr;text-align:left;" id="invGrandTotal">0.00</td>
<td></td>
</tr>
</tfoot>
</table>
</div>
</div>
<div style="text-align:left;">
<button type="submit" class="btn btn-primary" style="padding:10px 30px;">
<i data-lucide="save" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> حفظ الفاتورة
</button>
</div>
</form>
<script>
var invRowIdx = 0;
function addInvRow(data) {
data = data || {};
var idx = invRowIdx++;
var tr = document.createElement('tr');
tr.innerHTML = ''
+ '<td><input type="text" name="items['+idx+'][description]" class="form-input" value="'+(data.description||'')+'" placeholder="وصف البند"></td>'
+ '<td><input type="number" name="items['+idx+'][quantity]" class="form-input" step="0.001" min="0.001" value="'+(data.quantity||'')+'" onchange="calcInvRow('+idx+')" required></td>'
+ '<td><input type="number" name="items['+idx+'][unit_cost]" class="form-input" step="0.01" min="0" value="'+(data.unit_cost||'')+'" onchange="calcInvRow('+idx+')" required></td>'
+ '<td><input type="number" name="items['+idx+'][tax_rate]" class="form-input" step="0.01" min="0" value="'+(data.tax_rate||'14')+'" onchange="calcInvRow('+idx+')"></td>'
+ '<td style="font-weight:700;direction:ltr;text-align:left;" id="invLine_'+idx+'">0.00</td>'
+ '<td><button type="button" onclick="this.closest(\'tr\').remove();calcInvGrand();" style="background:none;border:none;cursor:pointer;color:#DC2626;"><i data-lucide="trash-2" style="width:16px;height:16px;"></i></button></td>';
document.getElementById('invBody').appendChild(tr);
if (typeof lucide !== 'undefined') lucide.createIcons();
calcInvRow(idx);
}
function calcInvRow(idx) {
var q = parseFloat(document.querySelector('[name="items['+idx+'][quantity]"]').value) || 0;
var c = parseFloat(document.querySelector('[name="items['+idx+'][unit_cost]"]').value) || 0;
var t = parseFloat(document.querySelector('[name="items['+idx+'][tax_rate]"]').value) || 0;
var base = q * c;
var tax = base * (t / 100);
var el = document.getElementById('invLine_' + idx);
if (el) el.textContent = (base + tax).toFixed(2);
calcInvGrand();
}
function calcInvGrand() {
var total = 0;
document.querySelectorAll('[id^="invLine_"]').forEach(function(el) { total += parseFloat(el.textContent) || 0; });
document.getElementById('invGrandTotal').textContent = total.toFixed(2);
}
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
<?php if ($isEdit && !empty($invoiceItems)): ?>
<?= json_encode($invoiceItems) ?>.forEach(function(item) { addInvRow(item); });
<?php else: ?>
addInvRow();
<?php endif; ?>
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>فواتير الموردين<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/procurement/invoices/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' => '#6B7280', 'verified' => '#D97706', 'approved' => '#0D7377', 'paid' => '#059669', 'partial_paid' => '#2563EB', 'cancelled' => '#DC2626'];
$statusBgs = ['draft' => '#F3F4F6', 'verified' => '#FFF7ED', 'approved' => '#F0FDFA', 'paid' => '#ECFDF5', 'partial_paid' => '#EFF6FF', 'cancelled' => '#FEE2E2'];
$matchColors = ['unmatched' => '#6B7280', 'matched' => '#059669', 'discrepancy' => '#DC2626', 'tolerance_pass' => '#D97706'];
$matchBgs = ['unmatched' => '#F3F4F6', 'matched' => '#ECFDF5', 'discrepancy' => '#FEE2E2', 'tolerance_pass' => '#FFF7ED'];
?>
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/procurement/invoices" 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:140px;">
<label class="form-label" style="font-size:12px;">المطابقة</label>
<select name="match_status" class="form-select">
<option value="">الكل</option>
<?php foreach ($matchStatuses as $val => $label): ?>
<option value="<?= e($val) ?>" <?= ($filters['match_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>
<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="/procurement/invoices" class="btn btn-sm btn-outline" style="color:#6B7280;">مسح</a>
</form>
</div>
<?php if (!empty($invoices)): ?>
<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 ($invoices as $inv): ?>
<?php
$st = $inv['status'] ?? 'draft';
$ms = $inv['match_status'] ?? '';
?>
<tr>
<td style="font-weight:600;">
<a href="/procurement/invoices/<?= (int) $inv['id'] ?>" style="color:#0D7377;text-decoration:none;">
<code style="font-size:12px;background:#F3F4F6;padding:2px 8px;border-radius:4px;"><?= e($inv['internal_number']) ?></code>
</a>
</td>
<td style="font-size:13px;"><?= e($inv['invoice_number'] ?? '—') ?></td>
<td style="font-weight:600;"><?= e($inv['supplier_name'] ?? '—') ?></td>
<td>
<?php if (!empty($inv['po_number'])): ?>
<code style="font-size:11px;background:#F3F4F6;padding:2px 6px;border-radius:4px;"><?= e($inv['po_number']) ?></code>
<?php else: ?><?php endif; ?>
</td>
<td>
<span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;background:<?= $statusBgs[$st] ?? '#F3F4F6' ?>;color:<?= $statusColors[$st] ?? '#6B7280' ?>;">
<?= e($statuses[$st] ?? $st) ?>
</span>
</td>
<td>
<?php if ($ms): ?>
<span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:11px;font-weight:600;background:<?= $matchBgs[$ms] ?? '#F3F4F6' ?>;color:<?= $matchColors[$ms] ?? '#6B7280' ?>;">
<?= e($matchStatuses[$ms] ?? $ms) ?>
</span>
<?php else: ?><?php endif; ?>
</td>
<td style="font-weight:700;direction:ltr;text-align:left;"><?= money($inv['total_amount'] ?? 0) ?></td>
<td style="font-size:12px;"><?= e($inv['due_date'] ?? '—') ?></td>
<td>
<a href="/procurement/invoices/<?= (int) $inv['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="file-text" 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;">ابدأ بإنشاء فاتورة مورد جديدة.</p>
<a href="/procurement/invoices/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($invoice['internal_number']) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/procurement/invoices/<?= (int) $invoice['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'); ?>
<?php
$matchStatusLabels = ['unmatched' => 'غير مطابق', 'matched' => 'مطابق', 'discrepancy' => 'يوجد اختلاف', 'tolerance_pass' => 'مطابق (ضمن التسامح)'];
$matchStatusColors = ['unmatched' => '#6B7280', 'matched' => '#059669', 'discrepancy' => '#DC2626', 'tolerance_pass' => '#D97706'];
$matchStatusBgs = ['unmatched' => '#F3F4F6', 'matched' => '#ECFDF5', 'discrepancy' => '#FEE2E2', 'tolerance_pass' => '#FFF7ED'];
$ms = $matchResult['status'] ?? 'unmatched';
?>
<!-- Match Summary -->
<div class="card" style="margin-bottom:20px;padding:20px;text-align:center;background:<?= $matchStatusBgs[$ms] ?? '#F3F4F6' ?>;">
<div style="font-size:16px;font-weight:700;color:<?= $matchStatusColors[$ms] ?? '#6B7280' ?>;margin-bottom:6px;">
<?= e($matchStatusLabels[$ms] ?? $ms) ?>
</div>
<div style="font-size:14px;color:#4B5563;"><?= e($matchResult['notes'] ?? '') ?></div>
</div>
<!-- Match 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="git-compare" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">تفاصيل المطابقة (أمر شراء ↔ إذن استلام ↔ فاتورة)</h3>
</div>
<?php if (!empty($matchResult['details'])): ?>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>#</th>
<th>كمية أمر الشراء</th>
<th>كمية المقبول (GRN)</th>
<th>كمية الفاتورة</th>
<th>كمية</th>
<th>سعر أمر الشراء</th>
<th>سعر الفاتورة</th>
<th>سعر</th>
<th>ملاحظات</th>
</tr>
</thead>
<tbody>
<?php foreach ($matchResult['details'] as $i => $d): ?>
<tr>
<td><?= $i + 1 ?></td>
<td style="direction:ltr;text-align:left;"><?= number_format((float) ($d['po_qty'] ?? 0), 3) ?></td>
<td style="direction:ltr;text-align:left;"><?= number_format((float) ($d['grn_accepted_qty'] ?? 0), 3) ?></td>
<td style="direction:ltr;text-align:left;"><?= number_format((float) ($d['invoice_qty'] ?? 0), 3) ?></td>
<td>
<?php if ($d['qty_match'] ?? false): ?>
<span style="color:#059669;font-weight:600;"><i data-lucide="check" style="width:14px;height:14px;vertical-align:middle;"></i></span>
<?php else: ?>
<span style="color:#DC2626;font-weight:600;"><i data-lucide="x" style="width:14px;height:14px;vertical-align:middle;"></i></span>
<?php endif; ?>
</td>
<td style="direction:ltr;text-align:left;"><?= money($d['po_cost'] ?? 0) ?></td>
<td style="direction:ltr;text-align:left;"><?= money($d['invoice_cost'] ?? 0) ?></td>
<td>
<?php if ($d['cost_match'] ?? false): ?>
<span style="color:#059669;font-weight:600;"><i data-lucide="check" style="width:14px;height:14px;vertical-align:middle;"></i></span>
<?php else: ?>
<span style="color:#DC2626;font-weight:600;"><i data-lucide="x" style="width:14px;height:14px;vertical-align:middle;"></i></span>
<?php endif; ?>
</td>
<td style="font-size:12px;color:#6B7280;"><?= e(implode(' | ', $d['notes'] ?? [])) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div style="padding:40px 20px;text-align:center;color:#6B7280;"><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($invoice['internal_number']) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<?php if ($invoice['status'] === 'draft'): ?>
<form method="POST" action="/procurement/invoices/<?= (int) $invoice['id'] ?>/verify" style="display:inline;">
<?= csrf_field() ?>
<button type="submit" class="btn" style="background:#D97706;color:#fff;border:none;">
<i data-lucide="check-square" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> تحقق ومطابقة
</button>
</form>
<?php endif; ?>
<?php if ($invoice['status'] === 'verified'): ?>
<form method="POST" action="/procurement/invoices/<?= (int) $invoice['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 (!empty($invoice['purchase_order_id'])): ?>
<a href="/procurement/invoices/<?= (int) $invoice['id'] ?>/match" class="btn btn-outline">
<i data-lucide="git-compare" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> عرض المطابقة
</a>
<?php endif; ?>
<?php if (in_array($invoice['status'], ['draft', 'verified'])): ?>
<form method="POST" action="/procurement/invoices/<?= (int) $invoice['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="/procurement/invoices" 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' => 'مسودة', 'verified' => 'تم التحقق', 'approved' => 'معتمدة', 'paid' => 'مدفوعة', 'partial_paid' => 'مدفوعة جزئياً', 'cancelled' => 'ملغاة'];
$statusColorMap = [
'draft' => ['bg' => '#F3F4F6', 'color' => '#6B7280'], 'verified' => ['bg' => '#FFF7ED', 'color' => '#D97706'],
'approved' => ['bg' => '#F0FDFA', 'color' => '#0D7377'], 'paid' => ['bg' => '#ECFDF5', 'color' => '#059669'],
'partial_paid' => ['bg' => '#EFF6FF', 'color' => '#2563EB'], 'cancelled' => ['bg' => '#FEE2E2', 'color' => '#DC2626'],
];
$matchLabels = ['unmatched' => 'غير مطابق', 'matched' => 'مطابق', 'discrepancy' => 'يوجد اختلاف', 'tolerance_pass' => 'مطابق (تسامح)'];
$matchColorMap = ['unmatched' => ['bg' => '#F3F4F6', 'color' => '#6B7280'], 'matched' => ['bg' => '#ECFDF5', 'color' => '#059669'], 'discrepancy' => ['bg' => '#FEE2E2', 'color' => '#DC2626'], 'tolerance_pass' => ['bg' => '#FFF7ED', 'color' => '#D97706']];
$st = $invoice['status'] ?? 'draft';
$ms = $invoice['match_status'] ?? '';
$stColors = $statusColorMap[$st] ?? $statusColorMap['draft'];
$msColors = $matchColorMap[$ms] ?? ['bg' => '#F3F4F6', 'color' => '#6B7280'];
?>
<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;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="background:#F3F4F6;padding:2px 8px;border-radius:4px;"><?= e($invoice['internal_number']) ?></code></td></tr>
<tr><td style="padding:10px 0;color:#6B7280;">رقم فاتورة المورد</td><td style="padding:10px 0;"><?= e($invoice['invoice_number'] ?? '—') ?></td></tr>
<tr><td style="padding:10px 0;color:#6B7280;">المورد</td><td style="padding:10px 0;font-weight:600;"><?= e($invoice['supplier_name'] ?? '') ?></td></tr>
<tr><td style="padding:10px 0;color:#6B7280;">أمر الشراء</td><td style="padding:10px 0;"><?php if (!empty($invoice['po_number'])): ?><a href="/inventory/purchase-orders/<?= (int) $invoice['purchase_order_id'] ?>" style="color:#0D7377;"><?= e($invoice['po_number']) ?></a><?php else: ?><?php endif; ?></td></tr>
<tr><td style="padding:10px 0;color:#6B7280;">إذن الاستلام</td><td style="padding:10px 0;"><?php if (!empty($invoice['grn_number'])): ?><a href="/procurement/grn/<?= (int) $invoice['grn_id'] ?>" style="color:#0D7377;"><?= e($invoice['grn_number']) ?></a><?php else: ?><?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;"><span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;background:<?= $stColors['bg'] ?>;color:<?= $stColors['color'] ?>;"><?= e($statusLabels[$st] ?? $st) ?></span></td></tr>
<tr><td style="padding:10px 0;color:#6B7280;">المطابقة</td><td style="padding:10px 0;"><?php if ($ms): ?><span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;background:<?= $msColors['bg'] ?>;color:<?= $msColors['color'] ?>;"><?= e($matchLabels[$ms] ?? $ms) ?></span><?php else: ?><?php endif; ?></td></tr>
<tr><td style="padding:10px 0;color:#6B7280;">تاريخ الفاتورة</td><td style="padding:10px 0;"><?= e($invoice['invoice_date'] ?? '—') ?></td></tr>
<tr><td style="padding:10px 0;color:#6B7280;">تاريخ الاستحقاق</td><td style="padding:10px 0;"><?= e($invoice['due_date'] ?? '—') ?></td></tr>
</table>
</div>
</div>
</div>
<!-- Totals -->
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:10px;margin-bottom:20px;">
<div class="card" style="padding:15px;text-align:center;">
<div style="font-size:12px;color:#6B7280;">المبلغ الأساسي</div>
<div style="font-size:20px;font-weight:700;color:#4B5563;direction:ltr;"><?= money($invoice['subtotal'] ?? 0) ?></div>
</div>
<div class="card" style="padding:15px;text-align:center;">
<div style="font-size:12px;color:#6B7280;">الضريبة</div>
<div style="font-size:20px;font-weight:700;color:#D97706;direction:ltr;"><?= money($invoice['tax_amount'] ?? 0) ?></div>
</div>
<div class="card" style="padding:15px;text-align:center;">
<div style="font-size:12px;color:#6B7280;">الخصم</div>
<div style="font-size:20px;font-weight:700;color:#059669;direction:ltr;"><?= money($invoice['discount_amount'] ?? 0) ?></div>
</div>
<div class="card" style="padding:15px;text-align:center;background:linear-gradient(135deg, #0D737710, #0D737720);">
<div style="font-size:12px;color:#6B7280;">الإجمالي</div>
<div style="font-size:24px;font-weight:800;color:#0D7377;direction:ltr;"><?= money($invoice['total_amount'] ?? 0) ?></div>
</div>
</div>
<!-- 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>الكمية</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'] ?? $item['description'] ?? '—') ?></td>
<td style="direction:ltr;text-align:left;"><?= number_format((float) ($item['quantity'] ?? 0), 3) ?></td>
<td style="direction:ltr;text-align:left;"><?= money($item['unit_cost'] ?? 0) ?></td>
<td style="direction:ltr;text-align:left;"><?= number_format((float) ($item['tax_rate'] ?? 0), 2) ?>%</td>
<td style="direction:ltr;text-align:left;"><?= money($item['tax_amount'] ?? 0) ?></td>
<td style="font-weight:700;direction:ltr;text-align:left;"><?= money($item['line_total'] ?? 0) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div style="padding:40px 20px;text-align:center;color:#6B7280;"><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'); ?>
<a href="/procurement/payments" 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="/procurement/payments">
<?= 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="banknote" 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:15px;">
<div>
<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'] ?>"><?= e($sup['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label">الفاتورة</label>
<select name="invoice_id" class="form-select">
<option value="">— بدون فاتورة —</option>
<?php foreach ($approvedInvoices as $inv): ?>
<option value="<?= (int) $inv['id'] ?>" data-supplier="<?= (int) $inv['supplier_id'] ?>" data-amount="<?= e($inv['total_amount']) ?>">
<?= e($inv['internal_number']) ?><?= e($inv['supplier_name']) ?> (<?= money($inv['total_amount']) ?>)
</option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label">المبلغ <span style="color:#DC2626;">*</span></label>
<input type="number" name="amount" class="form-input" step="0.01" min="0.01" required placeholder="0.00">
</div>
<div>
<label class="form-label">طريقة الدفع <span style="color:#DC2626;">*</span></label>
<select name="payment_method" class="form-select" required>
<?php foreach ($paymentMethods as $val => $label): ?>
<option value="<?= e($val) ?>"><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label">الحساب البنكي</label>
<select name="bank_account_id" class="form-select">
<option value="">— اختياري —</option>
<?php foreach ($bankAccounts as $ba): ?>
<option value="<?= (int) $ba['id'] ?>"><?= e($ba['name_ar'] ?? $ba['account_number'] ?? '') ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label">تاريخ الدفع</label>
<input type="date" name="payment_date" class="form-input" value="<?= date('Y-m-d') ?>">
</div>
<div>
<label class="form-label">رقم الشيك</label>
<input type="text" name="check_number" class="form-input" placeholder="اختياري">
</div>
<div>
<label class="form-label">الرقم المرجعي</label>
<input type="text" name="reference_number" class="form-input" placeholder="اختياري">
</div>
</div>
<div style="margin-top:15px;">
<label class="form-label">ملاحظات</label>
<textarea name="notes" class="form-input" rows="2" placeholder="ملاحظات إضافية..."></textarea>
</div>
</div>
</div>
<div style="text-align:left;">
<button type="submit" class="btn btn-primary" style="padding:10px 30px;">
<i data-lucide="save" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> حفظ الدفعة
</button>
</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="/procurement/payments/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' => '#6B7280', 'approved' => '#D97706', 'completed' => '#059669', 'voided' => '#DC2626'];
$statusBgs = ['draft' => '#F3F4F6', 'approved' => '#FFF7ED', 'completed' => '#ECFDF5', 'voided' => '#FEE2E2'];
?>
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/procurement/payments" 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><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="/procurement/payments" class="btn btn-sm btn-outline" style="color:#6B7280;">مسح</a>
</form>
</div>
<?php if (!empty($payments)): ?>
<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 ($payments as $p): ?>
<?php $st = $p['status'] ?? 'draft'; ?>
<tr>
<td style="font-weight:600;"><a href="/procurement/payments/<?= (int) $p['id'] ?>" style="color:#0D7377;text-decoration:none;"><code style="font-size:12px;background:#F3F4F6;padding:2px 8px;border-radius:4px;"><?= e($p['payment_number']) ?></code></a></td>
<td style="font-weight:600;"><?= e($p['supplier_name'] ?? '—') ?></td>
<td><?php if (!empty($p['invoice_number'])): ?><code style="font-size:11px;background:#F3F4F6;padding:2px 6px;border-radius:4px;"><?= e($p['invoice_number']) ?></code><?php else: ?><?php endif; ?></td>
<td><span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;background:<?= $statusBgs[$st] ?? '#F3F4F6' ?>;color:<?= $statusColors[$st] ?? '#6B7280' ?>;"><?= e($statuses[$st] ?? $st) ?></span></td>
<td style="font-weight:700;direction:ltr;text-align:left;"><?= money($p['amount'] ?? 0) ?></td>
<td style="font-size:12px;"><?= e(['cash' => 'نقدي', 'bank_transfer' => 'تحويل', 'check' => 'شيك', 'wire' => 'حوالة'][$p['payment_method'] ?? ''] ?? $p['payment_method'] ?? '—') ?></td>
<td style="font-size:12px;"><?= e($p['payment_date'] ?? '') ?></td>
<td><a href="/procurement/payments/<?= (int) $p['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="banknote" 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;">ابدأ بإنشاء دفعة مورد جديدة.</p>
<a href="/procurement/payments/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($payment['payment_number']) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<?php if ($payment['status'] === 'draft'): ?>
<form method="POST" action="/procurement/payments/<?= (int) $payment['id'] ?>/approve" style="display:inline;"><?= csrf_field() ?><button type="submit" class="btn" style="background:#D97706;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 ($payment['status'] === 'approved'): ?>
<form method="POST" action="/procurement/payments/<?= (int) $payment['id'] ?>/complete" 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 (in_array($payment['status'], ['draft', 'approved', 'completed'])): ?>
<button type="button" class="btn" style="background:#FEE2E2;color:#DC2626;border:none;" onclick="document.getElementById('voidModal').style.display='flex';"><i data-lucide="x-circle" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> إلغاء</button>
<?php endif; ?>
<a href="/procurement/payments" 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' => 'مسودة', 'approved' => 'معتمدة', 'completed' => 'مكتملة', 'voided' => 'ملغاة'];
$statusColorMap = ['draft' => ['bg' => '#F3F4F6', 'color' => '#6B7280'], 'approved' => ['bg' => '#FFF7ED', 'color' => '#D97706'], 'completed' => ['bg' => '#ECFDF5', 'color' => '#059669'], 'voided' => ['bg' => '#FEE2E2', 'color' => '#DC2626']];
$methodLabels = ['cash' => 'نقدي', 'bank_transfer' => 'تحويل بنكي', 'check' => 'شيك', 'wire' => 'حوالة'];
$st = $payment['status'] ?? 'draft';
$stColors = $statusColorMap[$st] ?? $statusColorMap['draft'];
?>
<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($payment['amount'] ?? 0) ?></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="banknote" 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="background:#F3F4F6;padding:2px 8px;border-radius:4px;"><?= e($payment['payment_number']) ?></code></td></tr>
<tr><td style="padding:10px 0;color:#6B7280;">المورد</td><td style="padding:10px 0;font-weight:600;"><?= e($payment['supplier_name'] ?? '') ?></td></tr>
<tr><td style="padding:10px 0;color:#6B7280;">الفاتورة</td><td style="padding:10px 0;"><?php if (!empty($payment['invoice_internal'])): ?><a href="/procurement/invoices/<?= (int) $payment['invoice_id'] ?>" style="color:#0D7377;"><?= e($payment['invoice_internal']) ?></a><?php else: ?><?php endif; ?></td></tr>
<tr><td style="padding:10px 0;color:#6B7280;">طريقة الدفع</td><td style="padding:10px 0;"><?= e($methodLabels[$payment['payment_method'] ?? ''] ?? $payment['payment_method'] ?? '—') ?></td></tr>
<?php if (!empty($payment['check_number'])): ?>
<tr><td style="padding:10px 0;color:#6B7280;">رقم الشيك</td><td style="padding:10px 0;"><?= e($payment['check_number']) ?></td></tr>
<?php endif; ?>
<?php if (!empty($payment['reference_number'])): ?>
<tr><td style="padding:10px 0;color:#6B7280;">الرقم المرجعي</td><td style="padding:10px 0;"><?= e($payment['reference_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;"><span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;background:<?= $stColors['bg'] ?>;color:<?= $stColors['color'] ?>;"><?= e($statusLabels[$st] ?? $st) ?></span></td></tr>
<tr><td style="padding:10px 0;color:#6B7280;">تاريخ الدفع</td><td style="padding:10px 0;"><?= e($payment['payment_date'] ?? '—') ?></td></tr>
<?php if (!empty($payment['approved_at'])): ?>
<tr><td style="padding:10px 0;color:#6B7280;">تاريخ الاعتماد</td><td style="padding:10px 0;"><?= e($payment['approved_at']) ?><?= e($payment['approver_name'] ?? '') ?></td></tr>
<?php endif; ?>
<?php if (!empty($payment['completed_at'])): ?>
<tr><td style="padding:10px 0;color:#6B7280;">تاريخ الإتمام</td><td style="padding:10px 0;"><?= e($payment['completed_at']) ?><?= e($payment['completer_name'] ?? '') ?></td></tr>
<?php endif; ?>
<?php if (!empty($payment['voided_at'])): ?>
<tr><td style="padding:10px 0;color:#6B7280;">تاريخ الإلغاء</td><td style="padding:10px 0;"><?= e($payment['voided_at']) ?><?= e($payment['voider_name'] ?? '') ?></td></tr>
<?php endif; ?>
</table>
</div>
<?php if (!empty($payment['void_reason'])): ?>
<div style="margin-top:15px;padding:12px;background:#FEE2E2;border-radius:8px;">
<div style="font-size:12px;color:#DC2626;font-weight:600;margin-bottom:4px;">سبب الإلغاء</div>
<div style="font-size:14px;color:#7F1D1D;"><?= e($payment['void_reason']) ?></div>
</div>
<?php endif; ?>
<?php if (!empty($payment['notes'])): ?>
<div style="margin-top:15px;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;"><?= e($payment['notes']) ?></div>
</div>
<?php endif; ?>
</div>
</div>
<!-- Void Modal -->
<?php if (in_array($payment['status'], ['draft', 'approved', 'completed'])): ?>
<div id="voidModal" style="display:none;position:fixed;inset:0;z-index:999;background:rgba(0,0,0,0.5);align-items:center;justify-content:center;">
<div style="background:#fff;border-radius:12px;padding:25px;width:90%;max-width:480px;">
<h3 style="margin:0 0 15px;color:#DC2626;">إلغاء الدفعة</h3>
<form method="POST" action="/procurement/payments/<?= (int) $payment['id'] ?>/void">
<?= csrf_field() ?>
<div style="margin-bottom:15px;">
<label class="form-label">سبب الإلغاء <span style="color:#DC2626;">*</span></label>
<textarea name="void_reason" class="form-input" rows="3" required placeholder="أدخل سبب الإلغاء..."></textarea>
</div>
<div style="display:flex;gap:10px;justify-content:flex-end;">
<button type="button" class="btn btn-outline" onclick="document.getElementById('voidModal').style.display='none';">تراجع</button>
<button type="submit" class="btn" style="background:#DC2626;color:#fff;border:none;">تأكيد الإلغاء</button>
</div>
</form>
</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="/procurement" 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 $s = $summary ?? []; ?>
<!-- Summary Cards -->
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:15px;margin-bottom:20px;">
<div class="card" style="padding:20px;text-align:center;">
<div style="font-size:12px;color:#6B7280;margin-bottom:6px;">متطابقة</div>
<div style="font-size:32px;font-weight:800;color:#059669;"><?= (int) ($s['matched'] ?? 0) ?></div>
</div>
<div class="card" style="padding:20px;text-align:center;">
<div style="font-size:12px;color:#6B7280;margin-bottom:6px;">ضمن التحمل</div>
<div style="font-size:32px;font-weight:800;color:#D97706;"><?= (int) ($s['tolerance_pass'] ?? 0) ?></div>
</div>
<div class="card" style="padding:20px;text-align:center;">
<div style="font-size:12px;color:#6B7280;margin-bottom:6px;">تباين</div>
<div style="font-size:32px;font-weight:800;color:#DC2626;"><?= (int) ($s['discrepancy'] ?? 0) ?></div>
</div>
<div class="card" style="padding:20px;text-align:center;">
<div style="font-size:12px;color:#6B7280;margin-bottom:6px;">غير مطابقة</div>
<div style="font-size:32px;font-weight:800;color:#6B7280;"><?= (int) ($s['unmatched'] ?? 0) ?></div>
</div>
</div>
<?php
$matchLabels = ['matched' => 'متطابقة', 'tolerance_pass' => 'ضمن التحمل', 'discrepancy' => 'تباين', 'unmatched' => 'غير مطابقة'];
$matchColors = ['matched' => '#059669', 'tolerance_pass' => '#D97706', 'discrepancy' => '#DC2626', 'unmatched' => '#6B7280'];
$matchBgs = ['matched' => '#ECFDF5', 'tolerance_pass' => '#FFF7ED', 'discrepancy' => '#FEE2E2', 'unmatched' => '#F3F4F6'];
$statusLabels = ['draft' => 'مسودة', 'verified' => 'تم التحقق', 'approved' => 'معتمدة', 'paid' => 'مدفوعة', 'partial_paid' => 'مدفوعة جزئياً', 'cancelled' => 'ملغاة'];
?>
<!-- Invoice List -->
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="git-compare" style="width:18px;height:18px;color:#2563EB;"></i>
<h3 style="margin:0;color:#2563EB;font-size:15px;">فواتير الموردين — حالة المطابقة</h3>
</div>
<?php if (!empty($invoices)): ?>
<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 ($invoices as $inv): ?>
<?php $ms = $inv['match_status'] ?? 'unmatched'; ?>
<tr>
<td style="font-weight:600;"><a href="/procurement/invoices/<?= (int) $inv['id'] ?>" style="color:#0D7377;text-decoration:none;"><code style="font-size:12px;background:#F3F4F6;padding:2px 8px;border-radius:4px;"><?= e($inv['internal_number'] ?? '') ?></code></a></td>
<td style="font-size:13px;"><?= e($inv['invoice_number'] ?? '—') ?></td>
<td style="font-weight:600;"><?= e($inv['supplier_name'] ?? '') ?></td>
<td><?php if (!empty($inv['po_number'])): ?><code style="font-size:11px;background:#F3F4F6;padding:2px 6px;border-radius:4px;"><?= e($inv['po_number']) ?></code><?php else: ?><?php endif; ?></td>
<td style="font-weight:700;direction:ltr;text-align:left;"><?= money($inv['total_amount'] ?? 0) ?></td>
<td><span style="font-size:12px;color:#6B7280;"><?= e($statusLabels[$inv['status'] ?? ''] ?? ($inv['status'] ?? '')) ?></span></td>
<td>
<span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;background:<?= $matchBgs[$ms] ?? '#F3F4F6' ?>;color:<?= $matchColors[$ms] ?? '#6B7280' ?>;">
<?= e($matchLabels[$ms] ?? $ms) ?>
</span>
</td>
<td>
<a href="/procurement/invoices/<?= (int) $inv['id'] ?>/match" class="btn btn-sm btn-outline" style="font-size:11px;padding:3px 8px;">
<i data-lucide="eye" style="width:12px;height:12px;vertical-align:middle;"></i> تفاصيل
</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div style="padding:60px 20px;text-align:center;color:#6B7280;">
<i data-lucide="git-compare" style="width:48px;height:48px;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'); ?>
<a href="/procurement" 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 $a = $aging ?? []; ?>
<!-- Aging Summary -->
<div style="display:grid;grid-template-columns:repeat(5,1fr);gap:15px;margin-bottom:20px;">
<div class="card" style="padding:20px;text-align:center;">
<div style="font-size:12px;color:#6B7280;margin-bottom:6px;">1-30 يوم</div>
<div style="font-size:24px;font-weight:800;color:#D97706;direction:ltr;"><?= money($a['days_1_30'] ?? 0) ?></div>
</div>
<div class="card" style="padding:20px;text-align:center;">
<div style="font-size:12px;color:#6B7280;margin-bottom:6px;">31-60 يوم</div>
<div style="font-size:24px;font-weight:800;color:#EA580C;direction:ltr;"><?= money($a['days_31_60'] ?? 0) ?></div>
</div>
<div class="card" style="padding:20px;text-align:center;">
<div style="font-size:12px;color:#6B7280;margin-bottom:6px;">61-90 يوم</div>
<div style="font-size:24px;font-weight:800;color:#DC2626;direction:ltr;"><?= money($a['days_61_90'] ?? 0) ?></div>
</div>
<div class="card" style="padding:20px;text-align:center;">
<div style="font-size:12px;color:#6B7280;margin-bottom:6px;">أكثر من 90 يوم</div>
<div style="font-size:24px;font-weight:800;color:#991B1B;direction:ltr;"><?= money($a['over_90'] ?? 0) ?></div>
</div>
<div class="card" style="padding:20px;text-align:center;background:linear-gradient(135deg, #DC262610, #DC262620);">
<div style="font-size:12px;color:#6B7280;margin-bottom:6px;">إجمالي المتأخر</div>
<div style="font-size:24px;font-weight:800;color:#DC2626;direction:ltr;"><?= money($a['total_overdue'] ?? 0) ?></div>
</div>
</div>
<!-- Overdue List -->
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="clock" style="width:18px;height:18px;color:#DC2626;"></i>
<h3 style="margin:0;color:#DC2626;font-size:15px;">الفواتير المتأخرة</h3>
</div>
<?php if (!empty($overdue)): ?>
<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 ($overdue as $ap): ?>
<?php
$days = (int) ($ap['days_overdue'] ?? 0);
$urgencyColor = $days > 90 ? '#991B1B' : ($days > 60 ? '#DC2626' : ($days > 30 ? '#EA580C' : '#D97706'));
?>
<tr>
<td style="font-weight:600;"><code style="font-size:12px;background:#F3F4F6;padding:2px 8px;border-radius:4px;"><?= e($ap['invoice_number'] ?? '') ?></code></td>
<td style="font-weight:600;"><?= e($ap['supplier_name'] ?? '') ?></td>
<td style="font-size:13px;"><?= e($ap['due_date'] ?? '') ?></td>
<td>
<span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:700;background:<?= $urgencyColor ?>15;color:<?= $urgencyColor ?>;">
<?= $days ?> يوم
</span>
</td>
<td style="direction:ltr;text-align:left;"><?= money($ap['total_amount'] ?? 0) ?></td>
<td style="direction:ltr;text-align:left;color:#059669;"><?= money($ap['paid_amount'] ?? 0) ?></td>
<td style="font-weight:700;direction:ltr;text-align:left;color:#DC2626;"><?= money($ap['balance'] ?? 0) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div 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; ?>
</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="/procurement" 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'); ?>
<!-- Filters -->
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/procurement/reports/purchase-volume" style="display:flex;gap:10px;align-items:end;flex-wrap:wrap;">
<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>
</form>
</div>
<!-- Totals -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;margin-bottom:20px;">
<div class="card" style="padding:20px;text-align:center;background:linear-gradient(135deg, #0D737710, #0D737720);">
<div style="font-size:13px;color:#6B7280;margin-bottom:6px;">إجمالي الطلبات</div>
<div style="font-size:36px;font-weight:800;color:#0D7377;"><?= (int) ($totals['total_orders'] ?? 0) ?></div>
</div>
<div class="card" style="padding:20px;text-align:center;background:linear-gradient(135deg, #D9770610, #D9770620);">
<div style="font-size:13px;color:#6B7280;margin-bottom:6px;">إجمالي القيمة</div>
<div style="font-size:36px;font-weight:800;color:#D97706;direction:ltr;"><?= money($totals['total_value'] ?? 0) ?></div>
</div>
</div>
<!-- Monthly Volume -->
<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="bar-chart-3" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">حجم المشتريات الشهري</h3>
</div>
<?php if (!empty($monthly)): ?>
<div class="table-responsive">
<table class="data-table">
<thead><tr><th>الشهر</th><th>عدد الطلبات</th><th>إجمالي القيمة</th><th>الرسم البياني</th></tr></thead>
<tbody>
<?php
$maxValue = 0;
foreach ($monthly as $m) { $maxValue = max($maxValue, (float) $m['total_value']); }
?>
<?php foreach ($monthly as $m): ?>
<?php $pct = $maxValue > 0 ? ((float) $m['total_value'] / $maxValue * 100) : 0; ?>
<tr>
<td style="font-weight:600;"><?= e($m['month']) ?></td>
<td style="font-weight:600;"><?= (int) $m['order_count'] ?></td>
<td style="font-weight:700;direction:ltr;text-align:left;"><?= money($m['total_value']) ?></td>
<td style="width:40%;">
<div style="background:#E5E7EB;border-radius:4px;height:20px;overflow:hidden;">
<div style="background:#0D7377;height:100%;width:<?= round($pct, 1) ?>%;border-radius:4px;"></div>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div style="padding:40px;text-align:center;color:#6B7280;">لا توجد بيانات في هذه الفترة</div>
<?php endif; ?>
</div>
<!-- Top Suppliers -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;margin-bottom:20px;">
<div class="card">
<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:#D97706;"></i>
<h3 style="margin:0;color:#D97706;font-size:15px;">أكبر الموردين</h3>
</div>
<?php if (!empty($topSuppliers)): ?>
<div class="table-responsive">
<table class="data-table">
<thead><tr><th>المورد</th><th>الطلبات</th><th>القيمة</th></tr></thead>
<tbody>
<?php foreach ($topSuppliers as $s): ?>
<tr>
<td style="font-weight:600;"><?= e($s['name_ar']) ?></td>
<td><?= (int) $s['order_count'] ?></td>
<td style="font-weight:700;direction:ltr;text-align:left;"><?= money($s['total_value']) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div style="padding:30px;text-align:center;color:#6B7280;">لا توجد بيانات</div>
<?php endif; ?>
</div>
<!-- Top Items -->
<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:18px;height:18px;color:#2563EB;"></i>
<h3 style="margin:0;color:#2563EB;font-size:15px;">أكثر الأصناف شراءً</h3>
</div>
<?php if (!empty($topItems)): ?>
<div class="table-responsive">
<table class="data-table">
<thead><tr><th>الصنف</th><th>الكمية</th><th>القيمة</th></tr></thead>
<tbody>
<?php foreach ($topItems as $it): ?>
<tr>
<td style="font-weight:600;"><?= e($it['name_ar']) ?><?php if (!empty($it['sku'])): ?><br><code style="font-size:11px;background:#F3F4F6;padding:2px 6px;border-radius:4px;"><?= e($it['sku']) ?></code><?php endif; ?></td>
<td style="direction:ltr;text-align:left;"><?= number_format((float) $it['total_qty'], 3) ?></td>
<td style="font-weight:700;direction:ltr;text-align:left;"><?= money($it['total_value']) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div style="padding:30px;text-align:center;color:#6B7280;">لا توجد بيانات</div>
<?php endif; ?>
</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="/procurement" 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 if (!empty($suppliers)): ?>
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="award" 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>
<th>جودة الاستلام</th>
<th>متوسط المهلة</th>
<th>الرصيد المستحق</th>
<th>آخر طلب</th>
</tr>
</thead>
<tbody>
<?php foreach ($suppliers as $s): ?>
<?php
$total = (int) ($s['total_orders'] ?? 0);
$onTime = (int) ($s['on_time_delivery_count'] ?? 0);
$quality = (int) ($s['quality_acceptance_count'] ?? 0);
$onTimeRate = $total > 0 ? round(($onTime / $total) * 100, 1) : 0;
$qualityRate = $total > 0 ? round(($quality / $total) * 100, 1) : 0;
$avgLead = round((float) ($s['average_lead_days'] ?? 0), 1);
?>
<tr>
<td><code style="font-size:12px;background:#F3F4F6;padding:2px 8px;border-radius:4px;"><?= e($s['code'] ?? '') ?></code></td>
<td style="font-weight:600;"><a href="/inventory/suppliers/<?= (int) $s['id'] ?>" style="color:#0D7377;text-decoration:none;"><?= e($s['name_ar']) ?></a></td>
<td>
<?php
$rating = (int) ($s['rating'] ?? 0);
if ($rating > 0):
for ($i = 1; $i <= 5; $i++):
?>
<i data-lucide="star" style="width:12px;height:12px;vertical-align:middle;<?= $i <= $rating ? 'color:#D97706;fill:#D97706;' : 'color:#D1D5DB;' ?>"></i>
<?php endfor; else: ?>
<span style="color:#9CA3AF;font-size:12px;"></span>
<?php endif; ?>
</td>
<td style="font-weight:600;"><?= $total ?></td>
<td>
<div style="display:flex;align-items:center;gap:6px;">
<div style="flex:1;background:#E5E7EB;border-radius:4px;height:8px;overflow:hidden;">
<div style="background:<?= $onTimeRate >= 80 ? '#059669' : ($onTimeRate >= 50 ? '#D97706' : '#DC2626') ?>;height:100%;width:<?= $onTimeRate ?>%;border-radius:4px;"></div>
</div>
<span style="font-size:12px;font-weight:600;color:<?= $onTimeRate >= 80 ? '#059669' : ($onTimeRate >= 50 ? '#D97706' : '#DC2626') ?>;"><?= $onTimeRate ?>%</span>
</div>
</td>
<td>
<div style="display:flex;align-items:center;gap:6px;">
<div style="flex:1;background:#E5E7EB;border-radius:4px;height:8px;overflow:hidden;">
<div style="background:<?= $qualityRate >= 80 ? '#059669' : ($qualityRate >= 50 ? '#D97706' : '#DC2626') ?>;height:100%;width:<?= $qualityRate ?>%;border-radius:4px;"></div>
</div>
<span style="font-size:12px;font-weight:600;color:<?= $qualityRate >= 80 ? '#059669' : ($qualityRate >= 50 ? '#D97706' : '#DC2626') ?>;"><?= $qualityRate ?>%</span>
</div>
</td>
<td style="font-size:13px;"><?= $avgLead ?> يوم</td>
<td style="font-weight:700;direction:ltr;text-align:left;color:<?= bccomp((string) ($s['outstanding_balance'] ?? '0'), '0', 2) > 0 ? '#DC2626' : '#059669' ?>;"><?= money($s['outstanding_balance'] ?? 0) ?></td>
<td style="font-size:12px;color:#6B7280;"><?= e($s['last_order_date'] ?? '—') ?></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="award" 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'); ?><?= isset($pr) ? 'تعديل طلب شراء' : 'طلب شراء جديد' ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/procurement/requisitions" 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
$isEdit = isset($pr);
$action = $isEdit ? '/procurement/requisitions/' . (int) $pr['id'] . '/update' : '/procurement/requisitions';
?>
<form method="POST" action="<?= $action ?>" id="prForm">
<?= csrf_field() ?>
<!-- Header Info -->
<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:15px;">
<div>
<label class="form-label">القسم</label>
<input type="text" name="department" class="form-input" value="<?= e($pr['department'] ?? '') ?>" placeholder="مثال: قسم التشغيل">
</div>
<div>
<label class="form-label">الأولوية</label>
<select name="urgency" class="form-select">
<?php foreach ($urgencies as $val => $label): ?>
<option value="<?= e($val) ?>" <?= ($pr['urgency'] ?? 'normal') === $val ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label">تاريخ الاحتياج</label>
<input type="date" name="required_date" class="form-input" value="<?= e($pr['required_date'] ?? '') ?>">
</div>
<div>
<label class="form-label">المبرر</label>
<input type="text" name="justification" class="form-input" value="<?= e($pr['justification'] ?? '') ?>" placeholder="سبب الطلب">
</div>
</div>
<div style="margin-top:15px;">
<label class="form-label">ملاحظات</label>
<textarea name="notes" class="form-input" rows="2" placeholder="ملاحظات إضافية..."><?= e($pr['notes'] ?? '') ?></textarea>
</div>
</div>
</div>
<!-- Items -->
<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" class="btn btn-sm btn-primary" onclick="addItemRow()">
<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:200px;">الصنف</th>
<th>الوصف</th>
<th style="width:100px;">الكمية</th>
<th style="width:100px;">الوحدة</th>
<th style="width:120px;">التكلفة التقديرية</th>
<th style="width:120px;">الإجمالي</th>
<th style="width:180px;">المورد المفضل</th>
<th style="width:50px;"></th>
</tr>
</thead>
<tbody id="itemsBody">
</tbody>
<tfoot>
<tr>
<td colspan="5" style="text-align:left;font-weight:700;font-size:15px;">الإجمالي التقديري</td>
<td style="font-weight:800;font-size:15px;direction:ltr;text-align:left;" id="grandTotal">0.00</td>
<td colspan="2"></td>
</tr>
</tfoot>
</table>
</div>
</div>
<div style="text-align:left;">
<button type="submit" class="btn btn-primary" style="padding:10px 30px;">
<i data-lucide="save" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i>
<?= $isEdit ? 'تحديث الطلب' : 'حفظ الطلب' ?>
</button>
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
var inventoryItems = <?= json_encode($items ?? []) ?>;
var suppliers = <?= json_encode($suppliers ?? []) ?>;
window.inventoryItems = inventoryItems;
window.suppliers = suppliers;
<?php if ($isEdit && !empty($prItems)): ?>
var existingItems = <?= json_encode($prItems) ?>;
existingItems.forEach(function(item) {
addItemRow(item);
});
<?php else: ?>
addItemRow();
<?php endif; ?>
});
var rowIndex = 0;
function addItemRow(data) {
data = data || {};
var idx = rowIndex++;
var tbody = document.getElementById('itemsBody');
var tr = document.createElement('tr');
var itemOptions = '<option value="">-- اختر صنف --</option>';
window.inventoryItems.forEach(function(i) {
var sel = (data.item_id && data.item_id == i.id) ? ' selected' : '';
itemOptions += '<option value="'+i.id+'" data-cost="'+(i.cost_price||0)+'"'+sel+'>'+i.name_ar+'</option>';
});
var supplierOptions = '<option value="">—</option>';
window.suppliers.forEach(function(s) {
var sel = (data.preferred_supplier_id && data.preferred_supplier_id == s.id) ? ' selected' : '';
supplierOptions += '<option value="'+s.id+'"'+sel+'>'+s.name_ar+'</option>';
});
tr.innerHTML = ''
+ '<td><select name="items['+idx+'][item_id]" class="form-select" onchange="onItemSelect(this,'+idx+')">'+itemOptions+'</select></td>'
+ '<td><input type="text" name="items['+idx+'][description_ar]" class="form-input" value="'+(data.description_ar||'')+'" placeholder="وصف حر"></td>'
+ '<td><input type="number" name="items['+idx+'][quantity]" class="form-input" step="0.001" min="0.001" value="'+(data.quantity||'')+'" onchange="calcRow('+idx+')" required></td>'
+ '<td><input type="text" name="items['+idx+'][unit_of_measure]" class="form-input" value="'+(data.unit_of_measure||'')+'" placeholder="وحدة"></td>'
+ '<td><input type="number" name="items['+idx+'][estimated_unit_cost]" class="form-input" step="0.01" min="0" value="'+(data.estimated_unit_cost||'')+'" onchange="calcRow('+idx+')" id="cost_'+idx+'"></td>'
+ '<td style="font-weight:700;direction:ltr;text-align:left;" id="lineTotal_'+idx+'">0.00</td>'
+ '<td><select name="items['+idx+'][preferred_supplier_id]" class="form-select">'+supplierOptions+'</select></td>'
+ '<td><button type="button" onclick="this.closest(\'tr\').remove();calcGrandTotal();" style="background:none;border:none;cursor:pointer;color:#DC2626;"><i data-lucide="trash-2" style="width:16px;height:16px;"></i></button></td>';
tbody.appendChild(tr);
if (typeof lucide !== 'undefined') lucide.createIcons();
calcRow(idx);
}
function onItemSelect(sel, idx) {
var opt = sel.options[sel.selectedIndex];
var cost = opt.getAttribute('data-cost') || '';
document.getElementById('cost_' + idx).value = cost;
calcRow(idx);
}
function calcRow(idx) {
var qtyInput = document.querySelector('input[name="items['+idx+'][quantity]"]');
var costInput = document.getElementById('cost_' + idx);
if (!qtyInput || !costInput) return;
var qty = parseFloat(qtyInput.value) || 0;
var cost = parseFloat(costInput.value) || 0;
var total = qty * cost;
var el = document.getElementById('lineTotal_' + idx);
if (el) el.textContent = total.toFixed(2);
calcGrandTotal();
}
function calcGrandTotal() {
var total = 0;
document.querySelectorAll('[id^="lineTotal_"]').forEach(function(el) {
total += parseFloat(el.textContent) || 0;
});
document.getElementById('grandTotal').textContent = total.toFixed(2);
}
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>طلبات الشراء<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/procurement/requisitions/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' => '#6B7280', 'submitted' => '#D97706', 'approved' => '#0D7377',
'rejected' => '#DC2626', 'converted' => '#059669', 'cancelled' => '#9CA3AF',
];
$statusBgs = [
'draft' => '#F3F4F6', 'submitted' => '#FFF7ED', 'approved' => '#F0FDFA',
'rejected' => '#FEE2E2', 'converted' => '#ECFDF5', 'cancelled' => '#F3F4F6',
];
$urgencyColors = ['low' => '#6B7280', 'normal' => '#2563EB', 'high' => '#D97706', 'critical' => '#DC2626'];
$urgencyBgs = ['low' => '#F3F4F6', 'normal' => '#EFF6FF', 'high' => '#FFF7ED', 'critical' => '#FEE2E2'];
?>
<!-- Search & Filters -->
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/procurement/requisitions" 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:140px;">
<label class="form-label" style="font-size:12px;">الأولوية</label>
<select name="urgency" class="form-select">
<option value="">الكل</option>
<?php foreach ($urgencies as $val => $label): ?>
<option value="<?= e($val) ?>" <?= ($filters['urgency'] ?? '') === $val ? 'selected' : '' ?>><?= e($label) ?></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="/procurement/requisitions" class="btn btn-sm btn-outline" style="color:#6B7280;">مسح</a>
</form>
</div>
<!-- Requisitions Table -->
<?php if (!empty($requisitions)): ?>
<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 ($requisitions as $r): ?>
<?php
$st = $r['status'] ?? 'draft';
$stColor = $statusColors[$st] ?? '#6B7280';
$stBg = $statusBgs[$st] ?? '#F3F4F6';
$stLabel = $statuses[$st] ?? $st;
$urg = $r['urgency'] ?? 'normal';
$urgColor = $urgencyColors[$urg] ?? '#6B7280';
$urgBg = $urgencyBgs[$urg] ?? '#F3F4F6';
$urgLabel = $urgencies[$urg] ?? $urg;
?>
<tr>
<td style="font-weight:600;">
<a href="/procurement/requisitions/<?= (int) $r['id'] ?>" style="color:#0D7377;text-decoration:none;">
<code style="font-size:12px;background:#F3F4F6;padding:2px 8px;border-radius:4px;"><?= e($r['pr_number']) ?></code>
</a>
</td>
<td style="font-weight:600;"><?= e($r['requester_name'] ?? '—') ?></td>
<td style="font-size:13px;"><?= e($r['department'] ?? '—') ?></td>
<td>
<span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;background:<?= $urgBg ?>;color:<?= $urgColor ?>;">
<?= e($urgLabel) ?>
</span>
</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($r['estimated_total'] ?? 0) ?></td>
<td style="font-size:12px;"><?= e(substr($r['created_at'] ?? '', 0, 10)) ?></td>
<td>
<a href="/procurement/requisitions/<?= (int) $r['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;">ابدأ بإنشاء طلب شراء جديد.</p>
<a href="/procurement/requisitions/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($pr['pr_number']) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<?php if ($pr['status'] === 'draft'): ?>
<a href="/procurement/requisitions/<?= (int) $pr['id'] ?>/edit" class="btn btn-outline">
<i data-lucide="edit" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> تعديل
</a>
<form method="POST" action="/procurement/requisitions/<?= (int) $pr['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 ($pr['status'] === 'submitted'): ?>
<form method="POST" action="/procurement/requisitions/<?= (int) $pr['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>
<button type="button" class="btn" style="background:#FEE2E2;color:#DC2626;border:none;" onclick="document.getElementById('rejectModal').style.display='flex';">
<i data-lucide="x-circle" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> رفض
</button>
<?php endif; ?>
<?php if ($pr['status'] === 'approved'): ?>
<button type="button" class="btn" style="background:#059669;color:#fff;border:none;" onclick="document.getElementById('convertModal').style.display='flex';">
<i data-lucide="repeat" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> تحويل لأمر شراء
</button>
<?php endif; ?>
<?php if (in_array($pr['status'], ['draft', 'submitted'])): ?>
<form method="POST" action="/procurement/requisitions/<?= (int) $pr['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="ban" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> إلغاء
</button>
</form>
<?php endif; ?>
<a href="/procurement/requisitions" 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' => 'معتمد',
'rejected' => 'مرفوض', 'converted' => 'تم التحويل', 'cancelled' => 'ملغي',
];
$statusColorMap = [
'draft' => ['bg' => '#F3F4F6', 'color' => '#6B7280'],
'submitted' => ['bg' => '#FFF7ED', 'color' => '#D97706'],
'approved' => ['bg' => '#F0FDFA', 'color' => '#0D7377'],
'rejected' => ['bg' => '#FEE2E2', 'color' => '#DC2626'],
'converted' => ['bg' => '#ECFDF5', 'color' => '#059669'],
'cancelled' => ['bg' => '#F3F4F6', 'color' => '#9CA3AF'],
];
$urgencyLabels = ['low' => 'منخفض', 'normal' => 'عادي', 'high' => 'عاجل', 'critical' => 'حرج'];
$urgencyColors = ['low' => '#6B7280', 'normal' => '#2563EB', 'high' => '#D97706', 'critical' => '#DC2626'];
$urgencyBgs = ['low' => '#F3F4F6', 'normal' => '#EFF6FF', 'high' => '#FFF7ED', 'critical' => '#FEE2E2'];
$st = $pr['status'] ?? 'draft';
$stColors = $statusColorMap[$st] ?? $statusColorMap['draft'];
$stLabel = $statusLabels[$st] ?? $st;
$urg = $pr['urgency'] ?? 'normal';
?>
<!-- PR 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($pr['pr_number']) ?></code>
</td>
</tr>
<tr>
<td style="padding:10px 0;color:#6B7280;">مقدم الطلب</td>
<td style="padding:10px 0;font-weight:600;"><?= e($pr['requester_name'] ?? '—') ?></td>
</tr>
<tr>
<td style="padding:10px 0;color:#6B7280;">القسم</td>
<td style="padding:10px 0;"><?= e($pr['department'] ?? '—') ?></td>
</tr>
<tr>
<td style="padding:10px 0;color:#6B7280;">تاريخ الاحتياج</td>
<td style="padding:10px 0;"><?= e($pr['required_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;">
<span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;background:<?= $urgencyBgs[$urg] ?? '#F3F4F6' ?>;color:<?= $urgencyColors[$urg] ?? '#6B7280' ?>;">
<?= e($urgencyLabels[$urg] ?? $urg) ?>
</span>
</td>
</tr>
<tr>
<td style="padding:10px 0;color:#6B7280;">تاريخ الإنشاء</td>
<td style="padding:10px 0;"><?= e($pr['created_at'] ?? '—') ?></td>
</tr>
<?php if (!empty($pr['approved_at'])): ?>
<tr>
<td style="padding:10px 0;color:#6B7280;">تاريخ الاعتماد</td>
<td style="padding:10px 0;"><?= e($pr['approved_at']) ?><?= e($pr['approver_name'] ?? '') ?></td>
</tr>
<?php endif; ?>
<?php if (!empty($pr['rejected_at'])): ?>
<tr>
<td style="padding:10px 0;color:#6B7280;">تاريخ الرفض</td>
<td style="padding:10px 0;"><?= e($pr['rejected_at']) ?><?= e($pr['rejector_name'] ?? '') ?></td>
</tr>
<?php endif; ?>
</table>
</div>
<?php if (!empty($pr['justification'])): ?>
<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($pr['justification']) ?></div>
</div>
<?php endif; ?>
<?php if (!empty($pr['rejection_reason'])): ?>
<div style="margin-top:15px;padding:12px;background:#FEE2E2;border-radius:8px;">
<div style="font-size:12px;color:#DC2626;font-weight:600;margin-bottom:4px;">سبب الرفض</div>
<div style="font-size:14px;color:#7F1D1D;"><?= e($pr['rejection_reason']) ?></div>
</div>
<?php endif; ?>
<?php if (!empty($pr['notes'])): ?>
<div style="margin-top:15px;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($pr['notes']) ?></div>
</div>
<?php endif; ?>
<?php if (!empty($pr['converted_po_id'])): ?>
<div style="margin-top:15px;padding:12px;background:#ECFDF5;border-radius:8px;">
<div style="font-size:12px;color:#059669;font-weight:600;margin-bottom:4px;">أمر الشراء المرتبط</div>
<a href="/inventory/purchase-orders/<?= (int) $pr['converted_po_id'] ?>" style="color:#059669;font-weight:600;">عرض أمر الشراء #<?= (int) $pr['converted_po_id'] ?></a>
</div>
<?php endif; ?>
</div>
</div>
<!-- Estimated 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($pr['estimated_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>الوصف</th>
<th>الكمية</th>
<th>الوحدة</th>
<th>التكلفة التقديرية</th>
<th>الإجمالي</th>
<th>المورد المفضل</th>
</tr>
</thead>
<tbody>
<?php foreach ($items as $item): ?>
<tr>
<td style="font-weight:600;">
<?php if (!empty($item['item_name'])): ?>
<?= e($item['item_name']) ?>
<?php if (!empty($item['sku'])): ?>
<br><code style="font-size:11px;background:#F3F4F6;padding:2px 6px;border-radius:4px;"><?= e($item['sku']) ?></code>
<?php endif; ?>
<?php else: ?>
<span style="color:#6B7280;font-style:italic;">صنف حر</span>
<?php endif; ?>
</td>
<td style="font-size:13px;"><?= e($item['description_ar'] ?? '—') ?></td>
<td style="font-weight:600;direction:ltr;text-align:left;"><?= number_format((float) ($item['quantity'] ?? 0), 3) ?></td>
<td style="font-size:13px;"><?= e($item['unit_of_measure'] ?? '—') ?></td>
<td style="direction:ltr;text-align:left;"><?= !empty($item['estimated_unit_cost']) ? money($item['estimated_unit_cost']) : '—' ?></td>
<td style="font-weight:700;direction:ltr;text-align:left;"><?= !empty($item['estimated_line_total']) ? money($item['estimated_line_total']) : '—' ?></td>
<td style="font-size:13px;"><?= e($item['preferred_supplier_name'] ?? '—') ?></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>
<!-- Reject Modal -->
<?php if ($pr['status'] === 'submitted'): ?>
<div id="rejectModal" style="display:none;position:fixed;inset:0;z-index:999;background:rgba(0,0,0,0.5);align-items:center;justify-content:center;">
<div style="background:#fff;border-radius:12px;padding:25px;width:90%;max-width:480px;">
<h3 style="margin:0 0 15px;color:#DC2626;">رفض طلب الشراء</h3>
<form method="POST" action="/procurement/requisitions/<?= (int) $pr['id'] ?>/reject">
<?= csrf_field() ?>
<div style="margin-bottom:15px;">
<label class="form-label">سبب الرفض <span style="color:#DC2626;">*</span></label>
<textarea name="rejection_reason" class="form-input" rows="3" required placeholder="أدخل سبب الرفض..."></textarea>
</div>
<div style="display:flex;gap:10px;justify-content:flex-end;">
<button type="button" class="btn btn-outline" onclick="document.getElementById('rejectModal').style.display='none';">إلغاء</button>
<button type="submit" class="btn" style="background:#DC2626;color:#fff;border:none;">تأكيد الرفض</button>
</div>
</form>
</div>
</div>
<?php endif; ?>
<!-- Convert to PO Modal -->
<?php if ($pr['status'] === 'approved'): ?>
<div id="convertModal" style="display:none;position:fixed;inset:0;z-index:999;background:rgba(0,0,0,0.5);align-items:center;justify-content:center;">
<div style="background:#fff;border-radius:12px;padding:25px;width:90%;max-width:480px;">
<h3 style="margin:0 0 15px;color:#059669;">تحويل لأمر شراء</h3>
<form method="POST" action="/procurement/requisitions/<?= (int) $pr['id'] ?>/convert">
<?= csrf_field() ?>
<div style="margin-bottom:15px;">
<label class="form-label">المورد <span style="color:#DC2626;">*</span></label>
<select name="supplier_id" class="form-select" required id="convertSupplier"></select>
</div>
<div style="margin-bottom:15px;">
<label class="form-label">المستودع <span style="color:#DC2626;">*</span></label>
<select name="warehouse_id" class="form-select" required id="convertWarehouse"></select>
</div>
<div style="margin-bottom:15px;">
<label class="form-label">تاريخ التسليم المتوقع</label>
<input type="date" name="expected_delivery_date" class="form-input" value="<?= e($pr['required_date'] ?? '') ?>">
</div>
<div style="display:flex;gap:10px;justify-content:flex-end;">
<button type="button" class="btn btn-outline" onclick="document.getElementById('convertModal').style.display='none';">إلغاء</button>
<button type="submit" class="btn" style="background:#059669;color:#fff;border:none;">تحويل</button>
</div>
</form>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Load suppliers and warehouses via inline data
fetch('/api/inventory/suppliers').then(r => r.json()).then(data => {
let sel = document.getElementById('convertSupplier');
sel.innerHTML = '<option value="">-- اختر المورد --</option>';
(data.data || data).forEach(s => {
sel.innerHTML += '<option value="'+s.id+'">'+s.name_ar+'</option>';
});
}).catch(() => {});
fetch('/api/inventory/warehouses').then(r => r.json()).then(data => {
let sel = document.getElementById('convertWarehouse');
sel.innerHTML = '<option value="">-- اختر المستودع --</option>';
(data.data || data).forEach(w => {
sel.innerHTML += '<option value="'+w.id+'">'+w.name_ar+'</option>';
});
}).catch(() => {});
});
</script>
<?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'); ?><?= isset($rtv) ? 'تعديل مرتجع مورد' : 'مرتجع مورد جديد' ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/procurement/rtv" 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 $isEdit = isset($rtv); $action = $isEdit ? '/procurement/rtv/' . (int) $rtv['id'] . '/update' : '/procurement/rtv'; ?>
<form method="POST" action="<?= $action ?>">
<?= csrf_field() ?>
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;"><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:15px;">
<div><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 $s): ?><option value="<?= (int) $s['id'] ?>" <?= ($rtv['supplier_id'] ?? '') == $s['id'] ? 'selected' : '' ?>><?= e($s['name_ar']) ?></option><?php endforeach; ?></select></div>
<div><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 $w): ?><option value="<?= (int) $w['id'] ?>" <?= ($rtv['warehouse_id'] ?? '') == $w['id'] ? 'selected' : '' ?>><?= e($w['name_ar']) ?></option><?php endforeach; ?></select></div>
<div><label class="form-label">تاريخ الإرجاع</label><input type="date" name="return_date" class="form-input" value="<?= e($rtv['return_date'] ?? date('Y-m-d')) ?>"></div>
<div><label class="form-label">سبب الإرجاع <span style="color:#DC2626;">*</span></label><input type="text" name="reason" class="form-input" value="<?= e($rtv['reason'] ?? '') ?>" required></div>
</div>
<div style="margin-top:15px;"><label class="form-label">ملاحظات</label><textarea name="notes" class="form-input" rows="2"><?= e($rtv['notes'] ?? '') ?></textarea></div>
</div>
</div>
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;justify-content:space-between;align-items:center;">
<h3 style="margin:0;color:#D97706;font-size:15px;">الأصناف</h3>
<button type="button" class="btn btn-sm btn-primary" onclick="addRtvRow()"><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"><thead><tr><th>الصنف</th><th style="width:100px;">الكمية</th><th style="width:120px;">سعر الوحدة</th><th style="width:120px;">الإجمالي</th><th>السبب</th><th style="width:50px;"></th></tr></thead>
<tbody id="rtvBody"></tbody>
<tfoot><tr><td colspan="3" style="text-align:left;font-weight:700;">الإجمالي</td><td style="font-weight:800;direction:ltr;text-align:left;" id="rtvTotal">0.00</td><td colspan="2"></td></tr></tfoot>
</table>
</div>
</div>
<div style="text-align:left;"><button type="submit" class="btn btn-primary" style="padding:10px 30px;"><i data-lucide="save" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> حفظ المرتجع</button></div>
</form>
<script>
var invItems = <?= json_encode($items ?? []) ?>;
var rtvIdx = 0;
function addRtvRow(d) {
d = d || {};
var i = rtvIdx++;
var opts = '<option value="">-- اختر --</option>';
invItems.forEach(function(it) { var s = (d.item_id && d.item_id == it.id) ? ' selected' : ''; opts += '<option value="'+it.id+'" data-cost="'+(it.cost_price||0)+'"'+s+'>'+it.name_ar+'</option>'; });
var tr = document.createElement('tr');
tr.innerHTML = '<td><select name="items['+i+'][item_id]" class="form-select" required onchange="var o=this.options[this.selectedIndex];document.getElementById(\'rc_'+i+'\').value=o.getAttribute(\'data-cost\')||0;calcRtv('+i+');">'+opts+'</select></td>'
+'<td><input type="number" name="items['+i+'][quantity]" class="form-input" step="0.001" min="0.001" value="'+(d.quantity||'')+'" onchange="calcRtv('+i+')" required></td>'
+'<td><input type="number" name="items['+i+'][unit_cost]" class="form-input" step="0.01" min="0" value="'+(d.unit_cost||'')+'" id="rc_'+i+'" onchange="calcRtv('+i+')"></td>'
+'<td style="font-weight:700;direction:ltr;text-align:left;" id="rl_'+i+'">0.00</td>'
+'<td><input type="text" name="items['+i+'][reason]" class="form-input" value="'+(d.reason||'')+'" placeholder="سبب"></td>'
+'<td><button type="button" onclick="this.closest(\'tr\').remove();calcRtvTotal();" style="background:none;border:none;cursor:pointer;color:#DC2626;"><i data-lucide="trash-2" style="width:16px;height:16px;"></i></button></td>';
document.getElementById('rtvBody').appendChild(tr);
if (typeof lucide !== 'undefined') lucide.createIcons();
calcRtv(i);
}
function calcRtv(i) {
var q = parseFloat(document.querySelector('[name="items['+i+'][quantity]"]').value) || 0;
var c = parseFloat(document.getElementById('rc_'+i).value) || 0;
document.getElementById('rl_'+i).textContent = (q*c).toFixed(2);
calcRtvTotal();
}
function calcRtvTotal() {
var t = 0; document.querySelectorAll('[id^="rl_"]').forEach(function(e) { t += parseFloat(e.textContent) || 0; });
document.getElementById('rtvTotal').textContent = t.toFixed(2);
}
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
<?php if ($isEdit && !empty($rtvItems)): ?><?= json_encode($rtvItems) ?>.forEach(function(it) { addRtvRow(it); });<?php else: ?>addRtvRow();<?php endif; ?>
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>مرتجعات الموردين<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/procurement/rtv/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' => '#6B7280', 'submitted' => '#D97706', 'approved' => '#0D7377', 'shipped' => '#2563EB', 'completed' => '#059669', 'cancelled' => '#DC2626'];
$statusBgs = ['draft' => '#F3F4F6', 'submitted' => '#FFF7ED', 'approved' => '#F0FDFA', 'shipped' => '#EFF6FF', 'completed' => '#ECFDF5', 'cancelled' => '#FEE2E2'];
?>
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/procurement/rtv" 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><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="/procurement/rtv" class="btn btn-sm btn-outline" style="color:#6B7280;">مسح</a>
</form>
</div>
<?php if (!empty($rtvs)): ?>
<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 ($rtvs as $r): ?>
<?php $st = $r['status'] ?? 'draft'; ?>
<tr>
<td style="font-weight:600;"><a href="/procurement/rtv/<?= (int) $r['id'] ?>" style="color:#0D7377;text-decoration:none;"><code style="font-size:12px;background:#F3F4F6;padding:2px 8px;border-radius:4px;"><?= e($r['rtv_number']) ?></code></a></td>
<td style="font-weight:600;"><?= e($r['supplier_name'] ?? '—') ?></td>
<td style="font-size:13px;"><?= e($r['warehouse_name'] ?? '—') ?></td>
<td><?php if (!empty($r['grn_number'])): ?><code style="font-size:11px;background:#F3F4F6;padding:2px 6px;border-radius:4px;"><?= e($r['grn_number']) ?></code><?php else: ?><?php endif; ?></td>
<td><span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;background:<?= $statusBgs[$st] ?? '#F3F4F6' ?>;color:<?= $statusColors[$st] ?? '#6B7280' ?>;"><?= e($statuses[$st] ?? $st) ?></span></td>
<td style="font-weight:700;direction:ltr;text-align:left;"><?= money($r['total_amount'] ?? 0) ?></td>
<td style="font-size:12px;"><?= e($r['return_date'] ?? '') ?></td>
<td><a href="/procurement/rtv/<?= (int) $r['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="undo-2" 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;">ابدأ بإنشاء مرتجع مورد جديد.</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($rtv['rtv_number']) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<?php if ($rtv['status'] === 'draft'): ?>
<form method="POST" action="/procurement/rtv/<?= (int) $rtv['id'] ?>/submit" style="display:inline;"><?= csrf_field() ?><button type="submit" class="btn" style="background:#D97706;color:#fff;border:none;"><i data-lucide="send" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> تقديم</button></form>
<?php endif; ?>
<?php if ($rtv['status'] === 'submitted'): ?>
<form method="POST" action="/procurement/rtv/<?= (int) $rtv['id'] ?>/approve" style="display:inline;"><?= csrf_field() ?><button type="submit" class="btn" style="background:#0D7377;color:#fff;border:none;"><i data-lucide="check-circle" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> اعتماد</button></form>
<?php endif; ?>
<?php if ($rtv['status'] === 'approved'): ?>
<form method="POST" action="/procurement/rtv/<?= (int) $rtv['id'] ?>/ship" style="display:inline;"><?= csrf_field() ?><button type="submit" class="btn" style="background:#2563EB;color:#fff;border:none;"><i data-lucide="truck" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> شحن</button></form>
<?php endif; ?>
<?php if ($rtv['status'] === 'shipped'): ?>
<form method="POST" action="/procurement/rtv/<?= (int) $rtv['id'] ?>/complete" 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 (in_array($rtv['status'], ['draft', 'submitted'])): ?>
<form method="POST" action="/procurement/rtv/<?= (int) $rtv['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="/procurement/rtv" 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' => 'معتمد', 'shipped' => 'تم الشحن', 'completed' => 'مكتمل', 'cancelled' => 'ملغي'];
$statusColorMap = ['draft' => ['bg' => '#F3F4F6', 'color' => '#6B7280'], 'submitted' => ['bg' => '#FFF7ED', 'color' => '#D97706'], 'approved' => ['bg' => '#F0FDFA', 'color' => '#0D7377'], 'shipped' => ['bg' => '#EFF6FF', 'color' => '#2563EB'], 'completed' => ['bg' => '#ECFDF5', 'color' => '#059669'], 'cancelled' => ['bg' => '#FEE2E2', 'color' => '#DC2626']];
$st = $rtv['status'] ?? 'draft';
$stColors = $statusColorMap[$st] ?? $statusColorMap['draft'];
?>
<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="undo-2" 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="background:#F3F4F6;padding:2px 8px;border-radius:4px;"><?= e($rtv['rtv_number']) ?></code></td></tr>
<tr><td style="padding:10px 0;color:#6B7280;">المورد</td><td style="padding:10px 0;font-weight:600;"><?= e($rtv['supplier_name'] ?? '') ?></td></tr>
<tr><td style="padding:10px 0;color:#6B7280;">المستودع</td><td style="padding:10px 0;"><?= e($rtv['warehouse_name'] ?? '') ?></td></tr>
<tr><td style="padding:10px 0;color:#6B7280;">إذن الاستلام</td><td style="padding:10px 0;"><?php if (!empty($rtv['grn_number'])): ?><a href="/procurement/grn/<?= (int) $rtv['grn_id'] ?>" style="color:#0D7377;"><?= e($rtv['grn_number']) ?></a><?php else: ?><?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;"><span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;background:<?= $stColors['bg'] ?>;color:<?= $stColors['color'] ?>;"><?= e($statusLabels[$st] ?? $st) ?></span></td></tr>
<tr><td style="padding:10px 0;color:#6B7280;">تاريخ الإرجاع</td><td style="padding:10px 0;"><?= e($rtv['return_date'] ?? '—') ?></td></tr>
<tr><td style="padding:10px 0;color:#6B7280;">تاريخ الإنشاء</td><td style="padding:10px 0;"><?= e($rtv['created_at'] ?? '—') ?></td></tr>
</table>
</div>
<?php if (!empty($rtv['reason'])): ?>
<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($rtv['reason']) ?></div>
</div>
<?php endif; ?>
</div>
</div>
<div class="card" style="margin-bottom:20px;padding:20px;text-align:center;background:linear-gradient(135deg, #DC262610, #DC262620);">
<div style="font-size:14px;color:#6B7280;margin-bottom:6px;">قيمة المرتجع</div>
<div style="font-size:36px;font-weight:800;color:#DC2626;direction:ltr;"><?= money($rtv['total_amount'] ?? 0) ?></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="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>سعر الوحدة</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'] ?? '') ?><?php if (!empty($item['sku'])): ?><br><code style="font-size:11px;background:#F3F4F6;padding:2px 6px;border-radius:4px;"><?= e($item['sku']) ?></code><?php endif; ?></td>
<td style="direction:ltr;text-align:left;font-weight:600;"><?= number_format((float) ($item['quantity'] ?? 0), 3) ?></td>
<td style="direction:ltr;text-align:left;"><?= money($item['unit_cost'] ?? 0) ?></td>
<td style="font-weight:700;direction:ltr;text-align:left;"><?= money($item['line_total'] ?? 0) ?></td>
<td style="font-size:12px;"><?= e($item['batch_number'] ?? '—') ?></td>
<td style="font-size:12px;"><?= e($item['reason'] ?? '—') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div style="padding:40px 20px;text-align:center;color:#6B7280;"><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
declare(strict_types=1);
use App\Core\Registries\MenuRegistry;
use App\Core\Registries\PermissionRegistry;
// ────────────────────────────────────────────────────────────
// Procurement — Permissions
// ────────────────────────────────────────────────────────────
PermissionRegistry::register('procurement', [
'procurement.dashboard' => ['ar' => 'لوحة تحكم المشتريات', 'en' => 'Procurement Dashboard'],
'procurement.pr.view' => ['ar' => 'عرض طلبات الشراء', 'en' => 'View Purchase Requisitions'],
'procurement.pr.create' => ['ar' => 'إنشاء طلبات شراء', 'en' => 'Create Purchase Requisitions'],
'procurement.pr.approve' => ['ar' => 'اعتماد طلبات الشراء', 'en' => 'Approve Purchase Requisitions'],
'procurement.pr.convert' => ['ar' => 'تحويل طلب شراء لأمر شراء', 'en' => 'Convert PR to PO'],
'procurement.grn.view' => ['ar' => 'عرض أذونات الاستلام', 'en' => 'View Goods Received Notes'],
'procurement.grn.create' => ['ar' => 'إنشاء إذن استلام', 'en' => 'Create Goods Received Notes'],
'procurement.grn.inspect' => ['ar' => 'فحص واعتماد الاستلام', 'en' => 'Inspect & Accept GRN'],
'procurement.invoice.view' => ['ar' => 'عرض فواتير الموردين', 'en' => 'View Vendor Invoices'],
'procurement.invoice.create' => ['ar' => 'إنشاء فاتورة مورد', 'en' => 'Create Vendor Invoices'],
'procurement.invoice.approve'=> ['ar' => 'اعتماد فواتير الموردين', 'en' => 'Approve Vendor Invoices'],
'procurement.payment.view' => ['ar' => 'عرض مدفوعات الموردين', 'en' => 'View Vendor Payments'],
'procurement.payment.create' => ['ar' => 'إنشاء دفعة مورد', 'en' => 'Create Vendor Payments'],
'procurement.payment.approve'=> ['ar' => 'اعتماد مدفوعات الموردين', 'en' => 'Approve Vendor Payments'],
'procurement.rtv.view' => ['ar' => 'عرض مرتجعات الموردين', 'en' => 'View Returns to Vendor'],
'procurement.rtv.create' => ['ar' => 'إنشاء مرتجع مورد', 'en' => 'Create Return to Vendor'],
'procurement.rtv.approve' => ['ar' => 'اعتماد مرتجعات الموردين', 'en' => 'Approve Returns to Vendor'],
'procurement.rtv.manage' => ['ar' => 'إدارة شحن وإتمام المرتجعات', 'en' => 'Manage RTV Ship & Complete'],
'procurement.report' => ['ar' => 'تقارير المشتريات', 'en' => 'Procurement Reports'],
]);
// ────────────────────────────────────────────────────────────
// Procurement — Sidebar menu (order 710, after Inventory at 700)
// ────────────────────────────────────────────────────────────
MenuRegistry::register('procurement', [
'label_ar' => 'المشتريات',
'label_en' => 'Procurement',
'icon' => 'shopping-cart',
'route' => '/procurement',
'permission' => 'procurement.dashboard',
'parent' => null,
'order' => 710,
'children' => [
['label_ar' => 'لوحة التحكم', 'label_en' => 'Dashboard', 'route' => '/procurement', 'permission' => 'procurement.dashboard', 'order' => 1],
['label_ar' => 'طلبات الشراء', 'label_en' => 'Purchase Requisitions', 'route' => '/procurement/requisitions', 'permission' => 'procurement.pr.view', 'order' => 2],
['label_ar' => 'أذونات الاستلام', 'label_en' => 'Goods Received Notes', 'route' => '/procurement/grn', 'permission' => 'procurement.grn.view', 'order' => 3],
['label_ar' => 'فواتير الموردين', 'label_en' => 'Vendor Invoices', 'route' => '/procurement/invoices', 'permission' => 'procurement.invoice.view', 'order' => 4],
['label_ar' => 'مدفوعات الموردين', 'label_en' => 'Vendor Payments', 'route' => '/procurement/payments', 'permission' => 'procurement.payment.view', 'order' => 5],
['label_ar' => 'مرتجعات الموردين', 'label_en' => 'Returns to Vendor', 'route' => '/procurement/rtv', 'permission' => 'procurement.rtv.view', 'order' => 6],
['label_ar' => 'تقارير المشتريات', 'label_en' => 'Procurement Reports', 'route' => '/procurement/reports/purchase-volume', 'permission' => 'procurement.report', 'order' => 7],
],
]);
<?php
declare(strict_types=1);
namespace CronJobs;
use App\Core\Database;
use App\Core\Logger;
use App\Core\EventBus;
/**
* Daily: scans vendor invoices and AP records past due date,
* updates status to 'overdue' and dispatches alerts.
*/
class OverdueInvoiceAlertJob
{
private Database $db;
public function __construct(Database $db) { $this->db = $db; }
public function shouldRun(): bool
{
return true; // Runs daily
}
public function run(): array
{
$today = date('Y-m-d');
$overdueCount = 0;
// Update AP records that are past due
$overdueAP = $this->db->select(
"SELECT ap.*, s.`name_ar` as supplier_name
FROM `accounts_payable` ap
JOIN `suppliers` s ON s.`id` = ap.`supplier_id`
WHERE ap.`due_date` < ?
AND ap.`status` IN ('pending', 'partial')
AND ap.`is_archived` = 0",
[$today]
);
foreach ($overdueAP as $ap) {
// Update status to overdue
$this->db->update('accounts_payable', [
'status' => 'overdue',
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ? AND `status` IN (\'pending\', \'partial\')', [(int) $ap['id']]);
EventBus::dispatch('procurement.invoice_overdue', [
'ap_id' => (int) $ap['id'],
'supplier_id' => (int) $ap['supplier_id'],
'supplier_name' => $ap['supplier_name'] ?? '',
'invoice_number' => $ap['invoice_number'] ?? '',
'due_date' => $ap['due_date'],
'balance' => (string) $ap['balance'],
'days_overdue' => (int) ((strtotime($today) - strtotime($ap['due_date'])) / 86400),
]);
$overdueCount++;
}
// Also update vendor_invoices that are approved but past due
$this->db->query(
"UPDATE `vendor_invoices` SET `status` = 'overdue'
WHERE `due_date` < ? AND `status` IN ('approved', 'partial_paid') AND `is_archived` = 0",
[$today]
);
if ($overdueCount > 0) {
Logger::info("Overdue invoice alert: {$overdueCount} AP record(s) marked overdue");
}
return ['overdue_invoices' => $overdueCount];
}
}
<?php
declare(strict_types=1);
namespace CronJobs;
use App\Core\Database;
use App\Core\Logger;
use App\Core\EventBus;
/**
* Daily: scans items below reorder point and dispatches alerts.
* Complements LowStockAlertJob with procurement-specific actions.
*/
class ReorderPointAlertJob
{
private Database $db;
public function __construct(Database $db) { $this->db = $db; }
public function shouldRun(): bool
{
return true; // Runs daily
}
public function run(): array
{
// Find items where current stock is at or below reorder point
// and there is no open PO or PR already covering them
$items = $this->db->select(
"SELECT iws.`item_id`, iws.`warehouse_id`, iws.`quantity`, iws.`min_level`,
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` AND i.`is_archived` = 0
JOIN `warehouses` w ON w.`id` = iws.`warehouse_id` AND w.`is_archived` = 0
WHERE iws.`quantity` <= iws.`min_level`
AND iws.`min_level` IS NOT NULL
AND iws.`min_level` > 0
AND NOT EXISTS (
SELECT 1 FROM `purchase_order_items` poi
JOIN `purchase_orders` po ON po.`id` = poi.`purchase_order_id`
WHERE poi.`item_id` = iws.`item_id`
AND po.`status` IN ('draft', 'submitted', 'approved')
)
ORDER BY (iws.`quantity` - iws.`min_level`) ASC"
);
$count = count($items);
foreach ($items as $item) {
EventBus::dispatch('procurement.reorder_point_alert', [
'item_id' => (int) $item['item_id'],
'warehouse_id' => (int) $item['warehouse_id'],
'item_name' => $item['name_ar'] ?? '',
'sku' => $item['sku'] ?? '',
'warehouse_name' => $item['warehouse_name'] ?? '',
'current_qty' => (string) $item['quantity'],
'min_level' => (string) $item['min_level'],
'deficit' => bcsub((string) $item['min_level'], (string) $item['quantity'], 3),
]);
}
if ($count > 0) {
Logger::info("Reorder point alert: {$count} item(s) need replenishment (no open PO)");
}
return ['items_below_reorder' => $count];
}
}
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `purchase_requisitions` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`pr_number` VARCHAR(50) NOT NULL,
`requested_by` BIGINT UNSIGNED NOT NULL COMMENT 'FK employees.id',
`department` VARCHAR(100) NULL,
`status` VARCHAR(20) NOT NULL DEFAULT 'draft' COMMENT 'draft,submitted,approved,rejected,converted,cancelled',
`urgency` ENUM('low','normal','high','critical') NOT NULL DEFAULT 'normal',
`required_date` DATE NULL,
`justification` TEXT NULL,
`estimated_total` DECIMAL(18,2) NOT NULL DEFAULT 0.00,
`approved_by` BIGINT UNSIGNED NULL,
`approved_at` DATETIME NULL,
`rejected_by` BIGINT UNSIGNED NULL,
`rejected_at` DATETIME NULL,
`rejection_reason` TEXT NULL,
`converted_po_id` BIGINT UNSIGNED NULL COMMENT 'FK purchase_orders.id',
`notes` TEXT NULL,
`branch_id` BIGINT UNSIGNED NULL,
`is_archived` TINYINT(1) NOT NULL DEFAULT 0,
`archived_at` DATETIME NULL,
`archived_by` BIGINT UNSIGNED NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
`updated_by` BIGINT UNSIGNED NULL,
UNIQUE INDEX `uq_pr_number` (`pr_number`),
INDEX `idx_pr_status` (`status`),
INDEX `idx_pr_requested_by` (`requested_by`),
INDEX `idx_pr_branch` (`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `purchase_requisitions`",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `purchase_requisition_items` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`requisition_id` BIGINT UNSIGNED NOT NULL,
`item_id` BIGINT UNSIGNED NULL COMMENT 'FK inventory_items.id — NULL for free-text items',
`description_ar` VARCHAR(300) NULL,
`description_en` VARCHAR(300) NULL,
`quantity` DECIMAL(15,3) NOT NULL,
`unit_of_measure` VARCHAR(50) NULL,
`estimated_unit_cost` DECIMAL(18,2) NULL,
`estimated_line_total` DECIMAL(18,2) NULL,
`specifications` TEXT NULL,
`preferred_supplier_id` BIGINT UNSIGNED NULL COMMENT 'FK suppliers.id',
INDEX `idx_pri_requisition` (`requisition_id`),
INDEX `idx_pri_item` (`item_id`),
CONSTRAINT `fk_pri_requisition` FOREIGN KEY (`requisition_id`) REFERENCES `purchase_requisitions`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `purchase_requisition_items`",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `goods_received_notes` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`grn_number` VARCHAR(50) NOT NULL,
`purchase_order_id` BIGINT UNSIGNED NOT NULL COMMENT 'FK purchase_orders.id',
`supplier_id` BIGINT UNSIGNED NOT NULL COMMENT 'FK suppliers.id',
`warehouse_id` BIGINT UNSIGNED NOT NULL COMMENT 'FK warehouses.id',
`received_by` BIGINT UNSIGNED NULL COMMENT 'FK employees.id',
`received_date` DATE NOT NULL,
`status` VARCHAR(20) NOT NULL DEFAULT 'draft' COMMENT 'draft,inspecting,accepted,partial_accept,cancelled',
`total_received_value` DECIMAL(18,2) NOT NULL DEFAULT 0.00,
`total_accepted_value` DECIMAL(18,2) NOT NULL DEFAULT 0.00,
`inspection_notes` TEXT NULL,
`notes` TEXT NULL,
`branch_id` BIGINT UNSIGNED NULL,
`is_archived` TINYINT(1) NOT NULL DEFAULT 0,
`archived_at` DATETIME NULL,
`archived_by` BIGINT UNSIGNED NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
`updated_by` BIGINT UNSIGNED NULL,
UNIQUE INDEX `uq_grn_number` (`grn_number`),
INDEX `idx_grn_po` (`purchase_order_id`),
INDEX `idx_grn_supplier` (`supplier_id`),
INDEX `idx_grn_warehouse` (`warehouse_id`),
INDEX `idx_grn_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `goods_received_notes`",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `grn_items` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`grn_id` BIGINT UNSIGNED NOT NULL,
`po_item_id` BIGINT UNSIGNED NOT NULL COMMENT 'FK purchase_order_items.id',
`item_id` BIGINT UNSIGNED NOT NULL COMMENT 'FK inventory_items.id',
`quantity_received` DECIMAL(15,3) NOT NULL,
`quantity_accepted` DECIMAL(15,3) NOT NULL DEFAULT 0.000,
`quantity_rejected` DECIMAL(15,3) NOT NULL DEFAULT 0.000,
`rejection_reason` TEXT NULL,
`batch_number` VARCHAR(100) NULL,
`expiry_date` DATE NULL,
`unit_cost` DECIMAL(18,2) NOT NULL,
`line_total` DECIMAL(18,2) NOT NULL,
`notes` TEXT NULL,
INDEX `idx_grni_grn` (`grn_id`),
INDEX `idx_grni_po_item` (`po_item_id`),
INDEX `idx_grni_item` (`item_id`),
CONSTRAINT `fk_grni_grn` FOREIGN KEY (`grn_id`) REFERENCES `goods_received_notes`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `grn_items`",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `vendor_invoices` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`invoice_number` VARCHAR(100) NOT NULL COMMENT 'Supplier invoice number',
`internal_number` VARCHAR(50) NOT NULL COMMENT 'System-generated VINV-YYYY-NNNNNN',
`supplier_id` BIGINT UNSIGNED NOT NULL COMMENT 'FK suppliers.id',
`purchase_order_id` BIGINT UNSIGNED NULL COMMENT 'FK purchase_orders.id',
`grn_id` BIGINT UNSIGNED NULL COMMENT 'FK goods_received_notes.id',
`invoice_date` DATE NOT NULL,
`due_date` DATE NULL,
`subtotal` DECIMAL(18,2) NOT NULL DEFAULT 0.00,
`tax_amount` DECIMAL(18,2) NOT NULL DEFAULT 0.00,
`discount_amount` DECIMAL(18,2) NOT NULL DEFAULT 0.00,
`total_amount` DECIMAL(18,2) NOT NULL DEFAULT 0.00,
`currency` VARCHAR(3) NOT NULL DEFAULT 'EGP',
`status` VARCHAR(20) NOT NULL DEFAULT 'draft' COMMENT 'draft,verified,approved,paid,partial_paid,cancelled',
`match_status` VARCHAR(20) NULL COMMENT 'unmatched,matched,discrepancy,tolerance_pass',
`match_notes` TEXT NULL,
`journal_entry_id` BIGINT UNSIGNED NULL COMMENT 'FK journal_entries.id',
`ap_record_id` BIGINT UNSIGNED NULL COMMENT 'FK accounts_payable.id',
`notes` TEXT NULL,
`branch_id` BIGINT UNSIGNED NULL,
`is_archived` TINYINT(1) NOT NULL DEFAULT 0,
`archived_at` DATETIME NULL,
`archived_by` BIGINT UNSIGNED NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
`updated_by` BIGINT UNSIGNED NULL,
UNIQUE INDEX `uq_vinv_internal` (`internal_number`),
INDEX `idx_vinv_supplier` (`supplier_id`),
INDEX `idx_vinv_po` (`purchase_order_id`),
INDEX `idx_vinv_grn` (`grn_id`),
INDEX `idx_vinv_status` (`status`),
INDEX `idx_vinv_match` (`match_status`),
INDEX `idx_vinv_due_date` (`due_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `vendor_invoices`",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `vendor_invoice_items` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`invoice_id` BIGINT UNSIGNED NOT NULL,
`po_item_id` BIGINT UNSIGNED NULL COMMENT 'FK purchase_order_items.id',
`item_id` BIGINT UNSIGNED NULL COMMENT 'FK inventory_items.id',
`description` VARCHAR(300) NULL,
`quantity` DECIMAL(15,3) NOT NULL,
`unit_cost` DECIMAL(18,2) NOT NULL,
`tax_rate` DECIMAL(5,2) NOT NULL DEFAULT 0.00,
`tax_amount` DECIMAL(18,2) NOT NULL DEFAULT 0.00,
`discount_amount` DECIMAL(18,2) NOT NULL DEFAULT 0.00,
`line_total` DECIMAL(18,2) NOT NULL,
INDEX `idx_vii_invoice` (`invoice_id`),
INDEX `idx_vii_item` (`item_id`),
CONSTRAINT `fk_vii_invoice` FOREIGN KEY (`invoice_id`) REFERENCES `vendor_invoices`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `vendor_invoice_items`",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `vendor_payments` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`payment_number` VARCHAR(50) NOT NULL COMMENT 'VPAY-YYYY-NNNNNN',
`supplier_id` BIGINT UNSIGNED NOT NULL COMMENT 'FK suppliers.id',
`invoice_id` BIGINT UNSIGNED NULL COMMENT 'FK vendor_invoices.id',
`amount` DECIMAL(18,2) NOT NULL,
`currency` VARCHAR(3) NOT NULL DEFAULT 'EGP',
`payment_method` VARCHAR(30) NOT NULL COMMENT 'cash,bank_transfer,check,wire',
`bank_account_id` BIGINT UNSIGNED NULL COMMENT 'FK bank_accounts.id',
`check_number` VARCHAR(50) NULL,
`reference_number` VARCHAR(100) NULL,
`payment_date` DATE NOT NULL,
`status` VARCHAR(20) NOT NULL DEFAULT 'draft' COMMENT 'draft,approved,completed,voided',
`approved_by` BIGINT UNSIGNED NULL,
`approved_at` DATETIME NULL,
`completed_by` BIGINT UNSIGNED NULL,
`completed_at` DATETIME NULL,
`voided_by` BIGINT UNSIGNED NULL,
`voided_at` DATETIME NULL,
`void_reason` TEXT NULL,
`journal_entry_id` BIGINT UNSIGNED NULL COMMENT 'FK journal_entries.id',
`notes` TEXT NULL,
`branch_id` BIGINT UNSIGNED NULL,
`is_archived` TINYINT(1) NOT NULL DEFAULT 0,
`archived_at` DATETIME NULL,
`archived_by` BIGINT UNSIGNED NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
`updated_by` BIGINT UNSIGNED NULL,
UNIQUE INDEX `uq_vpay_number` (`payment_number`),
INDEX `idx_vpay_supplier` (`supplier_id`),
INDEX `idx_vpay_invoice` (`invoice_id`),
INDEX `idx_vpay_status` (`status`),
INDEX `idx_vpay_date` (`payment_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `vendor_payments`",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `return_to_vendor` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`rtv_number` VARCHAR(50) NOT NULL COMMENT 'RTV-YYYY-NNNNNN',
`supplier_id` BIGINT UNSIGNED NOT NULL COMMENT 'FK suppliers.id',
`grn_id` BIGINT UNSIGNED NULL COMMENT 'FK goods_received_notes.id',
`purchase_order_id` BIGINT UNSIGNED NULL COMMENT 'FK purchase_orders.id',
`warehouse_id` BIGINT UNSIGNED NOT NULL COMMENT 'FK warehouses.id',
`reason` TEXT NOT NULL,
`return_date` DATE NOT NULL,
`status` VARCHAR(20) NOT NULL DEFAULT 'draft' COMMENT 'draft,submitted,approved,shipped,completed,cancelled',
`total_amount` DECIMAL(18,2) NOT NULL DEFAULT 0.00,
`debit_note_number` VARCHAR(100) NULL,
`debit_note_amount` DECIMAL(18,2) NULL,
`debit_note_date` DATE NULL,
`journal_entry_id` BIGINT UNSIGNED NULL COMMENT 'FK journal_entries.id',
`approved_by` BIGINT UNSIGNED NULL,
`approved_at` DATETIME NULL,
`shipped_by` BIGINT UNSIGNED NULL,
`shipped_at` DATETIME NULL,
`completed_by` BIGINT UNSIGNED NULL,
`completed_at` DATETIME NULL,
`notes` TEXT NULL,
`branch_id` BIGINT UNSIGNED NULL,
`is_archived` TINYINT(1) NOT NULL DEFAULT 0,
`archived_at` DATETIME NULL,
`archived_by` BIGINT UNSIGNED NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
`updated_by` BIGINT UNSIGNED NULL,
UNIQUE INDEX `uq_rtv_number` (`rtv_number`),
INDEX `idx_rtv_supplier` (`supplier_id`),
INDEX `idx_rtv_grn` (`grn_id`),
INDEX `idx_rtv_warehouse` (`warehouse_id`),
INDEX `idx_rtv_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `return_to_vendor`",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `rtv_items` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`rtv_id` BIGINT UNSIGNED NOT NULL,
`item_id` BIGINT UNSIGNED NOT NULL COMMENT 'FK inventory_items.id',
`grn_item_id` BIGINT UNSIGNED NULL COMMENT 'FK grn_items.id',
`quantity` DECIMAL(15,3) NOT NULL,
`unit_cost` DECIMAL(18,2) NOT NULL,
`line_total` DECIMAL(18,2) NOT NULL,
`batch_number` VARCHAR(100) NULL,
`reason` TEXT NULL,
INDEX `idx_rtvi_rtv` (`rtv_id`),
INDEX `idx_rtvi_item` (`item_id`),
INDEX `idx_rtvi_grn_item` (`grn_item_id`),
CONSTRAINT `fk_rtvi_rtv` FOREIGN KEY (`rtv_id`) REFERENCES `return_to_vendor`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `rtv_items`",
];
<?php
declare(strict_types=1);
return [
'up' => "
ALTER TABLE `purchase_orders`
ADD COLUMN `requisition_id` BIGINT UNSIGNED NULL COMMENT 'FK purchase_requisitions.id' AFTER `id`,
ADD INDEX `idx_po_requisition` (`requisition_id`)
",
'down' => "
ALTER TABLE `purchase_orders`
DROP INDEX `idx_po_requisition`,
DROP COLUMN `requisition_id`
",
];
<?php
declare(strict_types=1);
return [
'up' => "
ALTER TABLE `suppliers`
ADD COLUMN `total_orders` INT UNSIGNED NOT NULL DEFAULT 0 AFTER `updated_by`,
ADD COLUMN `on_time_delivery_count` INT UNSIGNED NOT NULL DEFAULT 0 AFTER `total_orders`,
ADD COLUMN `quality_acceptance_count` INT UNSIGNED NOT NULL DEFAULT 0 AFTER `on_time_delivery_count`,
ADD COLUMN `average_lead_days` DECIMAL(5,1) NOT NULL DEFAULT 0.0 AFTER `quality_acceptance_count`,
ADD COLUMN `last_order_date` DATE NULL AFTER `average_lead_days`
",
'down' => "
ALTER TABLE `suppliers`
DROP COLUMN `last_order_date`,
DROP COLUMN `average_lead_days`,
DROP COLUMN `quality_acceptance_count`,
DROP COLUMN `on_time_delivery_count`,
DROP COLUMN `total_orders`
",
];
<?php
declare(strict_types=1);
use App\Core\Database;
return function (Database $db): void {
$ts = date('Y-m-d H:i:s');
$definitions = [
// ── 1. Purchase Requisition Approval ──
[
'workflow_code' => 'procurement_pr_approval',
'name_ar' => 'اعتماد طلبات الشراء',
'name_en' => 'Purchase Requisition Approval',
'description_ar' => 'دورة عمل طلب واعتماد طلبات الشراء الداخلية',
'definition_json' => json_encode([
'states' => [
'draft' => ['label_ar' => 'مسودة', 'label_en' => 'Draft', 'type' => 'initial'],
'submitted' => ['label_ar' => 'تم التقديم', 'label_en' => 'Submitted', 'type' => 'intermediate'],
'approved' => ['label_ar' => 'معتمد', 'label_en' => 'Approved', 'type' => 'intermediate'],
'rejected' => ['label_ar' => 'مرفوض', 'label_en' => 'Rejected', 'type' => 'terminal'],
'converted' => ['label_ar' => 'تم التحويل لأمر شراء', 'label_en' => 'Converted to PO', 'type' => 'terminal'],
'cancelled' => ['label_ar' => 'ملغى', 'label_en' => 'Cancelled', 'type' => 'terminal'],
],
'transitions' => [
['name' => 'submit', 'from' => 'draft', 'to' => 'submitted', 'guards' => [['type' => 'permission', 'value' => 'procurement.pr.create']], 'actions' => [['type' => 'event', 'value' => 'procurement.requisition_submitted']]],
['name' => 'approve', 'from' => 'submitted', 'to' => 'approved', 'guards' => [['type' => 'permission', 'value' => 'procurement.pr.approve']], 'actions' => [['type' => 'event', 'value' => 'procurement.requisition_approved']]],
['name' => 'reject', 'from' => 'submitted', 'to' => 'rejected', 'guards' => [['type' => 'permission', 'value' => 'procurement.pr.approve']], 'actions' => [['type' => 'event', 'value' => 'procurement.requisition_rejected']]],
['name' => 'convert', 'from' => 'approved', 'to' => 'converted', 'guards' => [['type' => 'permission', 'value' => 'procurement.pr.convert']], 'actions' => [['type' => 'event', 'value' => 'procurement.requisition_converted']]],
['name' => 'cancel', 'from' => 'draft', 'to' => 'cancelled', 'guards' => [['type' => 'condition', 'value' => 'is_creator']], 'actions' => [['type' => 'event', 'value' => 'procurement.requisition_cancelled']]],
['name' => 'cancel_sub', 'from' => 'submitted', 'to' => 'cancelled', 'guards' => [['type' => 'permission', 'value' => 'procurement.pr.approve']], 'actions' => [['type' => 'event', 'value' => 'procurement.requisition_cancelled']]],
],
], JSON_UNESCAPED_UNICODE),
],
// ── 2. Return to Vendor Approval ──
[
'workflow_code' => 'procurement_rtv_approval',
'name_ar' => 'اعتماد مرتجعات الموردين',
'name_en' => 'Return to Vendor Approval',
'description_ar' => 'دورة عمل طلب واعتماد وشحن مرتجعات الموردين',
'definition_json' => json_encode([
'states' => [
'draft' => ['label_ar' => 'مسودة', 'label_en' => 'Draft', 'type' => 'initial'],
'submitted' => ['label_ar' => 'تم التقديم', 'label_en' => 'Submitted', 'type' => 'intermediate'],
'approved' => ['label_ar' => 'معتمد', 'label_en' => 'Approved', 'type' => 'intermediate'],
'shipped' => ['label_ar' => 'تم الشحن', 'label_en' => 'Shipped', 'type' => 'intermediate'],
'completed' => ['label_ar' => 'مكتمل', 'label_en' => 'Completed', 'type' => 'terminal'],
'cancelled' => ['label_ar' => 'ملغى', 'label_en' => 'Cancelled', 'type' => 'terminal'],
],
'transitions' => [
['name' => 'submit', 'from' => 'draft', 'to' => 'submitted', 'guards' => [['type' => 'permission', 'value' => 'procurement.rtv.create']], 'actions' => [['type' => 'event', 'value' => 'procurement.rtv_submitted']]],
['name' => 'approve', 'from' => 'submitted', 'to' => 'approved', 'guards' => [['type' => 'permission', 'value' => 'procurement.rtv.approve']], 'actions' => [['type' => 'event', 'value' => 'procurement.rtv_approved']]],
['name' => 'reject', 'from' => 'submitted', 'to' => 'cancelled', 'guards' => [['type' => 'permission', 'value' => 'procurement.rtv.approve']], 'actions' => [['type' => 'event', 'value' => 'procurement.rtv_rejected']]],
['name' => 'ship', 'from' => 'approved', 'to' => 'shipped', 'guards' => [['type' => 'permission', 'value' => 'procurement.rtv.manage']], 'actions' => [['type' => 'event', 'value' => 'procurement.rtv_shipped']]],
['name' => 'complete', 'from' => 'shipped', 'to' => 'completed', 'guards' => [['type' => 'permission', 'value' => 'procurement.rtv.manage']], 'actions' => [['type' => 'event', 'value' => 'procurement.rtv_completed']]],
['name' => 'cancel', 'from' => 'draft', 'to' => 'cancelled', 'guards' => [['type' => 'condition', 'value' => 'is_creator']], 'actions' => [['type' => 'event', 'value' => 'procurement.rtv_cancelled']]],
],
], JSON_UNESCAPED_UNICODE),
],
];
foreach ($definitions as $def) {
$existing = $db->selectOne(
"SELECT id FROM workflow_definitions WHERE workflow_code = ?",
[$def['workflow_code']]
);
if ($existing) {
continue;
}
$db->insert('workflow_definitions', [
'workflow_code' => $def['workflow_code'],
'name_ar' => $def['name_ar'],
'name_en' => $def['name_en'],
'description_ar' => $def['description_ar'],
'definition_json' => $def['definition_json'],
'version' => 1,
'is_active' => 1,
'created_at' => $ts,
'updated_at' => $ts,
]);
}
};
<?php
declare(strict_types=1);
use App\Core\Database;
return function (Database $db): void {
$ts = date('Y-m-d H:i:s');
$configs = [
[
'config_key' => 'procurement.match_tolerance_pct',
'config_value' => '2.00',
'description' => 'نسبة التسامح في المطابقة الثلاثية (%) - 3-way match tolerance percentage',
],
[
'config_key' => 'procurement.auto_create_pr_on_reorder',
'config_value' => '0',
'description' => 'إنشاء طلب شراء تلقائياً عند الوصول لحد إعادة الطلب - Auto-create PR when reorder point reached',
],
[
'config_key' => 'procurement.default_payment_terms_days',
'config_value' => '30',
'description' => 'شروط الدفع الافتراضية (بالأيام) - Default payment terms in days',
],
[
'config_key' => 'procurement.require_grn_before_invoice',
'config_value' => '1',
'description' => 'إلزام وجود إذن استلام قبل إنشاء فاتورة مورد - Require GRN before vendor invoice',
],
[
'config_key' => 'procurement.auto_approve_matched_invoices',
'config_value' => '0',
'description' => 'اعتماد الفواتير المطابقة تلقائياً - Auto-approve invoices that pass 3-way match',
],
];
foreach ($configs as $cfg) {
$existing = $db->selectOne(
"SELECT id FROM system_config WHERE config_key = ?",
[$cfg['config_key']]
);
if ($existing) {
continue;
}
$db->insert('system_config', [
'config_key' => $cfg['config_key'],
'config_value' => $cfg['config_value'],
'config_type' => is_numeric($cfg['config_value']) ? (str_contains($cfg['config_value'], '.') ? 'float' : 'integer') : 'string',
'group_name' => 'procurement',
'description_ar' => $cfg['description'],
'is_editable' => 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