diff --git a/car-clearance-system/.htaccess b/car-clearance-system/.htaccess new file mode 100644 index 0000000000..671ddb22de --- /dev/null +++ b/car-clearance-system/.htaccess @@ -0,0 +1,6 @@ +RewriteEngine On + +# Redirect all requests to public/index.php +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d +RewriteRule ^(.*)$ public/index.php [QSA,L] diff --git a/car-clearance-system/README.md b/car-clearance-system/README.md new file mode 100644 index 0000000000..9682eeaf66 --- /dev/null +++ b/car-clearance-system/README.md @@ -0,0 +1,183 @@ +# Car Clearance Management System + +A full-stack web application for managing car/truck clearance records, built with PHP 8+, MySQL, HTML, CSS, JavaScript, and Bootstrap 5. + +## Features + +- **Authentication & Authorization**: Secure login with role-based access (Admin/User) +- **Clearance Records**: Add, view, edit, and delete car clearance records +- **Dual Currency Support**: Track payments in USD or SSP +- **Reports**: Filterable, searchable, sortable reports with print functionality +- **User Management**: Admins can create, edit, and delete users +- **Responsive Design**: Mobile-friendly layout with Bootstrap 5 +- **Security**: PDO prepared statements, password hashing, input validation + +## Requirements + +- PHP 8.0+ +- MySQL 5.7+ / MariaDB 10.3+ +- Apache with mod_rewrite (XAMPP, Laragon, WAMP, etc.) + +## Setup Instructions + +### Option A: Using XAMPP + +1. **Install XAMPP** from [https://www.apachefriends.org/](https://www.apachefriends.org/) + +2. **Copy project files** to your XAMPP htdocs folder: + ``` + Copy the entire `car-clearance-system` folder to: + C:\xampp\htdocs\car-clearance-system + ``` + +3. **Start Apache and MySQL** from XAMPP Control Panel. + +4. **Create the database**: + - Open phpMyAdmin: [http://localhost/phpmyadmin](http://localhost/phpmyadmin) + - Click "Import" tab + - Select `database/schema.sql` file + - Click "Go" to execute + + **Or** run from command line: + ```bash + mysql -u root < database/schema.sql + ``` + +5. **Update database config** (if needed): + - Edit `config/database.php` + - Update `DB_HOST`, `DB_USER`, `DB_PASS` if different from defaults + +6. **Access the application**: + - Navigate to: [http://localhost/car-clearance-system/public/](http://localhost/car-clearance-system/public/) + - Login with: **admin** / **admin123** + +### Option B: Using Laragon + +1. **Install Laragon** from [https://laragon.org/](https://laragon.org/) + +2. **Copy project files** to Laragon's www folder: + ``` + Copy to: C:\laragon\www\car-clearance-system + ``` + +3. **Start All** services in Laragon. + +4. **Create the database**: + - Open Laragon Terminal + - Run: `mysql -u root < database/schema.sql` + - Or use HeidiSQL (included with Laragon) to import `database/schema.sql` + +5. **Access the application**: + - Navigate to: [http://car-clearance-system.test/public/](http://car-clearance-system.test/public/) + - Or: [http://localhost/car-clearance-system/public/](http://localhost/car-clearance-system/public/) + - Login with: **admin** / **admin123** + +### Option C: PHP Built-in Server (Quick Testing) + +```bash +cd car-clearance-system/public +php -S localhost:8080 +``` +Then visit: [http://localhost:8080](http://localhost:8080) + +> Note: Ensure MySQL is running separately and the database is imported. + +## Project Structure + +``` +car-clearance-system/ +├── config/ +│ └── database.php # Database connection configuration +├── controllers/ +│ ├── AuthController.php # Authentication (login/logout) +│ ├── ClearanceController.php # Clearance CRUD operations +│ ├── UserController.php # User management (admin) +│ └── ReportController.php # Report generation +├── models/ +│ ├── User.php # User model (database queries) +│ └── Clearance.php # Clearance model (database queries) +├── views/ +│ ├── layouts/ +│ │ ├── header.php # HTML head & opening tags +│ │ ├── sidebar.php # Navigation sidebar +│ │ └── footer.php # Scripts & closing tags +│ ├── auth/ +│ │ └── login.php # Login page +│ ├── dashboard/ +│ │ └── index.php # Main dashboard view +│ ├── clearances/ +│ │ └── form.php # Add/Edit clearance form +│ ├── users/ +│ │ └── list.php # User management page +│ └── reports/ +│ ├── index.php # Reports with filters +│ └── print.php # Print-friendly report +├── public/ +│ ├── index.php # Application entry point / router +│ ├── css/ +│ │ └── style.css # Custom styles +│ └── js/ +│ └── app.js # Client-side JavaScript +├── database/ +│ └── schema.sql # Database schema & seed data +├── .htaccess # Apache URL rewriting +└── README.md # This file +``` + +## Default Login Credentials + +| Username | Password | Role | +|----------|----------|-------| +| admin | admin123 | Admin | + +## User Roles + +| Permission | Admin | User | +|------------------------|-------|------| +| View clearance records | Yes | Yes | +| Add clearance records | Yes | Yes | +| Edit clearance records | Yes | No | +| Delete clearance records| Yes | No | +| View reports | Yes | Yes | +| Print reports | Yes | Yes | +| Manage users | Yes | No | + +## Currency Logic + +- **Paid in USD**: The USD amount is stored; SSP is set to 0 +- **Paid in SSP**: The SSP amount is stored; USD is set to 0 +- A radio button controls which currency field is active +- Server-side validation ensures only one currency has a value > 0 + +## Troubleshooting + +### "Access Denied" database error +- Check `config/database.php` credentials match your MySQL setup +- XAMPP default: user=`root`, password=`` (empty) +- Laragon default: user=`root`, password=`` (empty) + +### "Table doesn't exist" error +- Import `database/schema.sql` into MySQL first +- The application will auto-create the admin user if it doesn't exist + +### Page not found / 404 +- Ensure Apache `mod_rewrite` is enabled +- Check that `.htaccess` file is present in the root folder +- Try accessing directly: `http://localhost/car-clearance-system/public/index.php` + +### Styles not loading +- Access via `public/index.php` (not root `index.php`) +- Check browser console for 404 errors on CSS/JS files + +## Security Features + +- **Password Hashing**: Uses `password_hash()` with `PASSWORD_DEFAULT` (bcrypt) +- **SQL Injection Prevention**: All queries use PDO prepared statements +- **XSS Prevention**: All output escaped with `htmlspecialchars()` +- **Session Security**: PHP sessions for authentication state +- **Input Validation**: Both client-side (HTML5/JS) and server-side (PHP) +- **Role-Based Access**: Admin/User permissions enforced server-side + +## License + +This project is provided as-is for educational and business purposes. diff --git a/car-clearance-system/config/database.php b/car-clearance-system/config/database.php new file mode 100644 index 0000000000..7823c65c63 --- /dev/null +++ b/car-clearance-system/config/database.php @@ -0,0 +1,37 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, + ]; + + try { + $pdo = new PDO($dsn, DB_USER, DB_PASS, $options); + } catch (PDOException $e) { + die("Database connection failed: " . $e->getMessage()); + } + } + + return $pdo; +} diff --git a/car-clearance-system/controllers/AuthController.php b/car-clearance-system/controllers/AuthController.php new file mode 100644 index 0000000000..241760c19b --- /dev/null +++ b/car-clearance-system/controllers/AuthController.php @@ -0,0 +1,104 @@ +userModel = new User($db); + } + + /** + * Show login page + */ + public function showLogin(): void { + // Redirect if already logged in + if ($this->isLoggedIn()) { + header('Location: index.php?page=dashboard'); + exit; + } + include __DIR__ . '/../views/auth/login.php'; + } + + /** + * Process login form + */ + public function login(): void { + $username = trim($_POST['username'] ?? ''); + $password = $_POST['password'] ?? ''; + + // Validate inputs + if (empty($username) || empty($password)) { + $_SESSION['error'] = 'Username and password are required.'; + header('Location: index.php?page=login'); + exit; + } + + // Find user + $user = $this->userModel->findByUsername($username); + if (!$user || !$this->userModel->verifyPassword($user, $password)) { + $_SESSION['error'] = 'Invalid username or password.'; + header('Location: index.php?page=login'); + exit; + } + + // Set session + $_SESSION['user_id'] = $user['id']; + $_SESSION['username'] = $user['username']; + $_SESSION['role'] = $user['role']; + + $_SESSION['success'] = 'Welcome back, ' . htmlspecialchars($user['username']) . '!'; + header('Location: index.php?page=dashboard'); + exit; + } + + /** + * Logout + */ + public function logout(): void { + session_destroy(); + session_start(); + $_SESSION['success'] = 'You have been logged out.'; + header('Location: index.php?page=login'); + exit; + } + + /** + * Check if user is logged in + */ + public static function isLoggedIn(): bool { + return isset($_SESSION['user_id']); + } + + /** + * Check if current user is admin + */ + public static function isAdmin(): bool { + return isset($_SESSION['role']) && $_SESSION['role'] === 'admin'; + } + + /** + * Require login - redirect if not authenticated + */ + public static function requireLogin(): void { + if (!self::isLoggedIn()) { + $_SESSION['error'] = 'Please log in to access this page.'; + header('Location: index.php?page=login'); + exit; + } + } + + /** + * Require admin role + */ + public static function requireAdmin(): void { + self::requireLogin(); + if (!self::isAdmin()) { + $_SESSION['error'] = 'Access denied. Admin privileges required.'; + header('Location: index.php?page=dashboard'); + exit; + } + } +} diff --git a/car-clearance-system/controllers/ClearanceController.php b/car-clearance-system/controllers/ClearanceController.php new file mode 100644 index 0000000000..b0dca76943 --- /dev/null +++ b/car-clearance-system/controllers/ClearanceController.php @@ -0,0 +1,179 @@ +clearanceModel = new Clearance($db); + } + + /** + * List all clearances (Dashboard) + */ + public function index(): void { + AuthController::requireLogin(); + + // Get filter parameters + $dateFrom = $_GET['date_from'] ?? null; + $dateTo = $_GET['date_to'] ?? null; + $truckNumber = $_GET['truck_number'] ?? null; + $search = $_GET['search'] ?? null; + + $records = $this->clearanceModel->getAll($dateFrom, $dateTo, $truckNumber, $search); + $totals = $this->clearanceModel->getTotals($dateFrom, $dateTo, $truckNumber); + + include __DIR__ . '/../views/dashboard/index.php'; + } + + /** + * Show new clearance form + */ + public function create(): void { + AuthController::requireLogin(); + $record = null; + include __DIR__ . '/../views/clearances/form.php'; + } + + /** + * Store a new clearance record + */ + public function store(): void { + AuthController::requireLogin(); + + $errors = $this->validateInput($_POST); + if (!empty($errors)) { + $_SESSION['error'] = implode('
', $errors); + $_SESSION['old_input'] = $_POST; + header('Location: index.php?page=clearances&action=create'); + exit; + } + + $data = $this->sanitizeInput($_POST); + $data['cleared_by_username'] = $_SESSION['username']; + + if ($this->clearanceModel->create($data)) { + $_SESSION['success'] = 'Clearance record created successfully.'; + header('Location: index.php?page=dashboard'); + } else { + $_SESSION['error'] = 'Failed to create clearance record.'; + header('Location: index.php?page=clearances&action=create'); + } + exit; + } + + /** + * Show edit form (admin only) + */ + public function edit(int $id): void { + AuthController::requireAdmin(); + + $record = $this->clearanceModel->findById($id); + if (!$record) { + $_SESSION['error'] = 'Record not found.'; + header('Location: index.php?page=dashboard'); + exit; + } + + include __DIR__ . '/../views/clearances/form.php'; + } + + /** + * Update a clearance record (admin only) + */ + public function update(int $id): void { + AuthController::requireAdmin(); + + $errors = $this->validateInput($_POST); + if (!empty($errors)) { + $_SESSION['error'] = implode('
', $errors); + $_SESSION['old_input'] = $_POST; + header("Location: index.php?page=clearances&action=edit&id=$id"); + exit; + } + + $data = $this->sanitizeInput($_POST); + + if ($this->clearanceModel->update($id, $data)) { + $_SESSION['success'] = 'Clearance record updated successfully.'; + header('Location: index.php?page=dashboard'); + } else { + $_SESSION['error'] = 'Failed to update clearance record.'; + header("Location: index.php?page=clearances&action=edit&id=$id"); + } + exit; + } + + /** + * Delete a clearance record (admin only) + */ + public function delete(int $id): void { + AuthController::requireAdmin(); + + if ($this->clearanceModel->delete($id)) { + $_SESSION['success'] = 'Record deleted successfully.'; + } else { + $_SESSION['error'] = 'Failed to delete record.'; + } + header('Location: index.php?page=dashboard'); + exit; + } + + /** + * Validate form input + */ + private function validateInput(array $data): array { + $errors = []; + + if (empty($data['clearance_date'])) { + $errors[] = 'Date is required.'; + } + if (empty(trim($data['truck_number'] ?? ''))) { + $errors[] = 'Truck Number is required.'; + } + if (empty(trim($data['consignment'] ?? ''))) { + $errors[] = 'Consignment is required.'; + } + if (empty($data['truck_type']) || !in_array($data['truck_type'], ['Fuso', 'Trailer', 'Canter', 'Tipper', 'Pickup', 'Other'])) { + $errors[] = 'Valid Truck Type is required.'; + } + if (empty($data['paid_in_currency']) || !in_array($data['paid_in_currency'], ['USD', 'SSP'])) { + $errors[] = 'Payment currency must be specified.'; + } + + // Validate currency logic + if (!empty($data['paid_in_currency'])) { + if ($data['paid_in_currency'] === 'USD') { + if (empty($data['amount_usd']) || floatval($data['amount_usd']) <= 0) { + $errors[] = 'USD amount must be greater than 0 when paying in USD.'; + } + } elseif ($data['paid_in_currency'] === 'SSP') { + if (empty($data['amount_ssp']) || floatval($data['amount_ssp']) <= 0) { + $errors[] = 'SSP amount must be greater than 0 when paying in SSP.'; + } + } + } + + return $errors; + } + + /** + * Sanitize form input + */ + private function sanitizeInput(array $data): array { + $paidIn = $data['paid_in_currency']; + + return [ + 'clearance_date' => $data['clearance_date'], + 'truck_number' => trim($data['truck_number']), + 'consignment' => trim($data['consignment']), + 'items' => trim($data['items'] ?? ''), + 'truck_type' => $data['truck_type'], + 'amount_usd' => $paidIn === 'USD' ? floatval($data['amount_usd']) : 0.00, + 'amount_ssp' => $paidIn === 'SSP' ? floatval($data['amount_ssp']) : 0.00, + 'paid_in_currency' => $paidIn, + ]; + } +} diff --git a/car-clearance-system/controllers/ReportController.php b/car-clearance-system/controllers/ReportController.php new file mode 100644 index 0000000000..6b0c13fcd5 --- /dev/null +++ b/car-clearance-system/controllers/ReportController.php @@ -0,0 +1,44 @@ +clearanceModel = new Clearance($db); + } + + /** + * Show report page with filters + */ + public function index(): void { + AuthController::requireLogin(); + + $dateFrom = $_GET['date_from'] ?? null; + $dateTo = $_GET['date_to'] ?? null; + $truckNumber = $_GET['truck_number'] ?? null; + + $records = $this->clearanceModel->getAll($dateFrom, $dateTo, $truckNumber); + $totals = $this->clearanceModel->getTotals($dateFrom, $dateTo, $truckNumber); + + include __DIR__ . '/../views/reports/index.php'; + } + + /** + * Print-friendly report view + */ + public function printReport(): void { + AuthController::requireLogin(); + + $dateFrom = $_GET['date_from'] ?? null; + $dateTo = $_GET['date_to'] ?? null; + $truckNumber = $_GET['truck_number'] ?? null; + + $records = $this->clearanceModel->getAll($dateFrom, $dateTo, $truckNumber); + $totals = $this->clearanceModel->getTotals($dateFrom, $dateTo, $truckNumber); + + include __DIR__ . '/../views/reports/print.php'; + } +} diff --git a/car-clearance-system/controllers/UserController.php b/car-clearance-system/controllers/UserController.php new file mode 100644 index 0000000000..1e97f153a3 --- /dev/null +++ b/car-clearance-system/controllers/UserController.php @@ -0,0 +1,130 @@ +userModel = new User($db); + } + + /** + * List all users + */ + public function index(): void { + AuthController::requireAdmin(); + $users = $this->userModel->getAll(); + include __DIR__ . '/../views/users/list.php'; + } + + /** + * Store a new user + */ + public function store(): void { + AuthController::requireAdmin(); + + $username = trim($_POST['username'] ?? ''); + $password = $_POST['password'] ?? ''; + $role = $_POST['role'] ?? 'user'; + + // Validate + $errors = []; + if (empty($username)) { + $errors[] = 'Username is required.'; + } + if (strlen($username) < 3) { + $errors[] = 'Username must be at least 3 characters.'; + } + if (empty($password)) { + $errors[] = 'Password is required.'; + } + if (strlen($password) < 6) { + $errors[] = 'Password must be at least 6 characters.'; + } + if (!in_array($role, ['admin', 'user'])) { + $errors[] = 'Invalid role.'; + } + // Check username uniqueness + if ($this->userModel->findByUsername($username)) { + $errors[] = 'Username already exists.'; + } + + if (!empty($errors)) { + $_SESSION['error'] = implode('
', $errors); + header('Location: index.php?page=users'); + exit; + } + + if ($this->userModel->create($username, $password, $role)) { + $_SESSION['success'] = 'User created successfully.'; + } else { + $_SESSION['error'] = 'Failed to create user.'; + } + header('Location: index.php?page=users'); + exit; + } + + /** + * Update a user + */ + public function update(int $id): void { + AuthController::requireAdmin(); + + $username = trim($_POST['username'] ?? ''); + $password = $_POST['password'] ?? ''; + $role = $_POST['role'] ?? 'user'; + + // Validate + $errors = []; + if (empty($username)) { + $errors[] = 'Username is required.'; + } + if (!in_array($role, ['admin', 'user'])) { + $errors[] = 'Invalid role.'; + } + // Check username uniqueness (exclude current user) + $existing = $this->userModel->findByUsername($username); + if ($existing && $existing['id'] != $id) { + $errors[] = 'Username already exists.'; + } + + if (!empty($errors)) { + $_SESSION['error'] = implode('
', $errors); + header('Location: index.php?page=users'); + exit; + } + + $pass = !empty($password) ? $password : null; + if ($this->userModel->update($id, $username, $role, $pass)) { + $_SESSION['success'] = 'User updated successfully.'; + } else { + $_SESSION['error'] = 'Failed to update user.'; + } + header('Location: index.php?page=users'); + exit; + } + + /** + * Delete a user + */ + public function delete(int $id): void { + AuthController::requireAdmin(); + + // Prevent deleting own account + if ($id == $_SESSION['user_id']) { + $_SESSION['error'] = 'You cannot delete your own account.'; + header('Location: index.php?page=users'); + exit; + } + + if ($this->userModel->delete($id)) { + $_SESSION['success'] = 'User deleted successfully.'; + } else { + $_SESSION['error'] = 'Failed to delete user.'; + } + header('Location: index.php?page=users'); + exit; + } +} diff --git a/car-clearance-system/database/schema.sql b/car-clearance-system/database/schema.sql new file mode 100644 index 0000000000..a9bbf60e21 --- /dev/null +++ b/car-clearance-system/database/schema.sql @@ -0,0 +1,39 @@ +-- Car Clearance Management System Database Schema +-- Run this SQL file to set up the database + +CREATE DATABASE IF NOT EXISTS car_clearance_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +USE car_clearance_db; + +-- Users table +CREATE TABLE IF NOT EXISTS users ( + id INT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(50) NOT NULL UNIQUE, + password_hash VARCHAR(255) NOT NULL, + role ENUM('admin', 'user') NOT NULL DEFAULT 'user', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB; + +-- Car clearances table +CREATE TABLE IF NOT EXISTS car_clearances ( + id INT AUTO_INCREMENT PRIMARY KEY, + clearance_date DATE NOT NULL, + truck_number VARCHAR(50) NOT NULL, + consignment VARCHAR(255) NOT NULL, + items TEXT, + truck_type ENUM('Fuso', 'Trailer', 'Canter', 'Tipper', 'Pickup', 'Other') NOT NULL, + amount_usd DECIMAL(12,2) DEFAULT 0.00, + amount_ssp DECIMAL(12,2) DEFAULT 0.00, + paid_in_currency ENUM('USD', 'SSP') NOT NULL, + cleared_by_username VARCHAR(50) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX idx_clearance_date (clearance_date), + INDEX idx_truck_number (truck_number), + INDEX idx_cleared_by (cleared_by_username) +) ENGINE=InnoDB; + +-- The default admin user will be auto-created by the application on first run. +-- Username: admin, Password: admin123 +-- If you prefer to insert manually, generate a hash with: +-- php -r "echo password_hash('admin123', PASSWORD_DEFAULT);" +-- Then run: +-- INSERT INTO users (username, password_hash, role) VALUES ('admin', '', 'admin'); diff --git a/car-clearance-system/models/Clearance.php b/car-clearance-system/models/Clearance.php new file mode 100644 index 0000000000..a1b62a1171 --- /dev/null +++ b/car-clearance-system/models/Clearance.php @@ -0,0 +1,140 @@ +db = $db; + } + + /** + * Get all clearances with optional filters + */ + public function getAll(?string $dateFrom = null, ?string $dateTo = null, ?string $truckNumber = null, ?string $search = null): array { + $sql = "SELECT * FROM car_clearances WHERE 1=1"; + $params = []; + + if ($dateFrom) { + $sql .= " AND clearance_date >= :date_from"; + $params['date_from'] = $dateFrom; + } + if ($dateTo) { + $sql .= " AND clearance_date <= :date_to"; + $params['date_to'] = $dateTo; + } + if ($truckNumber) { + $sql .= " AND truck_number LIKE :truck_number"; + $params['truck_number'] = "%$truckNumber%"; + } + if ($search) { + $sql .= " AND (truck_number LIKE :search OR consignment LIKE :search2 OR items LIKE :search3)"; + $params['search'] = "%$search%"; + $params['search2'] = "%$search%"; + $params['search3'] = "%$search%"; + } + + $sql .= " ORDER BY clearance_date DESC, created_at DESC"; + + $stmt = $this->db->prepare($sql); + $stmt->execute($params); + return $stmt->fetchAll(); + } + + /** + * Find clearance by ID + */ + public function findById(int $id): ?array { + $stmt = $this->db->prepare("SELECT * FROM car_clearances WHERE id = :id"); + $stmt->execute(['id' => $id]); + $record = $stmt->fetch(); + return $record ?: null; + } + + /** + * Create a new clearance record + */ + public function create(array $data): bool { + $stmt = $this->db->prepare( + "INSERT INTO car_clearances + (clearance_date, truck_number, consignment, items, truck_type, amount_usd, amount_ssp, paid_in_currency, cleared_by_username) + VALUES + (:clearance_date, :truck_number, :consignment, :items, :truck_type, :amount_usd, :amount_ssp, :paid_in_currency, :cleared_by_username)" + ); + return $stmt->execute([ + 'clearance_date' => $data['clearance_date'], + 'truck_number' => $data['truck_number'], + 'consignment' => $data['consignment'], + 'items' => $data['items'] ?? '', + 'truck_type' => $data['truck_type'], + 'amount_usd' => $data['amount_usd'], + 'amount_ssp' => $data['amount_ssp'], + 'paid_in_currency' => $data['paid_in_currency'], + 'cleared_by_username'=> $data['cleared_by_username'], + ]); + } + + /** + * Update an existing clearance record + */ + public function update(int $id, array $data): bool { + $stmt = $this->db->prepare( + "UPDATE car_clearances SET + clearance_date = :clearance_date, + truck_number = :truck_number, + consignment = :consignment, + items = :items, + truck_type = :truck_type, + amount_usd = :amount_usd, + amount_ssp = :amount_ssp, + paid_in_currency = :paid_in_currency + WHERE id = :id" + ); + return $stmt->execute([ + 'clearance_date' => $data['clearance_date'], + 'truck_number' => $data['truck_number'], + 'consignment' => $data['consignment'], + 'items' => $data['items'] ?? '', + 'truck_type' => $data['truck_type'], + 'amount_usd' => $data['amount_usd'], + 'amount_ssp' => $data['amount_ssp'], + 'paid_in_currency' => $data['paid_in_currency'], + 'id' => $id, + ]); + } + + /** + * Delete a clearance record + */ + public function delete(int $id): bool { + $stmt = $this->db->prepare("DELETE FROM car_clearances WHERE id = :id"); + return $stmt->execute(['id' => $id]); + } + + /** + * Get totals for filtered records + */ + public function getTotals(?string $dateFrom = null, ?string $dateTo = null, ?string $truckNumber = null): array { + $sql = "SELECT COALESCE(SUM(amount_usd), 0) as total_usd, COALESCE(SUM(amount_ssp), 0) as total_ssp, COUNT(*) as total_records FROM car_clearances WHERE 1=1"; + $params = []; + + if ($dateFrom) { + $sql .= " AND clearance_date >= :date_from"; + $params['date_from'] = $dateFrom; + } + if ($dateTo) { + $sql .= " AND clearance_date <= :date_to"; + $params['date_to'] = $dateTo; + } + if ($truckNumber) { + $sql .= " AND truck_number LIKE :truck_number"; + $params['truck_number'] = "%$truckNumber%"; + } + + $stmt = $this->db->prepare($sql); + $stmt->execute($params); + return $stmt->fetch(); + } +} diff --git a/car-clearance-system/models/User.php b/car-clearance-system/models/User.php new file mode 100644 index 0000000000..27062b566e --- /dev/null +++ b/car-clearance-system/models/User.php @@ -0,0 +1,88 @@ +db = $db; + } + + /** + * Find user by username + */ + public function findByUsername(string $username): ?array { + $stmt = $this->db->prepare("SELECT * FROM users WHERE username = :username"); + $stmt->execute(['username' => $username]); + $user = $stmt->fetch(); + return $user ?: null; + } + + /** + * Find user by ID + */ + public function findById(int $id): ?array { + $stmt = $this->db->prepare("SELECT * FROM users WHERE id = :id"); + $stmt->execute(['id' => $id]); + $user = $stmt->fetch(); + return $user ?: null; + } + + /** + * Get all users + */ + public function getAll(): array { + $stmt = $this->db->query("SELECT id, username, role, created_at FROM users ORDER BY created_at DESC"); + return $stmt->fetchAll(); + } + + /** + * Create a new user + */ + public function create(string $username, string $password, string $role = 'user'): bool { + $hash = password_hash($password, PASSWORD_DEFAULT); + $stmt = $this->db->prepare( + "INSERT INTO users (username, password_hash, role) VALUES (:username, :password_hash, :role)" + ); + return $stmt->execute([ + 'username' => $username, + 'password_hash' => $hash, + 'role' => $role, + ]); + } + + /** + * Update a user + */ + public function update(int $id, string $username, string $role, ?string $password = null): bool { + if ($password) { + $hash = password_hash($password, PASSWORD_DEFAULT); + $stmt = $this->db->prepare( + "UPDATE users SET username = :username, password_hash = :hash, role = :role WHERE id = :id" + ); + return $stmt->execute(['username' => $username, 'hash' => $hash, 'role' => $role, 'id' => $id]); + } else { + $stmt = $this->db->prepare( + "UPDATE users SET username = :username, role = :role WHERE id = :id" + ); + return $stmt->execute(['username' => $username, 'role' => $role, 'id' => $id]); + } + } + + /** + * Delete a user + */ + public function delete(int $id): bool { + $stmt = $this->db->prepare("DELETE FROM users WHERE id = :id"); + return $stmt->execute(['id' => $id]); + } + + /** + * Verify password for a user + */ + public function verifyPassword(array $user, string $password): bool { + return password_verify($password, $user['password_hash']); + } +} diff --git a/car-clearance-system/public/css/style.css b/car-clearance-system/public/css/style.css new file mode 100644 index 0000000000..2cb29bf9ca --- /dev/null +++ b/car-clearance-system/public/css/style.css @@ -0,0 +1,343 @@ +/* Car Clearance Management System - Custom Styles */ + +:root { + --sidebar-width: 260px; + --primary-color: #2c3e50; + --secondary-color: #34495e; + --accent-color: #3498db; + --success-color: #27ae60; + --danger-color: #e74c3c; +} + +/* Layout */ +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background-color: #f8f9fa; +} + +.wrapper { + display: flex; + min-height: 100vh; +} + +/* Sidebar */ +.sidebar { + width: var(--sidebar-width); + background: var(--primary-color); + min-height: 100vh; + position: fixed; + top: 0; + left: 0; + z-index: 1000; + transition: all 0.3s; +} + +.sidebar .sidebar-header { + padding: 20px; + background: var(--secondary-color); + text-align: center; +} + +.sidebar .sidebar-header h3 { + color: #fff; + margin: 0; + font-size: 1.1rem; + font-weight: 600; +} + +.sidebar .sidebar-header small { + color: #bdc3c7; + font-size: 0.75rem; +} + +.sidebar ul.nav-list { + list-style: none; + padding: 0; + margin: 0; +} + +.sidebar ul.nav-list li a { + display: flex; + align-items: center; + padding: 14px 20px; + color: #ecf0f1; + text-decoration: none; + transition: all 0.2s; + border-left: 3px solid transparent; +} + +.sidebar ul.nav-list li a:hover, +.sidebar ul.nav-list li a.active { + background: var(--secondary-color); + border-left-color: var(--accent-color); + color: #fff; +} + +.sidebar ul.nav-list li a i { + margin-right: 12px; + width: 20px; + text-align: center; +} + +.sidebar .user-info { + position: absolute; + bottom: 0; + width: 100%; + padding: 15px 20px; + background: var(--secondary-color); + color: #ecf0f1; + font-size: 0.85rem; +} + +.sidebar .user-info .badge { + font-size: 0.7rem; +} + +/* Main Content */ +.main-content { + margin-left: var(--sidebar-width); + padding: 30px; + width: calc(100% - var(--sidebar-width)); + min-height: 100vh; +} + +.page-header { + margin-bottom: 25px; + padding-bottom: 15px; + border-bottom: 2px solid #e9ecef; +} + +.page-header h2 { + color: var(--primary-color); + font-weight: 600; + margin: 0; +} + +/* Cards */ +.stat-card { + border: none; + border-radius: 10px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08); + transition: transform 0.2s; +} + +.stat-card:hover { + transform: translateY(-2px); +} + +.stat-card .card-body { + padding: 20px; +} + +.stat-card .stat-icon { + width: 50px; + height: 50px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.5rem; + color: #fff; +} + +.stat-card .stat-value { + font-size: 1.5rem; + font-weight: 700; + color: var(--primary-color); +} + +.stat-card .stat-label { + color: #7f8c8d; + font-size: 0.85rem; +} + +/* Tables */ +.table-container { + background: #fff; + border-radius: 10px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08); + padding: 20px; +} + +.table th { + background: #f8f9fa; + font-weight: 600; + color: var(--primary-color); + border-top: none; + white-space: nowrap; +} + +.table td { + vertical-align: middle; +} + +/* Filters */ +.filter-card { + background: #fff; + border-radius: 10px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08); + padding: 20px; + margin-bottom: 20px; +} + +/* Login Page */ +.login-container { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, var(--primary-color) 0%, var(--accent-color) 100%); +} + +.login-card { + background: #fff; + border-radius: 15px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2); + padding: 40px; + width: 100%; + max-width: 420px; +} + +.login-card h2 { + color: var(--primary-color); + font-weight: 700; + margin-bottom: 5px; +} + +.login-card .subtitle { + color: #7f8c8d; + margin-bottom: 30px; +} + +/* Modals */ +.modal-header { + background: var(--primary-color); + color: #fff; + border-radius: 0; +} + +.modal-header .btn-close { + filter: invert(1); +} + +/* Buttons */ +.btn-primary { + background: var(--accent-color); + border-color: var(--accent-color); +} + +.btn-primary:hover { + background: #2980b9; + border-color: #2980b9; +} + +/* Responsive */ +@media (max-width: 768px) { + .sidebar { + margin-left: -260px; + } + + .sidebar.active { + margin-left: 0; + } + + .main-content { + margin-left: 0; + width: 100%; + } + + .main-content.active { + margin-left: var(--sidebar-width); + } +} + +/* Print Styles */ +@media print { + .sidebar, + .no-print, + .filter-card, + .btn, + nav, + .alert { + display: none !important; + } + + .main-content { + margin-left: 0 !important; + width: 100% !important; + padding: 0 !important; + } + + .table-container { + box-shadow: none !important; + padding: 0 !important; + } + + .print-header { + display: block !important; + text-align: center; + margin-bottom: 20px; + padding-bottom: 10px; + border-bottom: 2px solid #333; + } + + .print-header h1 { + font-size: 1.5rem; + margin-bottom: 5px; + } + + .print-footer { + display: block !important; + margin-top: 30px; + padding-top: 10px; + border-top: 1px solid #333; + font-size: 0.8rem; + } + + table { + font-size: 0.8rem; + } + + .stat-card { + box-shadow: none !important; + border: 1px solid #ddd; + } +} + +.print-header, +.print-footer { + display: none; +} + +/* Toast/Alert animations */ +.alert { + animation: slideIn 0.3s ease-out; +} + +@keyframes slideIn { + from { + transform: translateY(-10px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +/* Badge styles for truck types */ +.badge-truck { + font-size: 0.75rem; + padding: 5px 10px; +} + +/* Amount styling */ +.amount-usd { + color: #27ae60; + font-weight: 600; +} + +.amount-ssp { + color: #2980b9; + font-weight: 600; +} diff --git a/car-clearance-system/public/index.php b/car-clearance-system/public/index.php new file mode 100644 index 0000000000..d8bdadec33 --- /dev/null +++ b/car-clearance-system/public/index.php @@ -0,0 +1,110 @@ +findByUsername('admin')) { + $userModel->create('admin', 'admin123', 'admin'); +} + +// Initialize controllers +$authController = new AuthController($db); +$clearanceController = new ClearanceController($db); +$userController = new UserController($db); +$reportController = new ReportController($db); + +// Route handling +$page = $_GET['page'] ?? 'login'; +$action = $_GET['action'] ?? 'index'; +$id = isset($_GET['id']) ? intval($_GET['id']) : 0; + +switch ($page) { + case 'login': + if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $authController->login(); + } else { + $authController->showLogin(); + } + break; + + case 'logout': + $authController->logout(); + break; + + case 'dashboard': + $clearanceController->index(); + break; + + case 'clearances': + switch ($action) { + case 'create': + $clearanceController->create(); + break; + case 'store': + $clearanceController->store(); + break; + case 'edit': + $clearanceController->edit($id); + break; + case 'update': + $clearanceController->update($id); + break; + case 'delete': + $clearanceController->delete($id); + break; + default: + $clearanceController->index(); + } + break; + + case 'users': + switch ($action) { + case 'store': + $userController->store(); + break; + case 'update': + $userController->update($id); + break; + case 'delete': + $userController->delete($id); + break; + default: + $userController->index(); + } + break; + + case 'reports': + switch ($action) { + case 'print': + $reportController->printReport(); + break; + default: + $reportController->index(); + } + break; + + default: + if (AuthController::isLoggedIn()) { + header('Location: index.php?page=dashboard'); + } else { + header('Location: index.php?page=login'); + } + exit; +} diff --git a/car-clearance-system/public/js/app.js b/car-clearance-system/public/js/app.js new file mode 100644 index 0000000000..698626e710 --- /dev/null +++ b/car-clearance-system/public/js/app.js @@ -0,0 +1,162 @@ +/** + * Car Clearance Management System - Client-side JavaScript + */ + +document.addEventListener('DOMContentLoaded', function () { + // Auto-dismiss alerts after 5 seconds + const alerts = document.querySelectorAll('.alert-dismissible'); + alerts.forEach(function (alert) { + setTimeout(function () { + const bsAlert = new bootstrap.Alert(alert); + bsAlert.close(); + }, 5000); + }); + + // Currency toggle logic + const paidInRadios = document.querySelectorAll('input[name="paid_in_currency"]'); + const usdField = document.getElementById('amount_usd'); + const sspField = document.getElementById('amount_ssp'); + + if (paidInRadios.length > 0) { + paidInRadios.forEach(function (radio) { + radio.addEventListener('change', function () { + toggleCurrencyFields(this.value); + }); + }); + + // Initialize on page load + const checkedRadio = document.querySelector('input[name="paid_in_currency"]:checked'); + if (checkedRadio) { + toggleCurrencyFields(checkedRadio.value); + } + } + + function toggleCurrencyFields(currency) { + if (currency === 'USD') { + if (usdField) { + usdField.disabled = false; + usdField.required = true; + usdField.closest('.mb-3').style.opacity = '1'; + } + if (sspField) { + sspField.disabled = true; + sspField.required = false; + sspField.value = '0'; + sspField.closest('.mb-3').style.opacity = '0.5'; + } + } else if (currency === 'SSP') { + if (sspField) { + sspField.disabled = false; + sspField.required = true; + sspField.closest('.mb-3').style.opacity = '1'; + } + if (usdField) { + usdField.disabled = true; + usdField.required = false; + usdField.value = '0'; + usdField.closest('.mb-3').style.opacity = '0.5'; + } + } + } + + // Search functionality for tables + const searchInput = document.getElementById('tableSearch'); + if (searchInput) { + searchInput.addEventListener('keyup', function () { + const filter = this.value.toLowerCase(); + const table = document.getElementById('dataTable'); + if (!table) return; + + const rows = table.querySelectorAll('tbody tr'); + rows.forEach(function (row) { + const text = row.textContent.toLowerCase(); + row.style.display = text.includes(filter) ? '' : 'none'; + }); + }); + } + + // Table sorting + const sortableHeaders = document.querySelectorAll('th[data-sort]'); + sortableHeaders.forEach(function (header) { + header.style.cursor = 'pointer'; + header.addEventListener('click', function () { + const table = this.closest('table'); + const tbody = table.querySelector('tbody'); + const rows = Array.from(tbody.querySelectorAll('tr')); + const column = this.dataset.sort; + const colIndex = Array.from(this.parentNode.children).indexOf(this); + const isAsc = this.classList.contains('sort-asc'); + + // Remove sort classes from all headers + sortableHeaders.forEach(function (h) { + h.classList.remove('sort-asc', 'sort-desc'); + }); + + rows.sort(function (a, b) { + const aVal = a.children[colIndex]?.textContent.trim() || ''; + const bVal = b.children[colIndex]?.textContent.trim() || ''; + + if (column === 'number') { + return isAsc + ? parseFloat(bVal) - parseFloat(aVal) + : parseFloat(aVal) - parseFloat(bVal); + } + return isAsc + ? bVal.localeCompare(aVal) + : aVal.localeCompare(bVal); + }); + + this.classList.add(isAsc ? 'sort-desc' : 'sort-asc'); + rows.forEach(function (row) { + tbody.appendChild(row); + }); + }); + }); + + // Confirm delete + const deleteButtons = document.querySelectorAll('.btn-delete'); + deleteButtons.forEach(function (btn) { + btn.addEventListener('click', function (e) { + if (!confirm('Are you sure you want to delete this record? This action cannot be undone.')) { + e.preventDefault(); + } + }); + }); + + // Mobile sidebar toggle + const sidebarToggle = document.getElementById('sidebarToggle'); + if (sidebarToggle) { + sidebarToggle.addEventListener('click', function () { + document.querySelector('.sidebar').classList.toggle('active'); + document.querySelector('.main-content').classList.toggle('active'); + }); + } + + // Form validation + const forms = document.querySelectorAll('.needs-validation'); + forms.forEach(function (form) { + form.addEventListener('submit', function (event) { + if (!form.checkValidity()) { + event.preventDefault(); + event.stopPropagation(); + } + form.classList.add('was-validated'); + }); + }); + + // Re-enable disabled fields before form submit (for currency fields) + const clearanceForm = document.getElementById('clearanceForm'); + if (clearanceForm) { + clearanceForm.addEventListener('submit', function () { + if (usdField) usdField.disabled = false; + if (sspField) sspField.disabled = false; + }); + } +}); + +/** + * Print report function + */ +function printReport() { + window.print(); +} diff --git a/car-clearance-system/views/auth/login.php b/car-clearance-system/views/auth/login.php new file mode 100644 index 0000000000..92ce3d7207 --- /dev/null +++ b/car-clearance-system/views/auth/login.php @@ -0,0 +1,69 @@ + + + + + + Login - Car Clearance Management System + + + + + +
+ +
+ + + + + diff --git a/car-clearance-system/views/clearances/form.php b/car-clearance-system/views/clearances/form.php new file mode 100644 index 0000000000..3713beb451 --- /dev/null +++ b/car-clearance-system/views/clearances/form.php @@ -0,0 +1,157 @@ + + + + + + +
+ + + + + + + + +
+
+
+ +
+ + +
Date is required.
+
+ + +
+ + +
Truck number is required.
+
+ + +
+ + +
Consignment is required.
+
+ + +
+ + +
Please select a truck type.
+
+ + +
+ + +
+ + +
+ +
+
+ > + +
+
+ > + +
+
+
+ + +
+ +
+ $ + +
+
+ + +
+ +
+ SSP + +
+
+ + +
+ + +
+
+ +
+ +
+ + + Cancel + +
+
+
+
+ + diff --git a/car-clearance-system/views/dashboard/index.php b/car-clearance-system/views/dashboard/index.php new file mode 100644 index 0000000000..485f7d1fe1 --- /dev/null +++ b/car-clearance-system/views/dashboard/index.php @@ -0,0 +1,167 @@ + + + + +
+ + + + + + + + + + + + + + +
+
+
+
+
+ +
+
+
+
Total Records
+
+
+
+
+
+
+
+
+ +
+
+
$
+
Total USD
+
+
+
+
+
+
+
+
+ +
+
+
SSP
+
Total SSP
+
+
+
+
+
+ + +
+
+ +
+ + +
+
+ + +
+
+ + +
+
+ + + Clear + +
+
+
+ + +
+ +
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + $r): ?> + + + + + + + + + + + + + + + + +
#DateTruck No.ConsignmentTruck TypeUSDSSPCleared ByActions
+ +

