715 lines
21 KiB
PHP
715 lines
21 KiB
PHP
<?php
|
||
declare(strict_types=1);
|
||
|
||
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||
session_start();
|
||
}
|
||
|
||
require_once __DIR__ . '/../db/config.php';
|
||
|
||
date_default_timezone_set('UTC');
|
||
|
||
function app_env(string $key, string $default = ''): string
|
||
{
|
||
$serverValue = $_SERVER[$key] ?? null;
|
||
if (is_string($serverValue) && $serverValue !== '') {
|
||
return $serverValue;
|
||
}
|
||
|
||
$envValue = getenv($key);
|
||
return is_string($envValue) && $envValue !== '' ? $envValue : $default;
|
||
}
|
||
|
||
function app_base_path(): string
|
||
{
|
||
static $basePath = null;
|
||
if ($basePath !== null) {
|
||
return $basePath;
|
||
}
|
||
|
||
$scriptName = str_replace('\\', '/', (string) ($_SERVER['SCRIPT_NAME'] ?? ''));
|
||
$dir = str_replace('\\', '/', dirname($scriptName));
|
||
if ($dir === '/' || $dir === '.' || $dir === '\\') {
|
||
$basePath = '';
|
||
return $basePath;
|
||
}
|
||
|
||
$basePath = rtrim($dir, '/');
|
||
return $basePath;
|
||
}
|
||
|
||
function app_url(string $path = ''): string
|
||
{
|
||
$path = trim($path);
|
||
if ($path !== '' && (preg_match('#^[a-z][a-z0-9+.-]*://#i', $path) === 1 || str_starts_with($path, '//'))) {
|
||
return $path;
|
||
}
|
||
|
||
$basePath = app_base_path();
|
||
if ($path === '' || $path === '/') {
|
||
return $basePath !== '' ? $basePath . '/' : '/';
|
||
}
|
||
|
||
return ($basePath !== '' ? $basePath : '') . '/' . ltrim($path, '/');
|
||
}
|
||
|
||
function asset_url(string $path): string
|
||
{
|
||
return app_url($path);
|
||
}
|
||
|
||
function public_asset_url(string $path): string
|
||
{
|
||
$path = trim($path);
|
||
if ($path === '') {
|
||
return '';
|
||
}
|
||
|
||
if (preg_match('#^[a-z][a-z0-9+.-]*://#i', $path) === 1 || str_starts_with($path, '//') || str_starts_with($path, 'data:')) {
|
||
return $path;
|
||
}
|
||
|
||
return app_url(ltrim($path, '/'));
|
||
}
|
||
|
||
function e(?string $value): string
|
||
{
|
||
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
||
}
|
||
|
||
function schema_column_exists(string $table, string $column): bool
|
||
{
|
||
$stmt = db()->prepare(
|
||
'SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = :schema AND TABLE_NAME = :table_name AND COLUMN_NAME = :column_name'
|
||
);
|
||
$stmt->execute([
|
||
':schema' => DB_NAME,
|
||
':table_name' => $table,
|
||
':column_name' => $column,
|
||
]);
|
||
|
||
return (int) $stmt->fetchColumn() > 0;
|
||
}
|
||
|
||
function schema_index_exists(string $table, string $index): bool
|
||
{
|
||
$stmt = db()->prepare(
|
||
'SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = :schema AND TABLE_NAME = :table_name AND INDEX_NAME = :index_name'
|
||
);
|
||
$stmt->execute([
|
||
':schema' => DB_NAME,
|
||
':table_name' => $table,
|
||
':index_name' => $index,
|
||
]);
|
||
|
||
return (int) $stmt->fetchColumn() > 0;
|
||
}
|
||
|
||
function ensure_column_exists(string $table, string $column, string $definition): void
|
||
{
|
||
if (!schema_column_exists($table, $column)) {
|
||
db()->exec(sprintf('ALTER TABLE %s ADD COLUMN %s %s', $table, $column, $definition));
|
||
}
|
||
}
|
||
|
||
function ensure_index_exists(string $table, string $index, string $definition): void
|
||
{
|
||
if (!schema_index_exists($table, $index)) {
|
||
db()->exec(sprintf('ALTER TABLE %s ADD INDEX %s %s', $table, $index, $definition));
|
||
}
|
||
}
|
||
|
||
function ensure_app_schema(): void
|
||
{
|
||
static $done = false;
|
||
if ($done) {
|
||
return;
|
||
}
|
||
|
||
$usersSql = <<<'SQL'
|
||
CREATE TABLE IF NOT EXISTS app_users (
|
||
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||
name VARCHAR(120) NOT NULL,
|
||
email VARCHAR(190) NOT NULL,
|
||
password_hash VARCHAR(255) NOT NULL,
|
||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
UNIQUE KEY uniq_user_email (email)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||
SQL;
|
||
|
||
$itemsSql = <<<'SQL'
|
||
CREATE TABLE IF NOT EXISTS service_tracker_items (
|
||
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||
item_type ENUM('service','setting') NOT NULL DEFAULT 'service',
|
||
user_id INT UNSIGNED DEFAULT NULL,
|
||
slug_key VARCHAR(120) DEFAULT NULL,
|
||
vehicle_name VARCHAR(120) DEFAULT NULL,
|
||
vehicle_category VARCHAR(20) DEFAULT NULL,
|
||
plate_number VARCHAR(40) DEFAULT NULL,
|
||
service_name VARCHAR(80) DEFAULT NULL,
|
||
last_service_date DATE DEFAULT NULL,
|
||
reminder_interval_days INT DEFAULT NULL,
|
||
next_due_date DATE DEFAULT NULL,
|
||
odometer_km INT DEFAULT NULL,
|
||
notes TEXT DEFAULT NULL,
|
||
content_longtext LONGTEXT DEFAULT NULL,
|
||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
UNIQUE KEY uniq_item_slug (item_type, slug_key),
|
||
KEY idx_service_due (item_type, next_due_date),
|
||
KEY idx_vehicle_name (vehicle_name),
|
||
KEY idx_user_service_due (user_id, item_type, next_due_date)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||
SQL;
|
||
|
||
db()->exec($usersSql);
|
||
db()->exec($itemsSql);
|
||
|
||
ensure_column_exists('service_tracker_items', 'user_id', 'INT UNSIGNED DEFAULT NULL AFTER item_type');
|
||
ensure_index_exists('service_tracker_items', 'idx_user_service_due', '(user_id, item_type, next_due_date)');
|
||
|
||
$done = true;
|
||
}
|
||
|
||
ensure_app_schema();
|
||
|
||
function service_catalog(): array
|
||
{
|
||
return [
|
||
'Ganti oli mesin' => 90,
|
||
'Filter udara' => 120,
|
||
'Bersihin CVT' => 90,
|
||
'Vanbelt' => 180,
|
||
'Ganti oli gardan' => 180,
|
||
'Busi' => 180,
|
||
'Tune up ringan' => 120,
|
||
'Cek rem & kampas' => 90,
|
||
];
|
||
}
|
||
|
||
function normalize_email(string $email): string
|
||
{
|
||
return strtolower(trim($email));
|
||
}
|
||
|
||
function current_user(): ?array
|
||
{
|
||
$user = $_SESSION['user'] ?? null;
|
||
return is_array($user) ? $user : null;
|
||
}
|
||
|
||
function current_user_id(): int
|
||
{
|
||
return (int) ((current_user()['id'] ?? 0));
|
||
}
|
||
|
||
function current_user_name(): string
|
||
{
|
||
$user = current_user();
|
||
if (!$user) {
|
||
return '';
|
||
}
|
||
|
||
$name = trim((string) ($user['name'] ?? ''));
|
||
if ($name !== '') {
|
||
return $name;
|
||
}
|
||
|
||
return (string) ($user['email'] ?? '');
|
||
}
|
||
|
||
function is_user_logged_in(): bool
|
||
{
|
||
return current_user_id() > 0;
|
||
}
|
||
|
||
function login_user(array $user): void
|
||
{
|
||
$_SESSION['user'] = [
|
||
'id' => (int) ($user['id'] ?? 0),
|
||
'name' => (string) ($user['name'] ?? ''),
|
||
'email' => (string) ($user['email'] ?? ''),
|
||
];
|
||
session_regenerate_id(true);
|
||
}
|
||
|
||
function logout_user(): void
|
||
{
|
||
unset($_SESSION['user']);
|
||
session_regenerate_id(true);
|
||
}
|
||
|
||
function auth_redirect_target(string $default = ''): string
|
||
{
|
||
$fallback = $default !== '' ? $default : app_url('dashboard.php');
|
||
if (!str_starts_with($fallback, '/') && preg_match('#^[a-z][a-z0-9+.-]*://#i', $fallback) !== 1) {
|
||
$fallback = app_url($fallback);
|
||
}
|
||
|
||
$target = trim((string) ($_POST['redirect'] ?? $_GET['redirect'] ?? ''));
|
||
if ($target === '') {
|
||
return $fallback;
|
||
}
|
||
|
||
if (preg_match('#^[a-z][a-z0-9+.-]*://#i', $target) === 1 || str_starts_with($target, '//')) {
|
||
return $fallback;
|
||
}
|
||
|
||
return str_starts_with($target, '/') ? $target : app_url($target);
|
||
}
|
||
|
||
function require_user_login(): void
|
||
{
|
||
if (is_user_logged_in()) {
|
||
return;
|
||
}
|
||
|
||
set_flash('warning', 'Silakan login dulu supaya catatan servis hanya terlihat oleh akun kamu sendiri.');
|
||
$target = (string) ($_SERVER['REQUEST_URI'] ?? app_url('dashboard.php'));
|
||
header('Location: ' . app_url('login.php') . '?redirect=' . urlencode($target));
|
||
exit;
|
||
}
|
||
|
||
function find_user_by_email(string $email): ?array
|
||
{
|
||
$stmt = db()->prepare('SELECT * FROM app_users WHERE email = :email LIMIT 1');
|
||
$stmt->execute([
|
||
':email' => normalize_email($email),
|
||
]);
|
||
|
||
$row = $stmt->fetch();
|
||
return $row ?: null;
|
||
}
|
||
|
||
function create_user_account(string $name, string $email, string $password): int
|
||
{
|
||
$stmt = db()->prepare(
|
||
'INSERT INTO app_users (name, email, password_hash) VALUES (:name, :email, :password_hash)'
|
||
);
|
||
$stmt->execute([
|
||
':name' => trim($name),
|
||
':email' => normalize_email($email),
|
||
':password_hash' => password_hash($password, PASSWORD_DEFAULT),
|
||
]);
|
||
|
||
return (int) db()->lastInsertId();
|
||
}
|
||
|
||
function verify_user_login(string $email, string $password): ?array
|
||
{
|
||
$user = find_user_by_email($email);
|
||
if (!$user) {
|
||
return null;
|
||
}
|
||
|
||
return password_verify($password, (string) $user['password_hash']) ? $user : null;
|
||
}
|
||
|
||
function app_setting(string $key, string $default = ''): string
|
||
{
|
||
$stmt = db()->prepare('SELECT content_longtext FROM service_tracker_items WHERE item_type = :type AND slug_key = :slug LIMIT 1');
|
||
$stmt->execute([
|
||
':type' => 'setting',
|
||
':slug' => $key,
|
||
]);
|
||
$value = $stmt->fetchColumn();
|
||
|
||
return is_string($value) ? $value : $default;
|
||
}
|
||
|
||
function save_setting(string $key, string $value): void
|
||
{
|
||
$stmt = db()->prepare(
|
||
'INSERT INTO service_tracker_items (item_type, slug_key, content_longtext) VALUES (:type, :slug, :content)
|
||
ON DUPLICATE KEY UPDATE content_longtext = VALUES(content_longtext), updated_at = CURRENT_TIMESTAMP'
|
||
);
|
||
$stmt->execute([
|
||
':type' => 'setting',
|
||
':slug' => $key,
|
||
':content' => $value,
|
||
]);
|
||
}
|
||
|
||
function head_ad_code(): string
|
||
{
|
||
return app_setting('ads_head');
|
||
}
|
||
|
||
function body_ad_code(): string
|
||
{
|
||
return app_setting('ads_body');
|
||
}
|
||
|
||
function app_project_name(): string
|
||
{
|
||
return app_env('PROJECT_NAME', 'ServisIngat');
|
||
}
|
||
|
||
function app_brand_initial(): string
|
||
{
|
||
$projectName = trim(app_project_name());
|
||
if ($projectName === '') {
|
||
return 'S';
|
||
}
|
||
|
||
if (function_exists('mb_substr')) {
|
||
return mb_strtoupper((string) mb_substr($projectName, 0, 1, 'UTF-8'), 'UTF-8');
|
||
}
|
||
|
||
return strtoupper(substr($projectName, 0, 1));
|
||
}
|
||
|
||
function app_logo_url(): string
|
||
{
|
||
return app_setting('brand_logo_url');
|
||
}
|
||
|
||
function app_favicon_url(): string
|
||
{
|
||
return app_setting('brand_favicon_url');
|
||
}
|
||
|
||
function set_flash(string $type, string $message): void
|
||
{
|
||
$_SESSION['flash'] = [
|
||
'type' => $type,
|
||
'message' => $message,
|
||
];
|
||
}
|
||
|
||
function consume_flash(): ?array
|
||
{
|
||
if (empty($_SESSION['flash']) || !is_array($_SESSION['flash'])) {
|
||
return null;
|
||
}
|
||
|
||
$flash = $_SESSION['flash'];
|
||
unset($_SESSION['flash']);
|
||
return $flash;
|
||
}
|
||
|
||
function empty_dashboard_summary(): array
|
||
{
|
||
return [
|
||
'total_services' => 0,
|
||
'overdue_count' => 0,
|
||
'due_soon_count' => 0,
|
||
'last_update' => null,
|
||
];
|
||
}
|
||
|
||
function dashboard_summary(): array
|
||
{
|
||
$stmt = db()->query(
|
||
"SELECT
|
||
SUM(CASE WHEN item_type = 'service' THEN 1 ELSE 0 END) AS total_services,
|
||
SUM(CASE WHEN item_type = 'service' AND next_due_date < CURDATE() THEN 1 ELSE 0 END) AS overdue_count,
|
||
SUM(CASE WHEN item_type = 'service' AND next_due_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 14 DAY) THEN 1 ELSE 0 END) AS due_soon_count,
|
||
MAX(CASE WHEN item_type = 'service' THEN updated_at ELSE NULL END) AS last_update
|
||
FROM service_tracker_items"
|
||
);
|
||
|
||
$row = $stmt->fetch() ?: [];
|
||
return [
|
||
'total_services' => (int) ($row['total_services'] ?? 0),
|
||
'overdue_count' => (int) ($row['overdue_count'] ?? 0),
|
||
'due_soon_count' => (int) ($row['due_soon_count'] ?? 0),
|
||
'last_update' => $row['last_update'] ?? null,
|
||
];
|
||
}
|
||
|
||
function dashboard_summary_for_user(int $userId): array
|
||
{
|
||
if ($userId <= 0) {
|
||
return empty_dashboard_summary();
|
||
}
|
||
|
||
$stmt = db()->prepare(
|
||
"SELECT
|
||
COUNT(*) AS total_services,
|
||
SUM(CASE WHEN next_due_date < CURDATE() THEN 1 ELSE 0 END) AS overdue_count,
|
||
SUM(CASE WHEN next_due_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 14 DAY) THEN 1 ELSE 0 END) AS due_soon_count,
|
||
MAX(updated_at) AS last_update
|
||
FROM service_tracker_items
|
||
WHERE item_type = 'service' AND user_id = :user_id"
|
||
);
|
||
$stmt->execute([
|
||
':user_id' => $userId,
|
||
]);
|
||
|
||
$row = $stmt->fetch() ?: [];
|
||
return [
|
||
'total_services' => (int) ($row['total_services'] ?? 0),
|
||
'overdue_count' => (int) ($row['overdue_count'] ?? 0),
|
||
'due_soon_count' => (int) ($row['due_soon_count'] ?? 0),
|
||
'last_update' => $row['last_update'] ?? null,
|
||
];
|
||
}
|
||
|
||
function fetch_services(int $limit = 100): array
|
||
{
|
||
$stmt = db()->prepare(
|
||
'SELECT * FROM service_tracker_items WHERE item_type = :type ORDER BY next_due_date IS NULL, next_due_date ASC, updated_at DESC LIMIT :limit'
|
||
);
|
||
$stmt->bindValue(':type', 'service');
|
||
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
|
||
$stmt->execute();
|
||
return $stmt->fetchAll();
|
||
}
|
||
|
||
function fetch_services_for_user(int $userId, int $limit = 100): array
|
||
{
|
||
if ($userId <= 0) {
|
||
return [];
|
||
}
|
||
|
||
$stmt = db()->prepare(
|
||
'SELECT * FROM service_tracker_items WHERE item_type = :type AND user_id = :user_id ORDER BY next_due_date IS NULL, next_due_date ASC, updated_at DESC LIMIT :limit'
|
||
);
|
||
$stmt->bindValue(':type', 'service');
|
||
$stmt->bindValue(':user_id', $userId, PDO::PARAM_INT);
|
||
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
|
||
$stmt->execute();
|
||
return $stmt->fetchAll();
|
||
}
|
||
|
||
function fetch_recent_services(int $limit = 5): array
|
||
{
|
||
$stmt = db()->prepare(
|
||
'SELECT * FROM service_tracker_items WHERE item_type = :type ORDER BY created_at DESC LIMIT :limit'
|
||
);
|
||
$stmt->bindValue(':type', 'service');
|
||
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
|
||
$stmt->execute();
|
||
return $stmt->fetchAll();
|
||
}
|
||
|
||
function fetch_recent_services_for_user(int $userId, int $limit = 5): array
|
||
{
|
||
if ($userId <= 0) {
|
||
return [];
|
||
}
|
||
|
||
$stmt = db()->prepare(
|
||
'SELECT * FROM service_tracker_items WHERE item_type = :type AND user_id = :user_id ORDER BY created_at DESC LIMIT :limit'
|
||
);
|
||
$stmt->bindValue(':type', 'service');
|
||
$stmt->bindValue(':user_id', $userId, PDO::PARAM_INT);
|
||
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
|
||
$stmt->execute();
|
||
return $stmt->fetchAll();
|
||
}
|
||
|
||
function fetch_service_by_id(int $id): ?array
|
||
{
|
||
$stmt = db()->prepare('SELECT * FROM service_tracker_items WHERE item_type = :type AND id = :id LIMIT 1');
|
||
$stmt->execute([
|
||
':type' => 'service',
|
||
':id' => $id,
|
||
]);
|
||
$row = $stmt->fetch();
|
||
return $row ?: null;
|
||
}
|
||
|
||
function fetch_service_by_id_for_user(int $userId, int $id): ?array
|
||
{
|
||
if ($userId <= 0 || $id <= 0) {
|
||
return null;
|
||
}
|
||
|
||
$stmt = db()->prepare('SELECT * FROM service_tracker_items WHERE item_type = :type AND user_id = :user_id AND id = :id LIMIT 1');
|
||
$stmt->execute([
|
||
':type' => 'service',
|
||
':user_id' => $userId,
|
||
':id' => $id,
|
||
]);
|
||
$row = $stmt->fetch();
|
||
return $row ?: null;
|
||
}
|
||
|
||
function due_state(?string $dateValue): array
|
||
{
|
||
if (!$dateValue) {
|
||
return [
|
||
'label' => 'Belum dijadwalkan',
|
||
'class' => 'status-neutral',
|
||
'tone' => 'secondary',
|
||
'description' => 'Atur tanggal servis berikutnya untuk mulai dipantau.',
|
||
];
|
||
}
|
||
|
||
$today = new DateTimeImmutable('today');
|
||
$dueDate = new DateTimeImmutable($dateValue);
|
||
$diff = (int) $today->diff($dueDate)->format('%r%a');
|
||
|
||
if ($diff < 0) {
|
||
$days = abs($diff);
|
||
return [
|
||
'label' => 'Terlambat ' . $days . ' hari',
|
||
'class' => 'status-overdue',
|
||
'tone' => 'danger',
|
||
'description' => 'Sudah melewati jadwal, sebaiknya segera ditindaklanjuti.',
|
||
];
|
||
}
|
||
|
||
if ($diff <= 14) {
|
||
return [
|
||
'label' => 'Jatuh tempo ' . $diff . ' hari lagi',
|
||
'class' => 'status-soon',
|
||
'tone' => 'warning',
|
||
'description' => 'Sudah dekat dengan jadwal berikutnya, cocok masuk prioritas.',
|
||
];
|
||
}
|
||
|
||
return [
|
||
'label' => 'Aman ' . $diff . ' hari lagi',
|
||
'class' => 'status-ok',
|
||
'tone' => 'success',
|
||
'description' => 'Masih aman, tapi sudah tercatat untuk pengingat berikutnya.',
|
||
];
|
||
}
|
||
|
||
function validate_service_payload(array $data): array
|
||
{
|
||
$catalog = service_catalog();
|
||
$vehicleName = trim((string) ($data['vehicle_name'] ?? ''));
|
||
$vehicleCategory = trim((string) ($data['vehicle_category'] ?? ''));
|
||
$plateNumber = strtoupper(trim((string) ($data['plate_number'] ?? '')));
|
||
$serviceName = trim((string) ($data['service_name'] ?? ''));
|
||
$lastServiceDate = trim((string) ($data['last_service_date'] ?? ''));
|
||
$intervalDays = (int) ($data['reminder_interval_days'] ?? 0);
|
||
$odometerKm = trim((string) ($data['odometer_km'] ?? ''));
|
||
$notes = trim((string) ($data['notes'] ?? ''));
|
||
|
||
$errors = [];
|
||
|
||
if ($vehicleName === '' || strlen($vehicleName) < 3) {
|
||
$errors['vehicle_name'] = 'Nama kendaraan minimal 3 karakter.';
|
||
}
|
||
|
||
if (!in_array($vehicleCategory, ['Motor', 'Mobil'], true)) {
|
||
$errors['vehicle_category'] = 'Pilih jenis kendaraan.';
|
||
}
|
||
|
||
if ($serviceName === '') {
|
||
$errors['service_name'] = 'Pilih item servis utama.';
|
||
}
|
||
|
||
if ($lastServiceDate === '' || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $lastServiceDate)) {
|
||
$errors['last_service_date'] = 'Tanggal servis terakhir wajib diisi.';
|
||
}
|
||
|
||
if ($intervalDays < 14 || $intervalDays > 365) {
|
||
$errors['reminder_interval_days'] = 'Interval pengingat harus 14–365 hari.';
|
||
}
|
||
|
||
if ($odometerKm !== '' && (!ctype_digit($odometerKm) || (int) $odometerKm < 0)) {
|
||
$errors['odometer_km'] = 'Kilometer harus berupa angka positif.';
|
||
}
|
||
|
||
$nextDueDate = null;
|
||
if (!isset($errors['last_service_date']) && !isset($errors['reminder_interval_days'])) {
|
||
try {
|
||
$nextDueDate = (new DateTimeImmutable($lastServiceDate))
|
||
->modify('+' . $intervalDays . ' days')
|
||
->format('Y-m-d');
|
||
} catch (Throwable $exception) {
|
||
$errors['last_service_date'] = 'Tanggal servis terakhir tidak valid.';
|
||
}
|
||
}
|
||
|
||
return [
|
||
'errors' => $errors,
|
||
'clean' => [
|
||
'vehicle_name' => $vehicleName,
|
||
'vehicle_category' => $vehicleCategory,
|
||
'plate_number' => $plateNumber,
|
||
'service_name' => $serviceName,
|
||
'last_service_date' => $lastServiceDate,
|
||
'reminder_interval_days' => $intervalDays,
|
||
'next_due_date' => $nextDueDate,
|
||
'odometer_km' => $odometerKm === '' ? null : (int) $odometerKm,
|
||
'notes' => $notes,
|
||
'suggested_days' => $catalog[$serviceName] ?? null,
|
||
],
|
||
];
|
||
}
|
||
|
||
function create_service(array $payload): int
|
||
{
|
||
$userId = current_user_id();
|
||
if ($userId <= 0) {
|
||
throw new RuntimeException('User must be logged in before creating a service record.');
|
||
}
|
||
|
||
$stmt = db()->prepare(
|
||
'INSERT INTO service_tracker_items (
|
||
item_type, user_id, vehicle_name, vehicle_category, plate_number, service_name,
|
||
last_service_date, reminder_interval_days, next_due_date, odometer_km, notes
|
||
) VALUES (
|
||
:item_type, :user_id, :vehicle_name, :vehicle_category, :plate_number, :service_name,
|
||
:last_service_date, :reminder_interval_days, :next_due_date, :odometer_km, :notes
|
||
)'
|
||
);
|
||
|
||
$stmt->execute([
|
||
':item_type' => 'service',
|
||
':user_id' => $userId,
|
||
':vehicle_name' => $payload['vehicle_name'],
|
||
':vehicle_category' => $payload['vehicle_category'],
|
||
':plate_number' => $payload['plate_number'],
|
||
':service_name' => $payload['service_name'],
|
||
':last_service_date' => $payload['last_service_date'],
|
||
':reminder_interval_days' => $payload['reminder_interval_days'],
|
||
':next_due_date' => $payload['next_due_date'],
|
||
':odometer_km' => $payload['odometer_km'],
|
||
':notes' => $payload['notes'],
|
||
]);
|
||
|
||
return (int) db()->lastInsertId();
|
||
}
|
||
|
||
function is_admin_logged_in(): bool
|
||
{
|
||
return !empty($_SESSION['is_admin_logged_in']);
|
||
}
|
||
|
||
function admin_username(): string
|
||
{
|
||
return app_env('ADMIN_PORTAL_USER', 'admin');
|
||
}
|
||
|
||
function admin_default_password_hint(): string
|
||
{
|
||
return app_env('ADMIN_PORTAL_PASSWORD_HINT', 'servis123!');
|
||
}
|
||
|
||
function admin_password_hash(): string
|
||
{
|
||
$storedHash = app_setting('admin_password_hash');
|
||
if ($storedHash !== '') {
|
||
return $storedHash;
|
||
}
|
||
|
||
return app_env('ADMIN_PORTAL_PASSWORD_HASH', '$2y$10$riyTXC1R9fEPRr2T18rxUuycZVTjpQVvCDOmRQD4ID1EVWw9fyDHC');
|
||
}
|
||
|
||
function admin_has_custom_password(): bool
|
||
{
|
||
return app_setting('admin_password_hash') !== '';
|
||
}
|
||
|
||
function save_admin_password(string $password): void
|
||
{
|
||
save_setting('admin_password_hash', password_hash($password, PASSWORD_DEFAULT));
|
||
}
|
||
|
||
function verify_admin_login(string $username, string $password): bool
|
||
{
|
||
$expectedUser = admin_username();
|
||
$normalizedUsername = trim($username);
|
||
|
||
return hash_equals($expectedUser, $normalizedUsername) && password_verify($password, admin_password_hash());
|
||
}
|