Initial version upload

This commit is contained in:
Michael Staake
2025-11-03 11:04:46 -08:00
parent 8a3c48b98a
commit 0083f20e8c
26 changed files with 2281 additions and 0 deletions

147
DOCKER.md Normal file
View File

@@ -0,0 +1,147 @@
# Docker Setup for Mainty
## Quick Start
### Build and Run
```bash
# Build the Docker image
docker-compose build
# Start the container
docker-compose up -d
# View logs
docker-compose logs -f
```
The application will be available at: **http://localhost:8080**
### Stop the Application
```bash
docker-compose down
```
## What's Included
- **PHP 8.4** with Apache
- **SQLite** with PDO extension
- **mod_rewrite** enabled for pretty URLs
- **Persistent data** volume for the database
## Configuration
### Change Port
Edit `docker-compose.yml` and modify the ports section:
```yaml
ports:
- "3000:80" # Access on port 3000 instead
```
### Development Mode
To sync code changes without rebuilding, uncomment this line in `docker-compose.yml`:
```yaml
volumes:
- ./data:/var/www/html/data
- .:/var/www/html # Uncomment this line
```
Then restart:
```bash
docker-compose restart
```
## Data Persistence
The SQLite database is stored in the `./data` directory on your host machine, which is mounted to the container. This means:
- ✅ Your data persists even if you stop/remove the container
- ✅ You can backup by copying the `data` folder
- ✅ Database survives container rebuilds
## Useful Commands
```bash
# Rebuild after code changes
docker-compose up -d --build
# View container logs
docker-compose logs -f mainty
# Access container shell
docker-compose exec mainty bash
# Stop and remove everything
docker-compose down -v
# Check container status
docker-compose ps
```
## Troubleshooting
### Permission Issues
If you get permission errors with the database:
```bash
chmod -R 755 data
```
### Port Already in Use
If port 8080 is already in use, change it in `docker-compose.yml`:
```yaml
ports:
- "8081:80" # Use a different port
```
### Reset Everything
To start fresh:
```bash
docker-compose down -v
rm -rf data/mainty.db
docker-compose up -d
```
Then access http://localhost:8080 to run setup again.
## Production Deployment
For production:
1. Set `DEBUG` to `false` in `config.php`
2. Use environment variables for sensitive data
3. Consider using a reverse proxy (nginx) in front
4. Enable HTTPS
Example production `docker-compose.yml`:
```yaml
version: '3.8'
services:
mainty:
build: .
container_name: mainty-app
ports:
- "8080:80"
volumes:
- ./data:/var/www/html/data
environment:
- APACHE_DOCUMENT_ROOT=/var/www/html
restart: always
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
```

31
Dockerfile Normal file
View File

