11 Commits

Author SHA1 Message Date
18c78e37a4 In progress: offline fuel record creation 2025-01-26 17:21:30 +01:00
ccbb0eac64 Added: Stats in dashboard
All checks were successful
Build and Deploy Zola Website / build_and_deploy (push) Successful in 12s
2025-01-05 21:24:55 +01:00
21c2f4598b Added: Create new refuel record
All checks were successful
Build and Deploy Zola Website / build_and_deploy (push) Successful in 13s
2025-01-05 19:05:30 +01:00
860a20d946 Added: Fuel record create - not complete yet
All checks were successful
Build and Deploy Zola Website / build_and_deploy (push) Successful in 12s
2025-01-03 17:10:08 +01:00
c5955010cb Edited: vehicle list styled
All checks were successful
Build and Deploy Zola Website / build_and_deploy (push) Successful in 13s
2025-01-03 16:16:15 +01:00
fc163431f8 Added: Gitea Workflow
All checks were successful
Build and Deploy Zola Website / build_and_deploy (push) Successful in 12s
2025-01-03 00:45:54 +01:00
15029970d6 Fix: wrong action url in vehicle create 2025-01-03 00:18:29 +01:00
e13edeccfc Edited: vehicle create route renamed from vehicle add 2025-01-02 02:02:14 +01:00
be6b465684 Added: vehicle creation 2024-12-31 15:53:15 +01:00
aded859a79 Added: tables for fuel-stats 2024-12-31 14:21:41 +01:00
c29bd7cbab Edited: App is rebranded to Fuel Stats 2024-12-31 11:34:39 +01:00
36 changed files with 843 additions and 350 deletions

View File

