diff --git a/complete_schema.sql b/complete_schema.sql index fbe6e17..f4b9367 100644 --- a/complete_schema.sql +++ b/complete_schema.sql @@ -12,7 +12,7 @@ /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; -- Auto-generated full schema snapshot for fresh installs. --- Re-run refresh_complete_schema.php after schema changes so new installations stay current. +-- Re-run refresh_complete_schema.php after schema or migration changes so new installations stay current. -- -- Table structure for table `acc_accounts` diff --git a/db/install_baseline_migrations.php b/db/install_baseline_migrations.php new file mode 100644 index 0000000..3fe5232 --- /dev/null +++ b/db/install_baseline_migrations.php @@ -0,0 +1,56 @@ + 'VARCHAR(255) DEFAULT NULL', + 'name_ar' => 'VARCHAR(255) DEFAULT NULL', + 'created_at' => 'TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP', + ]; + + foreach ($columns as $column => $definition) { + if (payment_methods_schema_sync_20260502_column_exists($pdo, 'payment_methods', $column)) { + continue; + } + + payment_methods_schema_sync_20260502_exec( + $pdo, + sprintf( + 'ALTER TABLE `payment_methods` ADD COLUMN `%s` %s', + str_replace('`', '', $column), + $definition + ) + ); + } + + payment_methods_schema_sync_20260502_backfill_legacy_names($pdo); + payment_methods_schema_sync_20260502_seed_defaults($pdo); + } + + function payment_methods_schema_sync_20260502_table_exists(PDO $pdo, string $table): bool + { + $stmt = $pdo->prepare( + 'SELECT 1 FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :table LIMIT 1' + ); + $stmt->execute(['table' => $table]); + + return (bool)$stmt->fetchColumn(); + } + + function payment_methods_schema_sync_20260502_column_exists(PDO $pdo, string $table, string $column): bool + { + $stmt = $pdo->prepare( + 'SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :table AND COLUMN_NAME = :column LIMIT 1' + ); + $stmt->execute([ + 'table' => $table, + 'column' => $column, + ]); + + return (bool)$stmt->fetchColumn(); + } + + function payment_methods_schema_sync_20260502_backfill_legacy_names(PDO $pdo): void + { + if (!payment_methods_schema_sync_20260502_table_exists($pdo, 'payment_methods')) { + return; + } + + $hasLegacyName = payment_methods_schema_sync_20260502_column_exists($pdo, 'payment_methods', 'name'); + $hasNameEn = payment_methods_schema_sync_20260502_column_exists($pdo, 'payment_methods', 'name_en'); + $hasNameAr = payment_methods_schema_sync_20260502_column_exists($pdo, 'payment_methods', 'name_ar'); + + if ($hasLegacyName && $hasNameEn) { + $pdo->exec( + "UPDATE `payment_methods` + SET `name_en` = CASE + WHEN `name_en` IS NULL OR TRIM(`name_en`) = '' THEN `name` + ELSE `name_en` + END + WHERE `name` IS NOT NULL AND TRIM(`name`) <> ''" + ); + } + + if ($hasNameAr) { + $sourceExpression = $hasLegacyName ? "COALESCE(NULLIF(`name_en`, ''), `name`)" : '`name_en`'; + $pdo->exec( + "UPDATE `payment_methods` + SET `name_ar` = $sourceExpression + WHERE (`name_ar` IS NULL OR TRIM(`name_ar`) = '') + AND $sourceExpression IS NOT NULL + AND TRIM($sourceExpression) <> ''" + ); + } + } + + function payment_methods_schema_sync_20260502_seed_defaults(PDO $pdo): void + { + if (!payment_methods_schema_sync_20260502_table_exists($pdo, 'payment_methods')) { + return; + } + + $count = (int)$pdo->query('SELECT COUNT(*) FROM `payment_methods`')->fetchColumn(); + if ($count > 0) { + return; + } + + $defaults = [ + ['Cash', 'كاش'], + ['Credit Card', 'بطاقة بنكية'], + ['Bank Transfer', 'تحويل بنكي'], + ]; + + $stmt = $pdo->prepare('INSERT INTO `payment_methods` (`name_en`, `name_ar`) VALUES (?, ?)'); + foreach ($defaults as [$nameEn, $nameAr]) { + $stmt->execute([$nameEn, $nameAr]); + } + } + + function payment_methods_schema_sync_20260502_exec(PDO $pdo, string $statement): void + { + try { + $pdo->exec($statement); + } catch (PDOException $exception) { + if (payment_methods_schema_sync_20260502_is_ignorable($exception)) { + return; + } + + throw $exception; + } + } + + function payment_methods_schema_sync_20260502_is_ignorable(PDOException $exception): bool + { + $driverCode = isset($exception->errorInfo[1]) ? (int)$exception->errorInfo[1] : null; + $message = strtolower($exception->getMessage()); + $ignorableCodes = [1050, 1060, 1061, 1062, 1091, 1826]; + $ignorableSnippets = [ + 'already exists', + 'duplicate column name', + 'duplicate key name', + 'duplicate entry', + 'duplicate foreign key constraint name', + 'duplicate key on write or update', + 'errno: 121', + 'check that column/key exists', + ]; + + if ($driverCode !== null && in_array($driverCode, $ignorableCodes, true)) { + return true; + } + + foreach ($ignorableSnippets as $snippet) { + if (str_contains($message, $snippet)) { + return true; + } + } + + return false; + } +} + +payment_methods_schema_sync_20260502_run(); + +return true; diff --git a/includes/DatabaseInstaller.php b/includes/DatabaseInstaller.php index 28bf08f..25dfd4e 100644 --- a/includes/DatabaseInstaller.php +++ b/includes/DatabaseInstaller.php @@ -29,6 +29,7 @@ class DatabaseInstaller { self::executeSqlFile($seedFile); } + self::seedInstallMigrationBaseline($pdo, $schemaFile); self::ensureCurrentSchema(); return true; @@ -52,6 +53,57 @@ class DatabaseInstaller { return (int) $stmt->fetchColumn() === 0; } + private static function seedInstallMigrationBaseline(PDO $pdo, string $schemaFile): void { + if (!self::shouldSeedInstallMigrationBaseline($schemaFile)) { + return; + } + + $baselineMigrations = self::getInstallBaselineMigrations(); + if ($baselineMigrations === []) { + return; + } + + self::ensureMigrationsTable($pdo); + foreach ($baselineMigrations as $migrationName) { + self::recordMigration($pdo, $migrationName); + } + } + + private static function shouldSeedInstallMigrationBaseline(string $schemaFile): bool { + $completeSchemaFile = __DIR__ . '/../complete_schema.sql'; + $schemaRealPath = realpath($schemaFile); + $completeSchemaRealPath = realpath($completeSchemaFile); + + if ($schemaRealPath === false || $completeSchemaRealPath === false) { + return false; + } + + return $schemaRealPath === $completeSchemaRealPath; + } + + private static function getInstallBaselineMigrations(): array { + $baselineFile = __DIR__ . '/../db/install_baseline_migrations.php'; + if (!is_file($baselineFile)) { + return []; + } + + $baseline = require $baselineFile; + if (!is_array($baseline)) { + return []; + } + + $normalized = []; + foreach ($baseline as $migrationName) { + if (!is_string($migrationName) || trim($migrationName) === '') { + continue; + } + + $normalized[basename($migrationName)] = true; + } + + return array_keys($normalized); + } + public static function ensureCurrentSchema(): void { require_once __DIR__ . '/../db/config.php'; diff --git a/index.php b/index.php index 3fad45c..27131b1 100644 --- a/index.php +++ b/index.php @@ -1973,11 +1973,97 @@ function getPromotionalPrice($item) { } if (isset($_POST['add_payment_method'])) { - $name = $_POST['name'] ?? ''; - db()->prepare("INSERT INTO payment_methods (name) VALUES (?)")->execute([$name]); + $name_en = trim((string)($_POST['name_en'] ?? '')); + $name_ar = trim((string)($_POST['name_ar'] ?? '')); + + if ($name_en === '' && $name_ar === '') { + redirectWithMessage("Please enter a payment method name.", "index.php?page=payment_methods"); + } + + if ($name_en === '') { + $name_en = $name_ar; + } + if ($name_ar === '') { + $name_ar = $name_en; + } + + if (db_column_exists('payment_methods', 'name_en') || db_column_exists('payment_methods', 'name_ar')) { + $columns = []; + $placeholders = []; + $values = []; + + if (db_column_exists('payment_methods', 'name_en')) { + $columns[] = 'name_en'; + $placeholders[] = '?'; + $values[] = $name_en; + } + if (db_column_exists('payment_methods', 'name_ar')) { + $columns[] = 'name_ar'; + $placeholders[] = '?'; + $values[] = $name_ar; + } + + db()->prepare("INSERT INTO payment_methods (" . implode(', ', $columns) . ") VALUES (" . implode(', ', $placeholders) . ")")->execute($values); + } elseif (db_column_exists('payment_methods', 'name')) { + db()->prepare("INSERT INTO payment_methods (`name`) VALUES (?)")->execute([$name_en]); + } else { + throw new RuntimeException('payment_methods table is missing a usable name column.'); + } + redirectWithMessage("Payment method added!", "index.php?page=payment_methods"); } + if (isset($_POST['edit_payment_method'])) { + $id = (int)($_POST['id'] ?? 0); + $name_en = trim((string)($_POST['name_en'] ?? '')); + $name_ar = trim((string)($_POST['name_ar'] ?? '')); + + if ($id <= 0) { + redirectWithMessage("Invalid payment method.", "index.php?page=payment_methods"); + } + if ($name_en === '' && $name_ar === '') { + redirectWithMessage("Please enter a payment method name.", "index.php?page=payment_methods"); + } + + if ($name_en === '') { + $name_en = $name_ar; + } + if ($name_ar === '') { + $name_ar = $name_en; + } + + if (db_column_exists('payment_methods', 'name_en') || db_column_exists('payment_methods', 'name_ar')) { + $sets = []; + $values = []; + + if (db_column_exists('payment_methods', 'name_en')) { + $sets[] = 'name_en = ?'; + $values[] = $name_en; + } + if (db_column_exists('payment_methods', 'name_ar')) { + $sets[] = 'name_ar = ?'; + $values[] = $name_ar; + } + + $values[] = $id; + db()->prepare("UPDATE payment_methods SET " . implode(', ', $sets) . " WHERE id = ?")->execute($values); + } elseif (db_column_exists('payment_methods', 'name')) { + db()->prepare("UPDATE payment_methods SET `name` = ? WHERE id = ?")->execute([$name_en, $id]); + } else { + throw new RuntimeException('payment_methods table is missing a usable name column.'); + } + + redirectWithMessage("Payment method updated!", "index.php?page=payment_methods"); + } + + if (isset($_POST['delete_payment_method'])) { + $id = (int)($_POST['id'] ?? 0); + if ($id > 0) { + db()->prepare("DELETE FROM payment_methods WHERE id = ?")->execute([$id]); + } + redirectWithMessage("Payment method deleted!", "index.php?page=payment_methods"); + } + if (isset($_POST['delete_invoice'])) { $id = (int)$_POST['id']; $type = ($page === 'purchases') ? 'purchase' : 'sale'; @@ -7647,6 +7733,7 @@ $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System'; +