@@ -0,0 +1,31 @@
FROM php:8.4-apache
# Enable Apache mod_rewrite
RUN a2enmod rewrite
# Install SQLite extensions
RUN apt-get update && apt-get install -y \
sqlite3 \
libsqlite3-dev \
&& docker-php-ext-install pdo_sqlite \
&& rm -rf /var/lib/apt/lists/*
# Set the working directory
WORKDIR /var/www/html
# Copy application files
COPY . /var/www/html/
# Create data directory and set permissions
RUN mkdir -p /var/www/html/data && \
chown -R www-data:www-data /var/www/html/data && \
chmod -R 755 /var/www/html/data
# Configure Apache to allow .htaccess
RUN sed -i '/<Directory \/var\/www\/>/,/<\/Directory>/ s/AllowOverride None/AllowOverride All/' /etc/apache2/apache2.conf
# Expose port 80
EXPOSE 80
# Start Apache
CMD ["apache2-foreground"]

39
README.md Normal file
View File

@@ -0,0 +1,39 @@
# Mainty
A simple PHP web app for tracking vehicle maintenance records. Free, simple, open source, and self-hosted. Runs on any Apache/PHP web server, or use Docker. Uses SQLite for easy backup, with built-in Export via JSON or HTML so you can import that data into something else or print records for your mechanic or the next owner of your vehicle.
## Requirements
- Apache web server
- PHP 8 or higher
- SQLite extension
## Installation
### Option 1: Traditional Web Server
1. Upload the entire folder to your web server
2. Rename `example.htaccess` to `.htaccess`
3. If the app is not in the root directory, edit `.htaccess` and set the `RewriteBase`:
```apache
RewriteBase /subfolder/
```
4. Navigate to the app URL in your browser
5. If everything is configured correctly, you'll see the setup page
6. Set your password to initialize the database
### Option 2: Docker
```bash
docker-compose up -d
```
Then open http://localhost:8080
## First Time Setup
When you first access the app, you'll be prompted to:
1. Create a password
2. Initialize the database
That's it! You're ready to start tracking your vehicle maintenance.

35
config.php Normal file
View File

@@ -0,0 +1,35 @@
<?php
// Debug configuration
define('DEBUG', false);
if (DEBUG) {
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
} else {
ini_set('display_errors', 0);
error_reporting(0);
}
// Database configuration
define('DB_PATH', BASE_PATH . '/data/mainty.db');
define('DB_DIR', BASE_PATH . '/data');
// Application configuration
define('APP_NAME', 'Mainty');
define('ITEMS_PER_PAGE', 20);
// Base URL configuration - auto-detect the base path
$scriptName = dirname($_SERVER['SCRIPT_NAME']);
define('BASE_URL', $scriptName === '/' ? '' : $scriptName);
// Helper function for URLs
function url($path) {
return BASE_URL . $path;
}
// Ensure data directory exists
if (!file_exists(DB_DIR)) {
mkdir(DB_DIR, 0755, true);
}

View File

@@ -0,0 +1,41 @@
<?php
class AuthController extends Controller {
public function login(): void {
$this->requireSetup();
if (isset($_SESSION['user_id'])) {
$this->redirect('/home');
return;
}
$this->view('login');
}
public function authenticate(): void {
$this->requireSetup();
$password = $_POST['password'] ?? '';
if (empty($password)) {
$_SESSION['error'] = 'Password is required';
$this->redirect('/login');
return;
}
$user = new User();
if ($user->verifyPassword($password)) {
$_SESSION['user_id'] = $user->getId();
$this->redirect('/home');
} else {
$_SESSION['error'] = 'Invalid password';
$this->redirect('/login');
}
}
public function logout(): void {
session_destroy();
$this->redirect('/login');
}
}

View File

@@ -0,0 +1,119 @@
<?php
class MaintenanceController extends Controller {
private Maintenance $maintenanceModel;
public function __construct() {
$this->maintenanceModel = new Maintenance();
}
public function add(): void {
$this->requireSetup();
$this->requireAuth();
$vehicleId = (int)($_POST['vehicle_id'] ?? 0);
$name = trim($_POST['name'] ?? '');
$date = trim($_POST['date'] ?? '');
$mileage = str_replace(',', '', trim($_POST['mileage'] ?? ''));
if (empty($name) || empty($date) || empty($mileage)) {
$_SESSION['error'] = 'Name, date, and mileage are required';
$this->redirect('/vehicles/' . $vehicleId);
return;
}
$data = [
'vehicle_id' => $vehicleId,
'name' => $name,
'date' => $date,
'mileage' => (int)$mileage,
'description' => trim($_POST['description'] ?? ''),
'cost' => !empty($_POST['cost']) ? (float)$_POST['cost'] : null,
'parts_list' => trim($_POST['parts_list'] ?? ''),
'performed_by' => trim($_POST['performed_by'] ?? '')
];
$this->maintenanceModel->create($data);
$_SESSION['success'] = 'Maintenance item added successfully';
$this->redirect('/vehicles/' . $vehicleId);
}
public function edit(string $id): void {
$this->requireSetup();
$this->requireAuth();
$item = $this->maintenanceModel->getById((int)$id);
if (!$item) {
$_SESSION['error'] = 'Maintenance item not found';
$this->redirect('/home');
return;
}
$name = trim($_POST['name'] ?? '');
$date = trim($_POST['date'] ?? '');
$mileage = str_replace(',', '', trim($_POST['mileage'] ?? ''));
if (empty($name) || empty($date) || empty($mileage)) {
$_SESSION['error'] = 'Name, date, and mileage are required';
$this->redirect('/vehicles/' . $item['vehicle_id']);
return;
}
$data = [
'name' => $name,
'date' => $date,
'mileage' => (int)$mileage,
'description' => trim($_POST['description'] ?? ''),
'cost' => !empty($_POST['cost']) ? (float)$_POST['cost'] : null,
'parts_list' => trim($_POST['parts_list'] ?? ''),
'performed_by' => trim($_POST['performed_by'] ?? '')
];
if ($this->maintenanceModel->update((int)$id, $data)) {
$_SESSION['success'] = 'Maintenance item updated successfully';
} else {
$_SESSION['error'] = 'Failed to update maintenance item';
}
$this->redirect('/vehicles/' . $item['vehicle_id']);
}
public function delete(string $id): void {
$this->requireSetup();
$this->requireAuth();
$item = $this->maintenanceModel->getById((int)$id);
if (!$item) {
$_SESSION['error'] = 'Maintenance item not found';
$this->redirect('/home');
return;
}
$vehicleId = $item['vehicle_id'];
if ($this->maintenanceModel->delete((int)$id)) {
$_SESSION['success'] = 'Maintenance item deleted successfully';
} else {
$_SESSION['error'] = 'Failed to delete maintenance item';
}
$this->redirect('/vehicles/' . $vehicleId);
}
public function search(): void {
$this->requireSetup();
$this->requireAuth();
$query = $_GET['q'] ?? '';
if (strlen($query) < 2) {
$this->json([]);
return;
}
$results = $this->maintenanceModel->searchByName($query);
$this->json($results);
}
}

View File

@@ -0,0 +1,99 @@
<?php
class SettingsController extends Controller {
private QuickTask $quickTaskModel;
private User $userModel;
public function __construct() {
$this->quickTaskModel = new QuickTask();
$this->userModel = new User();
}
public function index(): void {
$this->requireSetup();
$this->requireAuth();
$quickTasks = $this->quickTaskModel->getAll();
$this->view('settings', [
'quickTasks' => $quickTasks
]);
}
public function changePassword(): void {
$this->requireSetup();
$this->requireAuth();
$currentPassword = $_POST['current_password'] ?? '';
$newPassword = $_POST['new_password'] ?? '';
$confirmPassword = $_POST['confirm_password'] ?? '';
if (empty($currentPassword) || empty($newPassword) || empty($confirmPassword)) {
$_SESSION['error'] = 'All password fields are required';
$this->redirect('/settings');
return;
}
if (!$this->userModel->verifyPassword($currentPassword)) {
$_SESSION['error'] = 'Current password is incorrect';
$this->redirect('/settings');
return;
}
if ($newPassword !== $confirmPassword) {
$_SESSION['error'] = 'New passwords do not match';
$this->redirect('/settings');
return;
}
if (strlen($newPassword) < 6) {
$_SESSION['error'] = 'New password must be at least 6 characters';
$this->redirect('/settings');
return;
}
if ($this->userModel->updatePassword($newPassword)) {
$_SESSION['success'] = 'Password changed successfully';
} else {
$_SESSION['error'] = 'Failed to change password';
}
$this->redirect('/settings');
}
public function addQuickTask(): void {
$this->requireSetup();
$this->requireAuth();
$name = trim($_POST['name'] ?? '');
if (empty($name)) {
$_SESSION['error'] = 'Task name is required';
$this->redirect('/settings');
return;
}
$id = $this->quickTaskModel->create($name);
if ($id > 0) {
$_SESSION['success'] = 'Quick task added successfully';
} else {
$_SESSION['error'] = 'Task already exists or could not be added';
}
$this->redirect('/settings');
}
public function deleteQuickTask(string $id): void {
$this->requireSetup();
$this->requireAuth();
if ($this->quickTaskModel->delete((int)$id)) {
$_SESSION['success'] = 'Quick task deleted successfully';
} else {
$_SESSION['error'] = 'Failed to delete quick task';
}
$this->redirect('/settings');
}
}

View File

@@ -0,0 +1,49 @@
<?php
class SetupController extends Controller {
public function index(): void {
// Check if database is already initialized
if (Database::isInitialized()) {
$this->redirect('/home');
return;
}
$this->view('setup');
}
public function setup(): void {
if (Database::isInitialized()) {
$this->redirect('/home');
return;
}
$password = $_POST['password'] ?? '';
$confirmPassword = $_POST['confirm_password'] ?? '';
if (empty($password)) {
$_SESSION['error'] = 'Password is required';
$this->redirect('/setup');
return;
}
if ($password !== $confirmPassword) {
$_SESSION['error'] = 'Passwords do not match';
$this->redirect('/setup');
return;
}
if (strlen($password) < 6) {
$_SESSION['error'] = 'Password must be at least 6 characters';
$this->redirect('/setup');
return;
}
if (Database::initialize($password)) {
$_SESSION['success'] = 'Setup completed successfully! Please login.';
$this->redirect('/login');
} else {
$_SESSION['error'] = 'Setup failed. Please try again.';
$this->redirect('/setup');
}
}
}

View File

@@ -0,0 +1,150 @@
<?php
class VehicleController extends Controller {
private Vehicle $vehicleModel;
private Maintenance $maintenanceModel;
public function __construct() {
$this->vehicleModel = new Vehicle();
$this->maintenanceModel = new Maintenance();
}
public function index(): void {
$this->requireSetup();
$this->requireAuth();
$vehicles = $this->vehicleModel->getAll();
$viewMode = $_GET['view'] ?? 'grid'; // grid or list
$this->view('index', [
'vehicles' => $vehicles,
'viewMode' => $viewMode
]);
}
public function show(string $id): void {
$this->requireSetup();
$this->requireAuth();
$vehicle = $this->vehicleModel->getById((int)$id);
if (!$vehicle) {
$_SESSION['error'] = 'Vehicle not found';
$this->redirect('/home');
return;
}
$maintenanceItems = $this->maintenanceModel->getByVehicleId((int)$id);
$this->view('vehicle', [
'vehicle' => $vehicle,
'maintenanceItems' => $maintenanceItems
]);
}
public function add(): void {
$this->requireSetup();
$this->requireAuth();
$name = trim($_POST['name'] ?? '');
if (empty($name)) {
$_SESSION['error'] = 'Vehicle name is required';
$this->redirect('/home');
return;
}
$data = [
'name' => $name,
'year' => trim($_POST['year'] ?? ''),
'make' => trim($_POST['make'] ?? ''),
'model' => trim($_POST['model'] ?? ''),
'color' => trim($_POST['color'] ?? ''),
'license_plate' => trim($_POST['license_plate'] ?? '')
];
$id = $this->vehicleModel->create($data);
$_SESSION['success'] = 'Vehicle added successfully';
$this->redirect('/vehicles/' . $id);
}
public function edit(string $id): void {
$this->requireSetup();
$this->requireAuth();
$name = trim($_POST['name'] ?? '');
if (empty($name)) {
$_SESSION['error'] = 'Vehicle name is required';
$this->redirect('/vehicles/' . $id);
return;
}
$data = [
'name' => $name,
'year' => trim($_POST['year'] ?? ''),
'make' => trim($_POST['make'] ?? ''),
'model' => trim($_POST['model'] ?? ''),
'color' => trim($_POST['color'] ?? ''),
'license_plate' => trim($_POST['license_plate'] ?? '')
];
if ($this->vehicleModel->update((int)$id, $data)) {
$_SESSION['success'] = 'Vehicle updated successfully';
} else {
$_SESSION['error'] = 'Failed to update vehicle';
}
$this->redirect('/vehicles/' . $id);
}
public function delete(string $id): void {
$this->requireSetup();
$this->requireAuth();
if ($this->vehicleModel->delete((int)$id)) {
$_SESSION['success'] = 'Vehicle deleted successfully';
} else {
$_SESSION['error'] = 'Failed to delete vehicle';
}
$this->redirect('/home');
}
public function export(string $id, string $format): void {
$this->requireSetup();
$this->requireAuth();
$vehicle = $this->vehicleModel->getById((int)$id);
if (!$vehicle) {
$_SESSION['error'] = 'Vehicle not found';
$this->redirect('/home');
return;
}
$maintenanceItems = $this->maintenanceModel->getByVehicleId((int)$id);
if ($format === 'json') {
header('Content-Type: application/json');
header('Content-Disposition: attachment; filename="vehicle-' . $id . '-export.json"');
echo json_encode([
'vehicle' => $vehicle,
'maintenance_items' => $maintenanceItems
], JSON_PRETTY_PRINT);
exit;
} elseif ($format === 'html') {
header('Content-Type: text/html');
header('Content-Disposition: attachment; filename="vehicle-' . $id . '-export.html"');
$this->view('export', [
'vehicle' => $vehicle,
'maintenanceItems' => $maintenanceItems
]);
exit;
} else {
$_SESSION['error'] = 'Invalid export format';
$this->redirect('/vehicles/' . $id);
}
}
}

41
core/Controller.php Normal file
View File

@@ -0,0 +1,41 @@
<?php
class Controller {
protected function view(string $viewName, array $data = []): void {
extract($data);
$viewPath = BASE_PATH . '/views/' . $viewName . '.php';
if (!file_exists($viewPath)) {
die("View not found: $viewName");
}
require_once $viewPath;
}
protected function redirect(string $path): void {
// Add base URL if path doesn't already include it
$url = BASE_URL . $path;
header('Location: ' . $url);
exit;
}
protected function json(mixed $data, int $statusCode = 200): void {
http_response_code($statusCode);
header('Content-Type: application/json');
echo json_encode($data);
exit;
}
protected function requireAuth(): void {
if (!isset($_SESSION['user_id'])) {
$this->redirect('/login');
}
}
protected function requireSetup(): void {
if (!Database::isInitialized()) {
$this->redirect('/setup');
}
}
}

130
core/Database.php Normal file
View File

@@ -0,0 +1,130 @@
<?php
require_once BASE_PATH . '/config.php';
class Database {
private static ?PDO $instance = null;
public static function getInstance(): PDO {
if (self::$instance === null) {
try {
self::$instance = new PDO('sqlite:' . DB_PATH);
self::$instance->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
self::$instance->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
} catch (PDOException $e) {
die('Database connection failed: ' . $e->getMessage());
}
}
return self::$instance;
}
public static function isInitialized(): bool {
if (!file_exists(DB_PATH)) {
return false;
}
try {
$db = self::getInstance();
$result = $db->query("SELECT name FROM sqlite_master WHERE type='table' AND name='users'");
return $result->fetch() !== false;
} catch (PDOException $e) {
return false;
}
}
public static function initialize(string $password): bool {
try {
$db = self::getInstance();
// Create users table
$db->exec("
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
password_hash TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
");
// Create vehicles table
$db->exec("
CREATE TABLE IF NOT EXISTS vehicles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
year TEXT,
make TEXT,
model TEXT,
color TEXT,
license_plate TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
");
// Create maintenance_items table
$db->exec("
CREATE TABLE IF NOT EXISTS maintenance_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
vehicle_id INTEGER NOT NULL,
name TEXT NOT NULL,
date DATE NOT NULL,
mileage INTEGER NOT NULL,
description TEXT,
cost REAL,
parts_list TEXT,
performed_by TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (vehicle_id) REFERENCES vehicles(id) ON DELETE CASCADE
)
");
// Create quick_tasks table (predefined maintenance items)
$db->exec("
CREATE TABLE IF NOT EXISTS quick_tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
");
// Insert default password
$stmt = $db->prepare("INSERT INTO users (password_hash) VALUES (?)");
$stmt->execute([password_hash($password, PASSWORD_DEFAULT)]);
// Insert default quick tasks
$defaultTasks = [
'Oil Change - Oil and Filter',
'Oil Change - Oil Only',
'Tire Rotation',
'Air Filter Replacement',
'Battery Replacement',
'Spark Plug Replacement',
'Wiper Blade Replacement - Front',
'Wiper Blade Replacement - Rear',
'Wiper Blade Replacement - Front and Rear',
'Cabin Air Filter Replacement',
'Wheel Alignment',
'Tire Replacement',
'Brake Fluid Change',
'Power Steering Fluid Change',
'Fuel Filter Replacement',
'Serpentine Belt Replacement',
'Timing Belt Replacement',
'Inspection/Emissions Test',
'Registration Renewed',
];
$stmt = $db->prepare("INSERT INTO quick_tasks (name) VALUES (?)");
foreach ($defaultTasks as $task) {
$stmt->execute([$task]);
}
return true;
} catch (PDOException $e) {
error_log('Database initialization failed: ' . $e->getMessage());
return false;
}
}
}

74
core/Router.php Normal file
View File

@@ -0,0 +1,74 @@
<?php
class Router {
private array $routes = [];
public function get(string $path, string $handler): void {
$this->addRoute('GET', $path, $handler);
}
public function post(string $path, string $handler): void {
$this->addRoute('POST', $path, $handler);
}
private function addRoute(string $method, string $path, string $handler): void {
$this->routes[] = [
'method' => $method,
'path' => $path,
'handler' => $handler
];
}
public function dispatch(string $uri, string $method): void {
// Remove query string
$uri = parse_url($uri, PHP_URL_PATH);
// Remove base directory from URI if running in subdirectory
$scriptName = dirname($_SERVER['SCRIPT_NAME']);
if ($scriptName !== '/' && strpos($uri, $scriptName) === 0) {
$uri = substr($uri, strlen($scriptName));
}
// Ensure URI starts with /
if (empty($uri) || $uri[0] !== '/') {
$uri = '/' . $uri;
}
// Remove trailing slash except for root
if ($uri !== '/' && str_ends_with($uri, '/')) {
$uri = rtrim($uri, '/');
}
foreach ($this->routes as $route) {
if ($route['method'] !== $method) {
continue;
}
$pattern = $this->convertToRegex($route['path']);
if (preg_match($pattern, $uri, $matches)) {
array_shift($matches); // Remove full match
$this->callHandler($route['handler'], $matches);
return;
}
}
// 404 Not Found
http_response_code(404);
echo '404 - Page Not Found<br>';
echo 'Requested URI: ' . htmlspecialchars($uri) . '<br>';
echo 'Request Method: ' . htmlspecialchars($method);
}
private function convertToRegex(string $path): string {
$path = preg_replace('/\{([a-zA-Z0-9_]+)\}/', '([a-zA-Z0-9_-]+)', $path);
return '#^' . $path . '$#';
}
private function callHandler(string $handler, array $params): void {
[$controller, $method] = explode('@', $handler);
$controllerInstance = new $controller();
call_user_func_array([$controllerInstance, $method], $params);
}
}

15
docker-compose.yml Normal file
View File

@@ -0,0 +1,15 @@
version: '3.8'
services:
mainty:
build: .
container_name: mainty-app
ports:
- "8080:80"
volumes:
- ./data:/var/www/html/data
# Uncomment below to sync code changes in development
# - .:/var/www/html
environment:
- APACHE_DOCUMENT_ROOT=/var/www/html
restart: unless-stopped

14
example.htaccess Normal file
View File

@@ -0,0 +1,14 @@
RewriteEngine On
#if this is in a folder, edit this. example.com/mainty/ would be /mainty/
RewriteBase /
# Redirect to HTTPS (optional, uncomment if needed)
# RewriteCond %{HTTPS} off
# RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
# Don't rewrite files or directories
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
# Rewrite everything else to index.php
RewriteRule ^(.*)$ index.php [L,QSA]

71
index.php Normal file
View File

@@ -0,0 +1,71 @@
<?php
// Start session
session_start();
// Require PHP 8.0 or higher
if (version_compare(PHP_VERSION, '8.0.0', '<')) {
die('This application requires PHP 8.0 or higher. Current version: ' . PHP_VERSION);
}
// Check if SQLite is available
if (!extension_loaded('pdo_sqlite')) {
die('SQLite PDO extension is not available. Please enable it in your PHP configuration.');
}
// Define base path
define('BASE_PATH', __DIR__);
// Autoloader
spl_autoload_register(function ($class) {
$directories = [
BASE_PATH . '/core/',
BASE_PATH . '/controllers/',
BASE_PATH . '/models/',
];
foreach ($directories as $directory) {
$file = $directory . $class . '.php';
if (file_exists($file)) {
require_once $file;
return;
}
}
});
// Initialize router
$router = new Router();
// Define routes
$router->get('/', 'SetupController@index');
$router->get('/setup', 'SetupController@index');
$router->post('/setup', 'SetupController@setup');
$router->get('/login', 'AuthController@login');
$router->post('/login', 'AuthController@authenticate');
$router->get('/logout', 'AuthController@logout');
$router->get('/home', 'VehicleController@index');
$router->get('/vehicles', 'VehicleController@index');
$router->post('/vehicles/add', 'VehicleController@add');
$router->get('/vehicles/{id}', 'VehicleController@show');
$router->post('/vehicles/{id}/edit', 'VehicleController@edit');
$router->post('/vehicles/{id}/delete', 'VehicleController@delete');
$router->get('/vehicles/{id}/export/{format}', 'VehicleController@export');
$router->post('/maintenance/add', 'MaintenanceController@add');
$router->post('/maintenance/{id}/edit', 'MaintenanceController@edit');
$router->post('/maintenance/{id}/delete', 'MaintenanceController@delete');
$router->get('/maintenance/search', 'MaintenanceController@search');
$router->get('/settings', 'SettingsController@index');
$router->post('/settings/password', 'SettingsController@changePassword');
$router->post('/settings/quick-tasks/add', 'SettingsController@addQuickTask');
$router->post('/settings/quick-tasks/{id}/delete', 'SettingsController@deleteQuickTask');
// Dispatch the request
try {
$router->dispatch($_SERVER['REQUEST_URI'], $_SERVER['REQUEST_METHOD']);
} catch (Exception $e) {
http_response_code(500);
echo "Error: " . $e->getMessage();
}

88
models/Maintenance.php Normal file
View File

@@ -0,0 +1,88 @@
<?php
class Maintenance {
private PDO $db;
public function __construct() {
$this->db = Database::getInstance();
}
public function getByVehicleId(int $vehicleId): array {
$stmt = $this->db->prepare("
SELECT * FROM maintenance_items
WHERE vehicle_id = ?
ORDER BY date DESC, id DESC
");
$stmt->execute([$vehicleId]);
return $stmt->fetchAll();
}
public function getById(int $id): ?array {
$stmt = $this->db->prepare("SELECT * FROM maintenance_items WHERE id = ?");
$stmt->execute([$id]);
$item = $stmt->fetch();
return $item ?: null;
}
public function create(array $data): int {
$stmt = $this->db->prepare("
INSERT INTO maintenance_items
(vehicle_id, name, date, mileage, description, cost, parts_list, performed_by)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
");
$stmt->execute([
$data['vehicle_id'],
$data['name'],
$data['date'],
$data['mileage'],
$data['description'] ?? null,
$data['cost'] ?? null,
$data['parts_list'] ?? null,
$data['performed_by'] ?? null
]);
return (int)$this->db->lastInsertId();
}
public function update(int $id, array $data): bool {
$stmt = $this->db->prepare("
UPDATE maintenance_items
SET name = ?, date = ?, mileage = ?, description = ?, cost = ?, parts_list = ?, performed_by = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
");
return $stmt->execute([
$data['name'],
$data['date'],
$data['mileage'],
$data['description'] ?? null,
$data['cost'] ?? null,
$data['parts_list'] ?? null,
$data['performed_by'] ?? null,
$id
]);
}
public function delete(int $id): bool {
$stmt = $this->db->prepare("DELETE FROM maintenance_items WHERE id = ?");
return $stmt->execute([$id]);
}
public function searchByName(string $query): array {
// Search in both quick_tasks and existing maintenance items
$stmt = $this->db->prepare("
SELECT DISTINCT name FROM (
SELECT name FROM quick_tasks WHERE name LIKE ?
UNION
SELECT DISTINCT name FROM maintenance_items WHERE name LIKE ?
)
ORDER BY name ASC
LIMIT 10
");
$searchTerm = '%' . $query . '%';
$stmt->execute([$searchTerm, $searchTerm]);
return $stmt->fetchAll(PDO::FETCH_COLUMN);
}
}

33
models/QuickTask.php Normal file
View File

@@ -0,0 +1,33 @@
<?php
class QuickTask {
private PDO $db;
public function __construct() {
$this->db = Database::getInstance();
}
public function getAll(): array {
$stmt = $this->db->query("SELECT * FROM quick_tasks ORDER BY name ASC");
return $stmt->fetchAll();
}
public function create(string $name): int {
try {
$stmt = $this->db->prepare("INSERT INTO quick_tasks (name) VALUES (?)");
$stmt->execute([$name]);
return (int)$this->db->lastInsertId();
} catch (PDOException $e) {
// Handle duplicate entry
if ($e->getCode() == 23000) {
return 0;
}
throw $e;
}
}
public function delete(int $id): bool {
$stmt = $this->db->prepare("DELETE FROM quick_tasks WHERE id = ?");
return $stmt->execute([$id]);
}
}

36
models/User.php Normal file
View File

@@ -0,0 +1,36 @@
<?php
class User {
private PDO $db;
public function __construct() {
$this->db = Database::getInstance();
}
public function verifyPassword(string $password): bool {
$stmt = $this->db->query("SELECT password_hash FROM users LIMIT 1");
$user = $stmt->fetch();
if (!$user) {
return false;
}
return password_verify($password, $user['password_hash']);
}
public function updatePassword(string $newPassword): bool {
try {
$stmt = $this->db->prepare("UPDATE users SET password_hash = ?, updated_at = CURRENT_TIMESTAMP");
return $stmt->execute([password_hash($newPassword, PASSWORD_DEFAULT)]);
} catch (PDOException $e) {
error_log('Password update failed: ' . $e->getMessage());
return false;
}
}
public function getId(): ?int {
$stmt = $this->db->query("SELECT id FROM users LIMIT 1");
$user = $stmt->fetch();
return $user ? (int)$user['id'] : null;
}
}

70
models/Vehicle.php Normal file
View File

@@ -0,0 +1,70 @@
<?php
class Vehicle {
private PDO $db;
public function __construct() {
$this->db = Database::getInstance();
}
public function getAll(): array {
$stmt = $this->db->query("
SELECT v.*,
COUNT(m.id) as maintenance_count,
MAX(m.date) as last_maintenance_date
FROM vehicles v
LEFT JOIN maintenance_items m ON v.id = m.vehicle_id
GROUP BY v.id
ORDER BY v.name ASC
");
return $stmt->fetchAll();
}
public function getById(int $id): ?array {
$stmt = $this->db->prepare("SELECT * FROM vehicles WHERE id = ?");
$stmt->execute([$id]);
$vehicle = $stmt->fetch();
return $vehicle ?: null;
}
public function create(array $data): int {
$stmt = $this->db->prepare("
INSERT INTO vehicles (name, year, make, model, color, license_plate)
VALUES (?, ?, ?, ?, ?, ?)
");
$stmt->execute([
$data['name'],
$data['year'] ?? null,
$data['make'] ?? null,
$data['model'] ?? null,
$data['color'] ?? null,
$data['license_plate'] ?? null
]);
return (int)$this->db->lastInsertId();
}
public function update(int $id, array $data): bool {
$stmt = $this->db->prepare("
UPDATE vehicles
SET name = ?, year = ?, make = ?, model = ?, color = ?, license_plate = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
");
return $stmt->execute([
$data['name'],
$data['year'] ?? null,
$data['make'] ?? null,
$data['model'] ?? null,
$data['color'] ?? null,
$data['license_plate'] ?? null,
$id
]);
}
public function delete(int $id): bool {
$stmt = $this->db->prepare("DELETE FROM vehicles WHERE id = ?");
return $stmt->execute([$id]);
}
}

2
robots.txt Normal file
View File

@@ -0,0 +1,2 @@
User-agent: *
Disallow: /

114
views/export.php Normal file
View File

@@ -0,0 +1,114 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo htmlspecialchars($vehicle['name']); ?> - Maintenance Export</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
</head>
<body class="bg-white p-8">
<div class="max-w-4xl mx-auto">
<div class="text-center mb-8">
<h1 class="text-3xl font-bold text-gray-800 mb-2"><?php echo APP_NAME; ?></h1>
<h2 class="text-xl text-gray-600">Vehicle Maintenance Export</h2>
</div>
<!-- Vehicle Information -->
<div class="bg-gray-50 rounded-lg p-6 mb-8">
<h3 class="text-xl font-semibold text-gray-800 mb-4">Vehicle Information</h3>
<div class="grid grid-cols-2 gap-4">
<div>
<span class="font-medium text-gray-700">Name:</span>
<span class="text-gray-600"><?php echo htmlspecialchars($vehicle['name']); ?></span>
</div>
<?php if ($vehicle['year']): ?>
<div>
<span class="font-medium text-gray-700">Year:</span>
<span class="text-gray-600"><?php echo htmlspecialchars($vehicle['year']); ?></span>
</div>
<?php endif; ?>
<?php if ($vehicle['make']): ?>
<div>
<span class="font-medium text-gray-700">Make:</span>
<span class="text-gray-600"><?php echo htmlspecialchars($vehicle['make']); ?></span>
</div>
<?php endif; ?>
<?php if ($vehicle['model']): ?>
<div>
<span class="font-medium text-gray-700">Model:</span>
<span class="text-gray-600"><?php echo htmlspecialchars($vehicle['model']); ?></span>
</div>
<?php endif; ?>
<?php if ($vehicle['color']): ?>
<div>
<span class="font-medium text-gray-700">Color:</span>
<span class="text-gray-600"><?php echo htmlspecialchars($vehicle['color']); ?></span>
</div>
<?php endif; ?>
<?php if ($vehicle['license_plate']): ?>
<div>
<span class="font-medium text-gray-700">License Plate:</span>
<span class="text-gray-600"><?php echo htmlspecialchars($vehicle['license_plate']); ?></span>
</div>
<?php endif; ?>
</div>
</div>
<!-- Maintenance History -->
<div>
<h3 class="text-xl font-semibold text-gray-800 mb-4">Maintenance History</h3>
<?php if (empty($maintenanceItems)): ?>
<p class="text-gray-500 text-center py-8">No maintenance records available.</p>
<?php else: ?>
<div class="space-y-4">
<?php foreach ($maintenanceItems as $item): ?>
<div class="border border-gray-300 rounded-lg p-4">
<div class="flex justify-between items-start mb-2">
<h4 class="text-lg font-semibold text-gray-800"><?php echo htmlspecialchars($item['name']); ?></h4>
<?php if ($item['cost']): ?>
<span class="bg-green-100 text-green-800 px-3 py-1 rounded">
$<?php echo number_format($item['cost'], 2); ?>
</span>
<?php endif; ?>
</div>
<div class="grid grid-cols-2 md:grid-cols-4 gap-2 text-sm text-gray-600 mb-2">
<div>
<strong>Date:</strong> <?php echo date('M d, Y', strtotime($item['date'])); ?>
</div>
<div>
<strong>Mileage:</strong> <?php echo number_format($item['mileage']); ?> miles
</div>
<?php if ($item['performed_by']): ?>
<div>
<strong>Performed By:</strong> <?php echo htmlspecialchars($item['performed_by']); ?>
</div>
<?php endif; ?>
<?php if ($item['parts_list']): ?>
<div>
<strong>Parts:</strong> <?php echo htmlspecialchars($item['parts_list']); ?>
</div>
<?php endif; ?>
</div>
<?php if ($item['description']): ?>
<div class="mt-2 text-sm text-gray-600">
<strong>Description:</strong><br>
<?php echo nl2br(htmlspecialchars($item['description'])); ?>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<div class="mt-8 text-center text-sm text-gray-500">
<p>Exported on <?php echo date('F j, Y \a\t g:i A'); ?></p>
<p>Generated by <?php echo APP_NAME; ?></p>
</div>
</div>
</body>
</html>

210
views/index.php Normal file
View File

@@ -0,0 +1,210 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo APP_NAME; ?> - Vehicles</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
</head>
<body class="bg-gray-100 min-h-screen">
<!-- Header -->
<header class="bg-white shadow-sm">
<div class="max-w-7xl mx-auto px-4 py-4 flex justify-between items-center">
<a href="<?php echo url('/home'); ?>" class="text-2xl font-bold text-gray-800 hover:text-gray-600 transition"><?php echo APP_NAME; ?></a>
<div class="flex items-center space-x-4">
<a href="<?php echo url('/settings'); ?>" class="text-gray-600 hover:text-gray-800">
<i class="bi bi-gear-fill text-2xl"></i>
</a>
</div>
</div>
</header>
<main class="max-w-7xl mx-auto px-4 py-8">
<?php if (isset($_SESSION['error'])): ?>
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
<?php echo htmlspecialchars($_SESSION['error']); unset($_SESSION['error']); ?>
</div>
<?php endif; ?>
<?php if (isset($_SESSION['success'])): ?>
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4">
<?php echo htmlspecialchars($_SESSION['success']); unset($_SESSION['success']); ?>
</div>
<?php endif; ?>
<!-- Header with view toggle and add button -->
<div class="flex justify-between items-center mb-6">
<h2 class="text-xl font-semibold text-gray-700">My Vehicles</h2>
<div class="flex items-center space-x-3">
<div class="flex bg-white rounded-lg shadow-sm">
<a href="<?php echo url('/home?view=grid'); ?>"
class="px-3 py-2 <?php echo $viewMode === 'grid' ? 'bg-blue-600 text-white' : 'text-gray-600 hover:bg-gray-100'; ?> rounded-l-lg transition">
<i class="bi bi-grid-3x3-gap-fill"></i>
</a>
<a href="<?php echo url('/home?view=list'); ?>"
class="px-3 py-2 <?php echo $viewMode === 'list' ? 'bg-blue-600 text-white' : 'text-gray-600 hover:bg-gray-100'; ?> rounded-r-lg transition">
<i class="bi bi-list-ul"></i>
</a>
</div>
<button onclick="showAddVehicleModal()"
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center space-x-2 transition">
<i class="bi bi-plus-lg"></i>
<span>Add Vehicle</span>
</button>
</div>
</div>
<!-- Vehicles Display -->
<?php if (empty($vehicles)): ?>
<div class="bg-white rounded-lg shadow-sm p-12 text-center">
<i class="bi bi-car-front text-6xl text-gray-300 mb-4"></i>
<h3 class="text-xl font-semibold text-gray-700 mb-2">No vehicles yet</h3>
<p class="text-gray-500 mb-4">Get started by adding your first vehicle</p>
<button onclick="showAddVehicleModal()"
class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg transition">
Add Your First Vehicle
</button>
</div>
<?php else: ?>
<?php if ($viewMode === 'grid'): ?>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<?php foreach ($vehicles as $vehicle): ?>
<a href="<?php echo url('/vehicles/' . $vehicle['id']); ?>"
class="bg-white rounded-lg shadow-sm hover:shadow-md transition p-6 block">
<div class="flex items-start justify-between mb-3">
<div class="bg-blue-100 p-3 rounded-lg">
<i class="bi bi-car-front-fill text-blue-600 text-2xl"></i>
</div>
<span class="text-sm text-gray-500"><?php echo $vehicle['maintenance_count']; ?> records</span>
</div>
<h3 class="text-lg font-semibold text-gray-800 mb-2"><?php echo htmlspecialchars($vehicle['name']); ?></h3>
<div class="space-y-1 text-sm text-gray-600">
<?php if ($vehicle['year'] || $vehicle['make'] || $vehicle['model']): ?>
<p><?php echo implode(' ', array_filter([$vehicle['year'], $vehicle['make'], $vehicle['model']])); ?></p>
<?php endif; ?>
<?php if ($vehicle['license_plate']): ?>
<p><i class="bi bi-credit-card-2-front"></i> <?php echo htmlspecialchars($vehicle['license_plate']); ?></p>
<?php endif; ?>
<?php if ($vehicle['last_maintenance_date']): ?>
<p class="text-xs text-gray-500 mt-2">Last service: <?php echo date('M d, Y', strtotime($vehicle['last_maintenance_date'])); ?></p>
<?php endif; ?>
</div>
</a>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="bg-white rounded-lg shadow-sm overflow-hidden">
<table class="w-full">
<thead class="bg-gray-50 border-b">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Vehicle</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Details</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">License Plate</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Records</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Last Service</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
<?php foreach ($vehicles as $vehicle): ?>
<tr class="hover:bg-gray-50 cursor-pointer" onclick="window.location='<?php echo url('/vehicles/' . $vehicle['id']); ?>'">
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="bg-blue-100 p-2 rounded">
<i class="bi bi-car-front-fill text-blue-600"></i>
</div>
<div class="ml-3">
<div class="text-sm font-medium text-gray-900"><?php echo htmlspecialchars($vehicle['name']); ?></div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
<?php echo implode(' ', array_filter([$vehicle['year'], $vehicle['make'], $vehicle['model']])) ?: '-'; ?>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
<?php echo htmlspecialchars($vehicle['license_plate']) ?: '-'; ?>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
<?php echo $vehicle['maintenance_count']; ?>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
<?php echo $vehicle['last_maintenance_date'] ? date('M d, Y', strtotime($vehicle['last_maintenance_date'])) : '-'; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
<?php endif; ?>
</main>
<!-- Add Vehicle Modal -->
<div id="addVehicleModal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white rounded-lg p-6 w-full max-w-md">
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-semibold">Add New Vehicle</h3>
<button onclick="hideAddVehicleModal()" class="text-gray-500 hover:text-gray-700">
<i class="bi bi-x-lg"></i>
</button>
</div>
<form method="POST" action="<?php echo url('/vehicles/add'); ?>">
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Name *</label>
<input type="text" name="name" required
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Year</label>
<input type="text" name="year"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Color</label>
<input type="text" name="color"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Make</label>
<input type="text" name="make"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Model</label>
<input type="text" name="model"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">License Plate</label>
<input type="text" name="license_plate"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
</div>
<div class="flex space-x-3 mt-6">
<button type="button" onclick="hideAddVehicleModal()"
class="flex-1 px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 transition">
Cancel
</button>
<button type="submit"
class="flex-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition">
Add Vehicle
</button>
</div>
</form>
</div>
</div>
<script>
function showAddVehicleModal() {
document.getElementById('addVehicleModal').classList.remove('hidden');
}
function hideAddVehicleModal() {
document.getElementById('addVehicleModal').classList.add('hidden');
}
</script>
</body>
</html>

41
views/login.php Normal file
View File

@@ -0,0 +1,41 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo APP_NAME; ?> - Login</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
</head>
<body class="bg-gray-100 min-h-screen flex items-center justify-center">
<div class="bg-white p-8 rounded-lg shadow-md w-full max-w-md">
<h1 class="text-3xl font-bold text-gray-800 mb-6 text-center"><?php echo APP_NAME; ?></h1>
<?php if (isset($_SESSION['error'])): ?>
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
<?php echo htmlspecialchars($_SESSION['error']); unset($_SESSION['error']); ?>
</div>
<?php endif; ?>
<?php if (isset($_SESSION['success'])): ?>
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4">
<?php echo htmlspecialchars($_SESSION['success']); unset($_SESSION['success']); ?>
</div>
<?php endif; ?>
<form method="POST" action="<?php echo url('/login'); ?>" class="space-y-4">
<div>
<label for="password" class="block text-sm font-medium text-gray-700 mb-1">Password</label>
<input type="password" id="password" name="password" required autofocus
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Enter your password">
</div>
<button type="submit"
class="w-full bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2 px-4 rounded-md transition duration-200">
Login
</button>
</form>
</div>
</body>
</html>

129
views/settings.php Normal file
View File

@@ -0,0 +1,129 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo APP_NAME; ?> - Settings</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
</head>
<body class="bg-gray-100 min-h-screen">
<!-- Header -->
<header class="bg-white shadow-sm">
<div class="max-w-7xl mx-auto px-4 py-4 flex justify-between items-center">
<a href="<?php echo url('/home'); ?>" class="text-2xl font-bold text-gray-800 hover:text-gray-600 transition"><?php echo APP_NAME; ?></a>
<div class="flex items-center space-x-4">
<a href="<?php echo url('/settings'); ?>" class="text-gray-600 hover:text-gray-800">
<i class="bi bi-gear-fill text-2xl"></i>
</a>
</div>
</div>
</header>
<main class="max-w-7xl mx-auto px-4 py-8">
<!-- Breadcrumb -->
<div class="mb-6">
<a href="<?php echo url('/home'); ?>" class="text-blue-600 hover:text-blue-800">← Back to Vehicles</a>
</div>
<?php if (isset($_SESSION['error'])): ?>
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
<?php echo htmlspecialchars($_SESSION['error']); unset($_SESSION['error']); ?>
</div>
<?php endif; ?>
<?php if (isset($_SESSION['success'])): ?>
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4">
<?php echo htmlspecialchars($_SESSION['success']); unset($_SESSION['success']); ?>
</div>
<?php endif; ?>
<h2 class="text-2xl font-bold text-gray-800 mb-6">Settings</h2>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Quick Tasks Section -->
<div class="bg-white rounded-lg shadow-sm p-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4 flex items-center">
<i class="bi bi-lightning-charge-fill text-yellow-500 mr-2"></i>
Quick Tasks
</h3>
<p class="text-sm text-gray-600 mb-4">Predefined maintenance items for quick selection when adding records.</p>
<!-- Add Quick Task Form -->
<form method="POST" action="<?php echo url('/settings/quick-tasks/add'); ?>" class="mb-4">
<div class="flex space-x-2">
<input type="text" name="name" required placeholder="New task name"
class="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<button type="submit"
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md transition">
<i class="bi bi-plus-lg"></i> Add
</button>
</div>
</form>
<!-- Quick Tasks List -->
<div class="space-y-2 max-h-96 overflow-y-auto">
<?php foreach ($quickTasks as $task): ?>
<div class="flex items-center justify-between p-2 hover:bg-gray-50 rounded border border-gray-200">
<span class="text-gray-700"><?php echo htmlspecialchars($task['name']); ?></span>
<form method="POST" action="<?php echo url('/settings/quick-tasks/' . $task['id'] . '/delete'); ?>"
onsubmit="return confirm('Are you sure you want to delete this quick task?')" class="inline">
<button type="submit" class="text-red-600 hover:text-red-800">
<i class="bi bi-trash"></i>
</button>
</form>
</div>
<?php endforeach; ?>
</div>
</div>
<!-- Change Password Section -->
<div class="bg-white rounded-lg shadow-sm p-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4 flex items-center">
<i class="bi bi-key-fill text-blue-500 mr-2"></i>
Change Password
</h3>
<p class="text-sm text-gray-600 mb-4">Update your login password.</p>
<form method="POST" action="<?php echo url('/settings/password'); ?>" class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Current Password</label>
<input type="password" name="current_password" required
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">New Password</label>
<input type="password" name="new_password" required
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="At least 6 characters">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Confirm New Password</label>
<input type="password" name="confirm_password" required
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<button type="submit"
class="w-full bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md transition">
Update Password
</button>
</form>
</div>
</div>
<!-- Logout Section -->
<div class="mt-6 bg-white rounded-lg shadow-sm p-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4 flex items-center">
<i class="bi bi-box-arrow-right text-red-500 mr-2"></i>
Account
</h3>
<a href="<?php echo url('/logout'); ?>"
class="inline-block bg-red-100 hover:bg-red-200 text-red-700 px-6 py-2 rounded-md transition">
<i class="bi bi-box-arrow-right"></i> Logout
</a>
</div>
</main>
</body>
</html>

78
views/setup.php Normal file
View File

@@ -0,0 +1,78 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo APP_NAME; ?> - Setup</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
</head>
<body class="bg-gray-100 min-h-screen flex items-center justify-center">
<div class="bg-white p-8 rounded-lg shadow-md w-full max-w-md">
<h1 class="text-3xl font-bold text-gray-800 mb-6 text-center"><?php echo APP_NAME; ?> Setup</h1>
<?php if (isset($_SESSION['error'])): ?>
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
<?php echo htmlspecialchars($_SESSION['error']); unset($_SESSION['error']); ?>
</div>
<?php endif; ?>
<div class="mb-6 p-4 bg-blue-50 border border-blue-200 rounded">
<h2 class="font-semibold text-blue-900 mb-2">System Requirements</h2>
<ul class="space-y-1 text-sm">
<li class="flex items-center">
<i class="bi bi-check-circle-fill text-green-600 mr-2"></i>
<span>PHP Version: <?php echo PHP_VERSION; ?>
<?php if (version_compare(PHP_VERSION, '8.0.0', '>=')): ?>
<span class="text-green-600">(✓ Meets requirement)</span>
<?php else: ?>
<span class="text-red-600">(✗ Requires 8.0+)</span>
<?php endif; ?>
</span>
</li>
<li class="flex items-center">
<?php if (extension_loaded('pdo_sqlite')): ?>
<i class="bi bi-check-circle-fill text-green-600 mr-2"></i>
<span>SQLite: <span class="text-green-600">Available (✓)</span></span>
<?php else: ?>
<i class="bi bi-x-circle-fill text-red-600 mr-2"></i>
<span>SQLite: <span class="text-red-600">Not Available (✗)</span></span>
<?php endif; ?>
</li>
</ul>
</div>
<?php
$canSetup = version_compare(PHP_VERSION, '8.0.0', '>=') && extension_loaded('pdo_sqlite');
?>
<?php if (!$canSetup): ?>
<div class="bg-yellow-100 border border-yellow-400 text-yellow-800 px-4 py-3 rounded mb-4">
<p class="font-semibold">System requirements not met!</p>
<p class="text-sm mt-1">Please ensure PHP 8.0+ and SQLite extension are available.</p>
</div>
<?php else: ?>
<form method="POST" action="<?php echo url('/setup'); ?>" class="space-y-4">
<div>
<label for="password" class="block text-sm font-medium text-gray-700 mb-1">Create Password</label>
<input type="password" id="password" name="password" required
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Enter password (min 6 characters)">
</div>
<div>
<label for="confirm_password" class="block text-sm font-medium text-gray-700 mb-1">Confirm Password</label>
<input type="password" id="confirm_password" name="confirm_password" required
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Confirm password">
</div>
<button type="submit"
class="w-full bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2 px-4 rounded-md transition duration-200">
Complete Setup
</button>
</form>
<?php endif; ?>
</div>
</body>
</html>

425
views/vehicle.php Normal file
View File

@@ -0,0 +1,425 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo APP_NAME; ?> - <?php echo htmlspecialchars($vehicle['name']); ?></title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
</head>
<body class="bg-gray-100 min-h-screen">
<!-- Header -->
<header class="bg-white shadow-sm">
<div class="max-w-7xl mx-auto px-4 py-4 flex justify-between items-center">
<a href="<?php echo url('/home'); ?>" class="text-2xl font-bold text-gray-800 hover:text-gray-600 transition"><?php echo APP_NAME; ?></a>
<div class="flex items-center space-x-4">
<a href="<?php echo url('/settings'); ?>" class="text-gray-600 hover:text-gray-800">
<i class="bi bi-gear-fill text-2xl"></i>
</a>
</div>
</div>
</header>
<main class="max-w-7xl mx-auto px-4 py-8">
<!-- Breadcrumb -->
<div class="mb-6">
<a href="<?php echo url('/home'); ?>" class="text-blue-600 hover:text-blue-800">← Back to Vehicles</a>
</div>
<?php if (isset($_SESSION['error'])): ?>
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
<?php echo htmlspecialchars($_SESSION['error']); unset($_SESSION['error']); ?>
</div>
<?php endif; ?>
<?php if (isset($_SESSION['success'])): ?>
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4">
<?php echo htmlspecialchars($_SESSION['success']); unset($_SESSION['success']); ?>
</div>
<?php endif; ?>
<!-- Vehicle Info Card -->
<div class="bg-white rounded-lg shadow-sm p-6 mb-6">
<div class="flex items-start justify-between">
<div class="flex items-start space-x-4">
<div class="bg-blue-100 p-4 rounded-lg">
<i class="bi bi-car-front-fill text-blue-600 text-3xl"></i>
</div>
<div>
<h2 class="text-2xl font-bold text-gray-800 mb-2"><?php echo htmlspecialchars($vehicle['name']); ?></h2>
<div class="space-y-1 text-gray-600">
<?php if ($vehicle['year'] || $vehicle['make'] || $vehicle['model']): ?>
<p class="flex items-center">
<i class="bi bi-info-circle mr-2"></i>
<?php echo implode(' ', array_filter([$vehicle['year'], $vehicle['make'], $vehicle['model']])); ?>
</p>
<?php endif; ?>
<?php if ($vehicle['color']): ?>
<p class="flex items-center">
<i class="bi bi-palette mr-2"></i>
<?php echo htmlspecialchars($vehicle['color']); ?>
</p>
<?php endif; ?>
<?php if ($vehicle['license_plate']): ?>
<p class="flex items-center">
<i class="bi bi-credit-card-2-front mr-2"></i>
<?php echo htmlspecialchars($vehicle['license_plate']); ?>
</p>
<?php endif; ?>
</div>
</div>
</div>
<div class="flex space-x-2">
<button onclick="showEditVehicleModal()"
class="px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition">
<i class="bi bi-pencil"></i> Edit
</button>
<div class="relative">
<button onclick="toggleExportMenu()"
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition">
<i class="bi bi-download"></i> Export
</button>
<div id="exportMenu" class="hidden absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg z-10">
<a href="<?php echo url('/vehicles/' . $vehicle['id'] . '/export/json'); ?>"
class="block px-4 py-2 text-gray-700 hover:bg-gray-100 rounded-t-lg">
<i class="bi bi-filetype-json"></i> Export as JSON
</a>
<a href="<?php echo url('/vehicles/' . $vehicle['id'] . '/export/html'); ?>"
class="block px-4 py-2 text-gray-700 hover:bg-gray-100 rounded-b-lg">
<i class="bi bi-filetype-html"></i> Export as HTML
</a>
</div>
</div>
<button onclick="confirmDelete()"
class="px-4 py-2 bg-red-100 hover:bg-red-200 text-red-700 rounded-lg transition">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
</div>
<!-- Add Maintenance Item Card -->
<div class="bg-white rounded-lg shadow-sm p-6 mb-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4">Add Maintenance Record</h3>
<form method="POST" action="<?php echo url('/maintenance/add'); ?>">
<input type="hidden" name="vehicle_id" value="<?php echo $vehicle['id']; ?>">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Maintenance Name *</label>
<input type="text" name="name" id="maintenanceName" required autocomplete="off"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<div id="suggestions" class="hidden absolute z-10 bg-white border border-gray-300 rounded-md shadow-lg mt-1 max-h-48 overflow-y-auto"></div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Date *</label>
<input type="date" name="date" required value="<?php echo date('Y-m-d'); ?>"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Mileage *</label>
<input type="number" name="mileage" required
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Cost</label>
<input type="number" step="0.01" name="cost"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Performed By</label>
<input type="text" name="performed_by"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Parts List</label>
<input type="text" name="parts_list"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">Description</label>
<textarea name="description" rows="3"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"></textarea>
</div>
<button type="submit"
class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg transition">
<i class="bi bi-plus-lg"></i> Add Record
</button>
</form>
</div>
<!-- Maintenance History -->
<div class="bg-white rounded-lg shadow-sm p-6">
<h3 class="text-lg font-semibold text-gray-800 mb-4">Maintenance History (<?php echo count($maintenanceItems); ?>)</h3>
<?php if (empty($maintenanceItems)): ?>
<div class="text-center py-8 text-gray-500">
<i class="bi bi-tools text-4xl mb-2"></i>
<p>No maintenance records yet</p>
</div>
<?php else: ?>
<div class="space-y-4">
<?php foreach ($maintenanceItems as $item): ?>
<div class="border border-gray-200 rounded-lg p-4 hover:border-blue-300 transition">
<div class="flex justify-between items-start">
<div class="flex-1">
<div class="flex items-center space-x-3 mb-2">
<h4 class="text-lg font-semibold text-gray-800"><?php echo htmlspecialchars($item['name']); ?></h4>
<?php if ($item['cost']): ?>
<span class="bg-green-100 text-green-800 px-2 py-1 rounded text-sm">
$<?php echo number_format($item['cost'], 2); ?>
</span>
<?php endif; ?>
</div>
<div class="grid grid-cols-2 md:grid-cols-4 gap-2 text-sm text-gray-600 mb-2">
<div>
<i class="bi bi-calendar3"></i>
<?php echo date('M d, Y', strtotime($item['date'])); ?>
</div>
<div>
<i class="bi bi-speedometer"></i>
<?php echo number_format($item['mileage']); ?> miles
</div>
<?php if ($item['performed_by']): ?>
<div>
<i class="bi bi-person"></i>
<?php echo htmlspecialchars($item['performed_by']); ?>
</div>
<?php endif; ?>
<?php if ($item['parts_list']): ?>
<div>
<i class="bi bi-box"></i>
<?php echo htmlspecialchars($item['parts_list']); ?>
</div>
<?php endif; ?>
</div>
<?php if ($item['description']): ?>
<p class="text-sm text-gray-600 mt-2"><?php echo nl2br(htmlspecialchars($item['description'])); ?></p>
<?php endif; ?>
</div>
<div class="flex space-x-2 ml-4">
<button onclick='showEditMaintenanceModal(<?php echo json_encode($item); ?>)'
class="text-blue-600 hover:text-blue-800">
<i class="bi bi-pencil"></i>
</button>
<form method="POST" action="<?php echo url('/maintenance/' . $item['id'] . '/delete'); ?>"
onsubmit="return confirm('Are you sure you want to delete this record?')" class="inline">
<button type="submit" class="text-red-600 hover:text-red-800">
<i class="bi bi-trash"></i>
</button>
</form>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</main>
<!-- Edit Vehicle Modal -->
<div id="editVehicleModal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white rounded-lg p-6 w-full max-w-md">
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-semibold">Edit Vehicle</h3>
<button onclick="hideEditVehicleModal()" class="text-gray-500 hover:text-gray-700">
<i class="bi bi-x-lg"></i>
</button>
</div>
<form method="POST" action="<?php echo url('/vehicles/' . $vehicle['id'] . '/edit'); ?>">
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Name *</label>
<input type="text" name="name" required value="<?php echo htmlspecialchars($vehicle['name']); ?>"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Year</label>
<input type="text" name="year" value="<?php echo htmlspecialchars($vehicle['year'] ?? ''); ?>"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Color</label>
<input type="text" name="color" value="<?php echo htmlspecialchars($vehicle['color'] ?? ''); ?>"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Make</label>
<input type="text" name="make" value="<?php echo htmlspecialchars($vehicle['make'] ?? ''); ?>"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Model</label>
<input type="text" name="model" value="<?php echo htmlspecialchars($vehicle['model'] ?? ''); ?>"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">License Plate</label>
<input type="text" name="license_plate" value="<?php echo htmlspecialchars($vehicle['license_plate'] ?? ''); ?>"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
</div>
<div class="flex space-x-3 mt-6">
<button type="button" onclick="hideEditVehicleModal()"
class="flex-1 px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 transition">
Cancel
</button>
<button type="submit"
class="flex-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition">
Save Changes
</button>
</div>
</form>
</div>
</div>
<!-- Edit Maintenance Modal -->
<div id="editMaintenanceModal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white rounded-lg p-6 w-full max-w-2xl max-h-screen overflow-y-auto">
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-semibold">Edit Maintenance Record</h3>
<button onclick="hideEditMaintenanceModal()" class="text-gray-500 hover:text-gray-700">
<i class="bi bi-x-lg"></i>
</button>
</div>
<form id="editMaintenanceForm" method="POST">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Maintenance Name *</label>
<input type="text" name="name" id="editName" required
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Date *</label>
<input type="date" name="date" id="editDate" required
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Mileage *</label>
<input type="number" name="mileage" id="editMileage" required
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Cost</label>
<input type="number" step="0.01" name="cost" id="editCost"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Performed By</label>
<input type="text" name="performed_by" id="editPerformedBy"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Parts List</label>
<input type="text" name="parts_list" id="editPartsList"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">Description</label>
<textarea name="description" id="editDescription" rows="3"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"></textarea>
</div>
<div class="flex space-x-3">
<button type="button" onclick="hideEditMaintenanceModal()"
class="flex-1 px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 transition">
Cancel
</button>
<button type="submit"
class="flex-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition">
Save Changes
</button>
</div>
</form>
</div>
</div>
<!-- Delete Vehicle Form -->
<form id="deleteVehicleForm" method="POST" action="<?php echo url('/vehicles/' . $vehicle['id'] . '/delete'); ?>" class="hidden"></form>
<script>
let debounceTimer;
const nameInput = document.getElementById('maintenanceName');
const suggestionsDiv = document.getElementById('suggestions');
nameInput.addEventListener('input', function() {
clearTimeout(debounceTimer);
const query = this.value;
if (query.length < 2) {
suggestionsDiv.classList.add('hidden');
return;
}
debounceTimer = setTimeout(() => {
fetch('<?php echo url('/maintenance/search'); ?>?q=' + encodeURIComponent(query))
.then(r => r.json())
.then(items => {
if (items.length > 0) {
suggestionsDiv.innerHTML = items.map(item =>
`<div class="px-3 py-2 hover:bg-gray-100 cursor-pointer" onclick="selectSuggestion('${item.replace(/'/g, "\\'")}')">${item}</div>`
).join('');
suggestionsDiv.classList.remove('hidden');
} else {
suggestionsDiv.classList.add('hidden');
}
});
}, 300);
});
function selectSuggestion(value) {
nameInput.value = value;
suggestionsDiv.classList.add('hidden');
}
document.addEventListener('click', function(e) {
if (!nameInput.contains(e.target) && !suggestionsDiv.contains(e.target)) {
suggestionsDiv.classList.add('hidden');
}
});
function showEditVehicleModal() {
document.getElementById('editVehicleModal').classList.remove('hidden');
}
function hideEditVehicleModal() {
document.getElementById('editVehicleModal').classList.add('hidden');
}
function showEditMaintenanceModal(item) {
document.getElementById('editName').value = item.name;
document.getElementById('editDate').value = item.date;
document.getElementById('editMileage').value = item.mileage;
document.getElementById('editCost').value = item.cost || '';
document.getElementById('editPerformedBy').value = item.performed_by || '';
document.getElementById('editPartsList').value = item.parts_list || '';
document.getElementById('editDescription').value = item.description || '';
document.getElementById('editMaintenanceForm').action = '<?php echo url('/maintenance/'); ?>' + item.id + '/edit';
document.getElementById('editMaintenanceModal').classList.remove('hidden');
}
function hideEditMaintenanceModal() {
document.getElementById('editMaintenanceModal').classList.add('hidden');
}
function confirmDelete() {
if (confirm('Are you sure you want to delete this vehicle and all its maintenance records?')) {
document.getElementById('deleteVehicleForm').submit();
}
}
function toggleExportMenu() {
document.getElementById('exportMenu').classList.toggle('hidden');
}
document.addEventListener('click', function(e) {
const exportMenu = document.getElementById('exportMenu');
if (!e.target.closest('#exportMenu') && !e.target.closest('button[onclick="toggleExportMenu()"]')) {
exportMenu.classList.add('hidden');
}
});
</script>
</body>
</html>