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';
@@ -427,6 +452,20 @@ require __DIR__ . '/includes/header.php';
+ +
+ + +
+
+ + +
+
@@ -434,7 +473,7 @@ require __DIR__ . '/includes/header.php';
- +
@@ -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: ''}); + } else if ( && (!document.getElementById('delivery_date') || !document.getElementById('delivery_date').value)) { + e.preventDefault(); + Swal.fire({icon: 'warning', text: ''}); } else if (paymentAmount > currentInvoiceTotal + 0.0005) { e.preventDefault(); Swal.fire({icon: 'warning', text: ''}); 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'; +?> +
+
+
+

+
+
+ + + + +
+
+ + + +
+
+
> + + + +
+
+
+ + +
+
+ + +
+
+
+
+
+
+ +
+ +
+ + +
+
+ + +
+
+ +
+ + +
+
+
+ + +
+
+ + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+ +
+
+

+

+ +
+ +
+
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+
+
+ + + + + + + + +
+ + +
+
+
+
+ + 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'; +?> + + +
+
+
+
+

+

+
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+

+
:
+
+
+ : + : + : +
+
+ + +
+ +
+ + + + tr('مدفوع', 'Paid'), 'partial' => tr('جزئي', 'Partial'), 'unpaid' => tr('غير مدفوع', 'Unpaid'), default => $paymentStatus } : tr('كل حالات الدفع', 'All payment statuses')) ?> +
+ +
+
+
+
+
+
+ +
+
+
+

+
+
+
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + +
+
+ +
+ +
+
+
+

+
+
+
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
×
+ +
+
+
+ +
+ +
+
+ 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; - +
-
+
@@ -110,6 +113,12 @@ $isPublic = !empty($forcePublic) || !isset($user) || !$user; + + + + + + 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';
+
@@ -388,13 +416,37 @@ require __DIR__ . '/header.php';
+ +
+ +
+
+ +
+ + +
+
+ + +
+
+
@@ -431,7 +483,7 @@ require __DIR__ . '/header.php';
@@ -800,17 +852,22 @@ if (paymentMethodField && paymentAmountField) { } // Intercept form submission to check if items exist +const isEidOrderForm = ; 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: ''}); } else if (saleStatus === 'order' && !customerId) { e.preventDefault(); Swal.fire({icon: 'warning', text: ''}); + } else if (isEidOrderForm && (!deliveryDateField || !deliveryDateField.value)) { + e.preventDefault(); + Swal.fire({icon: 'warning', text: ''}); } else if (paymentAmount > currentInvoiceTotal + 0.0005) { e.preventDefault(); Swal.fire({icon: 'warning', text: ''}); 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';