No records found.

+
0 ? '$' . number_format($r['amount_usd'], 2) : '-' ?> 0 ? number_format($r['amount_ssp'], 2) . ' SSP' : '-' ?> + + + + + + +
+
+
+
+ + diff --git a/car-clearance-system/views/layouts/footer.php b/car-clearance-system/views/layouts/footer.php new file mode 100644 index 0000000000..fbb4e2a202 --- /dev/null +++ b/car-clearance-system/views/layouts/footer.php @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/car-clearance-system/views/layouts/header.php b/car-clearance-system/views/layouts/header.php new file mode 100644 index 0000000000..2b95345c15 --- /dev/null +++ b/car-clearance-system/views/layouts/header.php @@ -0,0 +1,15 @@ + + + + + + Car Clearance Management System + + + + + + + + +
diff --git a/car-clearance-system/views/layouts/sidebar.php b/car-clearance-system/views/layouts/sidebar.php new file mode 100644 index 0000000000..a167796aab --- /dev/null +++ b/car-clearance-system/views/layouts/sidebar.php @@ -0,0 +1,43 @@ + + diff --git a/car-clearance-system/views/reports/index.php b/car-clearance-system/views/reports/index.php new file mode 100644 index 0000000000..a444fbb079 --- /dev/null +++ b/car-clearance-system/views/reports/index.php @@ -0,0 +1,155 @@ + + + + +
+ + + + + + + + + +
+
Report Filters
+
+ +
+ + +
+
+ + +
+
+ + +
+
+ + + Clear + +
+
+
+ + +
+
+
+
+
+ +
+
+
+
Total Records
+
+
+
+
+
+
+
+
+ +
+
+
$
+
Total USD Collected
+
+
+
+
+
+
+
+
+ +
+
+
SSP
+
Total SSP Collected
+
+
+
+
+
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + $r): ?> + + + + + + + + + + + + + + + + + + + + + + + + +
#DateTruck No.ConsignmentItemsTruck TypeUSDSSPCleared By
+ +

