Compare commits

..

No commits in common. "master" and "fr/offline-record" have entirely different histories.

23 changed files with 89 additions and 565 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

View File

@ -53,13 +53,5 @@ 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.

35
TODO.md
View File

@ -7,38 +7,9 @@
## 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
- [x] change/set default car - [ ] 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 - [ ] IndexDB
- [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

View File

@ -3,25 +3,29 @@ 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 = $default_car ? $refuel->latest_data($default_car['id'], 0) : []; $raw_data = $refuel->latest_data($default_car['id'], 5);
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_data = $default_car ? $refuel->latest_one($default_car['id']) : []; $latest_record = [
$latest_record = !empty($latest_data) ? $latest_data[0] : null; 'name',
'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,7 +18,6 @@ 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();
@ -30,7 +29,6 @@ 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"]);
@ -46,7 +44,6 @@ 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;
} }
@ -60,7 +57,6 @@ 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,21 +30,17 @@ 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('/'); $this->redirect('/vehicles');
} else { } else {
$this->view('vehicles/create', ['title' => 'Create vehicle', 'error' => $result, 'validationErrors' => []] ); $this->view('vehicles/create', ['title' => 'Create vehicle', 'error' => $result, 'validationErrors' => []] );
} }
@ -56,49 +52,10 @@ class VehicleController extends Controller {
public function edit() { public function edit() {
// TODO: Edit vehicle (to be implemented later) // Edit vehicle (to be implemented later)
} }
public function delete() { public function delete() {
if(!$_SERVER['REQUEST_METHOD'] === 'POST') { // Delete vehicle (to be implemented later)
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, mileage, created_at) INSERT INTO refueling_records (user_id, vehicle_id, fuel_type, note, liters, price_per_liter, total_price, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW()) VALUES (?, ?, ?, ?, ?, ?, ?, NOW())
"); ");
$stmt->bind_param( $stmt->bind_param(
"iissdddi", "iissddd",
$data['user_id'], $data['user_id'],
$data['vehicle_id'], $data['vehicle_id'],
$data['fuel_type'], $data['fuel_type'],
@ -23,7 +23,6 @@ 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()) {
@ -38,24 +37,15 @@ class Refuel {
public function latest_data($vehicle_id, $record_count) { public function latest_data($vehicle_id, $record_count) {
try { try {
$sql = " $stmt = $this->db->prepare("
SELECT `liters`, `price_per_liter`, `total_price`, `mileage`, `created_at` SELECT `liters`, `price_per_liter`, `total_price`, `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);
@ -69,35 +59,24 @@ class Refuel {
} }
} }
public function latest_one($vehicle_id, $record_count = 1) { public function latest_one($user_id, $record_count = 1) {
try { try {
$sql = " $stmt = $this->db->prepare("
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`.`vehicle_id` = ? WHERE `r`.`user_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, created_at) VALUES (?, ?, ?, NOW())"); $stmt = $this->db->prepare("INSERT INTO users (username, email, password, points, created_at) VALUES (?, ?, ?, 0, NOW())");
$stmt->bind_param("sss", $username, $email, $hashedPassword); $stmt->bind_param("sss", $username, $email, $hashedPassword);
if ($stmt->execute()) { if ($stmt->execute()) {

View File

@ -10,18 +10,17 @@ 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, is_default, created_at) INSERT INTO vehicles (user_id, name, registration_plate, fuel_type, note, created_at)
VALUES (?, ?, ?, ?, ?, ?, NOW()) VALUES (?, ?, ?, ?, ?, NOW())
"); ");
$stmt->bind_param( $stmt->bind_param(
"issssi", "issss",
$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()) {
@ -35,7 +34,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, is_default, created_at FROM vehicles WHERE user_id = ?"); $stmt = $this->db->prepare("SELECT id, name, registration_plate, fuel_type, note, created_at FROM vehicles WHERE user_id = ?");
$stmt->bind_param("i", $user_id); $stmt->bind_param("i", $user_id);
$stmt->execute(); $stmt->execute();
$result = $stmt->get_result(); $result = $stmt->get_result();
@ -61,61 +60,4 @@ 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,21 +1,11 @@
<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>
<?php if(!isset($data['default_car'])): ?> <div>
<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> <button class="btn-primary" id="btn-offline-add">Add new refuel record OFFLINE</button>
</div> </div>
<div class="card-wrapper"> <div class="card-wrapper">
<section class="card latest"> <section class="card latest">
@ -30,12 +20,6 @@
<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>
@ -58,25 +42,7 @@
<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>
@ -95,29 +61,4 @@
}, },
}); });
</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,12 +52,6 @@
<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,21 +13,9 @@
<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">
<div class="vehicle-actions"> <a href="/vehicles/edit?id=<?= $vehicle['id'] ?>">Edit</a>
<br> <a href="/vehicles/delete?id=<?= $vehicle['id'] ?>" onclick="return confirm('Are you sure you want to delete this habit?')">Delete</a>
<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,6 +63,7 @@ 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;";
@ -98,7 +99,6 @@ 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,11 +7,6 @@ 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,10 +6,6 @@ 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:
@ -19,11 +15,6 @@ 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:
@ -37,13 +28,4 @@ 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,6 +9,7 @@
flex-wrap: wrap; flex-wrap: wrap;
gap: 1rem; gap: 1rem;
justify-content: center; justify-content: center;
margin-top: 2rem;
} }
.card { .card {
@ -18,21 +19,3 @@
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,8 +20,7 @@ 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;
@ -47,13 +46,3 @@ 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,7 +5,6 @@
--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,10 +27,3 @@
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,17 +46,11 @@ $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', 'VehicleController@delete'); $router->add('/delete/{id}', '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

@ -18,163 +18,39 @@ async function checkOnline() {
} }
} }
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 () => { setInterval(async () => {
const isOnline = await checkOnline(); const isOnline = await checkOnline();
//const isOnline = false; // REMOVE!!!
if (!isOnline) { if (!isOnline) {
if (visible) { console.log("OFFLINE!!!");
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();
});
}
} }
}, 5000);
if (localStorage.getItem("refuelOfflineData")) { const offbtn = document.querySelector("#btn-offline-add");
Array.from(divActions.children).forEach( offbtn.addEventListener("click", (e) => {
(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(); 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"; 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"); const offline = document.createElement("div");
offline.classList.add("offline"); offline.classList.add("offline");
offline.innerHTML = ` offline.innerHTML = `
<div class="alert-warning"> <b>You're Offline</b>
<b>You're Offline</b> <p>You can create an fuel record locally on your device and sync it later</p>
<p>You can create an fuel record locally on your device and sync it later</p>
</div>
<section class="form"> <section class="form">
<h1 class="header-form">Create offline record</h1> <h1 class="header-form"><?= $this->get('title') ?></h1>
<form id="offline_refuel_add"> <!-- <?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> <label for="vehicle">Vehicle</label>
<select name="vehicle" id="vehicle"> <select name="vehicle" id="vehicle">
${vehicles <!-- <?php foreach ($this->get('vehicles') as $vehicle): ?> -->
.map( <!-- <option value="<?= $vehicle['id'] ?>"><?= $vehicle['name'] . " | " . $vehicle['registration_plate'] ?></option> -->
(el) => <!-- <?php endforeach; ?> -->
`<option value="${el.id}">${el.name} | ${el.registration_plate}</option>`,
)
.join("")}
</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">
@ -186,88 +62,37 @@ 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="<?= htmlspecialchars($_POST['liters'] ?? '0.0') ?>">
<!-- <small class="error"><?= $this->get('validationErrors')['liters'] ?></small> --> <!-- <?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> <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="<?= htmlspecialchars($_POST['price_per_liter'] ?? '0.0') ?>">
<!-- <small class="error"><?= $this->get('validationErrors')['price_per_liter'] ?></small> --> <!-- <?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> <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="<?= htmlspecialchars($_POST['total_price'] ?? '0.0') ?>">
<!-- <small class="error"><?= $this->get('validationErrors')['total_price'] ?></small> --> <!-- <?php if (isset($this->get('validationErrors')['total_price'])): ?> -->
<!-- <small class="error"><?= $this->get('validationErrors')['total_price'] ?></small> -->
<label for="mileage">Mileage</label> <!-- <?php endif; ?> -->
<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" value="<?= htmlspecialchars($_POST['note'] ?? '') ?>">
<!-- <small class="error"><?= $this->get('validationErrors')['note'] ?></small> --> <!-- <?php if (isset($this->get('validationErrors')['note'])): ?> -->
<!-- <small class="error"><?= $this->get('validationErrors')['note'] ?></small> -->
<!-- <?php endif; ?> -->
<input type="submit" id="btn-offline-submit" value="Create fuel record"> <input type="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");
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();
});
}); });