@ -0,0 +1,36 @@
name: Build and Deploy Zola Website
on:
push:
branches:
- master
env:
HOST: ${{ secrets.SERVER_IP }}
SSH_USERNAME: ${{ secrets.USERNAME }}
SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_KEY }}
DEST_FOLDER: "/srv/www/cz/filiprojek/fuelstats"
jobs:
build_and_deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Deploy
run: |
apt update -y && apt-get install -y --no-install-recommends rsync
eval "$(ssh-agent -s)"
ssh-add - <<< "${SSH_PRIVATE_KEY}"
mkdir -p ~/.ssh/
ssh-keyscan -H ${HOST} >> ~/.ssh/known_hosts
rsync -r --delete-after ./* "${SSH_USERNAME}@${HOST}:${{ env.DEST_FOLDER }}"
- name: Copy environment.php
run: |
eval "$(ssh-agent -s)"
ssh-add - <<< "${SSH_PRIVATE_KEY}"
mkdir -p ~/.ssh/
ssh-keyscan -H ${HOST} >> ~/.ssh/known_hosts
ssh ${SSH_USERNAME}@${HOST} "cp /var/websrvenv/environment.php /srv/www/cz/filiprojek/fuelstats/config/environment.php"

View File

@ -208,8 +208,8 @@ If you develop a new program, and you want it to be of the greatest possible use
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found.
habit-tracker
Copyright (C) 2024 fr
Fuel Stats
Copyright (C) 2024 Filip Rojek
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
@ -221,7 +221,7 @@ Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode:
habit-tracker Copyright (C) 2024 fr
Fuel Stats Copyright (C) 2024 Filip Rojek
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.

View File

@ -1,32 +1,32 @@
# Habit Tracker
# Fuel Stats
An app for tracking habits and motivation to achieve personal goals
An app for tracking your fuel consumption and optimizing your driving efficiency.
## Used technologies
## Used Technologies
- **Frontend:** HTML, CSS, JavaScript
- **Backend:** PHP (OOP)
- **Database:** MariaDB
## How to build
## How to Build
### Build using docker
Run the container using docker-compose
### Build Using Docker
Run the container using docker-compose:
```bash
docker-compose up
docker-compose --profile <dev|prod> up -d
```
The app should be available at http://localhost:8000
The app should be available at http://localhost:8000.
PhpMyAdmin should be available at http://localhost:8080
PhpMyAdmin should be available at http://localhost:8080.
### Build manually
1. Clone the repo
### Build Manually
1. Clone the repository:
```bash
git clone https://git.filiprojek.cz/fr/habit-tracker.git
git clone https://git.filiprojek.cz/fr/fuel-stats.git
```
2. Create `config/environment.php`
- It should have following structure:
2. Create `config/environment.php`:
- It should have the following structure:
```php
<?php
@ -35,21 +35,23 @@ define('DB_USER', 'your db username');
define('DB_PASS', 'your db password');
define('DB_NAME', 'your db name');
```
- For the database, you can use included `docker-compose.yaml` which have both MariaDB and PhpMyAdmin
- For the database, you can use the included `docker-compose.yaml` which includes both MariaDB and PhpMyAdmin.
3. Start an local web server
- You can use php's integrated server by running this:
3. Start a local web server:
- You can use PHP's integrated server by running this:
```bash
php -S localhost:8000 -t ./public
```
- You can use any host and any port you want.
- You can use any host and any port you prefer.
## Usage
1. Register and Login to the app.
2. Add your habits.
3. Mark your habits when you're done doing them.
4. Earn point and unlock achievements by completing you're habits!
## Licence
This project is licensed under GPL3.0 and later. More information is availabe in `LICENSE` file.
1. Register and log in to the app.
2. Add your vehicles with their details (fuel type, registration, etc.).
3. Record each refueling:
- Select your vehicle.
- Input the number of liters, price per liter, and total cost.
4. Track your fuel consumption and spending through the dashboard.
5. View detailed stats and graphs to analyze your driving habits.
## License
This project is licensed under GPL3.0 and later. More information is available in the `LICENSE` file.

17
TODO.md
View File

@ -6,13 +6,10 @@
- [ ] edit user data - change password, mail...
## Core of the app
- [ ] header and navbar
- [ ] dashboard
- [x] css
- [ ] its just plain
- [ ] graphs
- [x] Habits list
- [ ] css
- [ ] Habits create
- [ ] validate cron input
- [ ] Habits track
- [ ] intro tutorial when no car exist or just dont show anything
- [ ] change/set default car
- [ ] specific car view - charts, fuel records
- [ ] remove/edit fuel record
- [ ] IndexDB
- [ ]

View File

@ -1,12 +1,38 @@
<?php
class DashboardController extends Controller {
public function index() {
$habit = new Habit();
$habits = $habit->getHabitsByUser($_SESSION['user']['id']);
$vehicle = new Vehicle();
$vehicles = $vehicle->getVehiclesByUser($_SESSION['user']['id']);
$default_car = $vehicle->getDefaultVehicle($_SESSION['user']['id']);
$refuel = new Refuel();
$data = [
"date" => [],
"price" => [],
];
$raw_data = $refuel->latest_data($default_car['id'], 5);
foreach($raw_data as $one) {
array_push($data['date'], date('d. m.', strtotime($one['created_at'])));
array_push($data['price'], $one['price_per_liter']);
}
$latest_record = [
'name',
'liters',
'price_per_liter',
'total_price',
'created_at'
];
$latest_record = $refuel->latest_one($_SESSION['user']['id'])[0];
$this->view('dashboard/index', [
'title' => 'Dashboard',
'habits' => $habits,
'vehicles' => $vehicles,
'date_price_data' => $data,
'default_car' => $default_car,
'latest_record' => $latest_record,
]);
}

View File

@ -1,57 +0,0 @@
<?php
class HabitController extends Controller {
public function index() {
$habit = new Habit();
$habits = $habit->getHabitsByUser($_SESSION['user']['id']);
$this->view('habits/index', ['title' => 'Habits', 'habits' => $habits]);
}
public function create() {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$name = $_POST['name'] ?? '';
$frequency = $_POST['frequency'] ?? 'Daily';
$customFrequency = null;
if (empty($name)) {
$this->view('habits/create', ['error' => 'Habit name is required.']);
return;
}
if ($frequency === 'Custom') {
$daysOfWeek = $_POST['days_of_week'] ?? [];
$daysOfMonth = $_POST['days_of_month'] ?? '*';
$months = $_POST['months'] ?? '*';
// Combine into crontab-like string
$customFrequency = implode(',', $daysOfWeek) . " $daysOfMonth $months";
}
$habit = new Habit();
$result = $habit->create([
'name' => $name,
'frequency' => $frequency,
'custom_frequency' => $customFrequency,
'reward_points' => intval($_POST['difficulty'] ?? 1),
'user_id' => $_SESSION['user']['id'],
]);
if ($result) {
$this->redirect('/habits');
} else {
$this->view('habits/create', ['error' => 'Failed to create habit.']);
}
} else {
$this->view('habits/create', ['title' => 'Create Habit']);
}
}
public function edit() {
// Edit habit (to be implemented later)
}
public function delete() {
// Delete habit (to be implemented later)
}
}

View File

@ -0,0 +1,85 @@
<?php
class RefuelController extends Controller {
public function create() {
if($_SERVER['REQUEST_METHOD'] === 'GET'){
$vehicle = new Vehicle();
$vehicles = $vehicle->getVehiclesByUser($_SESSION['user']['id']);
$this->view('refuel/create', [
'title' => "New refuel record",
'vehicles' => $vehicles,
]);
return;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$vehicle_id = $_POST['vehicle'] ?? '';
$fuel_type = $_POST['fuel_type'] ?? '';
$liters = $_POST['liters'] ?? '';
$price_per_liter = $_POST['price_per_liter'] ?? '';
$total_price = $_POST['total_price'] ?? '';
$note = $_POST['note'] ?? '';
$validator = new Validator();
$validator->required('vehicle', $vehicle_id);
$validator->required('fuel_type', $fuel_type);
$validator->required('liters', $liters);
$validator->required('price_per_liter', $price_per_liter);
$validator->required('total_price', $total_price);
$validator->number('liters', $liters);
$validator->number('price_per_liter', $price_per_liter);
$validator->number('total_price', $total_price);
if (round($liters * $price_per_liter, 2) != $total_price) {
$validator->setErrors(["total_price" => "Price calculation is wrong"]);
}
if($note == "") $note = NULL;
if (!$validator->passes()) {
$vehicle = new Vehicle();
$vehicles = $vehicle->getVehiclesByUser($_SESSION['user']['id']);
$this->view('refuel/create', [
'error' => 'Please correct the errors below.',
'validationErrors' => $validator->errors() ?: [],
'vehicles' => $vehicles,
'title' => 'New refuel record',
]);
return;
}
$record = new Refuel();
$result = $record->create([
'user_id' => $_SESSION['user']['id'],
'vehicle_id' => $vehicle_id,
'fuel_type' => $fuel_type,
'note' => $note,
'liters' => $liters,
'price_per_liter' => $price_per_liter,
'total_price' => $total_price,
]);
if ($result === true) {
$this->redirect('/');
} else {
$vehicle = new Vehicle();
$vehicles = $vehicle->getVehiclesByUser($_SESSION['user']['id']);
$this->view('refuel/create', [
'title' => 'New refuel record',
'error' => $result,
'validationErrors' => [],
'vehicles' => $vehicles,
]);
}
return;
}
}
public function edit() {
// Edit refuel record (to be implemented later)
}
public function delete() {
// Delete refuel record (to be implemented later)
}
}

View File

@ -0,0 +1,61 @@
<?php
class VehicleController extends Controller {
public function index() {
$vehicle = new Vehicle();
$vehicles = $vehicle->getVehiclesByUser($_SESSION['user']['id']);
$this->view('vehicles/index', ['title' => 'Vehicles', 'vehicles' => $vehicles]);
}
public function create() {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$name = $_POST['name'] ?? '';
$registration_plate = $_POST['registration_plate'] ?? '';
$fuel_type = $_POST['fuel_type'] ?? '';
$note = $_POST['note'] ?? '';
$validator = new Validator();
$validator->required('name', $name);
$validator->required('registration_plate', $registration_plate);
$validator->required('fuel_type', $fuel_type);
if($note == "") $note = NULL;
if (!$validator->passes()) {
$this->view('vehicles/create', [
'error' => 'Please correct the errors below.',
'validationErrors' => $validator->errors() ?: [],
]);
return;
}
$vehicle = new Vehicle();
$result = $vehicle->create([
'name' => $name,
'registration_plate' => strtoupper($registration_plate),
'fuel_type' => $fuel_type,
'note' => $note,
'user_id' => $_SESSION['user']['id'],
]);
if ($result === true) {
$this->redirect('/vehicles');
} else {
$this->view('vehicles/create', ['title' => 'Create vehicle', 'error' => $result, 'validationErrors' => []] );
}
} else {
$this->view('vehicles/create', ['title' => 'Create Vehicle']);
}
}
public function edit() {
// Edit vehicle (to be implemented later)
}
public function delete() {
// Delete vehicle (to be implemented later)
}
}

View File

@ -1,46 +0,0 @@
<?php
class Habit {
private $db;
public function __construct() {
$this->db = Database::getInstance()->getConnection();
}
public function create($data) {
$stmt = $this->db->prepare("
INSERT INTO habits (user_id, title, frequency, custom_frequency, reward_points, created_at)
VALUES (?, ?, ?, ?, ?, NOW())
");
$stmt->bind_param(
"isssi", // Bind types: int, string, string, string, int
$data['user_id'],
$data['name'],
$data['frequency'],
$data['custom_frequency'], // Bind the custom_frequency field
$data['reward_points']
);
if ($stmt->execute()) {
return true;
} else {
error_log("Failed to create habit: " . $stmt->error);
return false;
}
}
public function getHabitsByUser($userId) {
$stmt = $this->db->prepare("SELECT id, title, frequency, custom_frequency, reward_points, created_at FROM habits WHERE user_id = ?");
$stmt->bind_param("i", $userId);
$stmt->execute();
$result = $stmt->get_result();
$habits = [];
while ($row = $result->fetch_assoc()) {
$habits[] = $row;
}
return $habits;
}
}

92
app/models/Refuel.php Normal file
View File

@ -0,0 +1,92 @@
<?php
class Refuel {
private $db;
public function __construct() {
$this->db = Database::getInstance()->getConnection();
}
public function create($data) {
try{
$stmt = $this->db->prepare("
INSERT INTO refueling_records (user_id, vehicle_id, fuel_type, note, liters, price_per_liter, total_price, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, NOW())
");
$stmt->bind_param(
"iissddd",
$data['user_id'],
$data['vehicle_id'],
$data['fuel_type'],
$data['note'],
$data['liters'],
$data['price_per_liter'],
$data['total_price'],
);
if ($stmt->execute()) {
return true;
} else {
return "Error: " . $stmt->error;
}
} catch(mysqli_sql_exception $e) {
return $e->getMessage();
}
}
public function latest_data($vehicle_id, $record_count) {
try {
$stmt = $this->db->prepare("
SELECT `liters`, `price_per_liter`, `total_price`, `created_at`
FROM `refueling_records`
WHERE `vehicle_id` = ?
ORDER BY created_at DESC
LIMIT ?;
");
$stmt->bind_param("ii", $vehicle_id, $record_count);
if ($stmt->execute()) {
$result = $stmt->get_result();
$data = $result->fetch_all(MYSQLI_ASSOC);
$stmt->close();
return array_reverse($data);
} else {
return "Error: " . $stmt->error;
}
} catch (mysqli_sql_exception $e) {
return $e->getMessage();
}
}
public function latest_one($user_id, $record_count = 1) {
try {
$stmt = $this->db->prepare("
SELECT
`r`.`vehicle_id`,
`v`.`name` AS `vehicle_name`,
`r`.`liters`,
`r`.`price_per_liter`,
`r`.`total_price`,
`r`.`created_at`
FROM `refueling_records` AS `r`
JOIN `vehicles` AS `v` ON `r`.`vehicle_id` = `v`.`id`
WHERE `r`.`user_id` = ?
ORDER BY `r`.`created_at` DESC
LIMIT ?;
");
$stmt->bind_param("ii", $user_id, $record_count);
if ($stmt->execute()) {
$result = $stmt->get_result();
$data = $result->fetch_all(MYSQLI_ASSOC);
$stmt->close();
return array_reverse($data);
} else {
return "Error: " . $stmt->error;
}
} catch (mysqli_sql_exception $e) {
return $e->getMessage();
}
}
}

63
app/models/Vehicle.php Normal file
View File

@ -0,0 +1,63 @@
<?php
class Vehicle {
private $db;
public function __construct() {
$this->db = Database::getInstance()->getConnection();
}
public function create($data) {
try{
$stmt = $this->db->prepare("
INSERT INTO vehicles (user_id, name, registration_plate, fuel_type, note, created_at)
VALUES (?, ?, ?, ?, ?, NOW())
");
$stmt->bind_param(
"issss",
$data['user_id'],
$data['name'],
$data['registration_plate'],
$data['fuel_type'],
$data['note'],
);
if ($stmt->execute()) {
return true;
} else {
return "Error: " . $stmt->error;
}
} catch(mysqli_sql_exception $e) {
return $e->getMessage();
}
}
public function getVehiclesByUser($user_id) {
$stmt = $this->db->prepare("SELECT id, name, registration_plate, fuel_type, note, created_at FROM vehicles WHERE user_id = ?");
$stmt->bind_param("i", $user_id);
$stmt->execute();
$result = $stmt->get_result();
$vehicles = [];
while ($row = $result->fetch_assoc()) {
$vehicles[] = $row;
}
return $vehicles;
}
public function getDefaultVehicle($user_id) {
$stmt = $this->db->prepare("
SELECT id, name, registration_plate, fuel_type, note, is_default
FROM vehicles
WHERE user_id = ? AND is_default = TRUE
LIMIT 1
");
$stmt->bind_param("i", $user_id);
$stmt->execute();
$result = $stmt->get_result();
return $result->fetch_assoc();
}
}

View File

@ -2,8 +2,8 @@
<link rel="stylesheet" href="/css/form.css">
<section class="form signin">
<div class="header-form">
<img src="/img/logo.jpg" alt="Habit Tracker Logo">
<h1>Sign in to Habit Tracker</h1>
<img src="/img/logo.jpg" alt="Fuel Stats Logo">
<h1>Sign in to Fuel Stats</h1>
</div>
<?php if ($this->get('error')): ?>
@ -27,7 +27,7 @@
</form>
<div class="bordered">
<p>New to Habit Tracker?</p>
<p>New to Fuel Stats?</p>
<a href="/auth/signup">Create an account</a>
</div>
</section>

View File

@ -2,8 +2,8 @@
<link rel="stylesheet" href="/css/form.css">
<section class="form signup">
<div class="header-form">
<img src="/img/logo.jpg" alt="Habit Tracker Logo">
<h1>Sign up to Habit Tracker</h1>
<img src="/img/logo.jpg" alt="Fuel Stats Logo">
<h1>Sign up to Fuel Stats</h1>
</div>
<?php if ($this->get('error')): ?>

View File

@ -3,45 +3,62 @@
<section class="dashboard">
<h1>Welcome, <?= htmlspecialchars($_SESSION['user']['username']) ?>!</h1>
<div>
<a href="/habits/create" class="btn-green">Create new habit!</a>
<a href="/habits" class="btn-primary">List all habits</a>
<a href="/refuel/create" class="btn-green">Add new refuel record!</a>
<a href="/vehicles" class="btn-primary">List all vehicles</a>
<button class="btn-primary" id="btn-offline-add">Add new refuel record OFFLINE</button>
</div>
<div class="card-wrapper">
<section class="card upcoming">
<h2>Upcoming</h2>
<div class="habit">
<b>Habit Title</b>
<p>Frequency</p>
<p>Reward points</p>
<section class="card latest">
<h2>Latest fuel record</h2>
<hr>
<div>
<b>Car:</b>
<p><?= $data['latest_record']['vehicle_name'] ?></p>
<b>Liters:</b>
<p><?= $data['latest_record']['liters'] ?> liters</p>
<b>Price per liter:</b>
<p><?= $data['latest_record']['price_per_liter'] ?>,-/liter</p>
<b>Total price:</b>
<p><?= $data['latest_record']['total_price'] ?>,-</p>
</div>
</section>
<section class="card recent">
<h2>Recent</h2>
<div class="habit">
<b>Habit Title</b>
<p>Frequency</p>
<p>Reward points</p>
</div>
</section>
<section class="card missed">
<h2>Missed</h2>
<div class="habit">
<b>Habit Title</b>
<p>Frequency</p>
<p>Reward points</p>
</div>
<section class="card">
<h2>Default car</h2>
<hr>
<b>Car</b>
<p><?= $data['default_car']['name'] ?></p>
<b>Registration plate</b>
<p><?= $data['default_car']['registration_plate'] ?></p>
<b>Fuel type</b>
<p><?= $data['default_car']['fuel_type'] ?></p>
<b>Note</b>
<p><?= $data['default_car']['note'] ?></p>
</section>
<section class="card history-graph">
<h2>Graph of History</h2>
</section>
<section class="card streak">
<h2>Streak</h2>
<p>You're current streak is 123 days</p>
<p>Good job!</p>
<h2>Chart of Gas price</h2>
<hr>
<p><?= $data['default_car']['name'] . " | " . $data['default_car']['registration_plate']?></p>
<canvas id="chart-gas-price"></canvas>
</section>
</div>
</section>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
const ctx = document.getElementById('chart-gas-price');
const data = <?= json_encode($data['date_price_data']); ?>;
new Chart(ctx, {
type: 'line',
data: {
labels: [...data['date']],
datasets: [{
label: 'Gas price history',
data: data['price'],
borderWidth: 1,
}]
},
});
</script>
<script defer src="/js/offline-records.js"></script>

View File

@ -4,7 +4,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Habit Tracker | Error 494</title>
<title>Fuel Stats | Error 494</title>
</head>
<body>
<h1>Error 404 - Page not found</h1>

View File

@ -1,70 +0,0 @@
<link rel="stylesheet" href="/css/form.css">
<link rel="stylesheet" href="/css/habits_create.css">
<section class="form habit-create">
<h1 class="header-form"><?= $this->get('title', 'Create Habit') ?></h1>
<?php if ($this->get('error')): ?>
<div class="error" style="color: red; margin-bottom: 1rem;">
<?= htmlspecialchars($this->get('error')) ?>
</div>
<?php endif; ?>
<form method="POST" action="/habits/create">
<label for="name">Habit Name</label>
<input type="text" name="name" id="name" required value="<?= htmlspecialchars($_POST['name'] ?? '') ?>">
<label for="frequency">Frequency</label>
<select name="frequency" id="frequency" onchange="toggleCustomFrequency(this.value)">
<option value="Daily">Daily</option>
<option value="Weekly">Weekly</option>
<option value="Custom">Custom</option>
</select>
<div id="custom-frequency" style="display: none;">
<label id="lbl_dow">Days of the Week</label>
<div class="dow_chb_wrapper">
<label for="dow_mon">Monday</label>
<input type="checkbox" name="days_of_week[]" id="dow_mon" value="1">
</div>
<div class="dow_chb_wrapper">
<label for="dow_tue">Tuesday</label>
<input type="checkbox" name="days_of_week[]" id="dow_tue" value="2">
</div>
<div class="dow_chb_wrapper">
<label for="dow_wed">Wednesday</label>
<input type="checkbox" name="days_of_week[]" id="dow_wed" value="3">
</div>
<div class="dow_chb_wrapper">
<label for="dow_thu">Thursday</label>
<input type="checkbox" name="days_of_week[]" id="dow_thu" value="4">
</div>
<div class="dow_chb_wrapper">
<label for="dow_fri">Friday</label>
<input type="checkbox" name="days_of_week[]" id="dow_fri" value="5">
</div>
<div class="dow_chb_wrapper">
<label for="dow_sat">Saturday</label>
<input type="checkbox" name="days_of_week[]" id="dow_sat" value="6">
</div>
<div class="dow_chb_wrapper">
<label for="dow_sun">Sunday</label>
<input type="checkbox" name="days_of_week[]" id="dow_sun" value="7">
</div>
<label for="days_of_month" id="lbl_dom">Days of the Month</label>
<input type="text" name="days_of_month" id="days_of_month" placeholder="1,15 (comma-separated)">
<label for="months">Months</label>
<input type="text" name="months" id="months" placeholder="1,7,12 (comma-separated)">
</div>
<input type="submit" value="Create Habit">
</form>
</section>
<script>
function toggleCustomFrequency(value) {
const customFrequencyDiv = document.getElementById('custom-frequency');
customFrequencyDiv.style.display = value === 'Custom' ? 'flex' : 'none';
}
</script>

View File

@ -1,24 +0,0 @@
<link rel="stylesheet" href="/css/habits_dashboard.css">
<section class="habits">
<?php if (empty($this->get('habits'))): ?>
<p>No habits yet. <a href="/habits/create">Create your first habit</a>.</p>
<?php else: ?>
<div class="habits-wrapper">
<?php foreach ($this->get('habits') as $habit): ?>
<div class="habit bordered">
<b><?= htmlspecialchars($habit['title']) ?></b>
<p>Frequency: <?= htmlspecialchars($habit['frequency']) ?></p>
<?php if (isset($habit['custom_frequency'])): ?>
<p><?= htmlspecialchars($habit['custom_frequency'] ?? 'N/A') ?></p>
<?php endif; ?>
<p><?= htmlspecialchars($habit['reward_points']) ?></p>
<p><?= htmlspecialchars($habit['created_at']) ?></p>
<a href="/habits/done">Mark as done</a> |
<a href="/habits/edit?id=<?= $habit['id'] ?>">Edit</a> |
<a href="/habits/delete?id=<?= $habit['id'] ?>" onclick="return confirm('Are you sure you want to delete this habit?')">Delete</a>
</div>
<?php endforeach; ?>
</div>
<a href="/habits/create" class="btn-green">Create new habit!</a>
<?php endif; ?>
</section>

View File

@ -1,4 +1,4 @@
<div>
<h1>Welcome to Habit Tracker!</h1>
<p>Track your habits and achieve your goals.</p>
<h1>Welcome to Fuel Stats!</h1>
<p>Keep track of your refuels.</p>
</div>

View File

@ -6,7 +6,7 @@
<meta name="author" content="Filip Rojek | http://filiprojek.cz">
<meta name="email" content="webmaster(@)fofrweb.com">
<meta name="copyright" content="(c) filiprojek.cz">
<title>Habit Tracker | <?= $data['title'] ?></title>
<title>Fuel Stats<?= isset($data['title']) ? " | " . $data['title'] : "" ?></title>
<link rel="stylesheet" href="/css/main.css">
<link rel="stylesheet" href="/css/global.css">
<link rel="stylesheet" href="/css/vars.css">
@ -17,7 +17,7 @@
<header>
<div id="hd-left">
<a href="/"><img src="/img/logo.jpg" alt="home"></a>
<label><?= $data['title'] ?></label>
<label><?= isset($data['title']) ? $data['title'] : "" ?></label>
</div>
<div id="hd-right">
<?php if (!isset($_SESSION['user'])): ?>

View File

@ -6,7 +6,7 @@
<meta name="author" content="Filip Rojek | http://filiprojek.cz">
<meta name="email" content="webmaster(@)fofrweb.com">
<meta name="copyright" content="(c) filiprojek.cz">
<title>Habit Tracker | <?= $data['title'] ?></title>
<title>Fuel Stats<?= isset($data['title']) ? " | " . $data['title'] : "" ?></title>
<link rel="stylesheet" href="/css/main.css">
<link rel="stylesheet" href="/css/global.css">
<link rel="stylesheet" href="/css/vars.css">

119
app/views/refuel/create.php Normal file
View File

@ -0,0 +1,119 @@
<link rel="stylesheet" href="/css/form.css">
<link rel="stylesheet" href="/css/vehicle_create.css">
<section class="form">
<h1 class="header-form"><?= $this->get('title') ?></h1>
<?php if ($this->get('error')): ?>
<div class="error" style="color: red; margin-bottom: 1rem;">
<?= htmlspecialchars($this->get('error')) ?>
</div>
<?php endif; ?>
<form method="POST" action="/refuel/create">
<label for="vehicle">Vehicle</label>
<select name="vehicle" id="vehicle">
<?php foreach ($this->get('vehicles') as $vehicle): ?>
<option value="<?= $vehicle['id'] ?>"><?= $vehicle['name'] . " | " . $vehicle['registration_plate'] ?></option>
<?php endforeach; ?>
</select>
<?php if (isset($this->get('validationErrors')['vehicle'])): ?>
<small class="error"><?= $this->get('validationErrors')['vehicle'] ?></small>
<?php endif; ?>
<label for="fuel_type">Fuel type</label>
<select name="fuel_type" id="fuel_type">
<option value="Diesel">Diesel</option>
<option value="Gasoline 95">Gasoline 95</option>
<option value="Gasoline 98">Gasoline 98</option>
<option value="Premium Diesel">Premium Diesel</option>
<option value="Premium Gasoline 95">Premium Gasoline 95</option>
<option value="Premium Gasoline 98">Premium Gasoline 98</option>
<option value="Other">Other</option>
</select>
<?php if (isset($this->get('validationErrors')['fuel_type'])): ?>
<small class="error"><?= $this->get('validationErrors')['fuel_type'] ?></small>
<?php endif; ?>
<label for="liters">Liters</label>
<input type="number" name="liters" id="liters" min="0" step=".01" value="<?= htmlspecialchars($_POST['liters'] ?? '0.0') ?>">
<?php if (isset($this->get('validationErrors')['liters'])): ?>
<small class="error"><?= $this->get('validationErrors')['liters'] ?></small>
<?php endif; ?>
<label for="price_per_liter">Price per liter</label>
<input type="number" name="price_per_liter" id="price_per_liter" min="0" step=".01" value="<?= htmlspecialchars($_POST['price_per_liter'] ?? '0.0') ?>">
<?php if (isset($this->get('validationErrors')['price_per_liter'])): ?>
<small class="error"><?= $this->get('validationErrors')['price_per_liter'] ?></small>
<?php endif; ?>
<label for="total_price">Total price</label>
<input type="number" name="total_price" id="total_price" min="0" step=".01" value="<?= htmlspecialchars($_POST['total_price'] ?? '0.0') ?>">
<?php if (isset($this->get('validationErrors')['total_price'])): ?>
<small class="error"><?= $this->get('validationErrors')['total_price'] ?></small>
<?php endif; ?>
<label for="note">Note</label>
<input type="text" name="note" id="note" value="<?= htmlspecialchars($_POST['note'] ?? '') ?>">
<?php if (isset($this->get('validationErrors')['note'])): ?>
<small class="error"><?= $this->get('validationErrors')['note'] ?></small>
<?php endif; ?>
<input type="submit" value="Create fuel record">
</form>
</section>
<script>
const inp_lit = document.querySelector("input#liters")
const inp_ppl = document.querySelector("input#price_per_liter")
const inp_tot = document.querySelector("input#total_price")
const rnd = (num) => Math.round((num + Number.EPSILON) * 100) / 100
function calculate(){
let liters = Number(inp_lit.value)
let price_per_liter = Number(inp_ppl.value)
let total_price = Number(inp_tot.value)
if(price_per_liter > 0 && liters > 0) {
total_price = rnd(liters * price_per_liter)
}
if(price_per_liter > 0 && total_price > 0) {
liters = rnd(total_price / price_per_liter)
}
if(liters > 0 && total_price > 0) {
price_per_liter = rnd(total_price / liters)
}
inp_lit.value = liters
inp_ppl.value = price_per_liter
inp_tot.value = total_price
}
[inp_lit, inp_ppl, inp_tot].forEach(inp => {
inp.addEventListener("change", () => {
calculate()
})
})
const vehicles = <?= json_encode($data['vehicles']); ?>;
const fuel_sel = document.querySelector("#fuel_type")
const vehic_sel = document.querySelector("#vehicle")
function selectFuel() {
const veh_id = vehic_sel.value
vehicles.forEach(el => {
if(el.id == veh_id) {
fuel_sel.value = el.fuel_type
return
}
})
}
selectFuel()
vehic_sel.addEventListener("change", () => {
selectFuel()
})
</script>

View File

@ -0,0 +1,35 @@
<link rel="stylesheet" href="/css/form.css">
<link rel="stylesheet" href="/css/vehicle_create.css">
<section class="form">
<h1 class="header-form"><?= $this->get('title', 'Create Vehicle') ?></h1>
<?php if ($this->get('error')): ?>
<div class="error" style="color: red; margin-bottom: 1rem;">
<?= htmlspecialchars($this->get('error')) ?>
</div>
<?php endif; ?>
<form method="POST" action="/vehicles/create">
<label for="name">Vehicle name</label>
<input type="text" name="name" id="name" required value="<?= htmlspecialchars($_POST['name'] ?? '') ?>">
<label for="registration_plate">Registration plate</label>
<input type="text" name="registration_plate" id="registration_plate" maxlength="10" onkeypress="return event.charCode != 32" required value="<?= htmlspecialchars($_POST['registration_plate'] ?? '') ?>">
<label for="fuel_type">Fuel type</label>
<select name="fuel_type" id="fuel_type">
<option value="Diesel">Diesel</option>
<option value="Gasoline 95">Gasoline 95</option>
<option value="Gasoline 98">Gasoline 98</option>
<option value="Premium Diesel">Premium Diesel</option>
<option value="Premium Gasoline 95">Premium Gasoline 95</option>
<option value="Premium Gasoline 98">Premium Gasoline 98</option>
<option value="Other">Other</option>
</select>
<label for="note">Note</label>
<input type="text" name="note" id="note" value="<?= htmlspecialchars($_POST['note'] ?? '') ?>">
<input type="submit" value="Create vehicle">
</form>
</section>

View File

@ -0,0 +1,24 @@
<link rel="stylesheet" href="/css/vehicles.css">
<section class="vehicles">
<?php if (empty($this->get('vehicles'))): ?>
<p>No vehicles yet. <a href="/vehicles/create">Add your first vehicle</a>.</p>
<?php else: ?>
<div class="btn-wrapper">
<a href="/vehicles/create" class="btn-green">Add new vehicle!</a>
</div>
<div class="vehicle-wrapper">
<?php foreach ($this->get('vehicles') as $vehicle): ?>
<div class="vehicle bordered">
<b><?= htmlspecialchars($vehicle['name']) ?></b>
<p><?= htmlspecialchars($vehicle['registration_plate']) ?></p>
<p><?= htmlspecialchars($vehicle['fuel_type']) ?></p>
<p><?= htmlspecialchars($vehicle['note'] ?? "") ?></p>
<div class="actions">
<a href="/vehicles/edit?id=<?= $vehicle['id'] ?>">Edit</a>
<a href="/vehicles/delete?id=<?= $vehicle['id'] ?>" onclick="return confirm('Are you sure you want to delete this habit?')">Delete</a>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</section>

View File

@ -1,6 +1,6 @@
<?php
define('DB_HOST', '0.0.0.0');
define('DB_NAME', 'habit_tracker');
define('DB_NAME', 'fuel_stats');
define('DB_USER', 'username');
define('DB_PASS', 'password');

View File

@ -57,7 +57,7 @@ class Database {
* Check and create required tables if they don't exist
*/
private function checkAndCreateTables() {
// Create users table
// Create tables
$usersTableQuery = "CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL,
@ -71,36 +71,56 @@ class Database {
die("Failed to create users table: " . $this->connection->error);
}
// Create progress table
$progressTableQuery = "CREATE TABLE IF NOT EXISTS progress (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
habit_id INT NOT NULL,
date DATE NOT NULL,
status ENUM('Done', 'Pending') DEFAULT 'Pending',
FOREIGN KEY (user_id) REFERENCES users(id)
) ENGINE=InnoDB;";
$vehiclesTableQuery = "
CREATE TABLE IF NOT EXISTS vehicles (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
name VARCHAR(100) NOT NULL,
registration_plate VARCHAR(50) NOT NULL UNIQUE,
fuel_type ENUM('Diesel', 'Gasoline 95', 'Gasoline 98', 'Premium Diesel', 'Premium Gasoline 95', 'Premium Gasoline 98', 'Other') NOT NULL,
note VARCHAR(150) NULL,
is_default BOOLEAN DEFAULT FALSE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB;
";
if (!$this->connection->query($progressTableQuery)) {
die("Failed to create progress table: " . $this->connection->error);
if (!$this->connection->query($vehiclesTableQuery)) {
die("Failed to create vehicles table: " . $this->connection->error);
}
// Create habits table
$habitsTableQuery = "CREATE TABLE IF NOT EXISTS habits (
$refuelingTableQuery = "
CREATE TABLE IF NOT EXISTS refueling_records (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
vehicle_id INT NOT NULL,
fuel_type ENUM('Diesel', 'Gasoline 95', 'Gasoline 98', 'Premium Diesel', 'Premium Gasoline 95', 'Premium Gasoline 98', 'Other') NOT NULL,
note VARCHAR(150) NULL,
liters DOUBLE(10, 2) NOT NULL,
price_per_liter DOUBLE(10, 2) NOT NULL,
total_price DOUBLE(10, 2) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (vehicle_id) REFERENCES vehicles(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB;
";
if (!$this->connection->query($refuelingTableQuery)) {
die("Failed to create refueling_records table: " . $this->connection->error);
}
/*
// Create table_name table
$usersTableQuery = "CREATE TABLE IF NOT EXISTS table_name (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
title VARCHAR(100) NOT NULL,
frequency ENUM('Daily', 'Weekly', 'Custom') NOT NULL,
custom_frequency VARCHAR(255) DEFAULT NULL, -- Store crontab-like string
reward_points INT DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
username VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;";
if (!$this->connection->query($habitsTableQuery)) {
die("Failed to create habits table: " . $this->connection->error);
if (!$this->connection->query($usersTableQuery)) {
die("Failed to create table_name table: " . $this->connection->error);
}
*/
}
/**

View File

@ -12,6 +12,15 @@ class Validator {
}
}
/**
* Check if a field contains numbers
*/
public function number($field, $value, $message = null) {
if(!is_numeric($value)) {
$this->errors[$field] = $message ?? "$field must be an number";
}
}
/**
* Check if a field meets minimum length
*/
@ -46,6 +55,10 @@ class Validator {
return $this->errors;
}
public function setErrors($errors) {
$this->errors = $errors;
}
/**
* Check if validation passed
*/

View File

@ -17,7 +17,7 @@ services:
- PMA_ARBITRARY=1
profiles: ["dev"]
habittracker:
fuelstats:
build:
context: .
dockerfile: Dockerfile

View File

@ -16,6 +16,6 @@
background-color: var(--clr-secondary);
border-radius: var(--border-radious);
border: var(--borderWidth-thin) solid var(--clr-border);
min-width: 17rem;
width: 18rem;
padding: 1rem;
}

View File

@ -27,6 +27,7 @@ h1 {
cursor: pointer;
border-radius: var(--border-radious);
border: var(--borderWidth-thin) solid var(--clr-border);
color: white;
}
.btn-secondary {

View File

@ -1,21 +0,0 @@
.form form .dow_chb_wrapper input[type="checkbox"] {
width: 1rem;
}
.form form .dow_chb_wrapper {
display: flex;
justify-content: space-between;
}
#lbl_dow {
margin-bottom: .5rem;
}
#lbl_dom {
margin-top: .5rem;
}
#custom-frequency {
flex-direction: column;
justify-content: space-between;
}

