14 Commits

Author SHA1 Message Date
3d86f1ae2e Fixed: data validation in offline refuel create form
All checks were successful
Build and Deploy Zola Website / build_and_deploy (push) Successful in 12s
2025-02-03 19:54:58 +01:00
c14bbd1930 Update: diagrams
All checks were successful
Build and Deploy Zola Website / build_and_deploy (push) Successful in 13s
2025-02-03 09:53:19 +01:00
88a78d60e9 Updated: fixed fuel consumption calculation on dashboard
All checks were successful
Build and Deploy Zola Website / build_and_deploy (push) Successful in 12s
2025-02-01 23:35:21 +01:00
4576800e27 Updated: Removed LIMIT for all refuel records, fixed fuel consumption calculation on dashboard
All checks were successful
Build and Deploy Zola Website / build_and_deploy (push) Successful in 12s
2025-02-01 22:56:32 +01:00
652b04bfa1 Updated: Vehicle list hides "Set as default" for default vehicle and improves styling
All checks were successful
Build and Deploy Zola Website / build_and_deploy (push) Successful in 12s
2025-02-01 19:02:24 +01:00
6df1f6574a Updated: Added persistent storage for MariaDB in docker-compose.yaml
All checks were successful
Build and Deploy Zola Website / build_and_deploy (push) Successful in 13s
2025-02-01 18:48:28 +01:00
7517bcb78f Updated: Improved vehicle and refuel data handling on dashboard
All checks were successful
Build and Deploy Zola Website / build_and_deploy (push) Successful in 14s
Updated:
- DashboardController now fetches latest refuel record only for the default vehicle
- VehicleController - first created vehicle is now set as default automatically
- Refuel model - latest_one() now accepts vehicle_id instead of user_id
- Dashboard view - improved handling when no vehicles or refuel records exist
- CSS styles - adjusted dashboard layout and global action padding
2025-02-01 18:38:49 +01:00
f90c707435 Updated: docker-compose.yml networking for proper DB access 2025-02-01 17:35:22 +01:00
60423d37ce Updated: TODO.md
All checks were successful
Build and Deploy Zola Website / build_and_deploy (push) Successful in 13s
2025-01-30 14:39:21 +01:00
5989fba225 Fix: unable to register
All checks were successful
Build and Deploy Zola Website / build_and_deploy (push) Successful in 22s
2025-01-27 02:07:01 +01:00
ba11c41147 Added: mileage, average fuel consumption
All checks were successful
Build and Deploy Zola Website / build_and_deploy (push) Successful in 12s
2025-01-27 01:57:31 +01:00
64c7fd15a1 Added: set default vehicle 2025-01-27 00:38:22 +01:00
ea3afa2507 Updated: README.md added diagrams
All checks were successful
Build and Deploy Zola Website / build_and_deploy (push) Successful in 13s
2025-01-26 23:16:00 +01:00
a5f99788fc Added: Delete vehicle 2025-01-26 23:01:35 +01:00
22 changed files with 360 additions and 78 deletions

BIN
.screenshots/class.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
.screenshots/dlm.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

BIN
.screenshots/usecase.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@@ -53,5 +53,13 @@ php -S localhost:8000 -t ./public
4. Track your fuel consumption and spending through the dashboard. 4. Track your fuel consumption and spending through the dashboard.
5. View detailed stats and graphs to analyze your driving habits. 5. View detailed stats and graphs to analyze your driving habits.
## Use case diagram
![](.screenshots/usecase.png)
## Data logical model
![](.screenshots/dlm.png)
## Class diagram
![](.screenshots/class.png)
## License ## License
This project is licensed under GPL3.0 and later. More information is available in the `LICENSE` file. This project is licensed under GPL3.0 and later. More information is available in the `LICENSE` file.

29
TODO.md
View File