No records found. Adjust your filters.

+
0 ? '$' . number_format($r['amount_usd'], 2) : '-' ?> 0 ? number_format($r['amount_ssp'], 2) . ' SSP' : '-' ?>
TOTALS:$ SSP
+
+
+
+ + diff --git a/car-clearance-system/views/reports/print.php b/car-clearance-system/views/reports/print.php new file mode 100644 index 0000000000..ad22bd5369 --- /dev/null +++ b/car-clearance-system/views/reports/print.php @@ -0,0 +1,212 @@ + + + + + + Print Report - Car Clearance Management System + + + + + + + + + + + + +
+ Filters Applied: + From: + To: + | Truck: +
+ + + +
+
+
+
Total Records
+
+
+
$
+
Total USD
+
+
+
SSP
+
Total SSP
+
+
+ + + + + + + + + + + + + + + + + + + + + + + $r): ?> + + + + + + + + + + + + + + + + + + + + +
#DateTruck NumberConsignmentItemsTruck TypeAmount USDAmount SSPCleared By
No records found.
0 ? '$' . number_format($r['amount_usd'], 2) : '-' ?> 0 ? number_format($r['amount_ssp'], 2) : '-' ?>
TOTALS:$ SSP
+ + + + + + + diff --git a/car-clearance-system/views/users/list.php b/car-clearance-system/views/users/list.php new file mode 100644 index 0000000000..f54a799e5e --- /dev/null +++ b/car-clearance-system/views/users/list.php @@ -0,0 +1,152 @@ + + + + +
+ + + + + + + + + + + + + +
+
+ + + + + + + + + + + + $u): ?> + + + + + + + + + + + + +
#UsernameRoleCreated AtActions
+ + + + + + + + + + +
+
+
+
+ + + + +