View File

@ -1,15 +0,0 @@
.habits-wrapper {
display: flex;
gap: 1rem;
flex-wrap: wrap;
justify-content: center;
}
.habits .bordered {
border-radius: var(--border-radious);
border: var(--borderWidth-thin) solid var(--clr-border);
width: 17rem;
padding: 1rem;
margin-top: 1rem;
text-align: center;
}

View File

@ -0,0 +1,3 @@
#registration_plate {
text-transform: uppercase;
}

29
public/css/vehicles.css Normal file
View File

@ -0,0 +1,29 @@
.vehicle-wrapper {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 1rem;
justify-content: center;
}
.vehicle {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: var(--clr-secondary);
border-radius: var(--border-radious);
border: var(--borderWidth-thin) solid var(--clr-border);
width: 17rem;
padding: 1rem;
margin-top: 1rem;
text-align: center;
min-height: 8rem;
}
.btn-wrapper {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 1rem;
}

View File

@ -20,7 +20,8 @@ require_once '../core/Database.php';
require_once '../core/middlewares/RequireAuth.php';
require_once models . 'User.php';
require_once models . 'Habit.php';
require_once models . 'Vehicle.php';
require_once models . 'Refuel.php';
// Initialize router
$router = new Router();
@ -40,12 +41,16 @@ $router->group('/auth', [], function ($router) {
// dashboard route
$router->add('/dashboard', 'DashboardController@index', ['RequireAuth']);
// habits routes
$router->group('/habits', ['RequireAuth'], function ($router) {
$router->add('', 'HabitController@index');
$router->add('/create', 'HabitController@create');
$router->add('/edit/{id}', 'HabitController@edit');
$router->add('/delete/{id}', 'HabitController@delete');
// vehicle routes
$router->group('/vehicles', ['RequireAuth'], function ($router) {
$router->add('', 'VehicleController@index');
$router->add('/create', 'VehicleController@create');
$router->add('/edit/{id}', 'VehicleController@edit');
$router->add('/delete/{id}', 'VehicleController@delete');
});
$router->group('/refuel', ['RequireAuth'], function ($router) {
$router->add('/create', 'RefuelController@create');
});
$router->dispatch();

View File

@ -0,0 +1,98 @@
async function checkOnline() {
if (!navigator.onLine) {
console.log("Offline (no network connection)");
return false;
}
try {
const response = await fetch(
"https://www.google.com/favicon.ico?" + new Date().getTime(),
{
mode: "no-cors",
},
);
return true;
} catch (error) {
console.log("Connected to network but no internet access");
return false;
}
}
setInterval(async () => {
const isOnline = await checkOnline();
if (!isOnline) {
console.log("OFFLINE!!!");
}
}, 5000);
const offbtn = document.querySelector("#btn-offline-add");
offbtn.addEventListener("click", (e) => {
e.preventDefault();
document.querySelector("section.dashboard").style.display = "none";
const offline = document.createElement("div");
offline.classList.add("offline");
offline.innerHTML = `
<b>You're Offline</b>
<p>You can create an fuel record locally on your device and sync it later</p>
<section class="form">
<h1 class="header-form"><?= $this->get('title') ?></h1>
<!-- <?php if ($this->get('error')): ?> -->
<!-- <div class="error" style="color: red; margin-bottom: 1rem;"> -->
<!-- <?= htmlspecialchars($this->get('error')) ?> -->
<!-- </div> -->
<!-- <?php endif; ?> -->
<form method="POST" action="/refuel/create">
<label for="vehicle">Vehicle</label>
<select name="vehicle" id="vehicle">
<!-- <?php foreach ($this->get('vehicles') as $vehicle): ?> -->
<!-- <option value="<?= $vehicle['id'] ?>"><?= $vehicle['name'] . " | " . $vehicle['registration_plate'] ?></option> -->
<!-- <?php endforeach; ?> -->
</select>
<!-- <?php if (isset($this->get('validationErrors')['vehicle'])): ?> -->
<!-- <small class="error"><?= $this->get('validationErrors')['vehicle'] ?></small> -->
<!-- <?php endif; ?> -->
<label for="fuel_type">Fuel type</label>
<select name="fuel_type" id="fuel_type">
<option value="Diesel">Diesel</option>
<option value="Gasoline 95">Gasoline 95</option>
<option value="Gasoline 98">Gasoline 98</option>
<option value="Premium Diesel">Premium Diesel</option>
<option value="Premium Gasoline 95">Premium Gasoline 95</option>
<option value="Premium Gasoline 98">Premium Gasoline 98</option>
<option value="Other">Other</option>
</select>
<!-- <?php if (isset($this->get('validationErrors')['fuel_type'])): ?> -->
<!-- <small class="error"><?= $this->get('validationErrors')['fuel_type'] ?></small> -->
<!-- <?php endif; ?> -->
<label for="liters">Liters</label>
<input type="number" name="liters" id="liters" min="0" step=".01" value="<?= htmlspecialchars($_POST['liters'] ?? '0.0') ?>">
<!-- <?php if (isset($this->get('validationErrors')['liters'])): ?> -->
<!-- <small class="error"><?= $this->get('validationErrors')['liters'] ?></small> -->
<!-- <?php endif; ?> -->
<label for="price_per_liter">Price per liter</label>
<input type="number" name="price_per_liter" id="price_per_liter" min="0" step=".01" value="<?= htmlspecialchars($_POST['price_per_liter'] ?? '0.0') ?>">
<!-- <?php if (isset($this->get('validationErrors')['price_per_liter'])): ?> -->
<!-- <small class="error"><?= $this->get('validationErrors')['price_per_liter'] ?></small> -->
<!-- <?php endif; ?> -->
<label for="total_price">Total price</label>
<input type="number" name="total_price" id="total_price" min="0" step=".01" value="<?= htmlspecialchars($_POST['total_price'] ?? '0.0') ?>">
<!-- <?php if (isset($this->get('validationErrors')['total_price'])): ?> -->
<!-- <small class="error"><?= $this->get('validationErrors')['total_price'] ?></small> -->
<!-- <?php endif; ?> -->
<label for="note">Note</label>
<input type="text" name="note" id="note" value="<?= htmlspecialchars($_POST['note'] ?? '') ?>">
<!-- <?php if (isset($this->get('validationErrors')['note'])): ?> -->
<!-- <small class="error"><?= $this->get('validationErrors')['note'] ?></small> -->
<!-- <?php endif; ?> -->
<input type="submit" value="Create fuel record">
</form>
</section>
`;
document.querySelector("main").appendChild(offline);
});