Compare commits
37 Commits
4d7edeea88
...
fr/offline
Author | SHA1 | Date | |
---|---|---|---|
18c78e37a4 | |||
ccbb0eac64 | |||
21c2f4598b | |||
860a20d946 | |||
c5955010cb | |||
fc163431f8 | |||
15029970d6 | |||
e13edeccfc | |||
be6b465684 | |||
aded859a79 | |||
c29bd7cbab | |||
eff5be49c4 | |||
d13c490efb | |||
d98c208df9 | |||
85af85b1ee | |||
1ff7fc454f | |||
e0dfc7120f | |||
9c90710bf3 | |||
4b8ee90d8a | |||
59246decd7 | |||
147e3b1499 | |||
daec4ec1c9 | |||
3c6ecfb5e2 | |||
b4e08f28ca | |||
c4366edb29 | |||
d9f632da26 | |||
2847231376 | |||
d33d233f0f | |||
43960ddcb9 | |||
85209ff134 | |||
4c44dac115 | |||
01b057986e | |||
ecb2a6b74b | |||
5bca9b12aa | |||
d7b8aba072 | |||
3144078860 | |||
7b4b349816 |
53
.docker/nginx.conf
Normal file
53
.docker/nginx.conf
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# User and worker process settings
|
||||||
|
user www-data;
|
||||||
|
worker_processes auto;
|
||||||
|
pid /run/nginx.pid;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
# General settings
|
||||||
|
sendfile on;
|
||||||
|
tcp_nopush on;
|
||||||
|
tcp_nodelay on;
|
||||||
|
keepalive_timeout 65;
|
||||||
|
types_hash_max_size 2048;
|
||||||
|
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
access_log /var/log/nginx/access.log;
|
||||||
|
error_log /var/log/nginx/error.log;
|
||||||
|
|
||||||
|
# Gzip compression
|
||||||
|
gzip on;
|
||||||
|
gzip_disable "msie6";
|
||||||
|
|
||||||
|
# Server configuration
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
root /var/www/html/public;
|
||||||
|
index index.php index.html;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.php?$query_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ \\.php$ {
|
||||||
|
include fastcgi_params;
|
||||||
|
fastcgi_pass 127.0.0.1:9000;
|
||||||
|
fastcgi_index index.php;
|
||||||
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ /\.ht {
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
22
.dockerignore
Normal file
22
.dockerignore
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
storage/logs/*
|
||||||
|
!storage/logs/.gitignore
|
||||||
|
|
||||||
|
Dockerfile
|
||||||
|
docker-compose.yaml
|
||||||
|
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*.idea/
|
||||||
|
*.vscode/
|
||||||
|
*.DS_Store
|
||||||
|
|
||||||
|
# Exclude sensitive config files (uncomment if you don't want to include environment config)
|
||||||
|
# config/environment.php
|
||||||
|
|
||||||
|
README.md
|
||||||
|
TODO.md
|
||||||
|
LICENSE
|
||||||
|
|
36
.gitea/workflows/deploy.yaml
Normal file
36
.gitea/workflows/deploy.yaml
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
name: Build and Deploy Zola Website
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
env:
|
||||||
|
HOST: ${{ secrets.SERVER_IP }}
|
||||||
|
SSH_USERNAME: ${{ secrets.USERNAME }}
|
||||||
|
SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_KEY }}
|
||||||
|
DEST_FOLDER: "/srv/www/cz/filiprojek/fuelstats"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build_and_deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Deploy
|
||||||
|
run: |
|
||||||
|
apt update -y && apt-get install -y --no-install-recommends rsync
|
||||||
|
eval "$(ssh-agent -s)"
|
||||||
|
ssh-add - <<< "${SSH_PRIVATE_KEY}"
|
||||||
|
mkdir -p ~/.ssh/
|
||||||
|
ssh-keyscan -H ${HOST} >> ~/.ssh/known_hosts
|
||||||
|
rsync -r --delete-after ./* "${SSH_USERNAME}@${HOST}:${{ env.DEST_FOLDER }}"
|
||||||
|
|
||||||
|
- name: Copy environment.php
|
||||||
|
run: |
|
||||||
|
eval "$(ssh-agent -s)"
|
||||||
|
ssh-add - <<< "${SSH_PRIVATE_KEY}"
|
||||||
|
mkdir -p ~/.ssh/
|
||||||
|
ssh-keyscan -H ${HOST} >> ~/.ssh/known_hosts
|
||||||
|
ssh ${SSH_USERNAME}@${HOST} "cp /var/websrvenv/environment.php /srv/www/cz/filiprojek/fuelstats/config/environment.php"
|
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
*.swp
|
18
Dockerfile
Normal file
18
Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
FROM php:8.2-fpm-alpine
|
||||||
|
|
||||||
|
WORKDIR /var/www/html
|
||||||
|
|
||||||
|
RUN apk add --no-cache nginx curl \
|
||||||
|
&& docker-php-ext-install mysqli
|
||||||
|
|
||||||
|
COPY . /var/www/html
|
||||||
|
|
||||||
|
RUN chown -R www-data:www-data /var/www/html \
|
||||||
|
&& chmod -R 755 /var/www/html
|
||||||
|
|
||||||
|
COPY .docker/nginx.conf /etc/nginx/nginx.conf
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
CMD php-fpm & nginx -g "daemon off;"
|
||||||
|
|
6
LICENSE
6
LICENSE
@@ -208,8 +208,8 @@ If you develop a new program, and you want it to be of the greatest possible use
|
|||||||
|
|
||||||
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found.
|
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
habit-tracker
|
Fuel Stats
|
||||||
Copyright (C) 2024 fr
|
Copyright (C) 2024 Filip Rojek
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
@@ -221,7 +221,7 @@ Also add information on how to contact you by electronic and paper mail.
|
|||||||
|
|
||||||
If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode:
|
If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode:
|
||||||
|
|
||||||
habit-tracker Copyright (C) 2024 fr
|
Fuel Stats Copyright (C) 2024 Filip Rojek
|
||||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.
|
This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
58
README.md
58
README.md
@@ -1,3 +1,57 @@
|
|||||||
# habit-tracker
|
# Fuel Stats
|
||||||
|
|
||||||
An app for tracking habits and motivation to achieve personal goals.
|
An app for tracking your fuel consumption and optimizing your driving efficiency.
|
||||||
|
|
||||||
|
## Used Technologies
|
||||||
|
- **Frontend:** HTML, CSS, JavaScript
|
||||||
|
- **Backend:** PHP (OOP)
|
||||||
|
- **Database:** MariaDB
|
||||||
|
|
||||||
|
## How to Build
|
||||||
|
|
||||||
|
### Build Using Docker
|
||||||
|
Run the container using docker-compose:
|
||||||
|
```bash
|
||||||
|
docker-compose --profile <dev|prod> up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
The app should be available at http://localhost:8000.
|
||||||
|
|
||||||
|
PhpMyAdmin should be available at http://localhost:8080.
|
||||||
|
|
||||||
|
### Build Manually
|
||||||
|
1. Clone the repository:
|
||||||
|
```bash
|
||||||
|
git clone https://git.filiprojek.cz/fr/fuel-stats.git
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Create `config/environment.php`:
|
||||||
|
- It should have the following structure:
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
define('DB_HOST', 'your db host');
|
||||||
|
define('DB_USER', 'your db username');
|
||||||
|
define('DB_PASS', 'your db password');
|
||||||
|
define('DB_NAME', 'your db name');
|
||||||
|
```
|
||||||
|
- For the database, you can use the included `docker-compose.yaml` which includes both MariaDB and PhpMyAdmin.
|
||||||
|
|
||||||
|
3. Start a local web server:
|
||||||
|
- You can use PHP's integrated server by running this:
|
||||||
|
```bash
|
||||||
|
php -S localhost:8000 -t ./public
|
||||||
|
```
|
||||||
|
- You can use any host and any port you prefer.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
1. Register and log in to the app.
|
||||||
|
2. Add your vehicles with their details (fuel type, registration, etc.).
|
||||||
|
3. Record each refueling:
|
||||||
|
- Select your vehicle.
|
||||||
|
- Input the number of liters, price per liter, and total cost.
|
||||||
|
4. Track your fuel consumption and spending through the dashboard.
|
||||||
|
5. View detailed stats and graphs to analyze your driving habits.
|
||||||
|
|
||||||
|
## License
|
||||||
|
This project is licensed under GPL3.0 and later. More information is available in the `LICENSE` file.
|
||||||
|
15
TODO.md
Normal file
15
TODO.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# TODO
|
||||||
|
|
||||||
|
## Auth and User stuff
|
||||||
|
- [x] signup
|
||||||
|
- [x] signin
|
||||||
|
- [ ] edit user data - change password, mail...
|
||||||
|
|
||||||
|
## Core of the app
|
||||||
|
- [ ] intro tutorial when no car exist or just dont show anything
|
||||||
|
- [ ] change/set default car
|
||||||
|
- [ ] specific car view - charts, fuel records
|
||||||
|
- [ ] remove/edit fuel record
|
||||||
|
|
||||||
|
- [ ] IndexDB
|
||||||
|
- [ ]
|
85
app/controllers/AuthController.php
Normal file
85
app/controllers/AuthController.php
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class AuthController extends Controller {
|
||||||
|
public function signin() {
|
||||||
|
if($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$email = $_POST['email'] ?? '';
|
||||||
|
$password = $_POST['password'] ?? '';
|
||||||
|
|
||||||
|
$validator = new Validator();
|
||||||
|
$validator->required('email', $email);
|
||||||
|
$validator->email('email', $email);
|
||||||
|
$validator->required('password', $password);
|
||||||
|
|
||||||
|
if (!$validator->passes()) {
|
||||||
|
$this->view('auth/signup', [
|
||||||
|
'error' => 'Please correct the errors below.',
|
||||||
|
'validationErrors' => $validator->errors() ?: [],
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = new User();
|
||||||
|
$result = $user->login($email, $password);
|
||||||
|
|
||||||
|
if($result === true) {
|
||||||
|
$this->redirect('/dashboard');
|
||||||
|
} else {
|
||||||
|
$this->view('auth/signin', ['error' => $result], 'noheader');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->view('auth/signin', ['title' => 'Log In'], 'noheader');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function signup() {
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$username = $_POST['username'] ?? '';
|
||||||
|
$email = $_POST['email'] ?? '';
|
||||||
|
$password = $_POST['password'] ?? '';
|
||||||
|
$password2 = $_POST['password-2'] ?? '';
|
||||||
|
|
||||||
|
$validator = new Validator();
|
||||||
|
$validator->required('username', $username);
|
||||||
|
$validator->email('email', $email);
|
||||||
|
$validator->required('password', $password);
|
||||||
|
$validator->minLength('password', $password, 8);
|
||||||
|
$validator->alphanumeric('password', $password);
|
||||||
|
|
||||||
|
if ($password !== $password2) {
|
||||||
|
$validator->errors()['password_confirmation'] = 'Passwords do not match.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$validator->passes()) {
|
||||||
|
$this->view('auth/signup', [
|
||||||
|
'error' => 'Please correct the errors below.',
|
||||||
|
'validationErrors' => $validator->errors() ?: [],
|
||||||
|
], 'noheader');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = new User();
|
||||||
|
$result = $user->register($username, $email, $password);
|
||||||
|
|
||||||
|
if ($result === true) {
|
||||||
|
$this->redirect('/auth/signin');
|
||||||
|
} else {
|
||||||
|
$this->view('auth/signup', [
|
||||||
|
'error' => $result,
|
||||||
|
'validationErrors' => [],
|
||||||
|
], 'noheader');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->view('auth/signup', [
|
||||||
|
'title' => 'Register',
|
||||||
|
'validationErrors' => [],
|
||||||
|
], 'noheader');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function logout() {
|
||||||
|
session_unset();
|
||||||
|
session_destroy();
|
||||||
|
$this->redirect('/auth/signin');
|
||||||
|
}
|
||||||
|
}
|
42
app/controllers/DashboardController.php
Normal file
42
app/controllers/DashboardController.php
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
class DashboardController extends Controller {
|
||||||
|
public function index() {
|
||||||
|
$vehicle = new Vehicle();
|
||||||
|
$vehicles = $vehicle->getVehiclesByUser($_SESSION['user']['id']);
|
||||||
|
|
||||||
|
$default_car = $vehicle->getDefaultVehicle($_SESSION['user']['id']);
|
||||||
|
|
||||||
|
$refuel = new Refuel();
|
||||||
|
$data = [
|
||||||
|
"date" => [],
|
||||||
|
"price" => [],
|
||||||
|
];
|
||||||
|
$raw_data = $refuel->latest_data($default_car['id'], 5);
|
||||||
|
foreach($raw_data as $one) {
|
||||||
|
array_push($data['date'], date('d. m.', strtotime($one['created_at'])));
|
||||||
|
array_push($data['price'], $one['price_per_liter']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$latest_record = [
|
||||||
|
'name',
|
||||||
|
'liters',
|
||||||
|
'price_per_liter',
|
||||||
|
'total_price',
|
||||||
|
'created_at'
|
||||||
|
];
|
||||||
|
|
||||||
|
$latest_record = $refuel->latest_one($_SESSION['user']['id'])[0];
|
||||||
|
|
||||||
|
$this->view('dashboard/index', [
|
||||||
|
'title' => 'Dashboard',
|
||||||
|
'vehicles' => $vehicles,
|
||||||
|
'date_price_data' => $data,
|
||||||
|
'default_car' => $default_car,
|
||||||
|
'latest_record' => $latest_record,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reroute(){
|
||||||
|
$this->redirect('/dashboard');
|
||||||
|
}
|
||||||
|
}
|
18
app/controllers/HomeController.php
Normal file
18
app/controllers/HomeController.php
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class HomeController extends Controller {
|
||||||
|
public function index() {
|
||||||
|
$data = [
|
||||||
|
'title' => 'Home'
|
||||||
|
];
|
||||||
|
$this->view('home/index', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function home() {
|
||||||
|
$this->index();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dashboard() {
|
||||||
|
$this->view("dashboard/index");
|
||||||
|
}
|
||||||
|
}
|
85
app/controllers/RefuelController.php
Normal file
85
app/controllers/RefuelController.php
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class RefuelController extends Controller {
|
||||||
|
public function create() {
|
||||||
|
if($_SERVER['REQUEST_METHOD'] === 'GET'){
|
||||||
|
$vehicle = new Vehicle();
|
||||||
|
$vehicles = $vehicle->getVehiclesByUser($_SESSION['user']['id']);
|
||||||
|
$this->view('refuel/create', [
|
||||||
|
'title' => "New refuel record",
|
||||||
|
'vehicles' => $vehicles,
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$vehicle_id = $_POST['vehicle'] ?? '';
|
||||||
|
$fuel_type = $_POST['fuel_type'] ?? '';
|
||||||
|
$liters = $_POST['liters'] ?? '';
|
||||||
|
$price_per_liter = $_POST['price_per_liter'] ?? '';
|
||||||
|
$total_price = $_POST['total_price'] ?? '';
|
||||||
|
$note = $_POST['note'] ?? '';
|
||||||
|
|
||||||
|
$validator = new Validator();
|
||||||
|
$validator->required('vehicle', $vehicle_id);
|
||||||
|
$validator->required('fuel_type', $fuel_type);
|
||||||
|
$validator->required('liters', $liters);
|
||||||
|
$validator->required('price_per_liter', $price_per_liter);
|
||||||
|
$validator->required('total_price', $total_price);
|
||||||
|
$validator->number('liters', $liters);
|
||||||
|
$validator->number('price_per_liter', $price_per_liter);
|
||||||
|
$validator->number('total_price', $total_price);
|
||||||
|
|
||||||
|
if (round($liters * $price_per_liter, 2) != $total_price) {
|
||||||
|
$validator->setErrors(["total_price" => "Price calculation is wrong"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if($note == "") $note = NULL;
|
||||||
|
|
||||||
|
if (!$validator->passes()) {
|
||||||
|
$vehicle = new Vehicle();
|
||||||
|
$vehicles = $vehicle->getVehiclesByUser($_SESSION['user']['id']);
|
||||||
|
$this->view('refuel/create', [
|
||||||
|
'error' => 'Please correct the errors below.',
|
||||||
|
'validationErrors' => $validator->errors() ?: [],
|
||||||
|
'vehicles' => $vehicles,
|
||||||
|
'title' => 'New refuel record',
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$record = new Refuel();
|
||||||
|
$result = $record->create([
|
||||||
|
'user_id' => $_SESSION['user']['id'],
|
||||||
|
'vehicle_id' => $vehicle_id,
|
||||||
|
'fuel_type' => $fuel_type,
|
||||||
|
'note' => $note,
|
||||||
|
'liters' => $liters,
|
||||||
|
'price_per_liter' => $price_per_liter,
|
||||||
|
'total_price' => $total_price,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($result === true) {
|
||||||
|
$this->redirect('/');
|
||||||
|
} else {
|
||||||
|
$vehicle = new Vehicle();
|
||||||
|
$vehicles = $vehicle->getVehiclesByUser($_SESSION['user']['id']);
|
||||||
|
$this->view('refuel/create', [
|
||||||
|
'title' => 'New refuel record',
|
||||||
|
'error' => $result,
|
||||||
|
'validationErrors' => [],
|
||||||
|
'vehicles' => $vehicles,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function edit() {
|
||||||
|
// Edit refuel record (to be implemented later)
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete() {
|
||||||
|
// Delete refuel record (to be implemented later)
|
||||||
|
}
|
||||||
|
}
|
61
app/controllers/VehicleController.php
Normal file
61
app/controllers/VehicleController.php
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class VehicleController extends Controller {
|
||||||
|
public function index() {
|
||||||
|
$vehicle = new Vehicle();
|
||||||
|
$vehicles = $vehicle->getVehiclesByUser($_SESSION['user']['id']);
|
||||||
|
$this->view('vehicles/index', ['title' => 'Vehicles', 'vehicles' => $vehicles]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create() {
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$name = $_POST['name'] ?? '';
|
||||||
|
$registration_plate = $_POST['registration_plate'] ?? '';
|
||||||
|
$fuel_type = $_POST['fuel_type'] ?? '';
|
||||||
|
$note = $_POST['note'] ?? '';
|
||||||
|
|
||||||
|
$validator = new Validator();
|
||||||
|
$validator->required('name', $name);
|
||||||
|
$validator->required('registration_plate', $registration_plate);
|
||||||
|
$validator->required('fuel_type', $fuel_type);
|
||||||
|
|
||||||
|
if($note == "") $note = NULL;
|
||||||
|
|
||||||
|
if (!$validator->passes()) {
|
||||||
|
$this->view('vehicles/create', [
|
||||||
|
'error' => 'Please correct the errors below.',
|
||||||
|
'validationErrors' => $validator->errors() ?: [],
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$vehicle = new Vehicle();
|
||||||
|
$result = $vehicle->create([
|
||||||
|
'name' => $name,
|
||||||
|
'registration_plate' => strtoupper($registration_plate),
|
||||||
|
'fuel_type' => $fuel_type,
|
||||||
|
'note' => $note,
|
||||||
|
'user_id' => $_SESSION['user']['id'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
if ($result === true) {
|
||||||
|
$this->redirect('/vehicles');
|
||||||
|
} else {
|
||||||
|
$this->view('vehicles/create', ['title' => 'Create vehicle', 'error' => $result, 'validationErrors' => []] );
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
$this->view('vehicles/create', ['title' => 'Create Vehicle']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function edit() {
|
||||||
|
// Edit vehicle (to be implemented later)
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete() {
|
||||||
|
// Delete vehicle (to be implemented later)
|
||||||
|
}
|
||||||
|
}
|
92
app/models/Refuel.php
Normal file
92
app/models/Refuel.php
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class Refuel {
|
||||||
|
private $db;
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
$this->db = Database::getInstance()->getConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create($data) {
|
||||||
|
try{
|
||||||
|
$stmt = $this->db->prepare("
|
||||||
|
INSERT INTO refueling_records (user_id, vehicle_id, fuel_type, note, liters, price_per_liter, total_price, created_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, NOW())
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->bind_param(
|
||||||
|
"iissddd",
|
||||||
|
$data['user_id'],
|
||||||
|
$data['vehicle_id'],
|
||||||
|
$data['fuel_type'],
|
||||||
|
$data['note'],
|
||||||
|
$data['liters'],
|
||||||
|
$data['price_per_liter'],
|
||||||
|
$data['total_price'],
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return "Error: " . $stmt->error;
|
||||||
|
}
|
||||||
|
} catch(mysqli_sql_exception $e) {
|
||||||
|
return $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function latest_data($vehicle_id, $record_count) {
|
||||||
|
try {
|
||||||
|
$stmt = $this->db->prepare("
|
||||||
|
SELECT `liters`, `price_per_liter`, `total_price`, `created_at`
|
||||||
|
FROM `refueling_records`
|
||||||
|
WHERE `vehicle_id` = ?
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT ?;
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->bind_param("ii", $vehicle_id, $record_count);
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
$data = $result->fetch_all(MYSQLI_ASSOC);
|
||||||
|
$stmt->close();
|
||||||
|
return array_reverse($data);
|
||||||
|
} else {
|
||||||
|
return "Error: " . $stmt->error;
|
||||||
|
}
|
||||||
|
} catch (mysqli_sql_exception $e) {
|
||||||
|
return $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function latest_one($user_id, $record_count = 1) {
|
||||||
|
try {
|
||||||
|
$stmt = $this->db->prepare("
|
||||||
|
SELECT
|
||||||
|
`r`.`vehicle_id`,
|
||||||
|
`v`.`name` AS `vehicle_name`,
|
||||||
|
`r`.`liters`,
|
||||||
|
`r`.`price_per_liter`,
|
||||||
|
`r`.`total_price`,
|
||||||
|
`r`.`created_at`
|
||||||
|
FROM `refueling_records` AS `r`
|
||||||
|
JOIN `vehicles` AS `v` ON `r`.`vehicle_id` = `v`.`id`
|
||||||
|
WHERE `r`.`user_id` = ?
|
||||||
|
ORDER BY `r`.`created_at` DESC
|
||||||
|
LIMIT ?;
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->bind_param("ii", $user_id, $record_count);
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
$data = $result->fetch_all(MYSQLI_ASSOC);
|
||||||
|
$stmt->close();
|
||||||
|
return array_reverse($data);
|
||||||
|
} else {
|
||||||
|
return "Error: " . $stmt->error;
|
||||||
|
}
|
||||||
|
} catch (mysqli_sql_exception $e) {
|
||||||
|
return $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
65
app/models/User.php
Normal file
65
app/models/User.php
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once '../core/Database.php';
|
||||||
|
|
||||||
|
class User {
|
||||||
|
private $db;
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
$this->db = Database::getInstance()->getConnection();
|
||||||
|
|
||||||
|
if ($this->db) {
|
||||||
|
error_log("Database connection established successfully.");
|
||||||
|
} else {
|
||||||
|
error_log("Failed to connect to the database.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function register($username, $email, $password) {
|
||||||
|
// Check if email already exists
|
||||||
|
$stmt = $this->db->prepare("SELECT id FROM users WHERE email = ?");
|
||||||
|
$stmt->bind_param("s", $email);
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
$stmt->close();
|
||||||
|
|
||||||
|
if ($result->num_rows > 0) {
|
||||||
|
return "Email is already registered";
|
||||||
|
}
|
||||||
|
|
||||||
|
$hashedPassword = password_hash($password, PASSWORD_BCRYPT);
|
||||||
|
|
||||||
|
$stmt = $this->db->prepare("INSERT INTO users (username, email, password, points, created_at) VALUES (?, ?, ?, 0, NOW())");
|
||||||
|
$stmt->bind_param("sss", $username, $email, $hashedPassword);
|
||||||
|
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return "Error: " . $stmt->error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function login($email, $password) {
|
||||||
|
$hashedPassword = password_hash($password, PASSWORD_BCRYPT);
|
||||||
|
|
||||||
|
$stmt = $this->db->prepare("SELECT id, username, password FROM users WHERE email = ?");
|
||||||
|
$stmt->bind_param("s", $email);
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
$stmt->close();
|
||||||
|
|
||||||
|
if ($result->num_rows === 1) {
|
||||||
|
$user = $result->fetch_assoc();
|
||||||
|
if (password_verify($password, $user['password'])) {
|
||||||
|
$_SESSION['user'] = [
|
||||||
|
'id' => $user['id'],
|
||||||
|
'username' => $user['username'],
|
||||||
|
'email' => $email,
|
||||||
|
];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Incorrect username or password.";
|
||||||
|
}
|
||||||
|
}
|
63
app/models/Vehicle.php
Normal file
63
app/models/Vehicle.php
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class Vehicle {
|
||||||
|
private $db;
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
$this->db = Database::getInstance()->getConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create($data) {
|
||||||
|
try{
|
||||||
|
$stmt = $this->db->prepare("
|
||||||
|
INSERT INTO vehicles (user_id, name, registration_plate, fuel_type, note, created_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, NOW())
|
||||||
|
");
|
||||||
|
|
||||||
|
$stmt->bind_param(
|
||||||
|
"issss",
|
||||||
|
$data['user_id'],
|
||||||
|
$data['name'],
|
||||||
|
$data['registration_plate'],
|
||||||
|
$data['fuel_type'],
|
||||||
|
$data['note'],
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($stmt->execute()) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return "Error: " . $stmt->error;
|
||||||
|
}
|
||||||
|
} catch(mysqli_sql_exception $e) {
|
||||||
|
return $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getVehiclesByUser($user_id) {
|
||||||
|
$stmt = $this->db->prepare("SELECT id, name, registration_plate, fuel_type, note, created_at FROM vehicles WHERE user_id = ?");
|
||||||
|
$stmt->bind_param("i", $user_id);
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
|
||||||
|
$vehicles = [];
|
||||||
|
while ($row = $result->fetch_assoc()) {
|
||||||
|
$vehicles[] = $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $vehicles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDefaultVehicle($user_id) {
|
||||||
|
$stmt = $this->db->prepare("
|
||||||
|
SELECT id, name, registration_plate, fuel_type, note, is_default
|
||||||
|
FROM vehicles
|
||||||
|
WHERE user_id = ? AND is_default = TRUE
|
||||||
|
LIMIT 1
|
||||||
|
");
|
||||||
|
$stmt->bind_param("i", $user_id);
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
|
||||||
|
return $result->fetch_assoc();
|
||||||
|
}
|
||||||
|
}
|
33
app/views/auth/signin.php
Normal file
33
app/views/auth/signin.php
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<link rel="stylesheet" href="/css/login.css">
|
||||||
|
<link rel="stylesheet" href="/css/form.css">
|
||||||
|
<section class="form signin">
|
||||||
|
<div class="header-form">
|
||||||
|
<img src="/img/logo.jpg" alt="Fuel Stats Logo">
|
||||||
|
<h1>Sign in to Fuel Stats</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($this->get('error')): ?>
|
||||||
|
<div class="error"><?= $this->get('error') ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<form action="/auth/signin" method="POST">
|
||||||
|
<label for="email">Email</label>
|
||||||
|
<input type="email" name="email" id="email" value="<?= htmlspecialchars($_POST['email'] ?? '') ?>" required>
|
||||||
|
<?php if (isset($this->get('validationErrors')['email'])): ?>
|
||||||
|
<small class="error"><?= $this->get('validationErrors')['email'] ?></small>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input type="password" name="password" id="password" required>
|
||||||
|
<?php if (isset($this->get('validationErrors')['password'])): ?>
|
||||||
|
<small class="error"><?= $this->get('validationErrors')['password'] ?></small>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<input type="submit" value="Sign In">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="bordered">
|
||||||
|
<p>New to Fuel Stats?</p>
|
||||||
|
<a href="/auth/signup">Create an account</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
45
app/views/auth/signup.php
Normal file
45
app/views/auth/signup.php
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<link rel="stylesheet" href="/css/login.css">
|
||||||
|
<link rel="stylesheet" href="/css/form.css">
|
||||||
|
<section class="form signup">
|
||||||
|
<div class="header-form">
|
||||||
|
<img src="/img/logo.jpg" alt="Fuel Stats Logo">
|
||||||
|
<h1>Sign up to Fuel Stats</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($this->get('error')): ?>
|
||||||
|
<div class="error"><?= $this->get('error') ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<form action="/auth/signup" method="POST">
|
||||||
|
<label for="username">Username</label>
|
||||||
|
<input type="text" name="username" id="username" value="<?= htmlspecialchars($_POST['username'] ?? '') ?>" required>
|
||||||
|
<?php if (isset($this->get('validationErrors')['username'])): ?>
|
||||||
|
<small class="error"><?= $this->get('validationErrors')['username'] ?></small>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<label for="email">Email</label>
|
||||||
|
<input type="email" name="email" id="email" value="<?= htmlspecialchars($_POST['email'] ?? '') ?>" required>
|
||||||
|
<?php if (isset($this->get('validationErrors')['email'])): ?>
|
||||||
|
<small class="error"><?= $this->get('validationErrors')['email'] ?></small>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input type="password" name="password" id="password" required>
|
||||||
|
<?php if (isset($this->get('validationErrors')['password'])): ?>
|
||||||
|
<small class="error"><?= $this->get('validationErrors')['password'] ?></small>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<label for="password-2">Password again</label>
|
||||||
|
<input type="password" name="password-2" id="password-2" required>
|
||||||
|
<?php if (isset($this->get('validationErrors')['password_confirmation'])): ?>
|
||||||
|
<small class="error"><?= $this->get('validationErrors')['password_confirmation'] ?></small>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<input type="submit" value="Sign Up">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="bordered">
|
||||||
|
<p>Already have an account?</p>
|
||||||
|
<a href="/auth/signin">Sign in</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
64
app/views/dashboard/index.php
Normal file
64
app/views/dashboard/index.php
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<link rel="stylesheet" href="/css/dashboard.css">
|
||||||
|
|
||||||
|
<section class="dashboard">
|
||||||
|
<h1>Welcome, <?= htmlspecialchars($_SESSION['user']['username']) ?>!</h1>
|
||||||
|
<div>
|
||||||
|
<a href="/refuel/create" class="btn-green">Add new refuel record!</a>
|
||||||
|
<a href="/vehicles" class="btn-primary">List all vehicles</a>
|
||||||
|
<button class="btn-primary" id="btn-offline-add">Add new refuel record OFFLINE</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-wrapper">
|
||||||
|
<section class="card latest">
|
||||||
|
<h2>Latest fuel record</h2>
|
||||||
|
<hr>
|
||||||
|
<div>
|
||||||
|
<b>Car:</b>
|
||||||
|
<p><?= $data['latest_record']['vehicle_name'] ?></p>
|
||||||
|
<b>Liters:</b>
|
||||||
|
<p><?= $data['latest_record']['liters'] ?> liters</p>
|
||||||
|
<b>Price per liter:</b>
|
||||||
|
<p><?= $data['latest_record']['price_per_liter'] ?>,-/liter</p>
|
||||||
|
<b>Total price:</b>
|
||||||
|
<p><?= $data['latest_record']['total_price'] ?>,-</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<h2>Default car</h2>
|
||||||
|
<hr>
|
||||||
|
<b>Car</b>
|
||||||
|
<p><?= $data['default_car']['name'] ?></p>
|
||||||
|
<b>Registration plate</b>
|
||||||
|
<p><?= $data['default_car']['registration_plate'] ?></p>
|
||||||
|
<b>Fuel type</b>
|
||||||
|
<p><?= $data['default_car']['fuel_type'] ?></p>
|
||||||
|
<b>Note</b>
|
||||||
|
<p><?= $data['default_car']['note'] ?></p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card history-graph">
|
||||||
|
<h2>Chart of Gas price</h2>
|
||||||
|
<hr>
|
||||||
|
<p><?= $data['default_car']['name'] . " | " . $data['default_car']['registration_plate']?></p>
|
||||||
|
<canvas id="chart-gas-price"></canvas>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<script>
|
||||||
|
const ctx = document.getElementById('chart-gas-price');
|
||||||
|
const data = <?= json_encode($data['date_price_data']); ?>;
|
||||||
|
|
||||||
|
new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: [...data['date']],
|
||||||
|
datasets: [{
|
||||||
|
label: 'Gas price history',
|
||||||
|
data: data['price'],
|
||||||
|
borderWidth: 1,
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<script defer src="/js/offline-records.js"></script>
|
13
app/views/errors/404.php
Normal file
13
app/views/errors/404.php
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Fuel Stats | Error 494</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Error 404 - Page not found</h1>
|
||||||
|
<a href="/">Go back home</a>
|
||||||
|
</body>
|
||||||
|
</html>
|
4
app/views/home/index.php
Normal file
4
app/views/home/index.php
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<div>
|
||||||
|
<h1>Welcome to Fuel Stats!</h1>
|
||||||
|
<p>Keep track of your refuels.</p>
|
||||||
|
</div>
|
35
app/views/layouts/base.php
Normal file
35
app/views/layouts/base.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="author" content="Filip Rojek | http://filiprojek.cz">
|
||||||
|
<meta name="email" content="webmaster(@)fofrweb.com">
|
||||||
|
<meta name="copyright" content="(c) filiprojek.cz">
|
||||||
|
<title>Fuel Stats<?= isset($data['title']) ? " | " . $data['title'] : "" ?></title>
|
||||||
|
<link rel="stylesheet" href="/css/main.css">
|
||||||
|
<link rel="stylesheet" href="/css/global.css">
|
||||||
|
<link rel="stylesheet" href="/css/vars.css">
|
||||||
|
<link rel="stylesheet" href="/css/header.css">
|
||||||
|
<link rel="icon" type="image/x-icon" href="/img/favicon.ico">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<div id="hd-left">
|
||||||
|
<a href="/"><img src="/img/logo.jpg" alt="home"></a>
|
||||||
|
<label><?= isset($data['title']) ? $data['title'] : "" ?></label>
|
||||||
|
</div>
|
||||||
|
<div id="hd-right">
|
||||||
|
<?php if (!isset($_SESSION['user'])): ?>
|
||||||
|
<a href="/auth/signin">Log In</a>
|
||||||
|
<a href="/auth/signup">Sign Up</a>
|
||||||
|
<?php else: ?>
|
||||||
|
<a href="/auth/logout" class="btn-secondary">Sign out</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main class="content">
|
||||||
|
<?= $content ?>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
20
app/views/layouts/noheader.php
Normal file
20
app/views/layouts/noheader.php
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="author" content="Filip Rojek | http://filiprojek.cz">
|
||||||
|
<meta name="email" content="webmaster(@)fofrweb.com">
|
||||||
|
<meta name="copyright" content="(c) filiprojek.cz">
|
||||||
|
<title>Fuel Stats<?= isset($data['title']) ? " | " . $data['title'] : "" ?></title>
|
||||||
|
<link rel="stylesheet" href="/css/main.css">
|
||||||
|
<link rel="stylesheet" href="/css/global.css">
|
||||||
|
<link rel="stylesheet" href="/css/vars.css">
|
||||||
|
<link rel="icon" type="image/x-icon" href="/img/favicon.ico">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="content">
|
||||||
|
<?= $content ?>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
119
app/views/refuel/create.php
Normal file
119
app/views/refuel/create.php
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
<link rel="stylesheet" href="/css/form.css">
|
||||||
|
<link rel="stylesheet" href="/css/vehicle_create.css">
|
||||||
|
<section class="form">
|
||||||
|
<h1 class="header-form"><?= $this->get('title') ?></h1>
|
||||||
|
|
||||||
|
<?php if ($this->get('error')): ?>
|
||||||
|
<div class="error" style="color: red; margin-bottom: 1rem;">
|
||||||
|
<?= htmlspecialchars($this->get('error')) ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<form method="POST" action="/refuel/create">
|
||||||
|
<label for="vehicle">Vehicle</label>
|
||||||
|
<select name="vehicle" id="vehicle">
|
||||||
|
<?php foreach ($this->get('vehicles') as $vehicle): ?>
|
||||||
|
<option value="<?= $vehicle['id'] ?>"><?= $vehicle['name'] . " | " . $vehicle['registration_plate'] ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<?php if (isset($this->get('validationErrors')['vehicle'])): ?>
|
||||||
|
<small class="error"><?= $this->get('validationErrors')['vehicle'] ?></small>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<label for="fuel_type">Fuel type</label>
|
||||||
|
<select name="fuel_type" id="fuel_type">
|
||||||
|
<option value="Diesel">Diesel</option>
|
||||||
|
<option value="Gasoline 95">Gasoline 95</option>
|
||||||
|
<option value="Gasoline 98">Gasoline 98</option>
|
||||||
|
<option value="Premium Diesel">Premium Diesel</option>
|
||||||
|
<option value="Premium Gasoline 95">Premium Gasoline 95</option>
|
||||||
|
<option value="Premium Gasoline 98">Premium Gasoline 98</option>
|
||||||
|
<option value="Other">Other</option>
|
||||||
|
</select>
|
||||||
|
<?php if (isset($this->get('validationErrors')['fuel_type'])): ?>
|
||||||
|
<small class="error"><?= $this->get('validationErrors')['fuel_type'] ?></small>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<label for="liters">Liters</label>
|
||||||
|
<input type="number" name="liters" id="liters" min="0" step=".01" value="<?= htmlspecialchars($_POST['liters'] ?? '0.0') ?>">
|
||||||
|
<?php if (isset($this->get('validationErrors')['liters'])): ?>
|
||||||
|
<small class="error"><?= $this->get('validationErrors')['liters'] ?></small>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<label for="price_per_liter">Price per liter</label>
|
||||||
|
<input type="number" name="price_per_liter" id="price_per_liter" min="0" step=".01" value="<?= htmlspecialchars($_POST['price_per_liter'] ?? '0.0') ?>">
|
||||||
|
<?php if (isset($this->get('validationErrors')['price_per_liter'])): ?>
|
||||||
|
<small class="error"><?= $this->get('validationErrors')['price_per_liter'] ?></small>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<label for="total_price">Total price</label>
|
||||||
|
<input type="number" name="total_price" id="total_price" min="0" step=".01" value="<?= htmlspecialchars($_POST['total_price'] ?? '0.0') ?>">
|
||||||
|
<?php if (isset($this->get('validationErrors')['total_price'])): ?>
|
||||||
|
<small class="error"><?= $this->get('validationErrors')['total_price'] ?></small>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<label for="note">Note</label>
|
||||||
|
<input type="text" name="note" id="note" value="<?= htmlspecialchars($_POST['note'] ?? '') ?>">
|
||||||
|
<?php if (isset($this->get('validationErrors')['note'])): ?>
|
||||||
|
<small class="error"><?= $this->get('validationErrors')['note'] ?></small>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<input type="submit" value="Create fuel record">
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const inp_lit = document.querySelector("input#liters")
|
||||||
|
const inp_ppl = document.querySelector("input#price_per_liter")
|
||||||
|
const inp_tot = document.querySelector("input#total_price")
|
||||||
|
|
||||||
|
const rnd = (num) => Math.round((num + Number.EPSILON) * 100) / 100
|
||||||
|
|
||||||
|
function calculate(){
|
||||||
|
let liters = Number(inp_lit.value)
|
||||||
|
let price_per_liter = Number(inp_ppl.value)
|
||||||
|
let total_price = Number(inp_tot.value)
|
||||||
|
|
||||||
|
if(price_per_liter > 0 && liters > 0) {
|
||||||
|
total_price = rnd(liters * price_per_liter)
|
||||||
|
}
|
||||||
|
|
||||||
|
if(price_per_liter > 0 && total_price > 0) {
|
||||||
|
liters = rnd(total_price / price_per_liter)
|
||||||
|
}
|
||||||
|
|
||||||
|
if(liters > 0 && total_price > 0) {
|
||||||
|
price_per_liter = rnd(total_price / liters)
|
||||||
|
}
|
||||||
|
|
||||||
|
inp_lit.value = liters
|
||||||
|
inp_ppl.value = price_per_liter
|
||||||
|
inp_tot.value = total_price
|
||||||
|
}
|
||||||
|
|
||||||
|
[inp_lit, inp_ppl, inp_tot].forEach(inp => {
|
||||||
|
inp.addEventListener("change", () => {
|
||||||
|
calculate()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const vehicles = <?= json_encode($data['vehicles']); ?>;
|
||||||
|
const fuel_sel = document.querySelector("#fuel_type")
|
||||||
|
const vehic_sel = document.querySelector("#vehicle")
|
||||||
|
|
||||||
|
function selectFuel() {
|
||||||
|
const veh_id = vehic_sel.value
|
||||||
|
vehicles.forEach(el => {
|
||||||
|
if(el.id == veh_id) {
|
||||||
|
fuel_sel.value = el.fuel_type
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
selectFuel()
|
||||||
|
|
||||||
|
vehic_sel.addEventListener("change", () => {
|
||||||
|
selectFuel()
|
||||||
|
})
|
||||||
|
</script>
|
0
app/views/shared/footer.php
Normal file
0
app/views/shared/footer.php
Normal file
35
app/views/vehicles/create.php
Normal file
35
app/views/vehicles/create.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<link rel="stylesheet" href="/css/form.css">
|
||||||
|
<link rel="stylesheet" href="/css/vehicle_create.css">
|
||||||
|
<section class="form">
|
||||||
|
<h1 class="header-form"><?= $this->get('title', 'Create Vehicle') ?></h1>
|
||||||
|
|
||||||
|
<?php if ($this->get('error')): ?>
|
||||||
|
<div class="error" style="color: red; margin-bottom: 1rem;">
|
||||||
|
<?= htmlspecialchars($this->get('error')) ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<form method="POST" action="/vehicles/create">
|
||||||
|
<label for="name">Vehicle name</label>
|
||||||
|
<input type="text" name="name" id="name" required value="<?= htmlspecialchars($_POST['name'] ?? '') ?>">
|
||||||
|
|
||||||
|
<label for="registration_plate">Registration plate</label>
|
||||||
|
<input type="text" name="registration_plate" id="registration_plate" maxlength="10" onkeypress="return event.charCode != 32" required value="<?= htmlspecialchars($_POST['registration_plate'] ?? '') ?>">
|
||||||
|
|
||||||
|
<label for="fuel_type">Fuel type</label>
|
||||||
|
<select name="fuel_type" id="fuel_type">
|
||||||
|
<option value="Diesel">Diesel</option>
|
||||||
|
<option value="Gasoline 95">Gasoline 95</option>
|
||||||
|
<option value="Gasoline 98">Gasoline 98</option>
|
||||||
|
<option value="Premium Diesel">Premium Diesel</option>
|
||||||
|
<option value="Premium Gasoline 95">Premium Gasoline 95</option>
|
||||||
|
<option value="Premium Gasoline 98">Premium Gasoline 98</option>
|
||||||
|
<option value="Other">Other</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label for="note">Note</label>
|
||||||
|
<input type="text" name="note" id="note" value="<?= htmlspecialchars($_POST['note'] ?? '') ?>">
|
||||||
|
|
||||||
|
<input type="submit" value="Create vehicle">
|
||||||
|
</form>
|
||||||
|
</section>
|
0
app/views/vehicles/edit.php
Normal file
0
app/views/vehicles/edit.php
Normal file
24
app/views/vehicles/index.php
Normal file
24
app/views/vehicles/index.php
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<link rel="stylesheet" href="/css/vehicles.css">
|
||||||
|
<section class="vehicles">
|
||||||
|
<?php if (empty($this->get('vehicles'))): ?>
|
||||||
|
<p>No vehicles yet. <a href="/vehicles/create">Add your first vehicle</a>.</p>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="btn-wrapper">
|
||||||
|
<a href="/vehicles/create" class="btn-green">Add new vehicle!</a>
|
||||||
|
</div>
|
||||||
|
<div class="vehicle-wrapper">
|
||||||
|
<?php foreach ($this->get('vehicles') as $vehicle): ?>
|
||||||
|
<div class="vehicle bordered">
|
||||||
|
<b><?= htmlspecialchars($vehicle['name']) ?></b>
|
||||||
|
<p><?= htmlspecialchars($vehicle['registration_plate']) ?></p>
|
||||||
|
<p><?= htmlspecialchars($vehicle['fuel_type']) ?></p>
|
||||||
|
<p><?= htmlspecialchars($vehicle['note'] ?? "") ?></p>
|
||||||
|
<div class="actions">
|
||||||
|
<a href="/vehicles/edit?id=<?= $vehicle['id'] ?>">Edit</a>
|
||||||
|
<a href="/vehicles/delete?id=<?= $vehicle['id'] ?>" onclick="return confirm('Are you sure you want to delete this habit?')">Delete</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</section>
|
1
config/.gitignore
vendored
Normal file
1
config/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
environment.php
|
6
config/environment.php.example
Normal file
6
config/environment.php.example
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
define('DB_HOST', '0.0.0.0');
|
||||||
|
define('DB_NAME', 'fuel_stats');
|
||||||
|
define('DB_USER', 'username');
|
||||||
|
define('DB_PASS', 'password');
|
23
core/Controller.php
Normal file
23
core/Controller.php
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
class Controller {
|
||||||
|
/**
|
||||||
|
* Redirect to a given URL
|
||||||
|
*
|
||||||
|
* @param string $url
|
||||||
|
*/
|
||||||
|
public function redirect($url) {
|
||||||
|
header("Location: $url");
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a view
|
||||||
|
*
|
||||||
|
* @param string $viewName
|
||||||
|
* @param array $data
|
||||||
|
*/
|
||||||
|
public function view($viewName, $data = [], $layout = "base") {
|
||||||
|
$view = new View();
|
||||||
|
$view->render($viewName, $data, $layout);
|
||||||
|
}
|
||||||
|
}
|
136
core/Database.php
Normal file
136
core/Database.php
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class Database {
|
||||||
|
private static $instance = null;
|
||||||
|
private $connection;
|
||||||
|
|
||||||
|
private function __construct() {
|
||||||
|
$this->initializeConnection();
|
||||||
|
$this->checkAndCreateDatabase();
|
||||||
|
$this->checkAndCreateTables();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the singleton instance of the Database
|
||||||
|
* @return Database
|
||||||
|
*/
|
||||||
|
public static function getInstance() {
|
||||||
|
if (self::$instance === null) {
|
||||||
|
self::$instance = new Database();
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the active database connection
|
||||||
|
* @return mysqli
|
||||||
|
*/
|
||||||
|
public function getConnection() {
|
||||||
|
return $this->connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the connection to the database
|
||||||
|
*/
|
||||||
|
private function initializeConnection() {
|
||||||
|
$this->connection = new mysqli(DB_HOST, DB_USER, DB_PASS);
|
||||||
|
|
||||||
|
if ($this->connection->connect_error) {
|
||||||
|
die("Database connection failed: " . $this->connection->connect_error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check and create the database if it doesn't exist
|
||||||
|
*/
|
||||||
|
private function checkAndCreateDatabase() {
|
||||||
|
$query = "CREATE DATABASE IF NOT EXISTS `" . DB_NAME . "`";
|
||||||
|
if (!$this->connection->query($query)) {
|
||||||
|
die("Failed to create or access database: " . $this->connection->error);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->connection->select_db(DB_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check and create required tables if they don't exist
|
||||||
|
*/
|
||||||
|
private function checkAndCreateTables() {
|
||||||
|
// Create tables
|
||||||
|
$usersTableQuery = "CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
username VARCHAR(50) NOT NULL,
|
||||||
|
email VARCHAR(100) NOT NULL UNIQUE,
|
||||||
|
password VARCHAR(255) NOT NULL,
|
||||||
|
points INT DEFAULT 0,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB;";
|
||||||
|
|
||||||
|
if (!$this->connection->query($usersTableQuery)) {
|
||||||
|
die("Failed to create users table: " . $this->connection->error);
|
||||||
|
}
|
||||||
|
|
||||||
|
$vehiclesTableQuery = "
|
||||||
|
CREATE TABLE IF NOT EXISTS vehicles (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_id INT NOT NULL,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
registration_plate VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
fuel_type ENUM('Diesel', 'Gasoline 95', 'Gasoline 98', 'Premium Diesel', 'Premium Gasoline 95', 'Premium Gasoline 98', 'Other') NOT NULL,
|
||||||
|
note VARCHAR(150) NULL,
|
||||||
|
is_default BOOLEAN DEFAULT FALSE NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB;
|
||||||
|
";
|
||||||
|
|
||||||
|
if (!$this->connection->query($vehiclesTableQuery)) {
|
||||||
|
die("Failed to create vehicles table: " . $this->connection->error);
|
||||||
|
}
|
||||||
|
|
||||||
|
$refuelingTableQuery = "
|
||||||
|
CREATE TABLE IF NOT EXISTS refueling_records (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_id INT NOT NULL,
|
||||||
|
vehicle_id INT NOT NULL,
|
||||||
|
fuel_type ENUM('Diesel', 'Gasoline 95', 'Gasoline 98', 'Premium Diesel', 'Premium Gasoline 95', 'Premium Gasoline 98', 'Other') NOT NULL,
|
||||||
|
note VARCHAR(150) NULL,
|
||||||
|
liters DOUBLE(10, 2) NOT NULL,
|
||||||
|
price_per_liter DOUBLE(10, 2) NOT NULL,
|
||||||
|
total_price DOUBLE(10, 2) NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (vehicle_id) REFERENCES vehicles(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB;
|
||||||
|
";
|
||||||
|
|
||||||
|
if (!$this->connection->query($refuelingTableQuery)) {
|
||||||
|
die("Failed to create refueling_records table: " . $this->connection->error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Create table_name table
|
||||||
|
$usersTableQuery = "CREATE TABLE IF NOT EXISTS table_name (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
username VARCHAR(50) NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB;";
|
||||||
|
|
||||||
|
if (!$this->connection->query($usersTableQuery)) {
|
||||||
|
die("Failed to create table_name table: " . $this->connection->error);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent cloning of the singleton instance
|
||||||
|
*/
|
||||||
|
private function __clone() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent unserialization of the singleton instance
|
||||||
|
*/
|
||||||
|
public function __wakeup() {}
|
||||||
|
|
||||||
|
}
|
84
core/Router.php
Normal file
84
core/Router.php
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class Router {
|
||||||
|
private $routes = [];
|
||||||
|
private $middlewares = [];
|
||||||
|
private $groupPrefix = '';
|
||||||
|
private $groupMiddlewares = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a route with a specific action and optional middleware
|
||||||
|
*
|
||||||
|
* @param string $route
|
||||||
|
* @param string $action
|
||||||
|
* @param array $middlewares Optional middlewares for this route
|
||||||
|
*/
|
||||||
|
public function add($route, $action, $middlewares = []) {
|
||||||
|
$route = $this->groupPrefix . $route;
|
||||||
|
$middlewares = array_merge($this->groupMiddlewares, $middlewares);
|
||||||
|
$this->routes[$route] = ['action' => $action, 'middlewares' => $middlewares];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define a group of routes with shared prefix and middlewares
|
||||||
|
*
|
||||||
|
* @param string $prefix
|
||||||
|
* @param array $middlewares
|
||||||
|
* @param callable $callback
|
||||||
|
*/
|
||||||
|
public function group($prefix, $middlewares, $callback) {
|
||||||
|
// Save the current state
|
||||||
|
$previousPrefix = $this->groupPrefix;
|
||||||
|
$previousMiddlewares = $this->groupMiddlewares;
|
||||||
|
|
||||||
|
// Set new group prefix and middlewares
|
||||||
|
$this->groupPrefix = $previousPrefix . $prefix;
|
||||||
|
$this->groupMiddlewares = array_merge($this->groupMiddlewares, $middlewares);
|
||||||
|
|
||||||
|
// Execute the callback to define routes in the group
|
||||||
|
$callback($this);
|
||||||
|
|
||||||
|
// Restore the previous state
|
||||||
|
$this->groupPrefix = $previousPrefix;
|
||||||
|
$this->groupMiddlewares = $previousMiddlewares;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatch the current request to the correct route and execute middlewares
|
||||||
|
*/
|
||||||
|
public function dispatch() {
|
||||||
|
$uri = $_SERVER['REQUEST_URI'];
|
||||||
|
$uri = parse_url($uri, PHP_URL_PATH);
|
||||||
|
|
||||||
|
// Normalize the URI by removing trailing slash (except for root "/")
|
||||||
|
if ($uri !== '/' && substr($uri, -1) === '/') {
|
||||||
|
$uri = rtrim($uri, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array_key_exists($uri, $this->routes)) {
|
||||||
|
$route = $this->routes[$uri];
|
||||||
|
$middlewares = $route['middlewares'];
|
||||||
|
|
||||||
|
// Execute middlewares
|
||||||
|
foreach ($middlewares as $middleware) {
|
||||||
|
$middlewareInstance = new $middleware();
|
||||||
|
if (!$middlewareInstance->handle()) {
|
||||||
|
return; // Stop execution if middleware fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute the route's controller and method
|
||||||
|
$action = $route['action'];
|
||||||
|
list($controllerName, $methodName) = explode('@', $action);
|
||||||
|
|
||||||
|
require_once controllers . "{$controllerName}.php";
|
||||||
|
|
||||||
|
$controller = new $controllerName();
|
||||||
|
$controller->$methodName();
|
||||||
|
} else {
|
||||||
|
http_response_code(404);
|
||||||
|
$view = new View();
|
||||||
|
$view->render('errors/404', [], 'noheader');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
68
core/Validator.php
Normal file
68
core/Validator.php
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class Validator {
|
||||||
|
private $errors = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a field is not empty
|
||||||
|
*/
|
||||||
|
public function required($field, $value, $message = null) {
|
||||||
|
if (empty(trim($value))) {
|
||||||
|
$this->errors[$field] = $message ?? "$field is required.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a field contains numbers
|
||||||
|
*/
|
||||||
|
public function number($field, $value, $message = null) {
|
||||||
|
if(!is_numeric($value)) {
|
||||||
|
$this->errors[$field] = $message ?? "$field must be an number";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a field meets minimum length
|
||||||
|
*/
|
||||||
|
public function minLength($field, $value, $length, $message = null) {
|
||||||
|
if (strlen($value) < $length) {
|
||||||
|
$this->errors[$field] = $message ?? "$field must be at least $length characters.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a field contains letters and numbers
|
||||||
|
*/
|
||||||
|
public function alphanumeric($field, $value, $message = null) {
|
||||||
|
if (!preg_match('/^(?=.*[a-zA-Z])(?=.*\d).+$/', $value)) {
|
||||||
|
$this->errors[$field] = $message ?? "$field must contain letters and numbers.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate an email
|
||||||
|
*/
|
||||||
|
public function email($field, $value, $message = null) {
|
||||||
|
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
$this->errors[$field] = $message ?? "Invalid email format.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get validation errors
|
||||||
|
*/
|
||||||
|
public function errors() {
|
||||||
|
return $this->errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setErrors($errors) {
|
||||||
|
$this->errors = $errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if validation passed
|
||||||
|
*/
|
||||||
|
public function passes() {
|
||||||
|
return empty($this->errors);
|
||||||
|
}
|
||||||
|
}
|
25
core/View.php
Normal file
25
core/View.php
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
class View
|
||||||
|
{
|
||||||
|
private $data = [];
|
||||||
|
|
||||||
|
public function render($view, $data = [], $layout = 'base') {
|
||||||
|
// Store the data
|
||||||
|
$this->data = $data;
|
||||||
|
|
||||||
|
// Capture the view content
|
||||||
|
ob_start();
|
||||||
|
require_once views . $view . '.php';
|
||||||
|
$content = ob_get_clean();
|
||||||
|
|
||||||
|
// Include the base layout and inject the view content
|
||||||
|
require_once views . "layouts/$layout.php";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely get a value from the data array
|
||||||
|
*/
|
||||||
|
public function get($key) {
|
||||||
|
return $this->data[$key] ?? null;
|
||||||
|
}
|
||||||
|
}
|
12
core/middlewares/RequireAuth.php
Normal file
12
core/middlewares/RequireAuth.php
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class RequireAuth {
|
||||||
|
public function handle() {
|
||||||
|
if (!isset($_SESSION['user'])) {
|
||||||
|
header('Location: /auth/signin');
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
31
docker-compose.yaml
Normal file
31
docker-compose.yaml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
services:
|
||||||
|
mariadb:
|
||||||
|
image: mariadb:11.4 # LTS at 25. 12. 2025
|
||||||
|
restart: on-failure:2
|
||||||
|
environment:
|
||||||
|
MARIADB_ROOT_PASSWORD: root
|
||||||
|
ports:
|
||||||
|
- 3306:3306
|
||||||
|
profiles: ["prod", "dev"]
|
||||||
|
|
||||||
|
phpmyadmin:
|
||||||
|
image: phpmyadmin
|
||||||
|
restart: on-failure:2
|
||||||
|
ports:
|
||||||
|
- 8080:80
|
||||||
|
environment:
|
||||||
|
- PMA_ARBITRARY=1
|
||||||
|
profiles: ["dev"]
|
||||||
|
|
||||||
|
fuelstats:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
volumes:
|
||||||
|
- .:/var/www/html
|
||||||
|
ports:
|
||||||
|
- 8000:80
|
||||||
|
depends_on:
|
||||||
|
- mariadb
|
||||||
|
restart: on-failure:2
|
||||||
|
profiles: ["prod"]
|
21
public/css/dashboard.css
Normal file
21
public/css/dashboard.css
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
.dashboard {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background-color: var(--clr-secondary);
|
||||||
|
border-radius: var(--border-radious);
|
||||||
|
border: var(--borderWidth-thin) solid var(--clr-border);
|
||||||
|
width: 18rem;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
78
public/css/form.css
Normal file
78
public/css/form.css
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
.form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--clr-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form .header-form {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form .header-form img {
|
||||||
|
height: 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: var(--clr-tertiary);
|
||||||
|
gap: .5rem;
|
||||||
|
border-radius: var(--border-radious);
|
||||||
|
border: var(--borderWidth-thin) solid var(--clr-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form form input,
|
||||||
|
select {
|
||||||
|
background-color: var(--clr-secondary);
|
||||||
|
caret-color: white;
|
||||||
|
color: white;
|
||||||
|
padding: .3rem;
|
||||||
|
border-radius: var(--border-radious);
|
||||||
|
border: var(--borderWidth-thin) solid var(--clr-border);
|
||||||
|
width: 15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form form input[type="submit"] {
|
||||||
|
background-color: var(--clr-green);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form .error {
|
||||||
|
width: 17rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: var(--clr-danger-muted);
|
||||||
|
border-radius: var(--border-radious);
|
||||||
|
border: var(--borderWidth-thin) solid var(--clr-border-danger);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form small.error {
|
||||||
|
width: 15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form .bordered {
|
||||||
|
border-radius: var(--border-radious);
|
||||||
|
border: var(--borderWidth-thin) solid var(--clr-border);
|
||||||
|
width: 17rem;
|
||||||
|
padding: 1rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form .bordered a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--clr-link-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form .bordered a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
48
public/css/global.css
Normal file
48
public/css/global.css
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
@import url("https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&family=Source+Code+Pro:ital,wght@0,200..900;1,200..900&display=swap");
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: "Open Sans", serif;
|
||||||
|
background-color: var(--clr-primary);
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-top: .5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary,
|
||||||
|
.btn-secondary,
|
||||||
|
.btn-tertiary,
|
||||||
|
.btn-green,
|
||||||
|
.btn-danger {
|
||||||
|
background-color: var(--clr-primary);
|
||||||
|
padding: .5rem;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: var(--border-radious);
|
||||||
|
border: var(--borderWidth-thin) solid var(--clr-border);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background-color: var(--clr-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-tertiary {
|
||||||
|
background-color: var(--clr-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-green {
|
||||||
|
background-color: var(--clr-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background-color: var(--clr-danger-muted);
|
||||||
|
border: var(--borderWidth-thin) solid var(--clr-border-danger);
|
||||||
|
}
|
26
public/css/header.css
Normal file
26
public/css/header.css
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 2rem;
|
||||||
|
width: 100vw;
|
||||||
|
height: 3rem;
|
||||||
|
background: var(--clr-secondary);
|
||||||
|
border-radius: var(--border-radious);
|
||||||
|
border-bottom: var(--borderWidth-thin) solid var(--clr-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
#hd-left,
|
||||||
|
#hd-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
header a img {
|
||||||
|
height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
header a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
13
public/css/main.css
Normal file
13
public/css/main.css
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-top: 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
padding: 0 var(--container-size);
|
||||||
|
}
|
20
public/css/vars.css
Normal file
20
public/css/vars.css
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
:root {
|
||||||
|
--container-size: 5vw;
|
||||||
|
--clr-primary: #010409;
|
||||||
|
--clr-secondary: #0d1117;
|
||||||
|
--clr-tertiary: #151b23;
|
||||||
|
--clr-green: #238636;
|
||||||
|
--clr-danger-muted: #f851491a;
|
||||||
|
|
||||||
|
--clr-link-blue: #4493f8;
|
||||||
|
--clr-light-blue: #39a2ae;
|
||||||
|
--clr-light-green: #71f79f;
|
||||||
|
--clr-red: #9b1d20;
|
||||||
|
--clr-orange: #e08e45;
|
||||||
|
--clr-gray-blue: #627c85;
|
||||||
|
|
||||||
|
--border-radious: 5px;
|
||||||
|
--borderWidth-thin: max(1px, 0.0625rem);
|
||||||
|
--clr-border: #3d444db3;
|
||||||
|
--clr-border-danger: #f8514966;
|
||||||
|
}
|
3
public/css/vehicle_create.css
Normal file
3
public/css/vehicle_create.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
#registration_plate {
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
29
public/css/vehicles.css
Normal file
29
public/css/vehicles.css
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
.vehicle-wrapper {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vehicle {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: var(--clr-secondary);
|
||||||
|
border-radius: var(--border-radious);
|
||||||
|
border: var(--borderWidth-thin) solid var(--clr-border);
|
||||||
|
width: 17rem;
|
||||||
|
padding: 1rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
min-height: 8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
BIN
public/img/favicon.ico
Normal file
BIN
public/img/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
BIN
public/img/logo.jpg
Normal file
BIN
public/img/logo.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 45 KiB |
56
public/index.php
Normal file
56
public/index.php
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
// Show errors and warnings
|
||||||
|
session_start();
|
||||||
|
ini_set('display_errors', '1');
|
||||||
|
ini_set('log_errors', '1');
|
||||||
|
ini_set('error_log', '../storage/logs/error_log.log');
|
||||||
|
|
||||||
|
define('models', __DIR__ . '/../app/models/');
|
||||||
|
define('views', __DIR__ . '/../app/views/');
|
||||||
|
define('controllers', __DIR__ . '/../app/controllers/');
|
||||||
|
define('config', __DIR__ . '/../config/');
|
||||||
|
|
||||||
|
require_once config . 'environment.php';
|
||||||
|
|
||||||
|
require_once '../core/Validator.php';
|
||||||
|
require_once '../core/Router.php';
|
||||||
|
require_once '../core/View.php';
|
||||||
|
require_once '../core/Controller.php';
|
||||||
|
require_once '../core/Database.php';
|
||||||
|
require_once '../core/middlewares/RequireAuth.php';
|
||||||
|
|
||||||
|
require_once models . 'User.php';
|
||||||
|
require_once models . 'Vehicle.php';
|
||||||
|
require_once models . 'Refuel.php';
|
||||||
|
|
||||||
|
// Initialize router
|
||||||
|
$router = new Router();
|
||||||
|
if(!isset($_SESSION['user'])) {
|
||||||
|
$router->add('/', 'HomeController@index');
|
||||||
|
} else {
|
||||||
|
$router->add('/', 'DashboardController@reroute', ['RequireAuth']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// auth routes
|
||||||
|
$router->group('/auth', [], function ($router) {
|
||||||
|
$router->add('/signin', 'AuthController@signin');
|
||||||
|
$router->add('/signup', 'AuthController@signup');
|
||||||
|
$router->add('/logout', 'AuthController@logout');
|
||||||
|
});
|
||||||
|
|
||||||
|
// dashboard route
|
||||||
|
$router->add('/dashboard', 'DashboardController@index', ['RequireAuth']);
|
||||||
|
|
||||||
|
// vehicle routes
|
||||||
|
$router->group('/vehicles', ['RequireAuth'], function ($router) {
|
||||||
|
$router->add('', 'VehicleController@index');
|
||||||
|
$router->add('/create', 'VehicleController@create');
|
||||||
|
$router->add('/edit/{id}', 'VehicleController@edit');
|
||||||
|
$router->add('/delete/{id}', 'VehicleController@delete');
|
||||||
|
});
|
||||||
|
|
||||||
|
$router->group('/refuel', ['RequireAuth'], function ($router) {
|
||||||
|
$router->add('/create', 'RefuelController@create');
|
||||||
|
});
|
||||||
|
|
||||||
|
$router->dispatch();
|
98
public/js/offline-records.js
Normal file
98
public/js/offline-records.js
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
async function checkOnline() {
|
||||||
|
if (!navigator.onLine) {
|
||||||
|
console.log("Offline (no network connection)");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
"https://www.google.com/favicon.ico?" + new Date().getTime(),
|
||||||
|
{
|
||||||
|
mode: "no-cors",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Connected to network but no internet access");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setInterval(async () => {
|
||||||
|
const isOnline = await checkOnline();
|
||||||
|
if (!isOnline) {
|
||||||
|
console.log("OFFLINE!!!");
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
const offbtn = document.querySelector("#btn-offline-add");
|
||||||
|
offbtn.addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
document.querySelector("section.dashboard").style.display = "none";
|
||||||
|
const offline = document.createElement("div");
|
||||||
|
offline.classList.add("offline");
|
||||||
|
offline.innerHTML = `
|
||||||
|
<b>You're Offline</b>
|
||||||
|
<p>You can create an fuel record locally on your device and sync it later</p>
|
||||||
|
<section class="form">
|
||||||
|
<h1 class="header-form"><?= $this->get('title') ?></h1>
|
||||||
|
<!-- <?php if ($this->get('error')): ?> -->
|
||||||
|
<!-- <div class="error" style="color: red; margin-bottom: 1rem;"> -->
|
||||||
|
<!-- <?= htmlspecialchars($this->get('error')) ?> -->
|
||||||
|
<!-- </div> -->
|
||||||
|
<!-- <?php endif; ?> -->
|
||||||
|
<form method="POST" action="/refuel/create">
|
||||||
|
<label for="vehicle">Vehicle</label>
|
||||||
|
<select name="vehicle" id="vehicle">
|
||||||
|
<!-- <?php foreach ($this->get('vehicles') as $vehicle): ?> -->
|
||||||
|
<!-- <option value="<?= $vehicle['id'] ?>"><?= $vehicle['name'] . " | " . $vehicle['registration_plate'] ?></option> -->
|
||||||
|
<!-- <?php endforeach; ?> -->
|
||||||
|
</select>
|
||||||
|
<!-- <?php if (isset($this->get('validationErrors')['vehicle'])): ?> -->
|
||||||
|
<!-- <small class="error"><?= $this->get('validationErrors')['vehicle'] ?></small> -->
|
||||||
|
<!-- <?php endif; ?> -->
|
||||||
|
|
||||||
|
<label for="fuel_type">Fuel type</label>
|
||||||
|
<select name="fuel_type" id="fuel_type">
|
||||||
|
<option value="Diesel">Diesel</option>
|
||||||
|
<option value="Gasoline 95">Gasoline 95</option>
|
||||||
|
<option value="Gasoline 98">Gasoline 98</option>
|
||||||
|
<option value="Premium Diesel">Premium Diesel</option>
|
||||||
|
<option value="Premium Gasoline 95">Premium Gasoline 95</option>
|
||||||
|
<option value="Premium Gasoline 98">Premium Gasoline 98</option>
|
||||||
|
<option value="Other">Other</option>
|
||||||
|
</select>
|
||||||
|
<!-- <?php if (isset($this->get('validationErrors')['fuel_type'])): ?> -->
|
||||||
|
<!-- <small class="error"><?= $this->get('validationErrors')['fuel_type'] ?></small> -->
|
||||||
|
<!-- <?php endif; ?> -->
|
||||||
|
|
||||||
|
<label for="liters">Liters</label>
|
||||||
|
<input type="number" name="liters" id="liters" min="0" step=".01" value="<?= htmlspecialchars($_POST['liters'] ?? '0.0') ?>">
|
||||||
|
<!-- <?php if (isset($this->get('validationErrors')['liters'])): ?> -->
|
||||||
|
<!-- <small class="error"><?= $this->get('validationErrors')['liters'] ?></small> -->
|
||||||
|
<!-- <?php endif; ?> -->
|
||||||
|
|
||||||
|
<label for="price_per_liter">Price per liter</label>
|
||||||
|
<input type="number" name="price_per_liter" id="price_per_liter" min="0" step=".01" value="<?= htmlspecialchars($_POST['price_per_liter'] ?? '0.0') ?>">
|
||||||
|
<!-- <?php if (isset($this->get('validationErrors')['price_per_liter'])): ?> -->
|
||||||
|
<!-- <small class="error"><?= $this->get('validationErrors')['price_per_liter'] ?></small> -->
|
||||||
|
<!-- <?php endif; ?> -->
|
||||||
|
|
||||||
|
<label for="total_price">Total price</label>
|
||||||
|
<input type="number" name="total_price" id="total_price" min="0" step=".01" value="<?= htmlspecialchars($_POST['total_price'] ?? '0.0') ?>">
|
||||||
|
<!-- <?php if (isset($this->get('validationErrors')['total_price'])): ?> -->
|
||||||
|
<!-- <small class="error"><?= $this->get('validationErrors')['total_price'] ?></small> -->
|
||||||
|
<!-- <?php endif; ?> -->
|
||||||
|
|
||||||
|
<label for="note">Note</label>
|
||||||
|
<input type="text" name="note" id="note" value="<?= htmlspecialchars($_POST['note'] ?? '') ?>">
|
||||||
|
<!-- <?php if (isset($this->get('validationErrors')['note'])): ?> -->
|
||||||
|
<!-- <small class="error"><?= $this->get('validationErrors')['note'] ?></small> -->
|
||||||
|
<!-- <?php endif; ?> -->
|
||||||
|
|
||||||
|
<input type="submit" value="Create fuel record">
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
`;
|
||||||
|
document.querySelector("main").appendChild(offline);
|
||||||
|
});
|
1
storage/logs/.gitignore
vendored
Normal file
1
storage/logs/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
*.log
|
Reference in New Issue
Block a user