diff --git a/db/migrations/2026-04-22_eid_orders_foundation.sql b/db/migrations/2026-04-22_eid_orders_foundation.sql
new file mode 100644
index 0000000..2bee0ff
--- /dev/null
+++ b/db/migrations/2026-04-22_eid_orders_foundation.sql
@@ -0,0 +1,65 @@
+-- Step 1 foundation for Eid Orders (طلبات العيد)
+-- Adds Eid-specific tracking columns to the shared sales_orders table.
+
+SET @db_name := DATABASE();
+
+SET @sql := IF (
+ EXISTS (
+ SELECT 1 FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = @db_name AND TABLE_NAME = 'sales_orders' AND COLUMN_NAME = 'order_type'
+ ),
+ 'SELECT 1',
+ "ALTER TABLE sales_orders ADD COLUMN order_type varchar(30) NOT NULL DEFAULT 'standard' AFTER status"
+);
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+SET @sql := IF (
+ EXISTS (
+ SELECT 1 FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = @db_name AND TABLE_NAME = 'sales_orders' AND COLUMN_NAME = 'delivery_status'
+ ),
+ 'SELECT 1',
+ "ALTER TABLE sales_orders ADD COLUMN delivery_status varchar(30) NOT NULL DEFAULT 'pending' AFTER order_type"
+);
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+SET @sql := IF (
+ EXISTS (
+ SELECT 1 FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = @db_name AND TABLE_NAME = 'sales_orders' AND COLUMN_NAME = 'delivery_date'
+ ),
+ 'SELECT 1',
+ "ALTER TABLE sales_orders ADD COLUMN delivery_date date DEFAULT NULL AFTER delivery_status"
+);
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+SET @sql := IF (
+ EXISTS (
+ SELECT 1 FROM information_schema.STATISTICS
+ WHERE TABLE_SCHEMA = @db_name AND TABLE_NAME = 'sales_orders' AND INDEX_NAME = 'idx_order_type'
+ ),
+ 'SELECT 1',
+ "ALTER TABLE sales_orders ADD INDEX idx_order_type (order_type)"
+);
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+SET @sql := IF (
+ EXISTS (
+ SELECT 1 FROM information_schema.STATISTICS
+ WHERE TABLE_SCHEMA = @db_name AND TABLE_NAME = 'sales_orders' AND INDEX_NAME = 'idx_delivery_date'
+ ),
+ 'SELECT 1',
+ "ALTER TABLE sales_orders ADD INDEX idx_delivery_date (delivery_date)"
+);
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+UPDATE sales_orders
+SET order_type = 'standard'
+WHERE order_type IS NULL OR order_type = '';
+
+UPDATE sales_orders
+SET delivery_status = CASE
+ WHEN COALESCE(status, 'completed') = 'completed' THEN 'delivered'
+ ELSE 'pending'
+END
+WHERE delivery_status IS NULL OR delivery_status = '';
diff --git a/edit_sale.php b/edit_sale.php
index c82a02b..e17b04e 100644
--- a/edit_sale.php
+++ b/edit_sale.php
@@ -20,12 +20,18 @@ if ($user['role'] !== 'owner' && $editSale['branch_code'] !== $user['branch_code
}
$pageTitle = tr('تعديل فاتورة', 'Edit Invoice') . ' #' . h($editSale['receipt_no']);
-$activeNav = 'sales';
+$isEidSale = (($editSale['order_type'] ?? 'standard') === 'eid');
+$activeNav = $isEidSale ? 'eid_orders' : 'sales';
$error = '';
$editPaymentSummary = sale_payment_summary($editSale);
$paymentAmountInput = (string) ($_POST['payment_amount'] ?? ($editPaymentSummary['paid_amount'] > 0 ? number_format((float) $editPaymentSummary['paid_amount'], 3, '.', '') : ''));
$catalog = catalog();
$allowedBranches = get_user_branches($user);
+$deliveryOptions = eid_delivery_status_options();
+$deliveryStatusInput = trim((string) ($_POST['delivery_status'] ?? ($editSale['delivery_status'] ?? ($isEidSale ? 'pending' : ''))));
+$deliveryDateInput = trim((string) ($_POST['delivery_date'] ?? ($editSale['delivery_date'] ?? '')));
+$notesInput = trim((string) ($_POST['notes'] ?? ($editSale['notes'] ?? '')));
+$saleStatusInput = trim((string) ($_POST['sale_status'] ?? ($editSale['status'] ?? 'completed')));
try {
$customers = db()->query('SELECT id, name, phone FROM customers ORDER BY name ASC')->fetchAll();
@@ -40,7 +46,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$paymentMethod = trim((string) ($_POST['payment_method'] ?? 'cash'));
$paymentAmountInput = trim((string) ($_POST['payment_amount'] ?? ''));
$saleStatus = trim((string) ($_POST['sale_status'] ?? 'completed'));
+ $saleStatusInput = $saleStatus;
$notes = trim((string) ($_POST['notes'] ?? ''));
+ $notesInput = $notes;
+ $deliveryStatus = trim((string) ($_POST['delivery_status'] ?? ($editSale['delivery_status'] ?? ($isEidSale ? 'pending' : ''))));
+ $deliveryStatusInput = $deliveryStatus;
+ $deliveryDate = trim((string) ($_POST['delivery_date'] ?? ($editSale['delivery_date'] ?? '')));
+ $deliveryDateInput = $deliveryDate;
$cartJson = (string) ($_POST['cart_json'] ?? '[]');
$items = json_decode($cartJson, true);
@@ -48,6 +60,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$error = tr('اختر فرعاً صالحاً لهذه الصلاحية.', 'Choose a valid branch for this role.');
} elseif (!in_array($paymentMethod, ['cash', 'card', 'transfer', 'pay_later'], true)) {
$error = tr('اختر طريقة دفع صحيحة.', 'Choose a valid payment method.');
+ } elseif ($isEidSale && !isset($deliveryOptions[$deliveryStatus])) {
+ $error = tr('اختر حالة تجهيز صحيحة لطلب العيد.', 'Choose a valid prep status for the Eid order.');
+ } elseif ($isEidSale && ($deliveryDate === '' || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $deliveryDate))) {
+ $error = tr('حدد تاريخ تسليم صحيح لطلب العيد.', 'Choose a valid delivery date for the Eid order.');
} elseif (!is_array($items) || $items === []) {
$error = tr('أضف صنفاً واحداً على الأقل إلى الفاتورة.', 'Add at least one item to the invoice.');
} else {
@@ -123,6 +139,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
vat_amount = :vat_amount,
total_amount = :total_amount,
status = :status,
+ delivery_status = :delivery_status,
+ delivery_date = :delivery_date,
notes = :notes
WHERE id = :id');
$stmt->execute([
@@ -139,6 +157,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
':vat_amount' => $totalVat,
':total_amount' => $totalAmount,
':status' => $saleStatus,
+ ':delivery_status' => $isEidSale ? $deliveryStatus : ($editSale['delivery_status'] ?? 'pending'),
+ ':delivery_date' => $isEidSale && $deliveryDate !== '' ? $deliveryDate : ($isEidSale ? null : ($editSale['delivery_date'] ?? null)),
':notes' => $notes !== '' ? $notes : null,
':id' => $editSaleId,
]);
@@ -326,8 +346,8 @@ require __DIR__ . '/includes/header.php';
@@ -414,8 +434,13 @@ require __DIR__ . '/includes/header.php';
= h(tr('نوع العملية', 'Entry Type')) ?>
- >= h(tr('فاتورة بيع (تم الدفع)', 'Sale Bill (Paid)')) ?>
- >= h(tr('طلب مسبق (دفع لاحق)', 'Order (Pay Later)')) ?>
+
+ >= h(tr('طلب عيد قيد التجهيز', 'Eid order in preparation')) ?>
+ >= h(tr('تم التسليم / مكتمل', 'Delivered / Completed')) ?>
+
+ >= h(tr('فاتورة بيع (تم الدفع)', 'Sale Bill (Paid)')) ?>
+ >= h(tr('طلب مسبق (دفع لاحق)', 'Order (Pay Later)')) ?>
+
@@ -427,6 +452,20 @@ require __DIR__ . '/includes/header.php';
>= h(tr('آجل (Pay Later)', 'Pay Later')) ?>
+
+
+ = h(tr('تاريخ التسليم', 'Delivery date')) ?>
+
+
+
+ = h(tr('حالة التجهيز', 'Prep status')) ?>
+
+ $label): ?>
+ >= h($label) ?>
+
+
+
+
= h(tr('المبلغ المدفوع الآن', 'Paid Now')) ?>
@@ -434,7 +473,7 @@ require __DIR__ . '/includes/header.php';
= h(tr('ملاحظات (اختياري)', 'Notes (Optional)')) ?>
-
+
@@ -801,6 +840,9 @@ document.getElementById('smart-sale-form').addEventListener('submit', function(e
if (Object.keys(invoiceItems).length === 0) {
e.preventDefault();
Swal.fire({icon: 'warning', text: '= h(tr('الرجاء إضافة أصناف للفاتورة أولاً.', 'Please add items to the invoice first.')) ?>'});
+ } else if (= $isEidSale ? 'true' : 'false' ?> && (!document.getElementById('delivery_date') || !document.getElementById('delivery_date').value)) {
+ e.preventDefault();
+ Swal.fire({icon: 'warning', text: '= h(tr('حدد تاريخ التسليم لطلب العيد.', 'Please choose a delivery date for the Eid order.')) ?>'});
} else if (paymentAmount > currentInvoiceTotal + 0.0005) {
e.preventDefault();
Swal.fire({icon: 'warning', text: '= h(tr('المبلغ المدفوع لا يمكن أن يتجاوز إجمالي الفاتورة.', 'Paid amount cannot exceed the invoice total.')) ?>'});
diff --git a/eid_orders.php b/eid_orders.php
new file mode 100644
index 0000000..2784f8a
--- /dev/null
+++ b/eid_orders.php
@@ -0,0 +1,529 @@
+ 'receipt_no',
+ 'customer' => 'customer_name',
+ 'branch' => 'branch_code',
+ 'delivery_date' => 'COALESCE(delivery_date, DATE(sale_date))',
+ 'delivery_status' => 'delivery_status',
+ 'item_count' => 'item_count',
+ 'total_amount' => 'total_amount',
+];
+if (!isset($sortMap[$sort])) {
+ $sort = 'delivery_date';
+}
+if (!in_array($dir, ['asc', 'desc'], true)) {
+ $dir = 'asc';
+}
+$page = max(1, (int) ($_GET['p'] ?? 1));
+$limit = 15;
+$offset = ($page - 1) * $limit;
+$allowedBranches = $user && $user['role'] !== 'owner' ? get_user_branches($user) : [];
+$deliveryOptions = eid_delivery_status_options();
+$dbError = null;
+$totalPages = 1;
+$orders = [];
+$summary = [
+ 'total_orders' => 0,
+ 'total_items' => 0,
+ 'total_amount' => 0,
+ 'prep_orders' => 0,
+];
+
+try {
+ $params = [':order_type' => 'eid'];
+ $where = ' WHERE order_type = :order_type ';
+
+ if ($mode) {
+ $where .= ' AND sale_mode = :sale_mode ';
+ $params[':sale_mode'] = $mode;
+ }
+
+ if ($branch) {
+ $where .= ' AND branch_code = :branch_code ';
+ $params[':branch_code'] = $branch;
+ }
+
+ if ($user && $user['role'] !== 'owner') {
+ if ($allowedBranches === []) {
+ $where .= ' AND 1=0 ';
+ } else {
+ $namedParams = [];
+ foreach ($allowedBranches as $i => $allowedBranch) {
+ $key = ':v_branch_' . $i;
+ $namedParams[] = $key;
+ $params[$key] = $allowedBranch;
+ }
+ $where .= ' AND branch_code IN (' . implode(', ', $namedParams) . ') ';
+ }
+ }
+
+ if ($search !== '') {
+ $where .= ' AND (receipt_no LIKE :search OR customer_name LIKE :search OR cashier_name LIKE :search OR notes LIKE :search) ';
+ $params[':search'] = '%' . $search . '%';
+ }
+
+ if (in_array($paymentStatus, ['paid', 'partial', 'unpaid'], true)) {
+ $where .= ' AND payment_status = :payment_status ';
+ $params[':payment_status'] = $paymentStatus;
+ }
+
+ if (isset($deliveryOptions[$deliveryStatus])) {
+ $where .= ' AND delivery_status = :delivery_status ';
+ $params[':delivery_status'] = $deliveryStatus;
+ }
+
+ if ($dateFrom !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateFrom)) {
+ $where .= ' AND DATE(COALESCE(delivery_date, sale_date)) >= :date_from ';
+ $params[':date_from'] = $dateFrom;
+ } else {
+ $dateFrom = '';
+ }
+
+ if ($dateTo !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateTo)) {
+ $where .= ' AND DATE(COALESCE(delivery_date, sale_date)) <= :date_to ';
+ $params[':date_to'] = $dateTo;
+ } else {
+ $dateTo = '';
+ }
+
+ $summarySql = "SELECT COUNT(*) AS total_orders, COALESCE(SUM(item_count), 0) AS total_items, COALESCE(SUM(total_amount), 0) AS total_amount, COALESCE(SUM(CASE WHEN delivery_status IN ('pending', 'preparing') THEN 1 ELSE 0 END), 0) AS prep_orders FROM sales_orders" . $where;
+ $summaryStmt = db()->prepare($summarySql);
+ foreach ($params as $key => $value) {
+ $summaryStmt->bindValue($key, $value);
+ }
+ $summaryStmt->execute();
+ $summary = $summaryStmt->fetch() ?: $summary;
+
+ $countSql = 'SELECT COUNT(*) FROM sales_orders' . $where;
+ $countStmt = db()->prepare($countSql);
+ foreach ($params as $key => $value) {
+ $countStmt->bindValue($key, $value);
+ }
+ $countStmt->execute();
+ $total = (int) $countStmt->fetchColumn();
+ $totalPages = max(1, (int) ceil($total / $limit));
+
+ $primarySort = $sortMap[$sort] . ' ' . strtoupper($dir);
+ $secondarySort = $sort === 'delivery_date'
+ ? ', sale_date DESC'
+ : ', COALESCE(delivery_date, DATE(sale_date)) ASC, sale_date DESC';
+
+ $sql = 'SELECT * FROM sales_orders' . $where . ' ORDER BY ' . $primarySort . $secondarySort . ' LIMIT :limit OFFSET :offset';
+ $stmt = db()->prepare($sql);
+ foreach ($params as $key => $value) {
+ $stmt->bindValue($key, $value);
+ }
+ $stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
+ $stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
+ $stmt->execute();
+ $orders = $stmt->fetchAll();
+
+ foreach ($orders as &$order) {
+ $order['items'] = json_decode((string) ($order['items_json'] ?? '[]'), true) ?: [];
+
+ $itemPreview = [];
+ foreach ($order['items'] as $item) {
+ $name = trim((string) ($item['name'] ?? $item['name_ar'] ?? $item['name_en'] ?? $item['sku'] ?? ''));
+ $qty = max(0, (int) ($item['qty'] ?? 0));
+ if ($name === '') {
+ continue;
+ }
+
+ $itemPreview[] = $qty > 0 ? $name . ' ×' . $qty : $name;
+ if (count($itemPreview) >= 2) {
+ break;
+ }
+ }
+
+ $remainingItems = max(0, count($order['items']) - count($itemPreview));
+ $order['item_preview'] = $itemPreview;
+ $order['item_preview_more'] = $remainingItems;
+ }
+ unset($order);
+} catch (Throwable $e) {
+ $dbError = $e->getMessage();
+}
+
+$queryState = static function (array $extra = []) use ($search, $branch, $mode, $paymentStatus, $deliveryStatus, $dateFrom, $dateTo, $sort, $dir): array {
+ $params = [
+ 'q' => $search,
+ 'branch' => $branch,
+ 'mode' => $mode,
+ 'payment_status' => $paymentStatus,
+ 'delivery_status' => $deliveryStatus,
+ 'date_from' => $dateFrom,
+ 'date_to' => $dateTo,
+ 'sort' => $sort,
+ 'dir' => $dir,
+ ];
+
+ foreach ($extra as $key => $value) {
+ $params[$key] = $value;
+ }
+
+ return array_filter($params, static fn($value) => $value !== null && $value !== '');
+};
+
+$sortUrl = static function (string $column) use ($sort, $dir, $queryState): string {
+ $nextDir = $sort === $column && $dir === 'asc' ? 'desc' : 'asc';
+ return url_for('eid_orders.php', $queryState(['sort' => $column, 'dir' => $nextDir, 'p' => 1]));
+};
+
+$sortIcon = static function (string $column) use ($sort, $dir): string {
+ if ($sort !== $column) {
+ return 'bi-arrow-down-up text-muted';
+ }
+
+ return $dir === 'asc' ? 'bi-sort-down-alt' : 'bi-sort-up';
+};
+
+require __DIR__ . '/includes/header.php';
+?>
+
+
+
+
= h(tr('طلبات العيد', 'Eid Orders')) ?>
+
+
+
+
+
+
+
+
+
+
+ = h(tr('بحث سريع', 'Quick search')) ?>
+
+
+
+ = h(tr('الفرع', 'Branch')) ?>
+
+ = h(tr('كل الفروع', 'All branches')) ?>
+ $branchLabel): ?>
+
+ >= h($branchLabel) ?>
+
+
+
+
+ = h(tr('التجهيز', 'Prep')) ?>
+
+ = h(tr('كل الحالات', 'All statuses')) ?>
+ $label): ?>
+ >= h($label) ?>
+
+
+
+
+
+
+
+
+
+
+
+
= h(tr('إجمالي الطلبات', 'Total orders')) ?>
+
= (int) ($summary['total_orders'] ?? 0) ?>
+
+
+
+
+
= h(tr('عدد القطع', 'Total items')) ?>
+
= (int) ($summary['total_items'] ?? 0) ?>
+
+
+
+
+
= h(tr('قيد التجهيز', 'Pending prep')) ?>
+
= (int) ($summary['prep_orders'] ?? 0) ?>
+
+
+
+
+
= h(tr('إجمالي القيمة', 'Total value')) ?>
+
= h(number_format((float) ($summary['total_amount'] ?? 0), 3)) ?>
+
+
+
+
+
+ = h($dbError) ?>
+
+
+
+
= h(tr('لا توجد طلبات عيد حتى الآن', 'No Eid orders yet')) ?>
+
= h(tr('لا توجد طلبات عيد حتى الآن. ابدأ بإنشاء أول طلب عيد ليظهر هنا مع الفلاتر والتواريخ.', 'There are no Eid orders yet. Create your first Eid order to see it here with filters and dates.')) ?>
+
= h(tr('إنشاء طلب عيد', 'Create Eid Order')) ?>
+
+
+
+
+
+ = h((int) ($summary['total_orders'] ?? 0)) ?> = h(tr('طلب', 'orders')) ?>
+
+
+
+
+
+
+
+
+ = h(tr('الفاتورة', 'Invoice')) ?>
+
+
+
+
+
+ = h(tr('العميل', 'Customer')) ?>
+
+
+
+
+
+ = h(tr('تسليم', 'Delivery')) ?>
+
+
+
+
+
+ = h(tr('حالة التجهيز', 'Prep status')) ?>
+
+
+
+
+
+ = h(tr('الفرع', 'Branch')) ?>
+
+
+
+
+
+ = h(tr('الأصناف', 'Items')) ?>
+
+
+
+
+
+ = h(tr('الإجمالي', 'Total')) ?>
+
+
+
+ = h(tr('إجراءات', 'Actions')) ?>
+
+
+
+
+
+
+
+ = h($order['receipt_no']) ?>
+
+
+ = h((string) ($order['customer_name'] ?: tr('عميل نقدي', 'Walk-in customer'))) ?>
+
+
+ = h(date('Y-m-d', strtotime((string) $effectiveDelivery))) ?>
+
+
+ = h(eid_delivery_status_label((string) ($order['delivery_status'] ?? 'pending'))) ?>
+
+
+ = h(branch_label((string) $order['branch_code'])) ?>
+
+
+
+ = (int) ($order['item_count'] ?? 0) ?> = h(tr('صنف', 'items')) ?>
+
+
+ = h(number_format((float) ($order['total_amount'] ?? 0), 3)) ?>
+
+
+
+
+
+
+
+
+
+
+ 1): ?>
+
+
+
+
+
+
+
diff --git a/eid_print.php b/eid_print.php
new file mode 100644
index 0000000..a7ac2b4
--- /dev/null
+++ b/eid_print.php
@@ -0,0 +1,394 @@
+ 0,
+ 'unique_items' => 0,
+ 'total_quantity' => 0,
+ 'total_amount' => 0,
+];
+
+try {
+ $params = [':order_type' => 'eid'];
+ $where = ' WHERE order_type = :order_type ';
+
+ if ($mode) {
+ $where .= ' AND sale_mode = :sale_mode ';
+ $params[':sale_mode'] = $mode;
+ }
+
+ if ($branch) {
+ $where .= ' AND branch_code = :branch_code ';
+ $params[':branch_code'] = $branch;
+ }
+
+ if ($user && $user['role'] !== 'owner') {
+ if ($allowedBranches === []) {
+ $where .= ' AND 1=0 ';
+ } else {
+ $namedParams = [];
+ foreach ($allowedBranches as $i => $allowedBranch) {
+ $key = ':v_branch_' . $i;
+ $namedParams[] = $key;
+ $params[$key] = $allowedBranch;
+ }
+ $where .= ' AND branch_code IN (' . implode(', ', $namedParams) . ') ';
+ }
+ }
+
+ if ($search !== '') {
+ $where .= ' AND (receipt_no LIKE :search OR customer_name LIKE :search OR cashier_name LIKE :search OR notes LIKE :search) ';
+ $params[':search'] = '%' . $search . '%';
+ }
+
+ if (in_array($paymentStatus, ['paid', 'partial', 'unpaid'], true)) {
+ $where .= ' AND payment_status = :payment_status ';
+ $params[':payment_status'] = $paymentStatus;
+ }
+
+ if (isset($deliveryOptions[$deliveryStatus])) {
+ $where .= ' AND delivery_status = :delivery_status ';
+ $params[':delivery_status'] = $deliveryStatus;
+ }
+
+ if ($dateFrom !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateFrom)) {
+ $where .= ' AND DATE(COALESCE(delivery_date, sale_date)) >= :date_from ';
+ $params[':date_from'] = $dateFrom;
+ } else {
+ $dateFrom = '';
+ }
+
+ if ($dateTo !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateTo)) {
+ $where .= ' AND DATE(COALESCE(delivery_date, sale_date)) <= :date_to ';
+ $params[':date_to'] = $dateTo;
+ } else {
+ $dateTo = '';
+ }
+
+ $sql = 'SELECT * FROM sales_orders' . $where . ' ORDER BY COALESCE(delivery_date, DATE(sale_date)) ASC, sale_date ASC';
+ $stmt = db()->prepare($sql);
+ foreach ($params as $key => $value) {
+ $stmt->bindValue($key, $value);
+ }
+ $stmt->execute();
+ $orders = $stmt->fetchAll();
+
+ $itemIndex = [];
+ foreach ($orders as &$order) {
+ $decodedItems = json_decode((string) ($order['items_json'] ?? '[]'), true);
+ $items = is_array($decodedItems) ? $decodedItems : [];
+ $order['items'] = $items;
+
+ foreach ($items as $item) {
+ $sku = trim((string) ($item['sku'] ?? ''));
+ $name = trim((string) ($item['name_ar'] ?? ''));
+ if ($name === '') {
+ $name = trim((string) ($item['name_en'] ?? ''));
+ }
+ if ($name === '') {
+ $name = $sku !== '' ? $sku : tr('صنف بدون اسم', 'Unnamed item');
+ }
+
+ $key = $sku !== '' ? $sku : md5($name);
+ $qty = max(0, (float) ($item['qty'] ?? 0));
+
+ if (!isset($itemIndex[$key])) {
+ $itemIndex[$key] = [
+ 'sku' => $sku,
+ 'name' => $name,
+ 'qty' => 0.0,
+ 'order_count' => 0,
+ ];
+ }
+
+ $itemIndex[$key]['qty'] += $qty;
+ $itemIndex[$key]['order_count']++;
+ }
+ }
+ unset($order);
+
+ usort($orders, static function (array $a, array $b): int {
+ $aDate = (string) ($a['delivery_date'] ?: substr((string) ($a['sale_date'] ?? ''), 0, 10));
+ $bDate = (string) ($b['delivery_date'] ?: substr((string) ($b['sale_date'] ?? ''), 0, 10));
+ return [$aDate, (string) ($a['receipt_no'] ?? '')] <=> [$bDate, (string) ($b['receipt_no'] ?? '')];
+ });
+
+ $itemRows = array_values($itemIndex);
+ usort($itemRows, static function (array $a, array $b): int {
+ if ($a['qty'] === $b['qty']) {
+ return strcasecmp((string) $a['name'], (string) $b['name']);
+ }
+ return $b['qty'] <=> $a['qty'];
+ });
+
+ $summary['total_orders'] = count($orders);
+ $summary['unique_items'] = count($itemRows);
+ $summary['total_quantity'] = array_sum(array_map(static fn(array $row): float => (float) $row['qty'], $itemRows));
+ $summary['total_amount'] = array_sum(array_map(static fn(array $row): float => (float) ($row['total_amount'] ?? 0), $orders));
+} catch (Throwable $e) {
+ $dbError = $e->getMessage();
+}
+
+$filterParams = array_filter([
+ 'q' => $search,
+ 'branch' => $branch,
+ 'mode' => $mode,
+ 'payment_status' => $paymentStatus,
+ 'delivery_status' => $deliveryStatus,
+ 'date_from' => $dateFrom,
+ 'date_to' => $dateTo,
+], static fn($value) => $value !== null && $value !== '');
+
+$generatedAt = date('Y-m-d H:i');
+require __DIR__ . '/includes/header.php';
+?>
+
+
+
+
+
+
+
= h(tr('ملخص تجهيزات طلبات العيد', 'Eid Order Prep Summary')) ?>
+
= h(tr('تقرير قابل للطباعة يوضح الأصناف المطلوبة وعدد الطلبات التي تحتوي كل صنف.', 'Printable report showing required items and how many orders contain each item.')) ?>
+
+
+
+
+
+
+
+
+
= h(tr('ملخص تجهيزات طلبات العيد', 'Eid Order Prep Summary')) ?>
+
= h(tr('تاريخ الطباعة', 'Printed at')) ?>: = h($generatedAt) ?>
+
+
+ = h(tr('عدد الطلبات', 'Orders')) ?>: = (int) $summary['total_orders'] ?>
+ = h(tr('الأصناف المميزة', 'Unique items')) ?>: = (int) $summary['unique_items'] ?>
+ = h(tr('إجمالي الكمية', 'Total quantity')) ?>: = h(number_format((float) $summary['total_quantity'], 0)) ?>
+
+
+
+
+ = h($dbError) ?>
+
+
+ = h($dateFrom !== '' || $dateTo !== '' ? (($dateFrom !== '' ? $dateFrom : '…') . ' → ' . ($dateTo !== '' ? $dateTo : '…')) : tr('كل التواريخ', 'All dates')) ?>
+ = h($branch ? (branches()[$branch] ?? $branch) : tr('كل الفروع', 'All branches')) ?>
+ = h($deliveryStatus !== '' && isset($deliveryOptions[$deliveryStatus]) ? $deliveryOptions[$deliveryStatus] : tr('كل حالات التجهيز', 'All prep statuses')) ?>
+ = h($paymentStatus !== '' ? match ($paymentStatus) { 'paid' => tr('مدفوع', 'Paid'), 'partial' => tr('جزئي', 'Partial'), 'unpaid' => tr('غير مدفوع', 'Unpaid'), default => $paymentStatus } : tr('كل حالات الدفع', 'All payment statuses')) ?>
+
+
+
+ = h(tr('إجمالي الطلبات', 'Total orders')) ?>
= (int) $summary['total_orders'] ?>
+ = h(tr('الأصناف المميزة', 'Unique items')) ?>
= (int) $summary['unique_items'] ?>
+ = h(tr('إجمالي الكمية', 'Total quantity')) ?>
= h(number_format((float) $summary['total_quantity'], 0)) ?>
+ = h(tr('إجمالي القيمة', 'Total value')) ?>
= h(number_format((float) $summary['total_amount'], 3)) ?>
+
+
+
+
+
+
= h(tr('ملخص الأصناف', 'Item Summary')) ?>
+
= h(tr('عدد الطلبات لكل صنف مع الكمية الإجمالية المطلوبة للتجهيز.', 'Order count per item with total quantity required for preparation.')) ?>
+
+
+
+
+ = h(tr('لا توجد بيانات ضمن هذه الفلاتر.', 'No data found for the selected filters.')) ?>
+
+
+
+
+
+ = h(tr('الصنف', 'Item')) ?>
+ = h(tr('SKU', 'SKU')) ?>
+ = h(tr('عدد الطلبات', 'Orders count')) ?>
+ = h(tr('إجمالي الكمية', 'Total quantity')) ?>
+
+
+
+
+
+ = h($row['name']) ?>
+ = h($row['sku'] !== '' ? $row['sku'] : '—') ?>
+ = (int) $row['order_count'] ?>
+ = h(rtrim(rtrim(number_format((float) $row['qty'], 3, '.', ''), '0'), '.')) ?>
+
+
+
+
+
+
+
+
+
+
+
+
= h(tr('الطلبات المشمولة', 'Included Orders')) ?>
+
= h(tr('قائمة الطلبات الداخلة في هذا الملخص مع الأصناف داخل كل طلب.', 'Orders included in this summary with the items inside each order.')) ?>
+
+
+
+
+ = h(tr('لا توجد طلبات عيد ضمن هذا النطاق.', 'There are no Eid orders in this range.')) ?>
+
+
+
+
+
+ = h(tr('الفاتورة', 'Invoice')) ?>
+ = h(tr('العميل', 'Customer')) ?>
+ = h(tr('الفرع', 'Branch')) ?>
+ = h(tr('تاريخ الاستلام', 'Delivery date')) ?>
+ = h(tr('حالة التجهيز', 'Prep status')) ?>
+ = h(tr('الأصناف', 'Items')) ?>
+ = h(tr('الإجمالي', 'Total')) ?>
+
+
+
+
+
+ = h($order['receipt_no'] ?? ('#' . (int) $order['id'])) ?>
+ = h($order['customer_name'] ?: tr('عميل نقدي', 'Walk-in customer')) ?>
+ = h(branches()[$order['branch_code']] ?? $order['branch_code']) ?>
+ = h($order['delivery_date'] ?: substr((string) ($order['sale_date'] ?? ''), 0, 10)) ?>
+ = h(eid_delivery_status_label((string) ($order['delivery_status'] ?? 'pending'))) ?>
+
+
+
+
+
= h($itemName) ?> × = h(rtrim(rtrim(number_format((float) ($item['qty'] ?? 0), 3, '.', ''), '0'), '.')) ?>
+
+
+
+ = h(number_format((float) ($order['total_amount'] ?? 0), 3)) ?>
+
+
+
+
+
+
+
+
+
+
+
diff --git a/eid_sale.php b/eid_sale.php
new file mode 100644
index 0000000..198060f
--- /dev/null
+++ b/eid_sale.php
@@ -0,0 +1,9 @@
+ tr('بانتظار التجهيز', 'Pending Prep'),
+ 'preparing' => tr('قيد التجهيز', 'Preparing'),
+ 'ready' => tr('جاهز', 'Ready'),
+ 'delivered' => tr('تم التسليم', 'Delivered'),
+ 'cancelled' => tr('ملغي', 'Cancelled'),
+ ];
+}
+
+function eid_delivery_status_label(string $status): string
+{
+ $options = eid_delivery_status_options();
+ return $options[$status] ?? $status;
+}
+
+function eid_delivery_status_badge_class(string $status): string
+{
+ return match ($status) {
+ 'preparing' => 'bg-info text-dark',
+ 'ready' => 'bg-primary text-white',
+ 'delivered' => 'bg-success text-white',
+ 'cancelled' => 'bg-danger text-white',
+ default => 'bg-warning text-dark',
+ };
+}
+
+function sale_order_type_label(string $type): string
+{
+ return match ($type) {
+ 'eid' => tr('طلبات العيد', 'Eid Orders'),
+ default => tr('بيع عادي', 'Standard Sale'),
+ };
+}
+
function online_payment_method_label(string $method): string
{
return match ($method) {
@@ -1673,15 +1709,47 @@ function ensure_sales_table(): void
paid_amount DECIMAL(10,3) NOT NULL DEFAULT 0.000,
due_amount DECIMAL(10,3) NOT NULL DEFAULT 0.000,
status VARCHAR(20) NOT NULL DEFAULT 'completed',
+ order_type VARCHAR(30) NOT NULL DEFAULT 'standard',
+ delivery_status VARCHAR(30) NOT NULL DEFAULT 'pending',
+ delivery_date DATE DEFAULT NULL,
notes TEXT DEFAULT NULL,
sale_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_sale_mode (sale_mode),
INDEX idx_branch_code (branch_code),
- INDEX idx_sale_date (sale_date)
+ INDEX idx_sale_date (sale_date),
+ INDEX idx_order_type (order_type),
+ INDEX idx_delivery_date (delivery_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci";
- db()->exec($sql);
+ $pdo = db();
+ $pdo->exec($sql);
+
+ $requiredColumns = [
+ 'order_type' => "ALTER TABLE sales_orders ADD COLUMN order_type varchar(30) NOT NULL DEFAULT 'standard' AFTER status",
+ 'delivery_status' => "ALTER TABLE sales_orders ADD COLUMN delivery_status varchar(30) NOT NULL DEFAULT 'pending' AFTER order_type",
+ 'delivery_date' => "ALTER TABLE sales_orders ADD COLUMN delivery_date date DEFAULT NULL AFTER delivery_status",
+ ];
+ foreach ($requiredColumns as $column => $columnSql) {
+ $exists = $pdo->query("SHOW COLUMNS FROM sales_orders LIKE " . $pdo->quote($column))->fetchColumn();
+ if (!$exists) {
+ $pdo->exec($columnSql);
+ }
+ }
+
+ $requiredIndexes = [
+ 'idx_order_type' => 'ALTER TABLE sales_orders ADD INDEX idx_order_type (order_type)',
+ 'idx_delivery_date' => 'ALTER TABLE sales_orders ADD INDEX idx_delivery_date (delivery_date)',
+ ];
+ foreach ($requiredIndexes as $indexName => $indexSql) {
+ $hasIndex = $pdo->query("SHOW INDEX FROM sales_orders WHERE Key_name = " . $pdo->quote($indexName))->fetchColumn();
+ if (!$hasIndex) {
+ $pdo->exec($indexSql);
+ }
+ }
+
+ $pdo->exec("UPDATE sales_orders SET order_type = 'standard' WHERE order_type IS NULL OR order_type = ''");
+ $pdo->exec("UPDATE sales_orders SET delivery_status = CASE WHEN COALESCE(status, 'completed') = 'completed' THEN 'delivered' ELSE 'pending' END WHERE delivery_status IS NULL OR delivery_status = ''");
}
function create_sale(array $data): int
@@ -1695,10 +1763,26 @@ function create_sale(array $data): int
? trim((string) $data['receipt_no'])
: next_receipt_code($pdo);
+ $orderType = trim((string) ($data['order_type'] ?? 'standard'));
+ if (!in_array($orderType, ['standard', 'eid'], true)) {
+ $orderType = 'standard';
+ }
+
+ $defaultDeliveryStatus = (($data['status'] ?? 'completed') === 'completed') ? 'delivered' : 'pending';
+ $deliveryStatus = trim((string) ($data['delivery_status'] ?? $defaultDeliveryStatus));
+ if (!array_key_exists($deliveryStatus, eid_delivery_status_options())) {
+ $deliveryStatus = $defaultDeliveryStatus;
+ }
+
+ $deliveryDate = isset($data['delivery_date']) ? trim((string) $data['delivery_date']) : '';
+ if ($deliveryDate !== '' && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $deliveryDate)) {
+ $deliveryDate = '';
+ }
+
$stmt = $pdo->prepare('INSERT INTO sales_orders
- (receipt_no, sale_mode, branch_code, cashier_username, cashier_name, role_name, customer_id, customer_name, payment_method, payment_status, items_json, item_count, subtotal, vat_amount, total_amount, paid_amount, due_amount, status, notes, sale_date)
+ (receipt_no, sale_mode, branch_code, cashier_username, cashier_name, role_name, customer_id, customer_name, payment_method, payment_status, items_json, item_count, subtotal, vat_amount, total_amount, paid_amount, due_amount, status, order_type, delivery_status, delivery_date, notes, sale_date)
VALUES
- (:receipt_no, :sale_mode, :branch_code, :cashier_username, :cashier_name, :role_name, :customer_id, :customer_name, :payment_method, :payment_status, :items_json, :item_count, :subtotal, :vat_amount, :total_amount, :paid_amount, :due_amount, :status, :notes, NOW())');
+ (:receipt_no, :sale_mode, :branch_code, :cashier_username, :cashier_name, :role_name, :customer_id, :customer_name, :payment_method, :payment_status, :items_json, :item_count, :subtotal, :vat_amount, :total_amount, :paid_amount, :due_amount, :status, :order_type, :delivery_status, :delivery_date, :notes, NOW())');
$stmt->bindValue(':receipt_no', $receiptNo);
$stmt->bindValue(':sale_mode', $data['sale_mode']);
@@ -1719,6 +1803,9 @@ function create_sale(array $data): int
$stmt->bindValue(':paid_amount', $data['paid_amount'] ?? $data['total_amount']);
$stmt->bindValue(':due_amount', $data['due_amount'] ?? 0.0);
$stmt->bindValue(':status', $data['status'] ?? 'completed');
+ $stmt->bindValue(':order_type', $orderType);
+ $stmt->bindValue(':delivery_status', $deliveryStatus);
+ $stmt->bindValue(':delivery_date', $deliveryDate !== '' ? $deliveryDate : null, $deliveryDate !== '' ? PDO::PARAM_STR : PDO::PARAM_NULL);
$stmt->bindValue(':notes', $data['notes']);
$stmt->execute();
@@ -2010,6 +2097,7 @@ function module_cards(): array
['title_ar' => 'نقاط البيع', 'title_en' => 'POS Sale', 'path' => 'pos.php', 'desc_ar' => 'إتمام البيع السريع مع تحديث السجل.', 'desc_en' => 'Fast checkout with instant sales logging.'],
['title_ar' => 'فاتورة', 'title_en' => 'Invoice', 'path' => 'normal_sale.php', 'desc_ar' => 'فاتورة يدوية مع العميل والملاحظات.', 'desc_en' => 'Manual invoice flow with customer details and notes.'],
['title_ar' => 'المبيعات', 'title_en' => 'Sales Ledger', 'path' => 'sales.php', 'desc_ar' => 'قائمة الفواتير مع التفاصيل والفرز.', 'desc_en' => 'Invoice list with filters and detail views.'],
+ ['title_ar' => 'طلبات العيد', 'title_en' => 'Eid Orders', 'path' => 'eid_orders.php', 'desc_ar' => 'قائمة مخصصة لطلبات العيد مع فلاتر التاريخ والتجهيز.', 'desc_en' => 'Dedicated Eid orders list with prep and date filters.'],
['title_ar' => 'المخزون', 'title_en' => 'Stock', 'path' => 'stock.php', 'desc_ar' => 'قراءة فورية للمخزون الحالي والتنبيهات.', 'desc_en' => 'Live stock snapshot and low-stock indicators.'],
['title_ar' => 'المشتريات', 'title_en' => 'Purchases', 'path' => 'purchases.php', 'desc_ar' => 'واجهة مبدئية لاستلام الموردين بين الفروع.', 'desc_en' => 'Starter receiving board for suppliers and branches.'],
['title_ar' => 'التقارير', 'title_en' => 'Reports', 'path' => 'reports.php', 'desc_ar' => 'مبيعات اليوم، الفروع، وأفضل الأصناف.', 'desc_en' => 'Daily sales, branch totals, and best sellers.'],
diff --git a/includes/header.php b/includes/header.php
index f9070aa..2b886be 100644
--- a/includes/header.php
+++ b/includes/header.php
@@ -29,6 +29,9 @@ $isPublic = !empty($forcePublic) || !isset($user) || !$user;
+
+
+
@@ -96,13 +99,13 @@ $isPublic = !empty($forcePublic) || !isset($user) || !$user;
-
+
= h(tr('المبيعات', 'Sales')) ?>
-
+
= h(tr('قائمة الفواتير', 'Invoice list')) ?>
@@ -110,6 +113,12 @@ $isPublic = !empty($forcePublic) || !isset($user) || !$user;
= h(tr('الطلبات', 'Orders')) ?>
+
+ = h(tr('طلبات العيد', 'Eid Orders')) ?>
+
+
+ = h(tr('طلب عيد جديد', 'New Eid Order')) ?>
+
= h(tr('طلبات المتجر', 'Store Orders')) ?>
diff --git a/includes/sale_form.php b/includes/sale_form.php
index 80d06b5..bcb0866 100644
--- a/includes/sale_form.php
+++ b/includes/sale_form.php
@@ -1,12 +1,24 @@
query('SELECT id, name, phone FROM customers ORDER BY name ASC')->fetchAll();
@@ -20,8 +32,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$customerName = trim((string) ($_POST['customer_name'] ?? ''));
$paymentMethod = trim((string) ($_POST['payment_method'] ?? 'cash'));
$paymentAmountInput = trim((string) ($_POST['payment_amount'] ?? ''));
- $saleStatus = trim((string) ($_POST['sale_status'] ?? 'completed'));
+ $saleStatus = trim((string) ($_POST['sale_status'] ?? ($isEidOrder ? 'order' : 'completed')));
+ $saleStatusInput = $saleStatus;
$notes = trim((string) ($_POST['notes'] ?? ''));
+ $notesInput = $notes;
+ $deliveryStatus = trim((string) ($_POST['delivery_status'] ?? ($isEidOrder ? 'pending' : '')));
+ $selectedDeliveryStatus = $deliveryStatus;
+ $deliveryDate = trim((string) ($_POST['delivery_date'] ?? ''));
+ $deliveryDateInput = $deliveryDate;
$cartJson = (string) ($_POST['cart_json'] ?? '[]');
$items = json_decode($cartJson, true);
@@ -31,6 +49,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$error = tr('اختر طريقة دفع صحيحة.', 'Choose a valid payment method.');
} elseif ($saleStatus === 'order' && !$customerId) {
$error = tr('يجب اختيار عميل للطلب المسبق.', 'You must select a customer for a pre-order.');
+ } elseif ($isEidOrder && !isset($deliveryOptions[$deliveryStatus])) {
+ $error = tr('اختر حالة تجهيز صحيحة لطلب العيد.', 'Choose a valid prep status for the Eid order.');
+ } elseif ($isEidOrder && ($deliveryDate === '' || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $deliveryDate))) {
+ $error = tr('حدد تاريخ تسليم صحيح لطلب العيد.', 'Choose a valid delivery date for the Eid order.');
} elseif (!is_array($items) || $items === []) {
$error = tr('أضف صنفاً واحداً على الأقل إلى الفاتورة.', 'Add at least one item to the invoice.');
} else {
@@ -117,14 +139,19 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
'vat_amount' => $totalVat,
'total_amount' => $totalAmount,
'status' => $saleStatus,
+ 'order_type' => $orderType,
+ 'delivery_status' => $isEidOrder ? $deliveryStatus : null,
+ 'delivery_date' => $isEidOrder ? $deliveryDate : null,
'notes' => $notes !== '' ? $notes : null,
]);
wablas_notify_sale_invoice($saleId);
- set_flash('success', $saleMode === 'normal'
- ? tr('تم حفظ الفاتورة بنجاح.', 'Invoice saved successfully.')
- : tr('تم حفظ عملية POS بنجاح.', 'POS sale saved successfully.'));
+ set_flash('success', $isEidOrder
+ ? tr('تم حفظ طلب العيد بنجاح.', 'Eid order saved successfully.')
+ : ($saleMode === 'normal'
+ ? tr('تم حفظ الفاتورة بنجاح.', 'Invoice saved successfully.')
+ : tr('تم حفظ عملية POS بنجاح.', 'POS sale saved successfully.')));
redirect_to('sale.php', ['id' => $saleId]);
}
}
@@ -303,8 +330,8 @@ require __DIR__ . '/header.php';
@@ -314,6 +341,7 @@ require __DIR__ . '/header.php';
- = h(tr('حفظ الفاتورة', 'Save Invoice')) ?>
+ = h($saveLabel) ?>
@@ -800,17 +852,22 @@ if (paymentMethodField && paymentAmountField) {
}
// Intercept form submission to check if items exist
+const isEidOrderForm = = $isEidOrder ? 'true' : 'false' ?>;
document.getElementById('smart-sale-form').addEventListener('submit', function(e) {
const paymentMethod = document.querySelector('select[name="payment_method"]').value;
const saleStatus = document.querySelector('select[name="sale_status"]').value;
const customerId = document.getElementById('formCustomerId').value;
const paymentAmount = Math.max(0, parseFloat(document.getElementById('payment_amount').value || '0') || 0);
+ const deliveryDateField = document.getElementById('delivery_date');
if (Object.keys(invoiceItems).length === 0) {
e.preventDefault();
Swal.fire({icon: 'warning', text: '= h(tr('الرجاء إضافة أصناف للفاتورة أولاً.', 'Please add items to the invoice first.')) ?>'});
} else if (saleStatus === 'order' && !customerId) {
e.preventDefault();
Swal.fire({icon: 'warning', text: '= h(tr('يجب اختيار عميل للطلب المسبق.', 'You must select a customer for a pre-order.')) ?>'});
+ } else if (isEidOrderForm && (!deliveryDateField || !deliveryDateField.value)) {
+ e.preventDefault();
+ Swal.fire({icon: 'warning', text: '= h(tr('حدد تاريخ التسليم لطلب العيد.', 'Please choose a delivery date for the Eid order.')) ?>'});
} else if (paymentAmount > currentInvoiceTotal + 0.0005) {
e.preventDefault();
Swal.fire({icon: 'warning', text: '= h(tr('المبلغ المدفوع لا يمكن أن يتجاوز إجمالي الفاتورة.', 'Paid amount cannot exceed the invoice total.')) ?>'});
diff --git a/sale.php b/sale.php
index 199e466..7e11052 100644
--- a/sale.php
+++ b/sale.php
@@ -14,6 +14,7 @@ if ($id > 0) {
}
}
$paymentSummary = $sale ? sale_payment_summary($sale) : ['paid_amount' => 0, 'due_amount' => 0, 'payment_status' => 'paid'];
+$isEidSale = $sale && (($sale['order_type'] ?? 'standard') === 'eid');
// Company Info for Invoice
$companyName = current_lang() === 'ar' ? get_setting('company_name_ar', 'حلوى الريامي') : get_setting('company_name_en', 'Al Riyami Sweets');
@@ -317,8 +318,8 @@ require __DIR__ . '/includes/header.php';