update customers
This commit is contained in:
parent
96dbdba460
commit
7377170520
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 = ?');
|
||||
|
||||
@ -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;
|
||||
@ -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`
|
||||
|
||||
@ -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: '<?= h(tr('تم إضافة العميل', 'Customer added')) ?>' });
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
text: '<?= h(tr('تم إضافة العميل', 'Customer added')) ?>',
|
||||
position: 'center',
|
||||
confirmButtonText: '<?= h(tr('حسناً', 'OK')) ?>',
|
||||
confirmButtonColor: '#0d6efd'
|
||||
});
|
||||
} else {
|
||||
Swal.fire({icon: 'warning', text: data.error});
|
||||
}
|
||||
|
||||
102
includes/app.php
102
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)) ?: [];
|
||||
|
||||
@ -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' : '';
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="<?= h(current_lang()) ?>" dir="<?= is_rtl() ? 'rtl' : 'ltr' ?>">
|
||||
@ -268,12 +268,10 @@ $bodyClasses = trim(($isPublic ? 'auth-body ' : '') . ($bodyClass ?? ''));
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
Swal.fire({
|
||||
icon: '<?= h($flash['type']) === "danger" ? "error" : (h($flash['type']) === "warning" ? "warning" : "success") ?>',
|
||||
title: '<?= h($flash['message']) ?>',
|
||||
toast: true,
|
||||
position: 'top-end',
|
||||
showConfirmButton: false,
|
||||
timer: 3000,
|
||||
timerProgressBar: true
|
||||
text: '<?= h($flash['message']) ?>',
|
||||
position: 'center',
|
||||
confirmButtonText: '<?= h(tr("حسناً", "OK")) ?>',
|
||||
confirmButtonColor: '#0d6efd'
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -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: '<?= h(tr('تم إضافة العميل', 'Customer added')) ?>' });
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
text: '<?= h(tr('تم إضافة العميل', 'Customer added')) ?>',
|
||||
position: 'center',
|
||||
confirmButtonText: '<?= h(tr('حسناً', 'OK')) ?>',
|
||||
confirmButtonColor: '#0d6efd'
|
||||
});
|
||||
} else {
|
||||
Swal.fire({icon: 'warning', text: data.error});
|
||||
}
|
||||
|
||||
71
index.php
71
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';
|
||||
<div class="alert alert-danger mb-4"><i class="bi bi-exclamation-triangle me-2"></i><?= h(tr('تعذر تحميل بيانات قاعدة البيانات حالياً:', 'Database data could not be loaded right now:')) ?> <?= h($dbError) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="dashboard-sunset-sample">
|
||||
<section class="dashboard-hero card border-0 overflow-hidden mb-4">
|
||||
<div class="card-body p-4 p-lg-5">
|
||||
<div class="row align-items-center g-4">
|
||||
<div class="col-xl-8">
|
||||
<span class="dashboard-kicker">
|
||||
<i class="bi bi-stars"></i>
|
||||
<?= h(tr('معاينة الهوية الرسمية الجديدة', 'Official style preview')) ?>
|
||||
</span>
|
||||
<h1 class="dashboard-hero-title mt-3 mb-3"><?= h(tr('لوحة تحكم بلمسة غروب أكثر تميزاً للإدارة اليومية', 'A more distinctive sunset-style dashboard for daily operations')) ?></h1>
|
||||
<p class="dashboard-hero-copy mb-0"><?= h(tr('هذه ما تزال مجرد معاينة على لوحة التحكم فقط، ولكن الآن باتجاه Sunset Contrast بدرجات برتقالية ودافئة مع لمسات تركواز وذهبية. إذا أعجبك الشكل سأطبّق نفس الهوية على الشريط الجانبي والجداول والنماذج وباقي الصفحات.', 'This is still a dashboard-only preview, but now in the Sunset Contrast direction with warm orange, teal, gold, and rose accents. If you like it, I can extend the same identity to the sidebar, tables, forms, and the rest of the app.')) ?></p>
|
||||
</div>
|
||||
<div class="col-xl-4">
|
||||
<div class="dashboard-hero-panel">
|
||||
<div class="dashboard-chip-grid">
|
||||
<span class="dashboard-chip dashboard-chip--orange">Sunset Orange</span>
|
||||
<span class="dashboard-chip dashboard-chip--teal">Teal</span>
|
||||
<span class="dashboard-chip dashboard-chip--gold">Gold</span>
|
||||
<span class="dashboard-chip dashboard-chip--rose">Rose</span>
|
||||
</div>
|
||||
<div class="dashboard-hero-meta mt-4">
|
||||
<div>
|
||||
<small><?= h(tr('النمط', 'Style')) ?></small>
|
||||
<strong>Sunset Contrast</strong>
|
||||
</div>
|
||||
<div>
|
||||
<small><?= h(tr('النطاق', 'Scope')) ?></small>
|
||||
<strong><?= h(tr('الصفحة الرئيسية فقط', 'Dashboard only')) ?></strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Metrics Row -->
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-sm-6 col-xl-3">
|
||||
<div class="card dashboard-stat-card dashboard-stat-card--cyan h-100 border-0">
|
||||
<div class="card h-100 border-0 bg-primary text-white shadow-sm">
|
||||
<div class="card-body d-flex flex-column justify-content-between">
|
||||
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||
<h6 class="text-white-50 text-uppercase mb-0 fw-bold"><?= h(tr('مبيعات اليوم', 'Today sales')) ?></h6>
|
||||
@ -100,7 +63,7 @@ require __DIR__ . '/includes/header.php';
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6 col-xl-3">
|
||||
<div class="card dashboard-stat-card dashboard-stat-card--coral h-100 border-0">
|
||||
<div class="card h-100 border-0 bg-success text-white shadow-sm">
|
||||
<div class="card-body d-flex flex-column justify-content-between">
|
||||
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||
<h6 class="text-white-50 text-uppercase mb-0 fw-bold"><?= h(tr('إيراد اليوم', 'Today revenue')) ?></h6>
|
||||
@ -112,7 +75,7 @@ require __DIR__ . '/includes/header.php';
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6 col-xl-3">
|
||||
<div class="card dashboard-stat-card dashboard-stat-card--lime h-100 border-0">
|
||||
<div class="card h-100 border-0 bg-info text-white shadow-sm">
|
||||
<div class="card-body d-flex flex-column justify-content-between">
|
||||
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||
<h6 class="text-white-50 text-uppercase mb-0 fw-bold">POS</h6>
|
||||
@ -124,7 +87,7 @@ require __DIR__ . '/includes/header.php';
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6 col-xl-3">
|
||||
<div class="card dashboard-stat-card dashboard-stat-card--gold h-100 border-0">
|
||||
<div class="card h-100 border-0 bg-dark text-white shadow-sm">
|
||||
<div class="card-body d-flex flex-column justify-content-between">
|
||||
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||
<h6 class="text-white-50 text-uppercase mb-0 fw-bold"><?= h(tr('فاتورة', 'Invoice')) ?></h6>
|
||||
@ -334,8 +297,6 @@ require __DIR__ . '/includes/header.php';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
@ -348,11 +309,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
datasets: [{
|
||||
data: <?= json_encode($branchData) ?>,
|
||||
backgroundColor: [
|
||||
'rgba(255, 122, 0, 0.88)',
|
||||
'rgba(0, 209, 178, 0.88)',
|
||||
'rgba(255, 209, 102, 0.88)',
|
||||
'rgba(255, 77, 109, 0.86)',
|
||||
'rgba(125, 145, 175, 0.82)'
|
||||
'rgba(13, 110, 253, 0.88)',
|
||||
'rgba(25, 135, 84, 0.88)',
|
||||
'rgba(13, 202, 240, 0.88)',
|
||||
'rgba(255, 193, 7, 0.88)',
|
||||
'rgba(108, 117, 125, 0.82)'
|
||||
],
|
||||
borderWidth: 2,
|
||||
borderColor: '#ffffff',
|
||||
@ -385,8 +346,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
// Create gradient for bars
|
||||
let gradient = productsCtx.createLinearGradient(0, 0, 0, 300);
|
||||
gradient.addColorStop(0, 'rgba(255, 122, 0, 0.95)');
|
||||
gradient.addColorStop(1, 'rgba(0, 209, 178, 0.2)');
|
||||
gradient.addColorStop(0, 'rgba(13, 110, 253, 0.92)');
|
||||
gradient.addColorStop(1, 'rgba(13, 202, 240, 0.18)');
|
||||
|
||||
new Chart(productsCtx, {
|
||||
type: 'bar',
|
||||
@ -396,7 +357,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
label: '<?= h(tr("الكمية المباعة", "Qty Sold")) ?>',
|
||||
data: <?= json_encode($productData) ?>,
|
||||
backgroundColor: gradient,
|
||||
borderColor: '#FF7A00',
|
||||
borderColor: '#0d6efd',
|
||||
borderWidth: 1,
|
||||
borderRadius: 6,
|
||||
barPercentage: 0.6
|
||||
@ -432,8 +393,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
// Create gradient for line chart area
|
||||
let gradientMonthly = monthlyCtx.createLinearGradient(0, 0, 0, 350);
|
||||
gradientMonthly.addColorStop(0, 'rgba(255, 209, 102, 0.32)');
|
||||
gradientMonthly.addColorStop(1, 'rgba(255, 122, 0, 0.04)');
|
||||
gradientMonthly.addColorStop(0, 'rgba(13, 110, 253, 0.26)');
|
||||
gradientMonthly.addColorStop(1, 'rgba(13, 110, 253, 0.05)');
|
||||
|
||||
new Chart(monthlyCtx, {
|
||||
type: 'line',
|
||||
@ -443,10 +404,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
label: '<?= h(tr("إجمالي المبيعات", "Total Sales")) ?>',
|
||||
data: <?= json_encode($monthlyData) ?>,
|
||||
backgroundColor: gradientMonthly,
|
||||
borderColor: '#FFD166',
|
||||
borderColor: '#0d6efd',
|
||||
borderWidth: 3,
|
||||
pointBackgroundColor: '#ffffff',
|
||||
pointBorderColor: '#FF7A00',
|
||||
pointBorderColor: '#0d6efd',
|
||||
pointBorderWidth: 2,
|
||||
pointRadius: 4,
|
||||
pointHoverRadius: 6,
|
||||
|
||||
9
pos.php
9
pos.php
@ -776,8 +776,13 @@ async function saveNewCustomer() {
|
||||
custInput.value = data.customer.name + (data.customer.phone ? ' - 968 ' + data.customer.phone : '');
|
||||
custInput.dataset.id = data.customer.id;
|
||||
newCustomerModalObj.hide();
|
||||
const Toast = Swal.mixin({ toast: true, position: 'top-end', showConfirmButton: false, timer: 2000 });
|
||||
Toast.fire({ icon: 'success', title: '<?= h(tr('تم إضافة العميل', 'Customer added')) ?>' });
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
text: '<?= h(tr('تم إضافة العميل', 'Customer added')) ?>',
|
||||
position: 'center',
|
||||
confirmButtonText: '<?= h(tr('حسناً', 'OK')) ?>',
|
||||
confirmButtonColor: '#0d6efd'
|
||||
});
|
||||
} else {
|
||||
Swal.fire({icon: 'warning', text: data.error});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user