Compare commits
26 Commits
last_habit
...
master
Author | SHA1 | Date | |
---|---|---|---|
3d86f1ae2e | |||
c14bbd1930 | |||
88a78d60e9 | |||
4576800e27 | |||
652b04bfa1 | |||
6df1f6574a | |||
7517bcb78f | |||
f90c707435 | |||
60423d37ce | |||
5989fba225 | |||
ba11c41147 | |||
64c7fd15a1 | |||
ea3afa2507 | |||
a5f99788fc | |||
2201430f59 | |||
18c78e37a4 | |||
ccbb0eac64 | |||
21c2f4598b | |||
860a20d946 | |||
c5955010cb | |||
fc163431f8 | |||
15029970d6 | |||
e13edeccfc | |||
be6b465684 | |||
aded859a79 | |||
c29bd7cbab |
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"
|
BIN
.screenshots/class.png
Normal file
BIN
.screenshots/class.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
BIN
.screenshots/dlm.png
Normal file
BIN
.screenshots/dlm.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 92 KiB |
BIN
.screenshots/usecase.png
Normal file
BIN
.screenshots/usecase.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 36 KiB |
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.
|
||||
|
||||
habit-tracker
|
||||
Copyright (C) 2024 fr
|
||||
Fuel Stats
|
||||
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.
|
||||
|
||||
@ -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:
|
||||
|
||||
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 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,32 +1,32 @@
|
||||
# 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
|
||||
## Used Technologies
|
||||
- **Frontend:** HTML, CSS, JavaScript
|
||||
- **Backend:** PHP (OOP)
|
||||
- **Database:** MariaDB
|
||||
|
||||
## How to build
|
||||
## How to Build
|
||||
|
||||
### Build using docker
|
||||
Run the container using docker-compose
|
||||
### Build Using Docker
|
||||
Run the container using docker-compose:
|
||||
```bash
|
||||
docker-compose up
|
||||
docker-compose --profile <dev|prod> up -d
|
||||
```
|
||||
|
||||
The app should be available at http://localhost:8000
|
||||
The app should be available at http://localhost:8000.
|
||||
|
||||
PhpMyAdmin should be available at http://localhost:8080
|
||||
PhpMyAdmin should be available at http://localhost:8080.
|
||||
|
||||
### Build manually
|
||||
1. Clone the repo
|
||||
### Build Manually
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone https://git.filiprojek.cz/fr/habit-tracker.git
|
||||
git clone https://git.filiprojek.cz/fr/fuel-stats.git
|
||||
```
|
||||
|
||||
2. Create `config/environment.php`
|
||||
- It should have following structure:
|
||||
2. Create `config/environment.php`:
|
||||
- It should have the following structure:
|
||||
```php
|
||||
<?php
|
||||
|
||||
@ -35,21 +35,31 @@ define('DB_USER', 'your db username');
|
||||
define('DB_PASS', 'your db password');
|
||||
define('DB_NAME', 'your db name');
|
||||
```
|
||||
- For the database, you can use included `docker-compose.yaml` which have both MariaDB and PhpMyAdmin
|
||||
- For the database, you can use the included `docker-compose.yaml` which includes both MariaDB and PhpMyAdmin.
|
||||
|
||||
3. Start an local web server
|
||||
- You can use php's integrated server by running this:
|
||||
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 want.
|
||||
- You can use any host and any port you prefer.
|
||||
|
||||
## Usage
|
||||
1. Register and Login to the app.
|
||||
2. Add your habits.
|
||||
3. Mark your habits when you're done doing them.
|
||||
4. Earn point and unlock achievements by completing you're habits!
|
||||
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.
|
||||
|
||||
## Licence
|
||||
This project is licensed under GPL3.0 and later. More information is availabe in `LICENSE` file.
|
||||
## Use case diagram
|
||||

|
||||
## Data logical model
|
||||

|
||||
|
||||
## Class diagram
|
||||

|
||||
|
||||
## License
|
||||
This project is licensed under GPL3.0 and later. More information is available in the `LICENSE` file.
|
||||
|
46
TODO.md
46
TODO.md
@ -6,13 +6,39 @@
|
||||
- [ ] edit user data - change password, mail...
|
||||
|
||||
## Core of the app
|
||||
- [ ] header and navbar
|
||||
- [ ] dashboard
|
||||
- [x] css
|
||||
- [ ] its just plain
|
||||
- [ ] graphs
|
||||
- [x] Habits list
|
||||
- [ ] css
|
||||
- [ ] Habits create
|
||||
- [ ] validate cron input
|
||||
- [ ] Habits track
|
||||
- [ ] intro tutorial when no car exist or just dont show anything
|
||||
- [x] change/set default car
|
||||
- [ ] specific car view - charts, fuel records
|
||||
- [ ] remove/edit fuel record
|
||||
|
||||
## Until release
|
||||
- [x] Sync offline data from locale storage
|
||||
- [x] Include kilometer state of an car
|
||||
- [ ] More charts
|
||||
- [x] Average fuel conusption
|
||||
- [ ] Kilometer state
|
||||
- [ ] More cards
|
||||
- [ ] Average fuel conusption in last 30 days
|
||||
- [ ] Kilometer state in last 30 days`
|
||||
- [ ] Offline navigation between dashboard and offline form
|
||||
|
||||
## What has to be done
|
||||
- [x] Vehicle delete
|
||||
- [ ] intro tutorial when no car exist or just dont show anything
|
||||
- [x] change/set default car
|
||||
- [x] hide errors
|
||||
|
||||
## Nice to have
|
||||
- [ ] specific car view - charts, fuel records
|
||||
- [ ] remove/edit fuel record
|
||||
- [x] Include kilometer state of an car
|
||||
- [ ] More charts
|
||||
- [x] Average fuel conusption
|
||||
- [ ] Kilometer state
|
||||
- [ ] More cards
|
||||
- [ ] Average fuel conusption in last 30 days
|
||||
- [ ] Kilometer state in last 30 days`
|
||||
- [ ] Offline navigation between dashboard and offline form
|
||||
- [ ] Fix vehicle deletion - wrong redirect
|
||||
- [ ] Update diagrams in README.md
|
||||
|
||||
|
@ -1,12 +1,34 @@
|
||||
<?php
|
||||
class DashboardController extends Controller {
|
||||
public function index() {
|
||||
$habit = new Habit();
|
||||
$habits = $habit->getHabitsByUser($_SESSION['user']['id']);
|
||||
$vehicle = new Vehicle();
|
||||
$vehicles = $vehicle->getVehiclesByUser($_SESSION['user']['id']);
|
||||
$default_car = $vehicle->getDefaultVehicle($_SESSION['user']['id']) ?? null;
|
||||
|
||||
$refuel = new Refuel();
|
||||
$data = [
|
||||
"date" => [],
|
||||
"price" => [],
|
||||
"mileage" => [],
|
||||
"liters" => []
|
||||
];
|
||||
$raw_data = $default_car ? $refuel->latest_data($default_car['id'], 0) : [];
|
||||
foreach($raw_data as $one) {
|
||||
array_push($data['date'], date('d. m.', strtotime($one['created_at'])));
|
||||
array_push($data['price'], $one['price_per_liter']);
|
||||
array_push($data['mileage'], $one['mileage']);
|
||||
array_push($data['liters'], $one['liters']);
|
||||
}
|
||||
|
||||
$latest_data = $default_car ? $refuel->latest_one($default_car['id']) : [];
|
||||
$latest_record = !empty($latest_data) ? $latest_data[0] : null;
|
||||
|
||||
$this->view('dashboard/index', [
|
||||
'title' => 'Dashboard',
|
||||
'habits' => $habits,
|
||||
'vehicles' => $vehicles,
|
||||
'date_price_data' => $data,
|
||||
'default_car' => $default_car,
|
||||
'latest_record' => $latest_record,
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -1,57 +0,0 @@
|
||||
<?php
|
||||
|
||||
class HabitController extends Controller {
|
||||
public function index() {
|
||||
$habit = new Habit();
|
||||
$habits = $habit->getHabitsByUser($_SESSION['user']['id']);
|
||||
$this->view('habits/index', ['title' => 'Habits', 'habits' => $habits]);
|
||||
}
|
||||
|
||||
public function create() {
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$name = $_POST['name'] ?? '';
|
||||
$frequency = $_POST['frequency'] ?? 'Daily';
|
||||
$customFrequency = null;
|
||||
|
||||
if (empty($name)) {
|
||||
$this->view('habits/create', ['error' => 'Habit name is required.']);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($frequency === 'Custom') {
|
||||
$daysOfWeek = $_POST['days_of_week'] ?? [];
|
||||
$daysOfMonth = $_POST['days_of_month'] ?? '*';
|
||||
$months = $_POST['months'] ?? '*';
|
||||
|
||||
// Combine into crontab-like string
|
||||
$customFrequency = implode(',', $daysOfWeek) . " $daysOfMonth $months";
|
||||
}
|
||||
|
||||
$habit = new Habit();
|
||||
$result = $habit->create([
|
||||
'name' => $name,
|
||||
'frequency' => $frequency,
|
||||
'custom_frequency' => $customFrequency,
|
||||
'reward_points' => intval($_POST['difficulty'] ?? 1),
|
||||
'user_id' => $_SESSION['user']['id'],
|
||||
]);
|
||||
|
||||
if ($result) {
|
||||
$this->redirect('/habits');
|
||||
} else {
|
||||
$this->view('habits/create', ['error' => 'Failed to create habit.']);
|
||||
}
|
||||
} else {
|
||||
$this->view('habits/create', ['title' => 'Create Habit']);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function edit() {
|
||||
// Edit habit (to be implemented later)
|
||||
}
|
||||
|
||||
public function delete() {
|
||||
// Delete habit (to be implemented later)
|
||||
}
|
||||
}
|
89
app/controllers/RefuelController.php
Normal file
89
app/controllers/RefuelController.php
Normal file
@ -0,0 +1,89 @@
|
||||
<?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'] ?? '';
|
||||
$mileage = $_POST['mileage'] ?? '';
|
||||
$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);
|
||||
$validator->number('mileage', $mileage);
|
||||
|
||||
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',
|
||||
'status' => '400'
|
||||
]);
|
||||
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,
|
||||
'mileage' => $mileage,
|
||||
]);
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
104
app/controllers/VehicleController.php
Normal file
104
app/controllers/VehicleController.php
Normal file
@ -0,0 +1,104 @@
|
||||
<?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();
|
||||
$default_vehicle = $vehicle->getDefaultVehicle($_SESSION['user']['id']);
|
||||
$is_default = $default_vehicle ? 0 : 1;
|
||||
|
||||
$result = $vehicle->create([
|
||||
'name' => $name,
|
||||
'registration_plate' => strtoupper($registration_plate),
|
||||
'fuel_type' => $fuel_type,
|
||||
'note' => $note,
|
||||
'user_id' => $_SESSION['user']['id'],
|
||||
'is_default' => $is_default
|
||||
]);
|
||||
|
||||
|
||||
if ($result === true) {
|
||||
$this->redirect('/');
|
||||
} else {
|
||||
$this->view('vehicles/create', ['title' => 'Create vehicle', 'error' => $result, 'validationErrors' => []] );
|
||||
}
|
||||
|
||||
} else {
|
||||
$this->view('vehicles/create', ['title' => 'Create Vehicle']);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function edit() {
|
||||
// TODO: Edit vehicle (to be implemented later)
|
||||
}
|
||||
|
||||
public function delete() {
|
||||
if(!$_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
echo "Wrong method";
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Validate the request
|
||||
$vehicle_id = $_POST['vehicle_id'];
|
||||
|
||||
$vehicle = new Vehicle();
|
||||
$result = $vehicle->delete($vehicle_id, $_SESSION['user']['id']);
|
||||
|
||||
if($result != true) {
|
||||
echo "Something went wrong";
|
||||
return;
|
||||
}
|
||||
|
||||
header("Location: /vehicles");
|
||||
}
|
||||
|
||||
public function setDefault() {
|
||||
$vehicle = new Vehicle();
|
||||
// TODO: Validate the request
|
||||
$result = $vehicle->setDefaultVehicle($_POST['vehicle_id'], $_SESSION['user']['id']);
|
||||
if($result != true) {
|
||||
echo "Something went wrong";
|
||||
return;
|
||||
}
|
||||
|
||||
header("Location: /");
|
||||
}
|
||||
|
||||
public function api_get() {
|
||||
if(!$_SERVER['REQUEST_METHOD'] === 'GET') {
|
||||
echo "Wrong method, use GET";
|
||||
return;
|
||||
}
|
||||
|
||||
$vehicle = new Vehicle();
|
||||
$result = $vehicle->getVehiclesByUser($_SESSION['user']['id']);
|
||||
echo json_encode($result);
|
||||
}
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
<?php
|
||||
|
||||
class Habit {
|
||||
private $db;
|
||||
|
||||
public function __construct() {
|
||||
$this->db = Database::getInstance()->getConnection();
|
||||
}
|
||||
|
||||
public function create($data) {
|
||||
$stmt = $this->db->prepare("
|
||||
INSERT INTO habits (user_id, title, frequency, custom_frequency, reward_points, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, NOW())
|
||||
");
|
||||
|
||||
$stmt->bind_param(
|
||||
"isssi", // Bind types: int, string, string, string, int
|
||||
$data['user_id'],
|
||||
$data['name'],
|
||||
$data['frequency'],
|
||||
$data['custom_frequency'], // Bind the custom_frequency field
|
||||
$data['reward_points']
|
||||
);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
return true;
|
||||
} else {
|
||||
error_log("Failed to create habit: " . $stmt->error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function getHabitsByUser($userId) {
|
||||
$stmt = $this->db->prepare("SELECT id, title, frequency, custom_frequency, reward_points, created_at FROM habits WHERE user_id = ?");
|
||||
$stmt->bind_param("i", $userId);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
|
||||
$habits = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$habits[] = $row;
|
||||
}
|
||||
|
||||
return $habits;
|
||||
}
|
||||
}
|
113
app/models/Refuel.php
Normal file
113
app/models/Refuel.php
Normal file
@ -0,0 +1,113 @@
|
||||
<?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, mileage, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW())
|
||||
");
|
||||
|
||||
$stmt->bind_param(
|
||||
"iissdddi",
|
||||
$data['user_id'],
|
||||
$data['vehicle_id'],
|
||||
$data['fuel_type'],
|
||||
$data['note'],
|
||||
$data['liters'],
|
||||
$data['price_per_liter'],
|
||||
$data['total_price'],
|
||||
$data['mileage'],
|
||||
);
|
||||
|
||||
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 {
|
||||
$sql = "
|
||||
SELECT `liters`, `price_per_liter`, `total_price`, `mileage`, `created_at`
|
||||
FROM `refueling_records`
|
||||
WHERE `vehicle_id` = ?
|
||||
ORDER BY created_at DESC";
|
||||
|
||||
if ($record_count > 0) {
|
||||
$sql .= " LIMIT ?";
|
||||
}
|
||||
|
||||
$stmt = $this->db->prepare($sql);
|
||||
|
||||
if ($record_count > 0) {
|
||||
$stmt->bind_param("ii", $vehicle_id, $record_count);
|
||||
} else {
|
||||
$stmt->bind_param("i", $vehicle_id);
|
||||
}
|
||||
|
||||
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($vehicle_id, $record_count = 1) {
|
||||
try {
|
||||
$sql = "
|
||||
SELECT
|
||||
`r`.`vehicle_id`,
|
||||
`v`.`name` AS `vehicle_name`,
|
||||
`r`.`liters`,
|
||||
`r`.`price_per_liter`,
|
||||
`r`.`total_price`,
|
||||
`r`.`mileage`,
|
||||
`r`.`note`,
|
||||
`r`.`created_at`
|
||||
FROM `refueling_records` AS `r`
|
||||
JOIN `vehicles` AS `v` ON `r`.`vehicle_id` = `v`.`id`
|
||||
WHERE `r`.`vehicle_id` = ?
|
||||
ORDER BY `r`.`created_at` DESC";
|
||||
|
||||
if ($record_count > 0) {
|
||||
$sql .= " LIMIT ?";
|
||||
}
|
||||
|
||||
$stmt = $this->db->prepare($sql);
|
||||
|
||||
if ($record_count > 0) {
|
||||
$stmt->bind_param("ii", $vehicle_id, $record_count);
|
||||
} else {
|
||||
$stmt->bind_param("i", $vehicle_id);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
@ -29,7 +29,7 @@ class User {
|
||||
|
||||
$hashedPassword = password_hash($password, PASSWORD_BCRYPT);
|
||||
|
||||
$stmt = $this->db->prepare("INSERT INTO users (username, email, password, points, created_at) VALUES (?, ?, ?, 0, NOW())");
|
||||
$stmt = $this->db->prepare("INSERT INTO users (username, email, password, created_at) VALUES (?, ?, ?, NOW())");
|
||||
$stmt->bind_param("sss", $username, $email, $hashedPassword);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
|
121
app/models/Vehicle.php
Normal file
121
app/models/Vehicle.php
Normal file
@ -0,0 +1,121 @@
|
||||
<?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, is_default, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, NOW())
|
||||
");
|
||||
|
||||
$stmt->bind_param(
|
||||
"issssi",
|
||||
$data['user_id'],
|
||||
$data['name'],
|
||||
$data['registration_plate'],
|
||||
$data['fuel_type'],
|
||||
$data['note'],
|
||||
$data['is_default'],
|
||||
);
|
||||
|
||||
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, is_default, 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();
|
||||
}
|
||||
|
||||
public function setDefaultVehicle($vehicle_id, $user_id) {
|
||||
try {
|
||||
$this->db->begin_transaction();
|
||||
|
||||
$stmt = $this->db->prepare("
|
||||
UPDATE `vehicles`
|
||||
SET `is_default` = 0
|
||||
WHERE `user_id` = ? AND `is_default` = 1
|
||||
");
|
||||
$stmt->bind_param("i", $user_id);
|
||||
$stmt->execute();
|
||||
$stmt->close();
|
||||
|
||||
$stmt = $this->db->prepare("
|
||||
UPDATE `vehicles`
|
||||
SET `is_default` = 1
|
||||
WHERE `id` = ? AND `user_id` = ?
|
||||
");
|
||||
$stmt->bind_param("ii", $vehicle_id, $user_id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
$this->db->commit();
|
||||
return true;
|
||||
} else {
|
||||
$this->db->rollback();
|
||||
return "Error: " . $stmt->error;
|
||||
}
|
||||
} catch (mysqli_sql_exception $e) {
|
||||
$this->db->rollback();
|
||||
return $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
public function delete($vehicle_id, $user_id) {
|
||||
try {
|
||||
$stmt = $this->db->prepare("SELECT id FROM vehicles WHERE id = ? AND user_id = ?");
|
||||
$stmt->bind_param("ii", $vehicle_id, $user_id);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
|
||||
if ($result->num_rows === 0) {
|
||||
return "Error: Unauthorized action or vehicle not found.";
|
||||
}
|
||||
|
||||
$stmt = $this->db->prepare("DELETE FROM vehicles WHERE id = ?");
|
||||
$stmt->bind_param("i", $vehicle_id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
return true;
|
||||
} else {
|
||||
return "Error: " . $stmt->error;
|
||||
}
|
||||
} catch (mysqli_sql_exception $e) {
|
||||
return $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
@ -2,8 +2,8 @@
|
||||
<link rel="stylesheet" href="/css/form.css">
|
||||
<section class="form signin">
|
||||
<div class="header-form">
|
||||
<img src="/img/logo.jpg" alt="Habit Tracker Logo">
|
||||
<h1>Sign in to Habit Tracker</h1>
|
||||
<img src="/img/logo.jpg" alt="Fuel Stats Logo">
|
||||
<h1>Sign in to Fuel Stats</h1>
|
||||
</div>
|
||||
|
||||
<?php if ($this->get('error')): ?>
|
||||
@ -27,7 +27,7 @@
|
||||
</form>
|
||||
|
||||
<div class="bordered">
|
||||
<p>New to Habit Tracker?</p>
|
||||
<p>New to Fuel Stats?</p>
|
||||
<a href="/auth/signup">Create an account</a>
|
||||
</div>
|
||||
</section>
|
||||
|
@ -2,8 +2,8 @@
|
||||
<link rel="stylesheet" href="/css/form.css">
|
||||
<section class="form signup">
|
||||
<div class="header-form">
|
||||
<img src="/img/logo.jpg" alt="Habit Tracker Logo">
|
||||
<h1>Sign up to Habit Tracker</h1>
|
||||
<img src="/img/logo.jpg" alt="Fuel Stats Logo">
|
||||
<h1>Sign up to Fuel Stats</h1>
|
||||
</div>
|
||||
|
||||
<?php if ($this->get('error')): ?>
|
||||
|
@ -1,47 +1,123 @@
|
||||
<link rel="stylesheet" href="/css/dashboard.css">
|
||||
|
||||
<link rel="stylesheet" href="/css/form.css">
|
||||
<link rel="stylesheet" href="/css/vehicle_create.css">
|
||||
|
||||
<section class="dashboard">
|
||||
<h1>Welcome, <?= htmlspecialchars($_SESSION['user']['username']) ?>!</h1>
|
||||
<div>
|
||||
<a href="/habits/create" class="btn-green">Create new habit!</a>
|
||||
<a href="/habits" class="btn-primary">List all habits</a>
|
||||
<?php if(!isset($data['default_car'])): ?>
|
||||
|
||||
<div id="intro">
|
||||
<a href="/vehicles/create">Create your first vehicle</a>
|
||||
</div>
|
||||
<?php elseif (isset($data['latest_record'])): ?>
|
||||
|
||||
<div id="actions">
|
||||
<a href="/refuel/create" class="btn-green">Add new refuel record</a>
|
||||
<a href="/vehicles" class="btn-primary">List all vehicles</a>
|
||||
<a class="btn-warning" id="btn-offline-add">Add new offline refuel record</a>
|
||||
</div>
|
||||
<div class="card-wrapper">
|
||||
<section class="card upcoming">
|
||||
<h2>Upcoming</h2>
|
||||
<div class="habit">
|
||||
<b>Habit Title</b>
|
||||
<p>Frequency</p>
|
||||
<p>Reward points</p>
|
||||
<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>
|
||||
<b>Mileage:</b>
|
||||
<p><?= $data['latest_record']['mileage'] ?> km</p>
|
||||
<?php if (isset($data['latest_record']['note'])): ?>
|
||||
<b>Note:</b>
|
||||
<p><?= $data['latest_record']['note'] ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card recent">
|
||||
<h2>Recent</h2>
|
||||
<div class="habit">
|
||||
<b>Habit Title</b>
|
||||
<p>Frequency</p>
|
||||
<p>Reward points</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card missed">
|
||||
<h2>Missed</h2>
|
||||
<div class="habit">
|
||||
<b>Habit Title</b>
|
||||
<p>Frequency</p>
|
||||
<p>Reward points</p>
|
||||
</div>
|
||||
<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>Graph of History</h2>
|
||||
<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>
|
||||
|
||||
<section class="card streak">
|
||||
<h2>Streak</h2>
|
||||
<p>You're current streak is 123 days</p>
|
||||
<p>Good job!</p>
|
||||
<section class="card history-graph">
|
||||
<h2>Average fuel consumption</h2>
|
||||
<hr>
|
||||
<p><?= $data['default_car']['name'] . " | " . $data['default_car']['registration_plate']?></p>
|
||||
<b id="avg-fl-cnsmp"></b>
|
||||
</section>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div id="actions">
|
||||
<a href="/refuel/create" class="btn-green">Add new refuel record</a>
|
||||
<a href="/vehicles" class="btn-primary">List all vehicles</a>
|
||||
<a class="btn-warning" id="btn-offline-add">Add new offline refuel record</a>
|
||||
</div>
|
||||
<div class="alert-warning">
|
||||
<p>Default vehicle <b><i><?= $data['default_car']['name'] ?></i></b> doesn't have any refuel record yet.</p>
|
||||
<p>Select another vehicle or create first refuel record.</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</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>
|
||||
const data2 = <?= json_encode($data['date_price_data']); ?>;
|
||||
let cnt_ltr = 0;
|
||||
let last_ltr = 0;
|
||||
let last_km = 0;
|
||||
let first_km = 0;
|
||||
|
||||
for (let i = 0; i < data2['liters'].length; i++) {
|
||||
if(i === 0) {
|
||||
first_km = data2['mileage'][i]
|
||||
continue; // skip bcs were expecting that this was first full tank refuel so this is base value
|
||||
}
|
||||
cnt_ltr += data2['liters'][i]
|
||||
last_ltr = data2['liters'][i]
|
||||
last_km = data2['mileage'][i]
|
||||
}
|
||||
|
||||
// cnt_ltr -= last_ltr // this would be used if we would track consumption from 0km
|
||||
last_km -= first_km
|
||||
|
||||
document.querySelector("#avg-fl-cnsmp").textContent =
|
||||
(cnt_ltr/last_km*100).toFixed(1) + " l/100km";
|
||||
</script>
|
||||
|
||||
<script defer src="/js/offline-records.js"></script>
|
||||
|
@ -4,7 +4,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Habit Tracker | Error 494</title>
|
||||
<title>Fuel Stats | Error 494</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Error 404 - Page not found</h1>
|
||||
|
@ -1,70 +0,0 @@
|
||||
<link rel="stylesheet" href="/css/form.css">
|
||||
<link rel="stylesheet" href="/css/habits_create.css">
|
||||
<section class="form habit-create">
|
||||
<h1 class="header-form"><?= $this->get('title', 'Create Habit') ?></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="/habits/create">
|
||||
<label for="name">Habit Name</label>
|
||||
<input type="text" name="name" id="name" required value="<?= htmlspecialchars($_POST['name'] ?? '') ?>">
|
||||
|
||||
<label for="frequency">Frequency</label>
|
||||
<select name="frequency" id="frequency" onchange="toggleCustomFrequency(this.value)">
|
||||
<option value="Daily">Daily</option>
|
||||
<option value="Weekly">Weekly</option>
|
||||
<option value="Custom">Custom</option>
|
||||
</select>
|
||||
|
||||
<div id="custom-frequency" style="display: none;">
|
||||
<label id="lbl_dow">Days of the Week</label>
|
||||
<div class="dow_chb_wrapper">
|
||||
<label for="dow_mon">Monday</label>
|
||||
<input type="checkbox" name="days_of_week[]" id="dow_mon" value="1">
|
||||
</div>
|
||||
<div class="dow_chb_wrapper">
|
||||
<label for="dow_tue">Tuesday</label>
|
||||
<input type="checkbox" name="days_of_week[]" id="dow_tue" value="2">
|
||||
</div>
|
||||
<div class="dow_chb_wrapper">
|
||||
<label for="dow_wed">Wednesday</label>
|
||||
<input type="checkbox" name="days_of_week[]" id="dow_wed" value="3">
|
||||
</div>
|
||||
<div class="dow_chb_wrapper">
|
||||
<label for="dow_thu">Thursday</label>
|
||||
<input type="checkbox" name="days_of_week[]" id="dow_thu" value="4">
|
||||
</div>
|
||||
<div class="dow_chb_wrapper">
|
||||
<label for="dow_fri">Friday</label>
|
||||
<input type="checkbox" name="days_of_week[]" id="dow_fri" value="5">
|
||||
</div>
|
||||
<div class="dow_chb_wrapper">
|
||||
<label for="dow_sat">Saturday</label>
|
||||
<input type="checkbox" name="days_of_week[]" id="dow_sat" value="6">
|
||||
</div>
|
||||
<div class="dow_chb_wrapper">
|
||||
<label for="dow_sun">Sunday</label>
|
||||
<input type="checkbox" name="days_of_week[]" id="dow_sun" value="7">
|
||||
</div>
|
||||
|
||||
<label for="days_of_month" id="lbl_dom">Days of the Month</label>
|
||||
<input type="text" name="days_of_month" id="days_of_month" placeholder="1,15 (comma-separated)">
|
||||
|
||||
<label for="months">Months</label>
|
||||
<input type="text" name="months" id="months" placeholder="1,7,12 (comma-separated)">
|
||||
</div>
|
||||
|
||||
<input type="submit" value="Create Habit">
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
function toggleCustomFrequency(value) {
|
||||
const customFrequencyDiv = document.getElementById('custom-frequency');
|
||||
customFrequencyDiv.style.display = value === 'Custom' ? 'flex' : 'none';
|
||||
}
|
||||
</script>
|
@ -1,24 +0,0 @@
|
||||
<link rel="stylesheet" href="/css/habits_dashboard.css">
|
||||
<section class="habits">
|
||||
<?php if (empty($this->get('habits'))): ?>
|
||||
<p>No habits yet. <a href="/habits/create">Create your first habit</a>.</p>
|
||||
<?php else: ?>
|
||||
<div class="habits-wrapper">
|
||||
<?php foreach ($this->get('habits') as $habit): ?>
|
||||
<div class="habit bordered">
|
||||
<b><?= htmlspecialchars($habit['title']) ?></b>
|
||||
<p>Frequency: <?= htmlspecialchars($habit['frequency']) ?></p>
|
||||
<?php if (isset($habit['custom_frequency'])): ?>
|
||||
<p><?= htmlspecialchars($habit['custom_frequency'] ?? 'N/A') ?></p>
|
||||
<?php endif; ?>
|
||||
<p><?= htmlspecialchars($habit['reward_points']) ?></p>
|
||||
<p><?= htmlspecialchars($habit['created_at']) ?></p>
|
||||
<a href="/habits/done">Mark as done</a> |
|
||||
<a href="/habits/edit?id=<?= $habit['id'] ?>">Edit</a> |
|
||||
<a href="/habits/delete?id=<?= $habit['id'] ?>" onclick="return confirm('Are you sure you want to delete this habit?')">Delete</a>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<a href="/habits/create" class="btn-green">Create new habit!</a>
|
||||
<?php endif; ?>
|
||||
</section>
|
@ -1,4 +1,4 @@
|
||||
<div>
|
||||
<h1>Welcome to Habit Tracker!</h1>
|
||||
<p>Track your habits and achieve your goals.</p>
|
||||
<h1>Welcome to Fuel Stats!</h1>
|
||||
<p>Keep track of your refuels.</p>
|
||||
</div>
|
||||
|
@ -6,7 +6,7 @@
|
||||
<meta name="author" content="Filip Rojek | http://filiprojek.cz">
|
||||
<meta name="email" content="webmaster(@)fofrweb.com">
|
||||
<meta name="copyright" content="(c) filiprojek.cz">
|
||||
<title>Habit Tracker | <?= $data['title'] ?></title>
|
||||
<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">
|
||||
@ -17,7 +17,7 @@
|
||||
<header>
|
||||
<div id="hd-left">
|
||||
<a href="/"><img src="/img/logo.jpg" alt="home"></a>
|
||||
<label><?= $data['title'] ?></label>
|
||||
<label><?= isset($data['title']) ? $data['title'] : "" ?></label>
|
||||
</div>
|
||||
<div id="hd-right">
|
||||
<?php if (!isset($_SESSION['user'])): ?>
|
||||
|
@ -6,7 +6,7 @@
|
||||
<meta name="author" content="Filip Rojek | http://filiprojek.cz">
|
||||
<meta name="email" content="webmaster(@)fofrweb.com">
|
||||
<meta name="copyright" content="(c) filiprojek.cz">
|
||||
<title>Habit Tracker | <?= $data['title'] ?></title>
|
||||
<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">
|
||||
|
125
app/views/refuel/create.php
Normal file
125
app/views/refuel/create.php
Normal file
@ -0,0 +1,125 @@
|
||||
<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="mileage">Mileage</label>
|
||||
<input type="number" name="mileage" id="mileage" min="0" step="1" value="<?= htmlspecialchars($_POST['mileage'] ?? '0') ?>">
|
||||
<?php if (isset($this->get('validationErrors')['mileage'])): ?>
|
||||
<small class="error"><?= $this->get('validationErrors')['mileage'] ?></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>
|
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>
|
36
app/views/vehicles/index.php
Normal file
36
app/views/vehicles/index.php
Normal file
@ -0,0 +1,36 @@
|
||||
<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="vehicle-actions">
|
||||
<br>
|
||||
<form method="POST" action="/vehicles/delete">
|
||||
<input type="number" name="vehicle_id" value="<?= $vehicle['id'] ?>" style="display: none">
|
||||
<input type="submit" value="Delete vehicle" class="btn-danger">
|
||||
</form>
|
||||
|
||||
<?php if(!$vehicle['is_default']): ?>
|
||||
<br>
|
||||
<form method="POST" action="/vehicles/default">
|
||||
<input type="number" name="vehicle_id" value="<?= $vehicle['id'] ?>" style="display: none">
|
||||
<input type="submit" value="Set as default" class="btn-primary">
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</section>
|
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
define('DB_HOST', '0.0.0.0');
|
||||
define('DB_NAME', 'habit_tracker');
|
||||
define('DB_NAME', 'fuel_stats');
|
||||
define('DB_USER', 'username');
|
||||
define('DB_PASS', 'password');
|
||||
|
@ -57,13 +57,12 @@ class Database {
|
||||
* Check and create required tables if they don't exist
|
||||
*/
|
||||
private function checkAndCreateTables() {
|
||||
// Create users table
|
||||
// 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;";
|
||||
|
||||
@ -71,36 +70,57 @@ class Database {
|
||||
die("Failed to create users table: " . $this->connection->error);
|
||||
}
|
||||
|
||||
// Create progress table
|
||||
$progressTableQuery = "CREATE TABLE IF NOT EXISTS progress (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
habit_id INT NOT NULL,
|
||||
date DATE NOT NULL,
|
||||
status ENUM('Done', 'Pending') DEFAULT 'Pending',
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
) ENGINE=InnoDB;";
|
||||
$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($progressTableQuery)) {
|
||||
die("Failed to create progress table: " . $this->connection->error);
|
||||
if (!$this->connection->query($vehiclesTableQuery)) {
|
||||
die("Failed to create vehicles table: " . $this->connection->error);
|
||||
}
|
||||
|
||||
|
||||
// Create habits table
|
||||
$habitsTableQuery = "CREATE TABLE IF NOT EXISTS habits (
|
||||
$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,
|
||||
mileage INT UNSIGNED 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,
|
||||
user_id INT NOT NULL,
|
||||
title VARCHAR(100) NOT NULL,
|
||||
frequency ENUM('Daily', 'Weekly', 'Custom') NOT NULL,
|
||||
custom_frequency VARCHAR(255) DEFAULT NULL, -- Store crontab-like string
|
||||
reward_points INT DEFAULT 1,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
username VARCHAR(50) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB;";
|
||||
|
||||
if (!$this->connection->query($habitsTableQuery)) {
|
||||
die("Failed to create habits table: " . $this->connection->error);
|
||||
if (!$this->connection->query($usersTableQuery)) {
|
||||
die("Failed to create table_name table: " . $this->connection->error);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -12,6 +12,15 @@ class Validator {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
@ -46,6 +55,10 @@ class Validator {
|
||||
return $this->errors;
|
||||
}
|
||||
|
||||
public function setErrors($errors) {
|
||||
$this->errors = $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if validation passed
|
||||
*/
|
||||
|
@ -7,6 +7,11 @@ class View
|
||||
// Store the data
|
||||
$this->data = $data;
|
||||
|
||||
// check for status code in data
|
||||
if(isset($this->data['status'])){
|
||||
http_response_code($this->data['status']);
|
||||
}
|
||||
|
||||
// Capture the view content
|
||||
ob_start();
|
||||
require_once views . $view . '.php';
|
||||
|
@ -6,6 +6,10 @@ services:
|
||||
MARIADB_ROOT_PASSWORD: root
|
||||
ports:
|
||||
- 3306:3306
|
||||
volumes:
|
||||
- fuelstats_mariadb_data:/var/lib/mysql
|
||||
networks:
|
||||
- fuelstats-network
|
||||
profiles: ["prod", "dev"]
|
||||
|
||||
phpmyadmin:
|
||||
@ -15,9 +19,14 @@ services:
|
||||
- 8080:80
|
||||
environment:
|
||||
- PMA_ARBITRARY=1
|
||||
- PMA_HOST=mariadb
|
||||
depends_on:
|
||||
- mariadb
|
||||
networks:
|
||||
- fuelstats-network
|
||||
profiles: ["dev"]
|
||||
|
||||
habittracker:
|
||||
fuelstats:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
@ -28,4 +37,13 @@ services:
|
||||
depends_on:
|
||||
- mariadb
|
||||
restart: on-failure:2
|
||||
networks:
|
||||
- fuelstats-network
|
||||
profiles: ["prod"]
|
||||
|
||||
networks:
|
||||
fuelstats-network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
fuelstats_mariadb_data:
|
||||
|
@ -9,13 +9,30 @@
|
||||
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);
|
||||
min-width: 17rem;
|
||||
width: 18rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
#btn-offline-add {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
background-color: var(--clr-warning-muted);
|
||||
border: var(--borderWidth-thin) solid var(--clr-border-danger);
|
||||
padding: 1rem;
|
||||
border-radius: var(--border-radious);
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
background-color: var(--clr-danger-muted);
|
||||
border: var(--borderWidth-thin) solid var(--clr-border-danger);
|
||||
padding: 1rem;
|
||||
border-radius: var(--border-radious);
|
||||
}
|
||||
|
@ -20,13 +20,15 @@ h1 {
|
||||
.btn-secondary,
|
||||
.btn-tertiary,
|
||||
.btn-green,
|
||||
.btn-danger {
|
||||
.btn-danger,
|
||||
.btn-warning {
|
||||
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 {
|
||||
@ -45,3 +47,13 @@ h1 {
|
||||
background-color: var(--clr-danger-muted);
|
||||
border: var(--borderWidth-thin) solid var(--clr-border-danger);
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background-color: var(--clr-warning-muted);
|
||||
border: var(--borderWidth-thin) solid var(--clr-border-danger);
|
||||
}
|
||||
|
||||
#actions {
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
|
@ -1,21 +0,0 @@
|
||||
.form form .dow_chb_wrapper input[type="checkbox"] {
|
||||
width: 1rem;
|
||||
}
|
||||
|
||||
.form form .dow_chb_wrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#lbl_dow {
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
#lbl_dom {
|
||||
margin-top: .5rem;
|
||||
}
|
||||
|
||||
#custom-frequency {
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
.habits-wrapper {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.habits .bordered {
|
||||
border-radius: var(--border-radious);
|
||||
border: var(--borderWidth-thin) solid var(--clr-border);
|
||||
width: 17rem;
|
||||
padding: 1rem;
|
||||
margin-top: 1rem;
|
||||
text-align: center;
|
||||
}
|
@ -5,6 +5,7 @@
|
||||
--clr-tertiary: #151b23;
|
||||
--clr-green: #238636;
|
||||
--clr-danger-muted: #f851491a;
|
||||
--clr-warning-muted: #e08e455e;
|
||||
|
||||
--clr-link-blue: #4493f8;
|
||||
--clr-light-blue: #39a2ae;
|
||||
|
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;
|
||||
}
|
36
public/css/vehicles.css
Normal file
36
public/css/vehicles.css
Normal file
@ -0,0 +1,36 @@
|
||||
.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;
|
||||
}
|
||||
|
||||
.vehicle-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: .5rem;
|
||||
margin-top: 1rem;
|
||||
}
|
@ -20,7 +20,8 @@ require_once '../core/Database.php';
|
||||
require_once '../core/middlewares/RequireAuth.php';
|
||||
|
||||
require_once models . 'User.php';
|
||||
require_once models . 'Habit.php';
|
||||
require_once models . 'Vehicle.php';
|
||||
require_once models . 'Refuel.php';
|
||||
|
||||
// Initialize router
|
||||
$router = new Router();
|
||||
@ -40,12 +41,22 @@ $router->group('/auth', [], function ($router) {
|
||||
// dashboard route
|
||||
$router->add('/dashboard', 'DashboardController@index', ['RequireAuth']);
|
||||
|
||||
// habits routes
|
||||
$router->group('/habits', ['RequireAuth'], function ($router) {
|
||||
$router->add('', 'HabitController@index');
|
||||
$router->add('/create', 'HabitController@create');
|
||||
$router->add('/edit/{id}', 'HabitController@edit');
|
||||
$router->add('/delete/{id}', 'HabitController@delete');
|
||||
// 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', 'VehicleController@delete');
|
||||
$router->add('/default', 'VehicleController@setDefault');
|
||||
});
|
||||
|
||||
$router->group('/refuel', ['RequireAuth'], function ($router) {
|
||||
$router->add('/create', 'RefuelController@create');
|
||||
});
|
||||
|
||||
// API
|
||||
$router->group('/api/v1', ['RequireAuth'], function ($router) {
|
||||
$router->add('/vehicles/get', 'VehicleController@api_get');
|
||||
});
|
||||
|
||||
$router->dispatch();
|
||||
|
273
public/js/offline-records.js
Normal file
273
public/js/offline-records.js
Normal file
@ -0,0 +1,273 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
function showDashboard() {
|
||||
const offline = document.querySelector(".offline");
|
||||
offline.remove();
|
||||
document.querySelector(".dashboard").style.display = "flex";
|
||||
}
|
||||
|
||||
const btnOffline = document.querySelector("#btn-offline-add");
|
||||
const divActions = document.querySelector("#actions");
|
||||
let visible = true;
|
||||
|
||||
setInterval(async () => {
|
||||
const isOnline = await checkOnline();
|
||||
//const isOnline = false; // REMOVE!!!
|
||||
if (!isOnline) {
|
||||
if (visible) {
|
||||
console.log("OFFLINE!!!");
|
||||
Array.from(divActions.children).forEach(
|
||||
(el) => (el.style.display = "none"),
|
||||
);
|
||||
visible = false;
|
||||
|
||||
btnOffline.style.display = "block";
|
||||
|
||||
document.querySelector(".hd-left").addEventListener("click", () => {
|
||||
showDashboard();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (localStorage.getItem("refuelOfflineData")) {
|
||||
Array.from(divActions.children).forEach(
|
||||
(el) => (el.style.display = "none"),
|
||||
);
|
||||
btnOffline.style.display = "block";
|
||||
btnOffline.textContent = "Sync data";
|
||||
btnOffline.setAttribute("disabled", "disabled");
|
||||
}
|
||||
|
||||
if (isOnline && !visible) {
|
||||
console.log("BACK ONLINE!!!");
|
||||
visible = true;
|
||||
btnOffline.removeAttribute("disabled", "disabled");
|
||||
// TODO: show buttons back, add sync button instead of record creation
|
||||
// If user is in a process of adding new offline refuel record, let him finish
|
||||
// Clear the local storage on each login
|
||||
}
|
||||
//}, 5000);
|
||||
}, 1000);
|
||||
|
||||
window.onload = async () => {
|
||||
const rawData = await fetch("/api/v1/vehicles/get", {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
});
|
||||
const data = await rawData.json();
|
||||
console.log("Fetched:", data);
|
||||
localStorage.setItem("vehicles", JSON.stringify(data));
|
||||
console.log(JSON.parse(localStorage.getItem("vehicles")));
|
||||
};
|
||||
|
||||
btnOffline.addEventListener("click", async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (btnOffline.textContent == "Sync data") {
|
||||
if (!visible) {
|
||||
alert("You're still offline. Try again later");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let data = localStorage.getItem("refuelOfflineData");
|
||||
if (!data) {
|
||||
console.error("No offline data found");
|
||||
alert("No offline data found");
|
||||
return;
|
||||
}
|
||||
|
||||
data = JSON.parse(data);
|
||||
const formData = new FormData();
|
||||
|
||||
for (const key in data) {
|
||||
if (data.hasOwnProperty(key)) {
|
||||
formData.append(key, data[key]);
|
||||
}
|
||||
}
|
||||
|
||||
const res = await fetch("/refuel/create", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
if (res.status == 400) {
|
||||
const html = await res.text();
|
||||
|
||||
document.open();
|
||||
document.write(html);
|
||||
document.close();
|
||||
|
||||
localStorage.removeItem("refuelOfflineData");
|
||||
return;
|
||||
} else {
|
||||
throw new Error(`Server error: ${res.statusText}`);
|
||||
}
|
||||
}
|
||||
|
||||
localStorage.removeItem("refuelOfflineData");
|
||||
location.reload();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("Something went wrong");
|
||||
location.reload();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
document.querySelector("section.dashboard").style.display = "none";
|
||||
|
||||
try {
|
||||
vehicles = localStorage.getItem("vehicles");
|
||||
if (vehicles === null) throw new Error("No data was saved locally");
|
||||
vehicles = JSON.parse(vehicles);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
const offline = document.createElement("div");
|
||||
offline.classList.add("offline");
|
||||
offline.innerHTML = `
|
||||
<div class="alert-danger">
|
||||
<b>You're Offline</b>
|
||||
<p>No data was saved locally, please try again later</p>
|
||||
</div>
|
||||
`;
|
||||
document.querySelector("main").appendChild(offline);
|
||||
// TODO: Add button to navigate back to offline dashboard
|
||||
return;
|
||||
}
|
||||
|
||||
const offline = document.createElement("div");
|
||||
offline.classList.add("offline");
|
||||
offline.innerHTML = `
|
||||
<div class="alert-warning">
|
||||
<b>You're Offline</b>
|
||||
<p>You can create an fuel record locally on your device and sync it later</p>
|
||||
</div>
|
||||
<section class="form">
|
||||
<h1 class="header-form">Create offline record</h1>
|
||||
<form id="offline_refuel_add">
|
||||
<label for="vehicle">Vehicle</label>
|
||||
<select name="vehicle" id="vehicle">
|
||||
${vehicles
|
||||
.map(
|
||||
(el) =>
|
||||
`<option value="${el.id}">${el.name} | ${el.registration_plate}</option>`,
|
||||
)
|
||||
.join("")}
|
||||
</select>
|
||||
|
||||
<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="liters">Liters</label>
|
||||
<input type="number" name="liters" id="liters" min="0" step=".01" value="0.0">
|
||||
<!-- <small class="error"><?= $this->get('validationErrors')['liters'] ?></small> -->
|
||||
|
||||
<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="0.0">
|
||||
<!-- <small class="error"><?= $this->get('validationErrors')['price_per_liter'] ?></small> -->
|
||||
|
||||
<label for="total_price">Total price</label>
|
||||
<input type="number" name="total_price" id="total_price" min="0" step=".01" value="0.0">
|
||||
<!-- <small class="error"><?= $this->get('validationErrors')['total_price'] ?></small> -->
|
||||
|
||||
<label for="mileage">Mileage</label>
|
||||
<input type="number" name="mileage" id="mileage" min="0" step="1" value="0">
|
||||
<!-- <small class="error"><?= $this->get('validationErrors')['mileage'] ?></small> -->
|
||||
|
||||
<label for="note">Note</label>
|
||||
<input type="text" name="note" id="note">
|
||||
<!-- <small class="error"><?= $this->get('validationErrors')['note'] ?></small> -->
|
||||
|
||||
<input type="submit" id="btn-offline-submit" value="Create fuel record">
|
||||
</form>
|
||||
</section>
|
||||
`;
|
||||
document.querySelector("main").appendChild(offline);
|
||||
|
||||
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 btnSubmit = document.querySelector("#btn-offline-submit");
|
||||
btnSubmit.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
const formData = {
|
||||
vehicle: document.querySelector("form#offline_refuel_add #vehicle").value,
|
||||
fuel_type: document.querySelector("form#offline_refuel_add #fuel_type")
|
||||
.value,
|
||||
liters: document.querySelector("form#offline_refuel_add #liters").value,
|
||||
price_per_liter: document.querySelector(
|
||||
"form#offline_refuel_add #price_per_liter",
|
||||
).value,
|
||||
total_price: document.querySelector(
|
||||
"form#offline_refuel_add #total_price",
|
||||
).value,
|
||||
mileage: document.querySelector("form#offline_refuel_add #mileage").value,
|
||||
note: document.querySelector("form#offline_refuel_add #note").value,
|
||||
};
|
||||
|
||||
console.log("formData", formData);
|
||||
localStorage.setItem("refuelOfflineData", JSON.stringify(formData));
|
||||
alert("Data was locally saved. Sync it later!");
|
||||
showDashboard();
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user