16 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
2201430f59 Partially done
All checks were successful
Build and Deploy Zola Website / build_and_deploy (push) Successful in 16s
2025-01-26 22:22:12 +01:00
18c78e37a4 In progress: offline fuel record creation 2025-01-26 17:21:30 +01:00
23 changed files with 632 additions and 58 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.

46
TODO.md
View File

@@ -6,13 +6,39 @@
- [ ] edit user data - change password, mail... - [ ] edit user data - change password, mail...
## Core of the app ## Core of the app
- [ ] header and navbar - [ ] intro tutorial when no car exist or just dont show anything
- [ ] dashboard - [x] change/set default car
- [x] css - [ ] specific car view - charts, fuel records
- [ ] its just plain - [ ] remove/edit fuel record
- [ ] graphs
- [x] Habits list ## Until release
- [ ] css - [x] Sync offline data from locale storage
- [ ] Habits create - [x] Include kilometer state of an car
- [ ] validate cron input - [ ] More charts
- [ ] Habits track - [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

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' => []] );
} }
@@ -52,10 +56,49 @@ class VehicleController extends Controller {
public function edit() { public function edit() {
// Edit vehicle (to be implemented later) // TODO: Edit vehicle (to be implemented later)
} }
public function delete() { public function delete() {
// 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() {
if(!$_SERVER['REQUEST_METHOD'] === 'GET') {
echo "Wrong method, use GET";
return;
}
$vehicle = new Vehicle();
$result = $vehicle->getVehiclesByUser($_SESSION['user']['id']);
echo json_encode($result);
} }
} }

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

@@ -1,10 +1,21 @@
<link rel="stylesheet" href="/css/dashboard.css"> <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"> <section class="dashboard">
<h1>Welcome, <?= htmlspecialchars($_SESSION['user']['username']) ?>!</h1> <h1>Welcome, <?= htmlspecialchars($_SESSION['user']['username']) ?>!</h1>
<div> <?php if(!isset($data['default_car'])): ?>
<a href="/refuel/create" class="btn-green">Add new refuel record!</a>
<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 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>
<div class="card-wrapper"> <div class="card-wrapper">
<section class="card latest"> <section class="card latest">
@@ -19,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>
@@ -41,10 +58,28 @@
<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>
const ctx = document.getElementById('chart-gas-price'); const ctx = document.getElementById('chart-gas-price');
const data = <?= json_encode($data['date_price_data']); ?>; const data = <?= json_encode($data['date_price_data']); ?>;
@@ -60,3 +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>

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 {
@@ -19,3 +18,21 @@
width: 18rem; width: 18rem;
padding: 1rem; 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);
}

View File

@@ -20,13 +20,15 @@ h1 {
.btn-secondary, .btn-secondary,
.btn-tertiary, .btn-tertiary,
.btn-green, .btn-green,
.btn-danger { .btn-danger,
.btn-warning {
background-color: var(--clr-primary); background-color: var(--clr-primary);
padding: .5rem; padding: .5rem;
text-decoration: none; text-decoration: none;
cursor: pointer; cursor: pointer;
border-radius: var(--border-radious); border-radius: var(--border-radious);
border: var(--borderWidth-thin) solid var(--clr-border); border: var(--borderWidth-thin) solid var(--clr-border);
color: white;
} }
.btn-secondary { .btn-secondary {
@@ -45,3 +47,13 @@ h1 {
background-color: var(--clr-danger-muted); background-color: var(--clr-danger-muted);
border: var(--borderWidth-thin) solid var(--clr-border-danger); 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;
}

View File

@@ -5,6 +5,7 @@
--clr-tertiary: #151b23; --clr-tertiary: #151b23;
--clr-green: #238636; --clr-green: #238636;
--clr-danger-muted: #f851491a; --clr-danger-muted: #f851491a;
--clr-warning-muted: #e08e455e;
--clr-link-blue: #4493f8; --clr-link-blue: #4493f8;
--clr-light-blue: #39a2ae; --clr-light-blue: #39a2ae;

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,11 +46,17 @@ $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) {
$router->add('/create', 'RefuelController@create'); $router->add('/create', 'RefuelController@create');
}); });
// API
$router->group('/api/v1', ['RequireAuth'], function ($router) {
$router->add('/vehicles/get', 'VehicleController@api_get');
});
$router->dispatch(); $router->dispatch();

View 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();
});
});