@@ -7,17 +7,38 @@
## Core of the app ## Core of the app
- [ ] intro tutorial when no car exist or just dont show anything - [ ] intro tutorial when no car exist or just dont show anything
- [ ] change/set default car - [x] change/set default car
- [ ] specific car view - charts, fuel records - [ ] specific car view - charts, fuel records
- [ ] remove/edit fuel record - [ ] remove/edit fuel record
## Until release ## Until release
- [ ] Sync offline data from locale storage - [x] Sync offline data from locale storage
- [ ] Include kilometer state of an car - [x] Include kilometer state of an car
- [ ] More charts - [ ] More charts
- [ ] Average fuel conusption - [x] Average fuel conusption
- [ ] Kilometer state - [ ] Kilometer state
- [ ] More cards - [ ] More cards
- [ ] Average fuel conusption in last 30 days - [ ] Average fuel conusption in last 30 days
- [ ] Kilometer state in last 30 days` - [ ] Kilometer state in last 30 days`
- [ ] Offline navigation between dashboard and offline form - [ ] 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

View File

@@ -3,29 +3,25 @@ class DashboardController extends Controller {
public function index() { public function index() {
$vehicle = new Vehicle(); $vehicle = new Vehicle();
$vehicles = $vehicle->getVehiclesByUser($_SESSION['user']['id']); $vehicles = $vehicle->getVehiclesByUser($_SESSION['user']['id']);
$default_car = $vehicle->getDefaultVehicle($_SESSION['user']['id']) ?? null;
$default_car = $vehicle->getDefaultVehicle($_SESSION['user']['id']);
$refuel = new Refuel(); $refuel = new Refuel();
$data = [ $data = [
"date" => [], "date" => [],
"price" => [], "price" => [],
"mileage" => [],
"liters" => []
]; ];
$raw_data = $refuel->latest_data($default_car['id'], 5); $raw_data = $default_car ? $refuel->latest_data($default_car['id'], 0) : [];
foreach($raw_data as $one) { foreach($raw_data as $one) {
array_push($data['date'], date('d. m.', strtotime($one['created_at']))); array_push($data['date'], date('d. m.', strtotime($one['created_at'])));
array_push($data['price'], $one['price_per_liter']); array_push($data['price'], $one['price_per_liter']);
array_push($data['mileage'], $one['mileage']);
array_push($data['liters'], $one['liters']);
} }
$latest_record = [ $latest_data = $default_car ? $refuel->latest_one($default_car['id']) : [];
'name', $latest_record = !empty($latest_data) ? $latest_data[0] : null;
'liters',
'price_per_liter',
'total_price',
'created_at'
];
$latest_record = $refuel->latest_one($_SESSION['user']['id'])[0];
$this->view('dashboard/index', [ $this->view('dashboard/index', [
'title' => 'Dashboard', 'title' => 'Dashboard',

View File

@@ -18,6 +18,7 @@ class RefuelController extends Controller {
$liters = $_POST['liters'] ?? ''; $liters = $_POST['liters'] ?? '';
$price_per_liter = $_POST['price_per_liter'] ?? ''; $price_per_liter = $_POST['price_per_liter'] ?? '';
$total_price = $_POST['total_price'] ?? ''; $total_price = $_POST['total_price'] ?? '';
$mileage = $_POST['mileage'] ?? '';
$note = $_POST['note'] ?? ''; $note = $_POST['note'] ?? '';
$validator = new Validator(); $validator = new Validator();
@@ -29,6 +30,7 @@ class RefuelController extends Controller {
$validator->number('liters', $liters); $validator->number('liters', $liters);
$validator->number('price_per_liter', $price_per_liter); $validator->number('price_per_liter', $price_per_liter);
$validator->number('total_price', $total_price); $validator->number('total_price', $total_price);
$validator->number('mileage', $mileage);
if (round($liters * $price_per_liter, 2) != $total_price) { if (round($liters * $price_per_liter, 2) != $total_price) {
$validator->setErrors(["total_price" => "Price calculation is wrong"]); $validator->setErrors(["total_price" => "Price calculation is wrong"]);
@@ -44,6 +46,7 @@ class RefuelController extends Controller {
'validationErrors' => $validator->errors() ?: [], 'validationErrors' => $validator->errors() ?: [],
'vehicles' => $vehicles, 'vehicles' => $vehicles,
'title' => 'New refuel record', 'title' => 'New refuel record',
'status' => '400'
]); ]);
return; return;
} }
@@ -57,6 +60,7 @@ class RefuelController extends Controller {
'liters' => $liters, 'liters' => $liters,
'price_per_liter' => $price_per_liter, 'price_per_liter' => $price_per_liter,
'total_price' => $total_price, 'total_price' => $total_price,
'mileage' => $mileage,
]); ]);
if ($result === true) { if ($result === true) {

View File

@@ -30,17 +30,21 @@ class VehicleController extends Controller {
} }
$vehicle = new Vehicle(); $vehicle = new Vehicle();
$default_vehicle = $vehicle->getDefaultVehicle($_SESSION['user']['id']);
$is_default = $default_vehicle ? 0 : 1;
$result = $vehicle->create([ $result = $vehicle->create([
'name' => $name, 'name' => $name,
'registration_plate' => strtoupper($registration_plate), 'registration_plate' => strtoupper($registration_plate),
'fuel_type' => $fuel_type, 'fuel_type' => $fuel_type,
'note' => $note, 'note' => $note,
'user_id' => $_SESSION['user']['id'], 'user_id' => $_SESSION['user']['id'],
'is_default' => $is_default
]); ]);
if ($result === true) { if ($result === true) {
$this->redirect('/vehicles'); $this->redirect('/');
} else { } else {
$this->view('vehicles/create', ['title' => 'Create vehicle', 'error' => $result, 'validationErrors' => []] ); $this->view('vehicles/create', ['title' => 'Create vehicle', 'error' => $result, 'validationErrors' => []] );
} }
@@ -56,7 +60,35 @@ class VehicleController extends Controller {
} }
public function delete() { public function delete() {
// TODO: Delete vehicle (to be implemented later) 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() { public function api_get() {

View File

@@ -10,12 +10,12 @@ class Refuel {
public function create($data) { public function create($data) {
try{ try{
$stmt = $this->db->prepare(" $stmt = $this->db->prepare("
INSERT INTO refueling_records (user_id, vehicle_id, fuel_type, note, liters, price_per_liter, total_price, created_at) INSERT INTO refueling_records (user_id, vehicle_id, fuel_type, note, liters, price_per_liter, total_price, mileage, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, NOW()) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW())
"); ");
$stmt->bind_param( $stmt->bind_param(
"iissddd", "iissdddi",
$data['user_id'], $data['user_id'],
$data['vehicle_id'], $data['vehicle_id'],
$data['fuel_type'], $data['fuel_type'],
@@ -23,6 +23,7 @@ class Refuel {
$data['liters'], $data['liters'],
$data['price_per_liter'], $data['price_per_liter'],
$data['total_price'], $data['total_price'],
$data['mileage'],
); );
if ($stmt->execute()) { if ($stmt->execute()) {
@@ -37,15 +38,24 @@ class Refuel {
public function latest_data($vehicle_id, $record_count) { public function latest_data($vehicle_id, $record_count) {
try { try {
$stmt = $this->db->prepare(" $sql = "
SELECT `liters`, `price_per_liter`, `total_price`, `created_at` SELECT `liters`, `price_per_liter`, `total_price`, `mileage`, `created_at`
FROM `refueling_records` FROM `refueling_records`
WHERE `vehicle_id` = ? WHERE `vehicle_id` = ?
ORDER BY created_at DESC ORDER BY created_at DESC";
LIMIT ?;
"); 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);
}
$stmt->bind_param("ii", $vehicle_id, $record_count);
if ($stmt->execute()) { if ($stmt->execute()) {
$result = $stmt->get_result(); $result = $stmt->get_result();
$data = $result->fetch_all(MYSQLI_ASSOC); $data = $result->fetch_all(MYSQLI_ASSOC);
@@ -59,24 +69,35 @@ class Refuel {
} }
} }
public function latest_one($user_id, $record_count = 1) { public function latest_one($vehicle_id, $record_count = 1) {
try { try {
$stmt = $this->db->prepare(" $sql = "
SELECT SELECT
`r`.`vehicle_id`, `r`.`vehicle_id`,
`v`.`name` AS `vehicle_name`, `v`.`name` AS `vehicle_name`,
`r`.`liters`, `r`.`liters`,
`r`.`price_per_liter`, `r`.`price_per_liter`,
`r`.`total_price`, `r`.`total_price`,
`r`.`mileage`,
`r`.`note`,
`r`.`created_at` `r`.`created_at`
FROM `refueling_records` AS `r` FROM `refueling_records` AS `r`
JOIN `vehicles` AS `v` ON `r`.`vehicle_id` = `v`.`id` JOIN `vehicles` AS `v` ON `r`.`vehicle_id` = `v`.`id`
WHERE `r`.`user_id` = ? WHERE `r`.`vehicle_id` = ?
ORDER BY `r`.`created_at` DESC ORDER BY `r`.`created_at` DESC";
LIMIT ?;
"); 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);
}
$stmt->bind_param("ii", $user_id, $record_count);
if ($stmt->execute()) { if ($stmt->execute()) {
$result = $stmt->get_result(); $result = $stmt->get_result();
$data = $result->fetch_all(MYSQLI_ASSOC); $data = $result->fetch_all(MYSQLI_ASSOC);

View File

@@ -29,7 +29,7 @@ class User {
$hashedPassword = password_hash($password, PASSWORD_BCRYPT); $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); $stmt->bind_param("sss", $username, $email, $hashedPassword);
if ($stmt->execute()) { if ($stmt->execute()) {

View File

@@ -10,17 +10,18 @@ class Vehicle {
public function create($data) { public function create($data) {
try{ try{
$stmt = $this->db->prepare(" $stmt = $this->db->prepare("
INSERT INTO vehicles (user_id, name, registration_plate, fuel_type, note, created_at) INSERT INTO vehicles (user_id, name, registration_plate, fuel_type, note, is_default, created_at)
VALUES (?, ?, ?, ?, ?, NOW()) VALUES (?, ?, ?, ?, ?, ?, NOW())
"); ");
$stmt->bind_param( $stmt->bind_param(
"issss", "issssi",
$data['user_id'], $data['user_id'],
$data['name'], $data['name'],
$data['registration_plate'], $data['registration_plate'],
$data['fuel_type'], $data['fuel_type'],
$data['note'], $data['note'],
$data['is_default'],
); );
if ($stmt->execute()) { if ($stmt->execute()) {
@@ -34,7 +35,7 @@ class Vehicle {
} }
public function getVehiclesByUser($user_id) { 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 = $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->bind_param("i", $user_id);
$stmt->execute(); $stmt->execute();
$result = $stmt->get_result(); $result = $stmt->get_result();
@@ -60,4 +61,61 @@ class Vehicle {
return $result->fetch_assoc(); 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();
}
}
} }

View File

@@ -5,8 +5,15 @@
<section class="dashboard"> <section class="dashboard">
<h1>Welcome, <?= htmlspecialchars($_SESSION['user']['username']) ?>!</h1> <h1>Welcome, <?= htmlspecialchars($_SESSION['user']['username']) ?>!</h1>
<?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"> <div id="actions">
<a href="/refuel/create" class="btn-green">Add new refuel record!</a> <a href="/refuel/create" class="btn-green">Add new refuel record</a>
<a href="/vehicles" class="btn-primary">List all vehicles</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> <a class="btn-warning" id="btn-offline-add">Add new offline refuel record</a>
</div> </div>
@@ -23,6 +30,12 @@
<p><?= $data['latest_record']['price_per_liter'] ?>,-/liter</p> <p><?= $data['latest_record']['price_per_liter'] ?>,-/liter</p>
<b>Total price:</b> <b>Total price:</b>
<p><?= $data['latest_record']['total_price'] ?>,-</p> <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> </div>
</section> </section>
@@ -45,7 +58,25 @@
<p><?= $data['default_car']['name'] . " | " . $data['default_car']['registration_plate']?></p> <p><?= $data['default_car']['name'] . " | " . $data['default_car']['registration_plate']?></p>
<canvas id="chart-gas-price"></canvas> <canvas id="chart-gas-price"></canvas>
</section> </section>
<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> </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> </section>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script> <script>
@@ -64,4 +95,29 @@
}, },
}); });
</script> </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> <script defer src="/js/offline-records.js"></script>

View File

@@ -52,6 +52,12 @@
<small class="error"><?= $this->get('validationErrors')['total_price'] ?></small> <small class="error"><?= $this->get('validationErrors')['total_price'] ?></small>
<?php endif; ?> <?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> <label for="note">Note</label>
<input type="text" name="note" id="note" value="<?= htmlspecialchars($_POST['note'] ?? '') ?>"> <input type="text" name="note" id="note" value="<?= htmlspecialchars($_POST['note'] ?? '') ?>">
<?php if (isset($this->get('validationErrors')['note'])): ?> <?php if (isset($this->get('validationErrors')['note'])): ?>

View File

@@ -4,7 +4,7 @@
<p>No vehicles yet. <a href="/vehicles/create">Add your first vehicle</a>.</p> <p>No vehicles yet. <a href="/vehicles/create">Add your first vehicle</a>.</p>
<?php else: ?> <?php else: ?>
<div class="btn-wrapper"> <div class="btn-wrapper">
<a href="/vehicles/create" class="btn-green">Add new vehicle!</a> <a href="/vehicles/create" class="btn-green">Add new vehicle</a>
</div> </div>
<div class="vehicle-wrapper"> <div class="vehicle-wrapper">
<?php foreach ($this->get('vehicles') as $vehicle): ?> <?php foreach ($this->get('vehicles') as $vehicle): ?>
@@ -13,9 +13,21 @@
<p><?= htmlspecialchars($vehicle['registration_plate']) ?></p> <p><?= htmlspecialchars($vehicle['registration_plate']) ?></p>
<p><?= htmlspecialchars($vehicle['fuel_type']) ?></p> <p><?= htmlspecialchars($vehicle['fuel_type']) ?></p>
<p><?= htmlspecialchars($vehicle['note'] ?? "") ?></p> <p><?= htmlspecialchars($vehicle['note'] ?? "") ?></p>
<div class="actions">
<a href="/vehicles/edit?id=<?= $vehicle['id'] ?>">Edit</a> <div class="vehicle-actions">
<a href="/vehicles/delete?id=<?= $vehicle['id'] ?>" onclick="return confirm('Are you sure you want to delete this habit?')">Delete</a> <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>
</div> </div>
<?php endforeach; ?> <?php endforeach; ?>

View File

@@ -63,7 +63,6 @@ class Database {
username VARCHAR(50) NOT NULL, username VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL UNIQUE, email VARCHAR(100) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL, password VARCHAR(255) NOT NULL,
points INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;"; ) ENGINE=InnoDB;";
@@ -99,6 +98,7 @@ class Database {
liters DOUBLE(10, 2) NOT NULL, liters DOUBLE(10, 2) NOT NULL,
price_per_liter DOUBLE(10, 2) NOT NULL, price_per_liter DOUBLE(10, 2) NOT NULL,
total_price DOUBLE(10, 2) NOT NULL, total_price DOUBLE(10, 2) NOT NULL,
mileage INT UNSIGNED NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (vehicle_id) REFERENCES vehicles(id) ON DELETE CASCADE, FOREIGN KEY (vehicle_id) REFERENCES vehicles(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE

View File

@@ -7,6 +7,11 @@ class View
// Store the data // Store the data
$this->data = $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 // Capture the view content
ob_start(); ob_start();
require_once views . $view . '.php'; require_once views . $view . '.php';

View File

@@ -6,6 +6,10 @@ services:
MARIADB_ROOT_PASSWORD: root MARIADB_ROOT_PASSWORD: root
ports: ports:
- 3306:3306 - 3306:3306
volumes:
- fuelstats_mariadb_data:/var/lib/mysql
networks:
- fuelstats-network
profiles: ["prod", "dev"] profiles: ["prod", "dev"]
phpmyadmin: phpmyadmin:
@@ -15,6 +19,11 @@ services:
- 8080:80 - 8080:80
environment: environment:
- PMA_ARBITRARY=1 - PMA_ARBITRARY=1
- PMA_HOST=mariadb
depends_on:
- mariadb
networks:
- fuelstats-network
profiles: ["dev"] profiles: ["dev"]
fuelstats: fuelstats:
@@ -28,4 +37,13 @@ services:
depends_on: depends_on:
- mariadb - mariadb
restart: on-failure:2 restart: on-failure:2
networks:
- fuelstats-network
profiles: ["prod"] profiles: ["prod"]
networks:
fuelstats-network:
driver: bridge
volumes:
fuelstats_mariadb_data:

View File

@@ -9,7 +9,6 @@
flex-wrap: wrap; flex-wrap: wrap;
gap: 1rem; gap: 1rem;
justify-content: center; justify-content: center;
margin-top: 2rem;
} }
.card { .card {

View File

@@ -52,3 +52,8 @@ h1 {
background-color: var(--clr-warning-muted); background-color: var(--clr-warning-muted);
border: var(--borderWidth-thin) solid var(--clr-border-danger); border: var(--borderWidth-thin) solid var(--clr-border-danger);
} }
#actions {
padding-top: 1rem;
padding-bottom: 2rem;
}

View File

@@ -27,3 +27,10 @@
align-items: center; align-items: center;
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.vehicle-actions {
display: flex;
flex-direction: row;
gap: .5rem;
margin-top: 1rem;
}

View File

@@ -46,7 +46,8 @@ $router->group('/vehicles', ['RequireAuth'], function ($router) {
$router->add('', 'VehicleController@index'); $router->add('', 'VehicleController@index');
$router->add('/create', 'VehicleController@create'); $router->add('/create', 'VehicleController@create');
$router->add('/edit/{id}', 'VehicleController@edit'); $router->add('/edit/{id}', 'VehicleController@edit');
$router->add('/delete/{id}', 'VehicleController@delete'); $router->add('/delete', 'VehicleController@delete');
$router->add('/default', 'VehicleController@setDefault');
}); });
$router->group('/refuel', ['RequireAuth'], function ($router) { $router->group('/refuel', ['RequireAuth'], function ($router) {

View File

@@ -45,11 +45,15 @@ setInterval(async () => {
showDashboard(); showDashboard();
}); });
} }
}
if (localStorage.getItem("refuelOfflineData")) { if (localStorage.getItem("refuelOfflineData")) {
btnOffline.textContent = "Sync data"; Array.from(divActions.children).forEach(
btnOffline.setAttribute("disabled", "disabled"); (el) => (el.style.display = "none"),
} );
btnOffline.style.display = "block";
btnOffline.textContent = "Sync data";
btnOffline.setAttribute("disabled", "disabled");
} }
if (isOnline && !visible) { if (isOnline && !visible) {
@@ -107,7 +111,18 @@ btnOffline.addEventListener("click", async (e) => {
}); });
if (!res.ok) { if (!res.ok) {
throw new Error(`Server error: ${res.statusText}`); 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"); localStorage.removeItem("refuelOfflineData");
@@ -150,11 +165,6 @@ btnOffline.addEventListener("click", async (e) => {
</div> </div>
<section class="form"> <section class="form">
<h1 class="header-form">Create offline record</h1> <h1 class="header-form">Create offline record</h1>
<!-- <?php if ($this->get('error')): ?> -->
<!-- <div class="error" style="color: red; margin-bottom: 1rem;"> -->
<!-- <?= htmlspecialchars($this->get('error')) ?> -->
<!-- </div> -->
<!-- <?php endif; ?> -->
<form id="offline_refuel_add"> <form id="offline_refuel_add">
<label for="vehicle">Vehicle</label> <label for="vehicle">Vehicle</label>
<select name="vehicle" id="vehicle"> <select name="vehicle" id="vehicle">
@@ -164,13 +174,7 @@ btnOffline.addEventListener("click", async (e) => {
`<option value="${el.id}">${el.name} | ${el.registration_plate}</option>`, `<option value="${el.id}">${el.name} | ${el.registration_plate}</option>`,
) )
.join("")} .join("")}
<!-- <?php foreach ($this->get('vehicles') as $vehicle): ?> -->
<!-- <option value="<?= $vehicle['id'] ?>"><?= $vehicle['name'] . " | " . $vehicle['registration_plate'] ?></option> -->
<!-- <?php endforeach; ?> -->
</select> </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> <label for="fuel_type">Fuel type</label>
<select name="fuel_type" id="fuel_type"> <select name="fuel_type" id="fuel_type">
@@ -182,39 +186,67 @@ btnOffline.addEventListener("click", async (e) => {
<option value="Premium Gasoline 98">Premium Gasoline 98</option> <option value="Premium Gasoline 98">Premium Gasoline 98</option>
<option value="Other">Other</option> <option value="Other">Other</option>
</select> </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> <label for="liters">Liters</label>
<input type="number" name="liters" id="liters" min="0" step=".01" value="0.0"> <input type="number" name="liters" id="liters" min="0" step=".01" value="0.0">
<!-- <?php if (isset($this->get('validationErrors')['liters'])): ?> --> <!-- <small class="error"><?= $this->get('validationErrors')['liters'] ?></small> -->
<!-- <small class="error"><?= $this->get('validationErrors')['liters'] ?></small> -->
<!-- <?php endif; ?> -->
<label for="price_per_liter">Price per liter</label> <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"> <input type="number" name="price_per_liter" id="price_per_liter" min="0" step=".01" value="0.0">
<!-- <?php if (isset($this->get('validationErrors')['price_per_liter'])): ?> --> <!-- <small class="error"><?= $this->get('validationErrors')['price_per_liter'] ?></small> -->
<!-- <small class="error"><?= $this->get('validationErrors')['price_per_liter'] ?></small> -->
<!-- <?php endif; ?> -->
<label for="total_price">Total price</label> <label for="total_price">Total price</label>
<input type="number" name="total_price" id="total_price" min="0" step=".01" value="0.0"> <input type="number" name="total_price" id="total_price" min="0" step=".01" value="0.0">
<!-- <?php if (isset($this->get('validationErrors')['total_price'])): ?> --> <!-- <small class="error"><?= $this->get('validationErrors')['total_price'] ?></small> -->
<!-- <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="0">
<!-- <small class="error"><?= $this->get('validationErrors')['mileage'] ?></small> -->
<label for="note">Note</label> <label for="note">Note</label>
<input type="text" name="note" id="note"> <input type="text" name="note" id="note">
<!-- <?php if (isset($this->get('validationErrors')['note'])): ?> --> <!-- <small class="error"><?= $this->get('validationErrors')['note'] ?></small> -->
<!-- <small class="error"><?= $this->get('validationErrors')['note'] ?></small> -->
<!-- <?php endif; ?> -->
<input type="submit" id="btn-offline-submit" value="Create fuel record"> <input type="submit" id="btn-offline-submit" value="Create fuel record">
</form> </form>
</section> </section>
`; `;
document.querySelector("main").appendChild(offline); 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"); const btnSubmit = document.querySelector("#btn-offline-submit");
btnSubmit.addEventListener("click", (e) => { btnSubmit.addEventListener("click", (e) => {
e.preventDefault(); e.preventDefault();
@@ -229,6 +261,7 @@ btnOffline.addEventListener("click", async (e) => {
total_price: document.querySelector( total_price: document.querySelector(
"form#offline_refuel_add #total_price", "form#offline_refuel_add #total_price",
).value, ).value,
mileage: document.querySelector("form#offline_refuel_add #mileage").value,
note: document.querySelector("form#offline_refuel_add #note").value, note: document.querySelector("form#offline_refuel_add #note").value,
}; };