From 73771705200c3c82adb9364d1f0615d57e7c4b28 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Sun, 26 Apr 2026 17:57:47 +0000 Subject: [PATCH] update customers --- api/customers.php | 15 +- assets/css/custom.css | 277 ------------------ customers.php | 40 ++- ...4-26_customers_phone_normalized_unique.sql | 54 ++++ db/schema.sql | 4 +- edit_sale.php | 9 +- includes/app.php | 102 +++++++ includes/header.php | 12 +- includes/sale_form.php | 9 +- index.php | 71 +---- pos.php | 9 +- 11 files changed, 247 insertions(+), 355 deletions(-) create mode 100644 db/migrations/2026-04-26_customers_phone_normalized_unique.sql diff --git a/api/customers.php b/api/customers.php index 4d5371f..fe0aa34 100644 --- a/api/customers.php +++ b/api/customers.php @@ -7,6 +7,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $name = trim($_POST['name'] ?? ''); $phoneInput = trim($_POST['phone'] ?? ''); $phone = $phoneInput === '' ? '' : normalize_oman_phone($phoneInput); + $phoneForStorage = $phone === '' ? null : $phone; if (!$name) { echo json_encode(['success' => false, 'error' => tr('الاسم مطلوب', 'Name is required')]); @@ -16,16 +17,24 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { echo json_encode(['success' => false, 'error' => tr('رقم الهاتف يجب أن يكون عمانياً من 8 خانات.', 'Phone must be an 8-digit Oman number.')]); exit; } + if ($phone !== '' && customer_phone_exists($phone)) { + echo json_encode(['success' => false, 'error' => tr('رقم الهاتف هذا مسجل لعميل آخر بالفعل.', 'This phone number is already assigned to another customer.')]); + exit; + } try { $pdo = db(); - $stmt = $pdo->prepare('INSERT INTO customers (name, phone) VALUES (?, ?)'); - $stmt->execute([$name, $phone]); + $stmt = $pdo->prepare('INSERT INTO customers (name, phone, phone_normalized) VALUES (?, ?, ?)'); + $stmt->execute([$name, $phoneForStorage, $phoneForStorage]); $id = $pdo->lastInsertId(); echo json_encode(['success' => true, 'customer' => ['id' => $id, 'name' => $name, 'phone' => $phone]]); } catch (Throwable $e) { - echo json_encode(['success' => false, 'error' => $e->getMessage()]); + if (is_customer_phone_unique_violation($e)) { + echo json_encode(['success' => false, 'error' => tr('رقم الهاتف هذا مسجل لعميل آخر بالفعل.', 'This phone number is already assigned to another customer.')]); + } else { + echo json_encode(['success' => false, 'error' => tr('تعذر حفظ العميل حالياً.', 'Could not save the customer right now.')]); + } } exit; } diff --git a/assets/css/custom.css b/assets/css/custom.css index 10e624c..498793b 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -392,280 +392,3 @@ details summary::-webkit-details-marker { display: none; } } -/* Dashboard-only bright preview */ -body.theme-preview-sunset { - background: - radial-gradient(circle at top left, rgba(255, 122, 0, 0.18), transparent 26%), - radial-gradient(circle at top right, rgba(0, 209, 178, 0.15), transparent 22%), - linear-gradient(180deg, #192132 0%, #111827 45%, #0f1724 100%); -} - -body.theme-preview-sunset #sidebar-wrapper { - background: - linear-gradient(180deg, rgba(255, 122, 0, 0.16), transparent 22%), - linear-gradient(215deg, #1b2433 0%, #111827 100%); - border-right: 1px solid rgba(255, 255, 255, 0.06); -} - -body.theme-preview-sunset .sidebar-heading { - background: rgba(255, 255, 255, 0.03); - border-bottom: 1px solid rgba(255,255,255,0.08); - color: #f9fafb; -} - -body.theme-preview-sunset #sidebar-wrapper .text-white, -body.theme-preview-sunset #sidebar-wrapper .text-white-50, -body.theme-preview-sunset #sidebar-wrapper .sidebar-heading span, -body.theme-preview-sunset #sidebar-wrapper .fw-semibold { - color: #f9fafb !important; -} - -body.theme-preview-sunset #sidebar-wrapper .list-group-item { - color: rgba(249, 250, 251, 0.72); -} - -body.theme-preview-sunset #sidebar-wrapper .list-group-item:hover, -body.theme-preview-sunset #sidebar-wrapper .list-group-item.active { - background: linear-gradient(135deg, rgba(255, 122, 0, 0.18), rgba(0, 209, 178, 0.14)); - color: #ffffff; - box-shadow: 0 12px 26px rgba(0, 0, 0, 0.16); -} - -body.theme-preview-sunset #sidebar-wrapper .list-group-item:hover i, -body.theme-preview-sunset #sidebar-wrapper .list-group-item.active i { - color: #ffd166; -} - -body.theme-preview-sunset .top-navbar { - background: rgba(17, 24, 39, 0.82); - backdrop-filter: blur(16px); - -webkit-backdrop-filter: blur(16px); - border-bottom: 1px solid rgba(255, 255, 255, 0.08); - box-shadow: 0 18px 40px rgba(0, 0, 0, 0.18); -} - -body.theme-preview-sunset .top-navbar h4, -body.theme-preview-sunset .top-navbar .dropdown-toggle, -body.theme-preview-sunset .top-navbar .btn { - color: #f9fafb; -} - -body.theme-preview-sunset .top-navbar .btn-outline-success, -body.theme-preview-sunset .top-navbar .btn-outline-primary, -body.theme-preview-sunset .top-navbar .btn-light { - border-color: rgba(255, 255, 255, 0.12); -} - -body.theme-preview-sunset .dashboard-sunset-sample { - position: relative; - z-index: 1; -} - -body.theme-preview-sunset .dashboard-hero { - position: relative; - background: - radial-gradient(circle at top right, rgba(255, 122, 0, 0.28), transparent 30%), - radial-gradient(circle at bottom left, rgba(0, 209, 178, 0.2), transparent 28%), - linear-gradient(135deg, rgba(27, 36, 51, 0.98) 0%, rgba(17, 24, 39, 0.98) 100%); - color: #f9fafb; - border: 1px solid rgba(255, 255, 255, 0.08) !important; - box-shadow: 0 28px 70px rgba(0, 0, 0, 0.28); -} - -body.theme-preview-sunset .dashboard-hero::before, -body.theme-preview-sunset .dashboard-hero::after { - content: ''; - position: absolute; - border-radius: 999px; - pointer-events: none; -} - -body.theme-preview-sunset .dashboard-hero::before { - width: 220px; - height: 220px; - right: -50px; - top: -50px; - background: radial-gradient(circle, rgba(255, 122, 0, 0.34), rgba(255, 122, 0, 0)); -} - -body.theme-preview-sunset .dashboard-hero::after { - width: 240px; - height: 240px; - left: -70px; - bottom: -80px; - background: radial-gradient(circle, rgba(0, 209, 178, 0.22), rgba(0, 209, 178, 0)); -} - -body.theme-preview-sunset .dashboard-kicker { - display: inline-flex; - align-items: center; - gap: 0.5rem; - padding: 0.55rem 0.9rem; - border-radius: 999px; - background: rgba(255, 255, 255, 0.08); - border: 1px solid rgba(255, 209, 102, 0.22); - color: #ffd166; - font-size: 0.85rem; - letter-spacing: 0.04em; - text-transform: uppercase; -} - -body.theme-preview-sunset .dashboard-hero-title { - color: #f9fafb; - font-size: clamp(2rem, 4vw, 3.2rem); - line-height: 1.05; - max-width: 13ch; -} - -body.theme-preview-sunset .dashboard-hero-copy { - color: rgba(249, 250, 251, 0.74); - font-size: 1rem; - max-width: 64ch; -} - -body.theme-preview-sunset .dashboard-hero-panel { - padding: 1.25rem; - border-radius: 1.5rem; - background: rgba(255, 255, 255, 0.06); - border: 1px solid rgba(255, 255, 255, 0.08); - box-shadow: inset 0 1px 0 rgba(255,255,255,0.05), 0 18px 40px rgba(0, 0, 0, 0.16); -} - -body.theme-preview-sunset .dashboard-chip-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 0.8rem; -} - -body.theme-preview-sunset .dashboard-chip { - display: inline-flex; - justify-content: center; - padding: 0.75rem 0.9rem; - border-radius: 1rem; - font-weight: 700; - letter-spacing: 0.02em; - color: #111827; -} - -body.theme-preview-sunset .dashboard-chip--orange { background: #ffb36b; } -body.theme-preview-sunset .dashboard-chip--teal { background: #7ce9d7; } -body.theme-preview-sunset .dashboard-chip--gold { background: #ffd166; } -body.theme-preview-sunset .dashboard-chip--rose { background: #ff9aae; } - -body.theme-preview-sunset .dashboard-hero-meta { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 0.9rem; -} - -body.theme-preview-sunset .dashboard-hero-meta small { - display: block; - color: rgba(249, 250, 251, 0.58); - margin-bottom: 0.25rem; -} - -body.theme-preview-sunset .dashboard-hero-meta strong { - color: #f9fafb; - font-size: 1rem; -} - -body.theme-preview-sunset .dashboard-sunset-sample .card { - border: 1px solid rgba(255, 255, 255, 0.08); - background: rgba(27, 36, 51, 0.94); - box-shadow: 0 18px 40px rgba(0, 0, 0, 0.18); -} - -body.theme-preview-sunset .dashboard-sunset-sample .card-header, -body.theme-preview-sunset .dashboard-sunset-sample .card-body, -body.theme-preview-sunset .dashboard-sunset-sample .table td, -body.theme-preview-sunset .dashboard-sunset-sample .table th, -body.theme-preview-sunset .dashboard-sunset-sample .text-muted, -body.theme-preview-sunset .dashboard-sunset-sample .small { - color: rgba(249, 250, 251, 0.72) !important; -} - -body.theme-preview-sunset .dashboard-sunset-sample .card-header { - border-bottom: 1px solid rgba(255, 255, 255, 0.08); -} - -body.theme-preview-sunset .dashboard-sunset-sample .table th { - background: rgba(255, 255, 255, 0.04); - color: rgba(249, 250, 251, 0.64) !important; - border-bottom-color: rgba(255, 255, 255, 0.08); -} - -body.theme-preview-sunset .dashboard-sunset-sample .table td { - border-bottom-color: rgba(255, 255, 255, 0.06); -} - -body.theme-preview-sunset .dashboard-sunset-sample .table-hover tbody tr:hover { - background: rgba(255, 255, 255, 0.03); -} - -body.theme-preview-sunset .dashboard-sunset-sample .dashboard-stat-card { - overflow: hidden; - position: relative; - color: #ffffff; -} - -body.theme-preview-sunset .dashboard-sunset-sample .dashboard-stat-card::before { - content: ''; - position: absolute; - inset: 0; - background: linear-gradient(135deg, rgba(255,255,255,0.18), rgba(255,255,255,0.03)); - opacity: 0.92; -} - -body.theme-preview-sunset .dashboard-sunset-sample .dashboard-stat-card > .card-body { - position: relative; - z-index: 1; -} - -body.theme-preview-sunset .dashboard-sunset-sample .dashboard-stat-card--cyan { - background: linear-gradient(135deg, #ff7a00 0%, #ff9d42 100%); -} - -body.theme-preview-sunset .dashboard-sunset-sample .dashboard-stat-card--coral { - background: linear-gradient(135deg, #00d1b2 0%, #1ee4c8 100%); -} - -body.theme-preview-sunset .dashboard-sunset-sample .dashboard-stat-card--lime { - background: linear-gradient(135deg, #ffd166 0%, #ffdf95 100%); - color: #1b2433; -} - -body.theme-preview-sunset .dashboard-sunset-sample .dashboard-stat-card--gold { - background: linear-gradient(135deg, #ff4d6d 0%, #ff7f95 100%); -} - -body.theme-preview-sunset .dashboard-sunset-sample .display-6, -body.theme-preview-sunset .dashboard-sunset-sample h5, -body.theme-preview-sunset .dashboard-sunset-sample h6 { - color: inherit; -} - -body.theme-preview-sunset .dashboard-sunset-sample .btn-primary { - background: linear-gradient(135deg, #ff7a00 0%, #ff9440 100%); - border: none; - box-shadow: 0 14px 28px rgba(255, 122, 0, 0.22); -} - -body.theme-preview-sunset .dashboard-sunset-sample .btn-light { - background: rgba(255,255,255,0.08); - border-color: rgba(255,255,255,0.12); - color: #f9fafb; -} - -body.theme-preview-sunset .dashboard-sunset-sample .badge { - border-radius: 999px; -} - -@media (max-width: 991.98px) { - body.theme-preview-sunset { - background: linear-gradient(180deg, #1b2433 0%, #111827 60%, #0f1724 100%); - } - - body.theme-preview-sunset .dashboard-hero-title { - max-width: none; - } -} diff --git a/customers.php b/customers.php index 8034137..89fd04a 100644 --- a/customers.php +++ b/customers.php @@ -13,26 +13,54 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($action === 'create') { $phoneInput = trim((string) ($_POST['phone'] ?? '')); $phone = $phoneInput === '' ? '' : normalize_oman_phone($phoneInput); + $phoneForStorage = $phone === '' ? null : $phone; if ($phoneInput !== '' && $phone === '') { set_flash('danger', tr('رقم الهاتف يجب أن يكون عمانياً من 8 خانات.', 'Phone must be an 8-digit Oman number.')); redirect_to('customers.php'); } + if ($phone !== '' && customer_phone_exists($phone)) { + set_flash('danger', tr('رقم الهاتف هذا مسجل لعميل آخر بالفعل.', 'This phone number is already assigned to another customer.')); + redirect_to('customers.php'); + } - $stmt = $pdo->prepare('INSERT INTO customers (name, phone, email, address) VALUES (?, ?, ?, ?)'); - $stmt->execute([$_POST['name'], $phone, $_POST['email'] ?? '', $_POST['address'] ?? '']); - set_flash('success', tr('تمت إضافة العميل بنجاح', 'Customer added successfully')); + try { + $stmt = $pdo->prepare('INSERT INTO customers (name, phone, phone_normalized, email, address) VALUES (?, ?, ?, ?, ?)'); + $stmt->execute([$_POST['name'], $phoneForStorage, $phoneForStorage, $_POST['email'] ?? '', $_POST['address'] ?? '']); + set_flash('success', tr('تمت إضافة العميل بنجاح', 'Customer added successfully')); + } catch (Throwable $e) { + if (is_customer_phone_unique_violation($e)) { + set_flash('danger', tr('رقم الهاتف هذا مسجل لعميل آخر بالفعل.', 'This phone number is already assigned to another customer.')); + } else { + throw $e; + } + } redirect_to('customers.php'); } elseif ($action === 'edit') { $phoneInput = trim((string) ($_POST['phone'] ?? '')); $phone = $phoneInput === '' ? '' : normalize_oman_phone($phoneInput); + $phoneForStorage = $phone === '' ? null : $phone; if ($phoneInput !== '' && $phone === '') { set_flash('danger', tr('رقم الهاتف يجب أن يكون عمانياً من 8 خانات.', 'Phone must be an 8-digit Oman number.')); redirect_to('customers.php'); } - $stmt = $pdo->prepare('UPDATE customers SET name = ?, phone = ?, email = ?, address = ? WHERE id = ?'); - $stmt->execute([$_POST['name'], $phone, $_POST['email'] ?? '', $_POST['address'] ?? '', $_POST['id']]); - set_flash('success', tr('تم التحديث بنجاح', 'Updated successfully')); + $customerId = (int) ($_POST['id'] ?? 0); + if ($phone !== '' && customer_phone_exists($phone, $customerId)) { + set_flash('danger', tr('رقم الهاتف هذا مسجل لعميل آخر بالفعل.', 'This phone number is already assigned to another customer.')); + redirect_to('customers.php'); + } + + try { + $stmt = $pdo->prepare('UPDATE customers SET name = ?, phone = ?, phone_normalized = ?, email = ?, address = ? WHERE id = ?'); + $stmt->execute([$_POST['name'], $phoneForStorage, $phoneForStorage, $_POST['email'] ?? '', $_POST['address'] ?? '', $_POST['id']]); + set_flash('success', tr('تم التحديث بنجاح', 'Updated successfully')); + } catch (Throwable $e) { + if (is_customer_phone_unique_violation($e)) { + set_flash('danger', tr('رقم الهاتف هذا مسجل لعميل آخر بالفعل.', 'This phone number is already assigned to another customer.')); + } else { + throw $e; + } + } redirect_to('customers.php'); } elseif ($action === 'delete') { $stmt = $pdo->prepare('DELETE FROM customers WHERE id = ?'); diff --git a/db/migrations/2026-04-26_customers_phone_normalized_unique.sql b/db/migrations/2026-04-26_customers_phone_normalized_unique.sql new file mode 100644 index 0000000..9abb524 --- /dev/null +++ b/db/migrations/2026-04-26_customers_phone_normalized_unique.sql @@ -0,0 +1,54 @@ +-- Add normalized customer phone storage and a unique index for duplicate protection. +-- Safe to import on an existing database before/after the app code change. + +DROP PROCEDURE IF EXISTS apply_customers_phone_normalized_unique; +DELIMITER $$ +CREATE PROCEDURE apply_customers_phone_normalized_unique() +BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'customers' + AND COLUMN_NAME = 'phone_normalized' + ) THEN + ALTER TABLE customers + ADD COLUMN phone_normalized VARCHAR(8) DEFAULT NULL AFTER phone; + END IF; + + UPDATE customers + SET phone = NULLIF(TRIM(phone), ''); + + UPDATE customers + SET phone_normalized = CASE + WHEN phone IS NULL OR TRIM(phone) = '' THEN NULL + WHEN REPLACE(REPLACE(REPLACE(REPLACE(phone, ' ', ''), '-', ''), '(', ''), ')', '') REGEXP '^[0-9]{8}$' + THEN REPLACE(REPLACE(REPLACE(REPLACE(phone, ' ', ''), '-', ''), '(', ''), ')', '') + WHEN REPLACE(REPLACE(REPLACE(REPLACE(phone, ' ', ''), '-', ''), '(', ''), ')', '') REGEXP '^0[0-9]{8}$' + THEN RIGHT(REPLACE(REPLACE(REPLACE(REPLACE(phone, ' ', ''), '-', ''), '(', ''), ')', ''), 8) + WHEN REPLACE(REPLACE(REPLACE(REPLACE(phone, ' ', ''), '-', ''), '(', ''), ')', '') REGEXP '^968[0-9]{8}$' + THEN RIGHT(REPLACE(REPLACE(REPLACE(REPLACE(phone, ' ', ''), '-', ''), '(', ''), ')', ''), 8) + WHEN REPLACE(REPLACE(REPLACE(REPLACE(phone, ' ', ''), '-', ''), '(', ''), ')', '') REGEXP '^00968[0-9]{8}$' + THEN RIGHT(REPLACE(REPLACE(REPLACE(REPLACE(phone, ' ', ''), '-', ''), '(', ''), ')', ''), 8) + ELSE NULL + END; + + IF NOT EXISTS ( + SELECT 1 + FROM INFORMATION_SCHEMA.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'customers' + AND INDEX_NAME = 'uniq_customers_phone_normalized' + ) THEN + ALTER TABLE customers + ADD UNIQUE KEY uniq_customers_phone_normalized (phone_normalized); + END IF; +END $$ +DELIMITER ; + +CALL apply_customers_phone_normalized_unique(); +DROP PROCEDURE IF EXISTS apply_customers_phone_normalized_unique; + +-- Optional verification after import: +-- SHOW COLUMNS FROM customers; +-- SHOW INDEX FROM customers; diff --git a/db/schema.sql b/db/schema.sql index ccfb4a6..103c871 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -29,10 +29,12 @@ CREATE TABLE IF NOT EXISTS `customers` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(150) NOT NULL, `phone` varchar(50) DEFAULT NULL, + `phone_normalized` varchar(8) DEFAULT NULL, `email` varchar(150) DEFAULT NULL, `address` text DEFAULT NULL, `created_at` datetime NOT NULL DEFAULT current_timestamp(), - PRIMARY KEY (`id`) + PRIMARY KEY (`id`), + UNIQUE KEY `uniq_customers_phone_normalized` (`phone_normalized`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Table structure for `expense_categories` diff --git a/edit_sale.php b/edit_sale.php index e17b04e..60a3a6e 100644 --- a/edit_sale.php +++ b/edit_sale.php @@ -628,8 +628,13 @@ async function saveNewCustomer() { custInput.value = data.customer.name + (data.customer.phone ? ' - ' + data.customer.phone : ''); document.getElementById('formCustomerId').value = data.customer.id; newCustomerModalObj.hide(); - const Toast = Swal.mixin({ toast: true, position: 'top-end', showConfirmButton: false, timer: 2000 }); - Toast.fire({ icon: 'success', title: '' }); + Swal.fire({ + icon: 'success', + text: '', + position: 'center', + confirmButtonText: '', + confirmButtonColor: '#0d6efd' + }); } else { Swal.fire({icon: 'warning', text: data.error}); } diff --git a/includes/app.php b/includes/app.php index d42eb50..b343dc7 100644 --- a/includes/app.php +++ b/includes/app.php @@ -159,6 +159,55 @@ try { @file_put_contents($flagFileV8, '1'); } + + $flagFileV9 = sys_get_temp_dir() . '/.schema_migrated_v9_' . md5(__DIR__); + if (!file_exists($flagFileV9)) { + $pdo = db(); + $hasCustomersTable = (bool) $pdo->query("SHOW TABLES LIKE 'customers'")->fetchColumn(); + if ($hasCustomersTable) { + $hasPhoneNormalizedColumn = (bool) $pdo->query("SHOW COLUMNS FROM customers LIKE 'phone_normalized'")->fetchColumn(); + if (!$hasPhoneNormalizedColumn) { + $pdo->exec("ALTER TABLE customers ADD COLUMN phone_normalized varchar(8) DEFAULT NULL AFTER phone"); + } + + $customerRows = $pdo->query("SELECT id, phone FROM customers ORDER BY id ASC")->fetchAll(PDO::FETCH_ASSOC); + $updateCustomerPhoneStmt = $pdo->prepare("UPDATE customers SET phone = :phone, phone_normalized = :phone_normalized WHERE id = :id"); + $seenNormalizedPhones = []; + $duplicateNormalizedPhones = []; + + foreach ($customerRows as $customerRow) { + $rawPhone = trim((string) ($customerRow['phone'] ?? '')); + $normalizedPhone = normalize_oman_phone($rawPhone); + $phoneForStorage = $normalizedPhone !== '' ? $normalizedPhone : ($rawPhone !== '' ? $rawPhone : null); + $phoneNormalizedForStorage = $normalizedPhone !== '' ? $normalizedPhone : null; + $customerId = (int) ($customerRow['id'] ?? 0); + + if ($phoneNormalizedForStorage !== null) { + if (isset($seenNormalizedPhones[$phoneNormalizedForStorage]) && $seenNormalizedPhones[$phoneNormalizedForStorage] !== $customerId) { + $duplicateNormalizedPhones[$phoneNormalizedForStorage] = true; + } else { + $seenNormalizedPhones[$phoneNormalizedForStorage] = $customerId; + } + } + + $updateCustomerPhoneStmt->bindValue(':phone', $phoneForStorage, $phoneForStorage === null ? PDO::PARAM_NULL : PDO::PARAM_STR); + $updateCustomerPhoneStmt->bindValue(':phone_normalized', $phoneNormalizedForStorage, $phoneNormalizedForStorage === null ? PDO::PARAM_NULL : PDO::PARAM_STR); + $updateCustomerPhoneStmt->bindValue(':id', $customerId, PDO::PARAM_INT); + $updateCustomerPhoneStmt->execute(); + } + + $hasPhoneNormalizedIndex = (bool) $pdo->query("SHOW INDEX FROM customers WHERE Key_name = 'uniq_customers_phone_normalized'")->fetchColumn(); + if (!$hasPhoneNormalizedIndex) { + if ($duplicateNormalizedPhones === []) { + $pdo->exec("ALTER TABLE customers ADD UNIQUE KEY uniq_customers_phone_normalized (phone_normalized)"); + } else { + error_log('Skipped adding uniq_customers_phone_normalized because duplicate normalized customer phones still exist: ' . implode(', ', array_keys($duplicateNormalizedPhones))); + } + } + } + + @file_put_contents($flagFileV9, '1'); + } } catch (\Throwable $e) {} @@ -298,6 +347,59 @@ function phone_display(?string $value): string return $local !== '' ? $local : $raw; } +function customer_phone_exists(string $phone, ?int $excludeCustomerId = null): bool +{ + $normalized = normalize_oman_phone($phone); + if ($normalized === '') { + return false; + } + + $variants = array_values(array_unique([ + $normalized, + '968' . $normalized, + '00968' . $normalized, + '0' . $normalized, + ])); + + $placeholders = []; + foreach ($variants as $index => $_variant) { + $placeholders[] = ':phone_' . $index; + } + + $sql = 'SELECT id FROM customers WHERE phone IN (' . implode(', ', $placeholders) . ')'; + if ($excludeCustomerId !== null && $excludeCustomerId > 0) { + $sql .= ' AND id <> :exclude_customer_id'; + } + $sql .= ' LIMIT 1'; + + try { + $stmt = db()->prepare($sql); + foreach ($variants as $index => $variant) { + $stmt->bindValue(':phone_' . $index, $variant); + } + if ($excludeCustomerId !== null && $excludeCustomerId > 0) { + $stmt->bindValue(':exclude_customer_id', $excludeCustomerId, PDO::PARAM_INT); + } + $stmt->execute(); + return (bool) $stmt->fetchColumn(); + } catch (Throwable $e) { + return false; + } +} + +function is_customer_phone_unique_violation(Throwable $e): bool +{ + $message = strtolower($e->getMessage()); + $code = (string) $e->getCode(); + + if (!in_array($code, ['23000', '1062'], true) && !str_contains($message, 'duplicate entry')) { + return false; + } + + return str_contains($message, 'uniq_customers_phone_normalized') + || str_contains($message, 'phone_normalized'); +} + function wablas_parse_phone_list(string $value): array { $parts = preg_split('/[\s,;،]+/', trim($value)) ?: []; diff --git a/includes/header.php b/includes/header.php index 2a1c9c4..2c32c38 100644 --- a/includes/header.php +++ b/includes/header.php @@ -12,7 +12,7 @@ $assetVersion = date('YmdHi'); // Determine if we are on a public page (like login) $isPublic = !empty($forcePublic) || !isset($user) || !$user; -$bodyClasses = trim(($isPublic ? 'auth-body ' : '') . ($bodyClass ?? '')); +$bodyClasses = $isPublic ? 'auth-body' : ''; ?> @@ -268,12 +268,10 @@ $bodyClasses = trim(($isPublic ? 'auth-body ' : '') . ($bodyClass ?? '')); document.addEventListener('DOMContentLoaded', function() { Swal.fire({ icon: '', - title: '', - toast: true, - position: 'top-end', - showConfirmButton: false, - timer: 3000, - timerProgressBar: true + text: '', + position: 'center', + confirmButtonText: '', + confirmButtonColor: '#0d6efd' }); }); diff --git a/includes/sale_form.php b/includes/sale_form.php index dad35d1..95830b6 100644 --- a/includes/sale_form.php +++ b/includes/sale_form.php @@ -635,8 +635,13 @@ async function saveNewCustomer() { custInput.value = data.customer.name + (data.customer.phone ? ' - 968 ' + data.customer.phone : ''); document.getElementById('formCustomerId').value = data.customer.id; newCustomerModalObj.hide(); - const Toast = Swal.mixin({ toast: true, position: 'top-end', showConfirmButton: false, timer: 2000 }); - Toast.fire({ icon: 'success', title: '' }); + Swal.fire({ + icon: 'success', + text: '', + position: 'center', + confirmButtonText: '', + confirmButtonColor: '#0d6efd' + }); } else { Swal.fire({icon: 'warning', text: data.error}); } diff --git a/index.php b/index.php index 47d9439..ab26d26 100644 --- a/index.php +++ b/index.php @@ -3,7 +3,6 @@ require_once __DIR__ . '/includes/app.php'; $user = require_auth(); $pageTitle = tr('لوحة التحكم', 'Dashboard'); $activeNav = 'dashboard'; -$bodyClass = 'theme-preview-sunset'; $dbError = null; $metrics = ['today_sales' => 0, 'today_revenue' => 0.0, 'pos_count' => 0, 'normal_count' => 0, 'recent' => []]; $reportMetrics = ['branch_totals' => [], 'payment_totals' => [], 'product_totals' => []]; @@ -49,46 +48,10 @@ require __DIR__ . '/includes/header.php';
-
-
-
-
-
- - - - -

-

-
-
-
-
- Sunset Orange - Teal - Gold - Rose -
-
-
- - Sunset Contrast -
-
- - -
-
-
-
-
-
-
-
-
+
@@ -100,7 +63,7 @@ require __DIR__ . '/includes/header.php';
-
+
@@ -112,7 +75,7 @@ require __DIR__ . '/includes/header.php';
-
+
POS
@@ -124,7 +87,7 @@ require __DIR__ . '/includes/header.php';
-
+
@@ -334,8 +297,6 @@ require __DIR__ . '/includes/header.php';
-
-