diff --git a/bin/healthcheck.php b/bin/healthcheck.php index b5be8e5..97db3c3 100644 --- a/bin/healthcheck.php +++ b/bin/healthcheck.php @@ -63,7 +63,7 @@ function deployment_format_check_report(array $checks): array function deployment_check_schema(PDO $pdo): bool { - foreach (['roles', 'permissions', 'role_permissions', 'users', 'app_settings', 'clients', 'client_contacts', 'jobcard_sequences', 'technical_information', 'credentials', 'sla_agreements', 'jobcards', 'jobcard_assignments', 'jobcard_status_history', 'time_entries', 'attachments', 'notifications', 'audit_events'] as $table) { + foreach (['roles', 'permissions', 'role_permissions', 'users', 'email_settings', 'saved_filters', 'app_settings', 'clients', 'client_contacts', 'jobcard_sequences', 'technical_information', 'credentials', 'sla_agreements', 'jobcards', 'jobcard_assignments', 'jobcard_status_history', 'time_entries', 'attachments', 'notifications', 'audit_events'] as $table) { $quoted = '`' . str_replace('`', '``', $table) . '`'; $pdo->query("SELECT 1 FROM {$quoted} LIMIT 1"); } diff --git a/database/schema.sql b/database/schema.sql index 7a1db94..8b1e747 100644 --- a/database/schema.sql +++ b/database/schema.sql @@ -32,6 +32,37 @@ CREATE TABLE IF NOT EXISTS users ( FOREIGN KEY (role_id) REFERENCES roles(id) ) ENGINE=InnoDB; +CREATE TABLE IF NOT EXISTS email_settings ( + id TINYINT UNSIGNED PRIMARY KEY, + smtp_host VARCHAR(190) NULL, + smtp_port SMALLINT UNSIGNED NULL, + smtp_encryption ENUM('none','tls','ssl') NOT NULL DEFAULT 'tls', + smtp_username VARCHAR(190) NULL, + smtp_password_ciphertext TEXT NULL, + from_email VARCHAR(190) NULL, + from_name VARCHAR(190) NULL, + notification_recipients TEXT NULL, + assignment_enabled BOOLEAN NOT NULL DEFAULT TRUE, + status_enabled BOOLEAN NOT NULL DEFAULT TRUE, + sla_enabled BOOLEAN NOT NULL DEFAULT TRUE, + overdue_enabled BOOLEAN NOT NULL DEFAULT TRUE, + updated_by BIGINT UNSIGNED NULL, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL +) ENGINE=InnoDB; + +CREATE TABLE IF NOT EXISTS saved_filters ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT UNSIGNED NOT NULL, + name VARCHAR(120) NOT NULL, + route VARCHAR(60) NOT NULL, + filter_json JSON NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + UNIQUE KEY saved_filter_user_name (user_id, name), + INDEX saved_filter_route_idx (user_id, route) +) ENGINE=InnoDB; + CREATE TABLE IF NOT EXISTS app_settings ( setting_key VARCHAR(80) PRIMARY KEY, setting_value TEXT NULL, diff --git a/database/upgrade.sql b/database/upgrade.sql index 48a919d..2b8cb2c 100644 --- a/database/upgrade.sql +++ b/database/upgrade.sql @@ -7,7 +7,38 @@ CREATE TABLE IF NOT EXISTS app_settings ( FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL ) ENGINE=InnoDB; --- Take a database backup first. Run with the target database selected: +CREATE TABLE IF NOT EXISTS email_settings ( + id TINYINT UNSIGNED PRIMARY KEY, + smtp_host VARCHAR(190) NULL, + smtp_port SMALLINT UNSIGNED NULL, + smtp_encryption ENUM('none','tls','ssl') NOT NULL DEFAULT 'tls', + smtp_username VARCHAR(190) NULL, + smtp_password_ciphertext TEXT NULL, + from_email VARCHAR(190) NULL, + from_name VARCHAR(190) NULL, + notification_recipients TEXT NULL, + assignment_enabled BOOLEAN NOT NULL DEFAULT TRUE, + status_enabled BOOLEAN NOT NULL DEFAULT TRUE, + sla_enabled BOOLEAN NOT NULL DEFAULT TRUE, + overdue_enabled BOOLEAN NOT NULL DEFAULT TRUE, + updated_by BIGINT UNSIGNED NULL, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL +) ENGINE=InnoDB; + +CREATE TABLE IF NOT EXISTS saved_filters ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT UNSIGNED NOT NULL, + name VARCHAR(120) NOT NULL, + route VARCHAR(60) NOT NULL, + filter_json JSON NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + UNIQUE KEY saved_filter_user_name (user_id, name), + INDEX saved_filter_route_idx (user_id, route) +) ENGINE=InnoDB; + + -- mysql --default-character-set=utf8mb4 -u USER -p DATABASE < database/upgrade.sql -- Resolve duplicate SLA rows before adding the unique client constraint. diff --git a/public/assets/app.css b/public/assets/app.css index be410b0..9bf4902 100644 --- a/public/assets/app.css +++ b/public/assets/app.css @@ -8,7 +8,7 @@ --jc-border: #e8eaf2; --jc-shadow: 0 14px 38px rgba(20, 28, 55, .08); } -.brand-logo{width:48px;height:48px;object-fit:contain;border-radius:8px;background:#fff;padding:4px}.brand-preview{max-width:220px;max-height:90px;object-fit:contain;border:1px solid var(--jc-border);border-radius:10px;padding:8px;background:#fff} +.brand-logo{width:48px;height:48px;object-fit:contain;border-radius:8px;background:#fff;padding:4px}.brand-preview{max-width:220px;max-height:90px;object-fit:contain;border:1px solid var(--jc-border);border-radius:10px;padding:8px;background:#fff}.nav-search{max-width:520px;width:100%}.technician-bottom-nav{display:none} * { box-sizing: border-box; } body { background: var(--jc-bg); color: var(--jc-ink); font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; letter-spacing: -.01em; } a { color: var(--jc-primary); } @@ -45,8 +45,14 @@ main { min-width: 0; } @media (max-width: 767.98px) { .desktop-table { display: none; } .mobile-card { display: block; } - .sidebar-link { padding: .55rem .7rem; } - .client-tabs { overflow-x: auto; flex-wrap: nowrap; scrollbar-width: thin; } + .sidebar-link { padding: .75rem .8rem; min-height:44px; } + .btn, .form-control, .form-select { min-height:44px; } + .technician-bottom-nav { display:flex; position:fixed; z-index:1030; left:0; right:0; bottom:0; background:#fff; border-top:1px solid var(--jc-border); box-shadow:0 -8px 24px rgba(20,28,55,.12); padding:.45rem; justify-content:space-around; } + .technician-bottom-nav a { color:var(--jc-primary); font-weight:700; text-decoration:none; padding:.55rem .8rem; } + body:has(.technician-bottom-nav) { padding-bottom:74px; } + .nav-search { max-width:none; } + main { padding-bottom:1.5rem; } + .client-tab { white-space: nowrap; padding: .62rem .8rem; } } @media (min-width: 768px) { .mobile-card { display: none; } } diff --git a/public/index.php b/public/index.php index dee38a2..9bdd81c 100644 --- a/public/index.php +++ b/public/index.php @@ -49,7 +49,7 @@ function render_header(string $title): void $brandLabel = $logoFile ? $brandMark : e($brandName); echo '