Compare commits
	
		
			20 Commits
		
	
	
		
			last_habit
			...
			fr/intro
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 7517bcb78f | |||
| f90c707435 | |||
| 60423d37ce | |||
| 5989fba225 | |||
| ba11c41147 | |||
| 64c7fd15a1 | |||
| ea3afa2507 | |||
| a5f99788fc | |||
| 2201430f59 | |||
| 18c78e37a4 | |||
| ccbb0eac64 | |||
| 21c2f4598b | |||
| 860a20d946 | |||
| c5955010cb | |||
| fc163431f8 | |||
| 15029970d6 | |||
| e13edeccfc | |||
| be6b465684 | |||
| aded859a79 | |||
| c29bd7cbab | 
							
								
								
									
										36
									
								
								.gitea/workflows/deploy.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								.gitea/workflows/deploy.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| name: Build and Deploy Zola Website | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: | ||||
|       - master | ||||
| env: | ||||
|   HOST: ${{ secrets.SERVER_IP }} | ||||
|   SSH_USERNAME: ${{ secrets.USERNAME }} | ||||
|   SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_KEY }} | ||||
|   DEST_FOLDER: "/srv/www/cz/filiprojek/fuelstats" | ||||
|  | ||||
| jobs: | ||||
|   build_and_deploy: | ||||
|     runs-on: ubuntu-latest | ||||
|  | ||||
|     steps: | ||||
|       - name: Checkout Repository | ||||
|         uses: actions/checkout@v4 | ||||
|  | ||||
|       - name: Deploy | ||||
|         run: | | ||||
|           apt update -y && apt-get install -y --no-install-recommends rsync | ||||
|           eval "$(ssh-agent -s)" | ||||
|           ssh-add - <<< "${SSH_PRIVATE_KEY}" | ||||
|           mkdir -p ~/.ssh/ | ||||
|           ssh-keyscan -H ${HOST} >> ~/.ssh/known_hosts | ||||
|           rsync -r --delete-after ./* "${SSH_USERNAME}@${HOST}:${{ env.DEST_FOLDER }}" | ||||
|  | ||||
|       - name: Copy environment.php | ||||
|         run: | | ||||
|           eval "$(ssh-agent -s)" | ||||
|           ssh-add - <<< "${SSH_PRIVATE_KEY}" | ||||
|           mkdir -p ~/.ssh/ | ||||
|           ssh-keyscan -H ${HOST} >> ~/.ssh/known_hosts | ||||
|           ssh ${SSH_USERNAME}@${HOST} "cp /var/websrvenv/environment.php /srv/www/cz/filiprojek/fuelstats/config/environment.php" | ||||
							
								
								
									
										
											BIN
										
									
								
								.screenshots/class.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								.screenshots/class.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 22 KiB | 
							
								
								
									
										
											BIN
										
									
								
								.screenshots/dlm.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								.screenshots/dlm.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 121 KiB | 
							
								
								
									
										
											BIN
										
									
								
								.screenshots/usecase.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								.screenshots/usecase.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 72 KiB | 
							
								
								
									
										6
									
								
								LICENSE
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								LICENSE
									
									
									
									
									
								
							| @@ -208,8 +208,8 @@ If you develop a new program, and you want it to be of the greatest possible use | ||||
|  | ||||
| To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found. | ||||
|  | ||||
|      habit-tracker | ||||
|      Copyright (C) 2024  fr | ||||
|      Fuel Stats | ||||
|      Copyright (C) 2024  Filip Rojek | ||||
|  | ||||
|      This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. | ||||
|  | ||||
| @@ -221,7 +221,7 @@ Also add information on how to contact you by electronic and paper mail. | ||||
|  | ||||
| If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: | ||||
|  | ||||
|      habit-tracker  Copyright (C) 2024  fr | ||||
|      Fuel Stats  Copyright (C) 2024  Filip Rojek | ||||
|      This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. | ||||
|      This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. | ||||
|  | ||||
|   | ||||
							
								
								
									
										58
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										58
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,32 +1,32 @@ | ||||
| # Habit Tracker | ||||
| # Fuel Stats | ||||
|  | ||||
| An app for tracking habits and motivation to achieve personal goals | ||||
| An app for tracking your fuel consumption and optimizing your driving efficiency. | ||||
|  | ||||
| ## Used technologies | ||||
| ## Used Technologies | ||||
| - **Frontend:** HTML, CSS, JavaScript | ||||
| - **Backend:** PHP (OOP) | ||||
| - **Database:** MariaDB | ||||
|  | ||||
| ## How to build | ||||
| ## How to Build | ||||
|  | ||||
| ### Build using docker | ||||
| Run the container using docker-compose | ||||
| ### Build Using Docker | ||||
| Run the container using docker-compose: | ||||
| ```bash | ||||
| docker-compose up | ||||
| docker-compose --profile <dev|prod> up -d | ||||
| ``` | ||||
|  | ||||
| The app should be available at http://localhost:8000 | ||||
| The app should be available at http://localhost:8000. | ||||
|  | ||||
| PhpMyAdmin should be available at http://localhost:8080 | ||||
| PhpMyAdmin should be available at http://localhost:8080. | ||||
|  | ||||
| ### Build manually | ||||
| 1. Clone the repo | ||||
| ### Build Manually | ||||
| 1. Clone the repository: | ||||
| ```bash | ||||
|    git clone https://git.filiprojek.cz/fr/habit-tracker.git | ||||
|    git clone https://git.filiprojek.cz/fr/fuel-stats.git | ||||
| ``` | ||||
|  | ||||
| 2. Create `config/environment.php` | ||||
| - It should have following structure: | ||||
| 2. Create `config/environment.php`: | ||||
| - It should have the following structure: | ||||
| ```php | ||||
| <?php | ||||
|  | ||||
| @@ -35,21 +35,31 @@ define('DB_USER', 'your db username'); | ||||
| define('DB_PASS', 'your db password'); | ||||
| define('DB_NAME', 'your db name');  | ||||
| ``` | ||||
| - For the database, you can use included `docker-compose.yaml` which have both MariaDB and PhpMyAdmin | ||||
| - For the database, you can use the included `docker-compose.yaml` which includes both MariaDB and PhpMyAdmin. | ||||
|  | ||||
| 3. Start an local web server | ||||
| - You can use php's integrated server by running this: | ||||
| 3. Start a local web server: | ||||
| - You can use PHP's integrated server by running this: | ||||
| ```bash | ||||
| php -S localhost:8000 -t ./public | ||||
| ``` | ||||
| - You can use any host and any port you want. | ||||
| - You can use any host and any port you prefer. | ||||
|  | ||||
| ## Usage | ||||
| 1. Register and Login to the app. | ||||
| 2. Add your habits. | ||||
| 3. Mark your habits when you're done doing them. | ||||
| 4. Earn point and unlock achievements by completing you're habits! | ||||
| 1. Register and log in to the app. | ||||
| 2. Add your vehicles with their details (fuel type, registration, etc.). | ||||
| 3. Record each refueling: | ||||
|    - Select your vehicle. | ||||
|    - Input the number of liters, price per liter, and total cost. | ||||
| 4. Track your fuel consumption and spending through the dashboard. | ||||
| 5. View detailed stats and graphs to analyze your driving habits. | ||||
|  | ||||
| ## Licence | ||||
| This project is licensed under GPL3.0 and later. More information is availabe in `LICENSE` file. | ||||
| ## Use case diagram | ||||
|  | ||||
| ## Data logical model | ||||
|  | ||||
|  | ||||
| ## Class diagram | ||||
|  | ||||
|  | ||||
| ## License | ||||
| This project is licensed under GPL3.0 and later. More information is available in the `LICENSE` file. | ||||
|   | ||||
							
								
								
									
										46
									
								
								TODO.md
									
									
									
									
									
								
							
							
						
						
									
										46
									
								
								TODO.md
									
									
									
									
									
								
							| @@ -6,13 +6,39 @@ | ||||
| - [ ] edit user data - change password, mail... | ||||
|  | ||||
| ## Core of the app | ||||
| - [ ] header and navbar | ||||
| - [ ] dashboard | ||||
|   - [x] css | ||||
|   - [ ] its just plain | ||||
|   - [ ] graphs | ||||
| - [x] Habits list | ||||
|   - [ ] css | ||||
| - [ ] Habits create | ||||
|   - [ ] validate cron input | ||||
| - [ ] Habits track | ||||
| - [ ] intro tutorial when no car exist or just dont show anything | ||||
| - [x] change/set default car | ||||
| - [ ] specific car view - charts, fuel records | ||||
| - [ ] remove/edit fuel record | ||||
|  | ||||
| ## Until release | ||||
| - [x] Sync offline data from locale storage | ||||
| - [x] Include kilometer state of an car | ||||
| - [ ] More charts | ||||
|   - [x] Average fuel conusption | ||||
|   - [ ] Kilometer state | ||||
| - [ ] More cards | ||||
|   - [ ] Average fuel conusption in last 30 days | ||||
|   - [ ] Kilometer state in last 30 days` | ||||
| - [ ] Offline navigation between dashboard and offline form | ||||
|  | ||||
| ## What has to be done | ||||
| - [x] Vehicle delete | ||||
| - [ ] intro tutorial when no car exist or just dont show anything | ||||
| - [x] change/set default car | ||||
| - [x] hide errors | ||||
|  | ||||
| ## Nice to have | ||||
| - [ ] specific car view - charts, fuel records | ||||
| - [ ] remove/edit fuel record | ||||
| - [x] Include kilometer state of an car | ||||
| - [ ] More charts | ||||
|   - [x] Average fuel conusption | ||||
|   - [ ] Kilometer state | ||||
| - [ ] More cards | ||||
|   - [ ] Average fuel conusption in last 30 days | ||||
|   - [ ] Kilometer state in last 30 days` | ||||
| - [ ] Offline navigation between dashboard and offline form | ||||
| - [ ] Fix vehicle deletion - wrong redirect | ||||
| - [ ] Update diagrams in README.md | ||||
|  | ||||
|   | ||||
| @@ -1,12 +1,34 @@ | ||||
| <?php | ||||
| class DashboardController extends Controller { | ||||
|     public function index() { | ||||
|         $habit = new Habit(); | ||||
|         $habits = $habit->getHabitsByUser($_SESSION['user']['id']); | ||||
|         $vehicle = new Vehicle(); | ||||
|         $vehicles = $vehicle->getVehiclesByUser($_SESSION['user']['id']); | ||||
|         $default_car = $vehicle->getDefaultVehicle($_SESSION['user']['id']) ?? null; | ||||
|  | ||||
|         $refuel = new Refuel(); | ||||
|         $data = [ | ||||
|             "date" => [], | ||||
|             "price" => [], | ||||
|             "mileage" => [], | ||||
|             "liters" => [] | ||||
|         ]; | ||||
|         $raw_data = $default_car ? $refuel->latest_data($default_car['id'], 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']); | ||||
|             array_push($data['mileage'], $one['mileage']); | ||||
|             array_push($data['liters'], $one['liters']); | ||||
|         } | ||||
|  | ||||
|         $latest_data = $default_car ? $refuel->latest_one($default_car['id']) : []; | ||||
|         $latest_record = !empty($latest_data) ? $latest_data[0] : null; | ||||
|  | ||||
|         $this->view('dashboard/index', [ | ||||
|             'title' => 'Dashboard', | ||||
|             'habits' => $habits, | ||||
|             'vehicles' => $vehicles, | ||||
|             'date_price_data' => $data, | ||||
|             'default_car' => $default_car, | ||||
|             'latest_record' => $latest_record, | ||||
|         ]); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -1,57 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| class HabitController extends Controller { | ||||
|     public function index() { | ||||
|         $habit = new Habit(); | ||||
|         $habits = $habit->getHabitsByUser($_SESSION['user']['id']); | ||||
|         $this->view('habits/index', ['title' => 'Habits', 'habits' => $habits]); | ||||
|     } | ||||
|  | ||||
|     public function create() { | ||||
|         if ($_SERVER['REQUEST_METHOD'] === 'POST') { | ||||
|             $name = $_POST['name'] ?? ''; | ||||
|             $frequency = $_POST['frequency'] ?? 'Daily'; | ||||
|             $customFrequency = null; | ||||
|  | ||||
|             if (empty($name)) { | ||||
|                 $this->view('habits/create', ['error' => 'Habit name is required.']); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             if ($frequency === 'Custom') { | ||||
|                 $daysOfWeek = $_POST['days_of_week'] ?? []; | ||||
|                 $daysOfMonth = $_POST['days_of_month'] ?? '*'; | ||||
|                 $months = $_POST['months'] ?? '*'; | ||||
|  | ||||
|                 // Combine into crontab-like string | ||||
|                 $customFrequency = implode(',', $daysOfWeek) . " $daysOfMonth $months"; | ||||
|             } | ||||
|  | ||||
|             $habit = new Habit(); | ||||
|             $result = $habit->create([ | ||||
|                 'name' => $name, | ||||
|                 'frequency' => $frequency, | ||||
|                 'custom_frequency' => $customFrequency, | ||||
|                 'reward_points' => intval($_POST['difficulty'] ?? 1), | ||||
|                 'user_id' => $_SESSION['user']['id'], | ||||
|             ]); | ||||
|  | ||||
|             if ($result) { | ||||
|                 $this->redirect('/habits'); | ||||
|             } else { | ||||
|                 $this->view('habits/create', ['error' => 'Failed to create habit.']); | ||||
|             } | ||||
|         } else { | ||||
|             $this->view('habits/create', ['title' => 'Create Habit']); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|     public function edit() { | ||||
|         // Edit habit (to be implemented later) | ||||
|     } | ||||
|  | ||||
|     public function delete() { | ||||
|         // Delete habit (to be implemented later) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										88
									
								
								app/controllers/RefuelController.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								app/controllers/RefuelController.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,88 @@ | ||||
| <?php | ||||
|  | ||||
| class RefuelController extends Controller { | ||||
|     public function create() { | ||||
|         if($_SERVER['REQUEST_METHOD'] === 'GET'){ | ||||
|             $vehicle = new Vehicle(); | ||||
|             $vehicles = $vehicle->getVehiclesByUser($_SESSION['user']['id']); | ||||
|             $this->view('refuel/create', [ | ||||
|                 'title' => "New refuel record", | ||||
|                 'vehicles' => $vehicles, | ||||
|             ]); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if ($_SERVER['REQUEST_METHOD'] === 'POST') { | ||||
|             $vehicle_id = $_POST['vehicle'] ?? ''; | ||||
|             $fuel_type = $_POST['fuel_type'] ?? ''; | ||||
|             $liters = $_POST['liters'] ?? ''; | ||||
|             $price_per_liter = $_POST['price_per_liter'] ?? ''; | ||||
|             $total_price = $_POST['total_price'] ?? ''; | ||||
|             $mileage = $_POST['mileage'] ?? ''; | ||||
|             $note = $_POST['note'] ?? ''; | ||||
|  | ||||
|             $validator = new Validator(); | ||||
|             $validator->required('vehicle', $vehicle_id); | ||||
|             $validator->required('fuel_type', $fuel_type); | ||||
|             $validator->required('liters', $liters); | ||||
|             $validator->required('price_per_liter', $price_per_liter); | ||||
|             $validator->required('total_price', $total_price); | ||||
|             $validator->number('liters', $liters); | ||||
|             $validator->number('price_per_liter', $price_per_liter); | ||||
|             $validator->number('total_price', $total_price); | ||||
|             $validator->number('mileage', $mileage); | ||||
|  | ||||
|             if (round($liters * $price_per_liter, 2) != $total_price) { | ||||
|                 $validator->setErrors(["total_price" => "Price calculation is wrong"]); | ||||
|             } | ||||
|   | ||||
|             if($note == "") $note = NULL; | ||||
|  | ||||
|             if (!$validator->passes()) { | ||||
|                 $vehicle = new Vehicle(); | ||||
|                 $vehicles = $vehicle->getVehiclesByUser($_SESSION['user']['id']); | ||||
|                 $this->view('refuel/create', [ | ||||
|                     'error' => 'Please correct the errors below.', | ||||
|                     'validationErrors' => $validator->errors() ?: [], | ||||
|                     'vehicles' => $vehicles, | ||||
|                     'title' => 'New refuel record', | ||||
|                 ]); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             $record = new Refuel(); | ||||
|             $result = $record->create([ | ||||
|                 'user_id' => $_SESSION['user']['id'], | ||||
|                 'vehicle_id' => $vehicle_id, | ||||
|                 'fuel_type' => $fuel_type, | ||||
|                 'note' => $note, | ||||
|                 'liters' => $liters, | ||||
|                 'price_per_liter' => $price_per_liter, | ||||
|                 'total_price' => $total_price, | ||||
|                 'mileage' => $mileage, | ||||
|             ]); | ||||
|  | ||||
|             if ($result === true) { | ||||
|                 $this->redirect('/'); | ||||
|             } else { | ||||
|                 $vehicle = new Vehicle(); | ||||
|                 $vehicles = $vehicle->getVehiclesByUser($_SESSION['user']['id']); | ||||
|                 $this->view('refuel/create', [ | ||||
|                     'title' => 'New refuel record', | ||||
|                     'error' => $result, | ||||
|                     'validationErrors' => [], | ||||
|                     'vehicles' => $vehicles, | ||||
|                 ]); | ||||
|             } | ||||
|             return; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public function edit() { | ||||
|         // Edit refuel record (to be implemented later) | ||||
|     } | ||||
|  | ||||
|     public function delete() { | ||||
|         // Delete refuel record (to be implemented later) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										104
									
								
								app/controllers/VehicleController.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								app/controllers/VehicleController.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,104 @@ | ||||
| <?php | ||||
|  | ||||
| class VehicleController extends Controller { | ||||
|     public function index() { | ||||
|         $vehicle = new Vehicle(); | ||||
|         $vehicles = $vehicle->getVehiclesByUser($_SESSION['user']['id']); | ||||
|         $this->view('vehicles/index', ['title' => 'Vehicles', 'vehicles' => $vehicles]); | ||||
|     } | ||||
|  | ||||
|     public function create() { | ||||
|         if ($_SERVER['REQUEST_METHOD'] === 'POST') { | ||||
|             $name = $_POST['name'] ?? ''; | ||||
|             $registration_plate = $_POST['registration_plate'] ?? ''; | ||||
|             $fuel_type = $_POST['fuel_type'] ?? ''; | ||||
|             $note = $_POST['note'] ?? ''; | ||||
|  | ||||
|             $validator = new Validator(); | ||||
|             $validator->required('name', $name); | ||||
|             $validator->required('registration_plate', $registration_plate); | ||||
|             $validator->required('fuel_type', $fuel_type); | ||||
|  | ||||
|             if($note == "") $note = NULL; | ||||
|  | ||||
|             if (!$validator->passes()) { | ||||
|                 $this->view('vehicles/create', [ | ||||
|                     'error' => 'Please correct the errors below.', | ||||
|                     'validationErrors' => $validator->errors() ?: [], | ||||
|                 ]); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             $vehicle = new Vehicle(); | ||||
|             $default_vehicle = $vehicle->getDefaultVehicle($_SESSION['user']['id']); | ||||
|             $is_default = $default_vehicle ? 0 : 1; | ||||
|  | ||||
|             $result = $vehicle->create([ | ||||
|                 'name' => $name, | ||||
|                 'registration_plate' => strtoupper($registration_plate), | ||||
|                 'fuel_type' => $fuel_type, | ||||
|                 'note' => $note, | ||||
|                 'user_id' => $_SESSION['user']['id'], | ||||
|                 'is_default' => $is_default | ||||
|             ]); | ||||
|  | ||||
|  | ||||
|             if ($result === true) { | ||||
|                 $this->redirect('/'); | ||||
|             } else { | ||||
|                 $this->view('vehicles/create', ['title' => 'Create vehicle', 'error' => $result, 'validationErrors' => []] ); | ||||
|             } | ||||
|  | ||||
|         } else { | ||||
|             $this->view('vehicles/create', ['title' => 'Create Vehicle']); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|     public function edit() { | ||||
|         // TODO: Edit vehicle (to be implemented later) | ||||
|     } | ||||
|  | ||||
|     public function delete() { | ||||
|         if(!$_SERVER['REQUEST_METHOD'] === 'POST') { | ||||
|             echo "Wrong method"; | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // TODO: Validate the request | ||||
|         $vehicle_id = $_POST['vehicle_id']; | ||||
|  | ||||
|         $vehicle = new Vehicle(); | ||||
|         $result = $vehicle->delete($vehicle_id, $_SESSION['user']['id']); | ||||
|  | ||||
|         if($result != true) { | ||||
|             echo "Something went wrong"; | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         header("Location: /vehicles"); | ||||
|     } | ||||
|  | ||||
|     public function setDefault() { | ||||
|         $vehicle = new Vehicle(); | ||||
|         // TODO: Validate the request | ||||
|         $result = $vehicle->setDefaultVehicle($_POST['vehicle_id'], $_SESSION['user']['id']); | ||||
|         if($result != true) { | ||||
|             echo "Something went wrong"; | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         header("Location: /"); | ||||
|     } | ||||
|  | ||||
|     public function api_get() { | ||||
|         if(!$_SERVER['REQUEST_METHOD'] === 'GET') { | ||||
|             echo "Wrong method, use GET"; | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         $vehicle = new Vehicle(); | ||||
|         $result = $vehicle->getVehiclesByUser($_SESSION['user']['id']); | ||||
|         echo json_encode($result); | ||||
|     } | ||||
| } | ||||
| @@ -1,46 +0,0 @@ | ||||
| <?php | ||||
|  | ||||
| class Habit { | ||||
|     private $db; | ||||
|  | ||||
|     public function __construct() { | ||||
|         $this->db = Database::getInstance()->getConnection(); | ||||
|     } | ||||
|  | ||||
|     public function create($data) { | ||||
|         $stmt = $this->db->prepare(" | ||||
|             INSERT INTO habits (user_id, title, frequency, custom_frequency, reward_points, created_at) | ||||
|             VALUES (?, ?, ?, ?, ?, NOW()) | ||||
|         "); | ||||
|  | ||||
|         $stmt->bind_param( | ||||
|             "isssi", // Bind types: int, string, string, string, int | ||||
|             $data['user_id'], | ||||
|             $data['name'], | ||||
|             $data['frequency'], | ||||
|             $data['custom_frequency'], // Bind the custom_frequency field | ||||
|             $data['reward_points'] | ||||
|         ); | ||||
|  | ||||
|         if ($stmt->execute()) { | ||||
|             return true; | ||||
|         } else { | ||||
|             error_log("Failed to create habit: " . $stmt->error); | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public function getHabitsByUser($userId) { | ||||
|         $stmt = $this->db->prepare("SELECT id, title, frequency, custom_frequency, reward_points, created_at FROM habits WHERE user_id = ?"); | ||||
|         $stmt->bind_param("i", $userId); | ||||
|         $stmt->execute(); | ||||
|         $result = $stmt->get_result(); | ||||
|  | ||||
|         $habits = []; | ||||
|         while ($row = $result->fetch_assoc()) { | ||||
|             $habits[] = $row; | ||||
|         } | ||||
|          | ||||
|         return $habits; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										95
									
								
								app/models/Refuel.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								app/models/Refuel.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,95 @@ | ||||
| <?php | ||||
|  | ||||
| class Refuel { | ||||
|     private $db; | ||||
|  | ||||
|     public function __construct() { | ||||
|         $this->db = Database::getInstance()->getConnection(); | ||||
|     } | ||||
|  | ||||
|     public function create($data) { | ||||
|         try{ | ||||
|             $stmt = $this->db->prepare(" | ||||
|                 INSERT INTO refueling_records (user_id, vehicle_id, fuel_type, note, liters, price_per_liter, total_price, mileage, created_at) | ||||
|                 VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW()) | ||||
|             "); | ||||
|  | ||||
|             $stmt->bind_param( | ||||
|                 "iissdddi", | ||||
|                 $data['user_id'], | ||||
|                 $data['vehicle_id'], | ||||
|                 $data['fuel_type'], | ||||
|                 $data['note'], | ||||
|                 $data['liters'], | ||||
|                 $data['price_per_liter'], | ||||
|                 $data['total_price'], | ||||
|                 $data['mileage'], | ||||
|             ); | ||||
|  | ||||
|             if ($stmt->execute()) { | ||||
|                 return true; | ||||
|             } else { | ||||
|                 return "Error: " . $stmt->error; | ||||
|             } | ||||
|         } catch(mysqli_sql_exception $e) { | ||||
|             return $e->getMessage(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public function latest_data($vehicle_id, $record_count) { | ||||
|         try { | ||||
|             $stmt = $this->db->prepare(" | ||||
|                 SELECT `liters`, `price_per_liter`, `total_price`, `mileage`, `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($vehicle_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`.`mileage`,  | ||||
|                     `r`.`note`,  | ||||
|                     `r`.`created_at` | ||||
|                 FROM `refueling_records` AS `r` | ||||
|                 JOIN `vehicles` AS `v` ON `r`.`vehicle_id` = `v`.`id` | ||||
|                 WHERE `r`.`vehicle_id` = ? | ||||
|                 ORDER BY `r`.`created_at` DESC | ||||
|                 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(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -29,7 +29,7 @@ class User { | ||||
|  | ||||
|         $hashedPassword = password_hash($password, PASSWORD_BCRYPT); | ||||
|  | ||||
|         $stmt = $this->db->prepare("INSERT INTO users (username, email, password, points, created_at) VALUES (?, ?, ?, 0, NOW())"); | ||||
|         $stmt = $this->db->prepare("INSERT INTO users (username, email, password, created_at) VALUES (?, ?, ?, NOW())"); | ||||
|         $stmt->bind_param("sss", $username, $email, $hashedPassword); | ||||
|  | ||||
|         if ($stmt->execute()) { | ||||
|   | ||||
							
								
								
									
										121
									
								
								app/models/Vehicle.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								app/models/Vehicle.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,121 @@ | ||||
| <?php | ||||
|  | ||||
| class Vehicle { | ||||
|     private $db; | ||||
|  | ||||
|     public function __construct() { | ||||
|         $this->db = Database::getInstance()->getConnection(); | ||||
|     } | ||||
|  | ||||
|     public function create($data) { | ||||
|         try{ | ||||
|             $stmt = $this->db->prepare(" | ||||
|                 INSERT INTO vehicles (user_id, name, registration_plate, fuel_type, note, is_default, created_at) | ||||
|                 VALUES (?, ?, ?, ?, ?, ?, NOW()) | ||||
|             "); | ||||
|  | ||||
|             $stmt->bind_param( | ||||
|                 "issssi", | ||||
|                 $data['user_id'], | ||||
|                 $data['name'], | ||||
|                 $data['registration_plate'], | ||||
|                 $data['fuel_type'], | ||||
|                 $data['note'], | ||||
|                 $data['is_default'], | ||||
|             ); | ||||
|  | ||||
|             if ($stmt->execute()) { | ||||
|                 return true; | ||||
|             } else { | ||||
|                 return "Error: " . $stmt->error; | ||||
|             } | ||||
|         } catch(mysqli_sql_exception $e) { | ||||
|             return $e->getMessage(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public function getVehiclesByUser($user_id) { | ||||
|         $stmt = $this->db->prepare("SELECT id, name, registration_plate, fuel_type, note, created_at FROM vehicles WHERE user_id = ?"); | ||||
|         $stmt->bind_param("i", $user_id); | ||||
|         $stmt->execute(); | ||||
|         $result = $stmt->get_result(); | ||||
|  | ||||
|         $vehicles = []; | ||||
|         while ($row = $result->fetch_assoc()) { | ||||
|             $vehicles[] = $row; | ||||
|         } | ||||
|          | ||||
|         return $vehicles; | ||||
|     } | ||||
|  | ||||
|     public function getDefaultVehicle($user_id) { | ||||
|         $stmt = $this->db->prepare(" | ||||
|             SELECT id, name, registration_plate, fuel_type, note, is_default | ||||
|             FROM vehicles | ||||
|             WHERE user_id = ? AND is_default = TRUE | ||||
|             LIMIT 1 | ||||
|         "); | ||||
|         $stmt->bind_param("i", $user_id); | ||||
|         $stmt->execute(); | ||||
|         $result = $stmt->get_result(); | ||||
|  | ||||
|         return $result->fetch_assoc(); | ||||
|     } | ||||
|  | ||||
|     public function setDefaultVehicle($vehicle_id, $user_id) { | ||||
|         try { | ||||
|             $this->db->begin_transaction(); | ||||
|  | ||||
|             $stmt = $this->db->prepare(" | ||||
|                 UPDATE `vehicles`  | ||||
|                 SET `is_default` = 0  | ||||
|                 WHERE `user_id` = ? AND `is_default` = 1 | ||||
|             "); | ||||
|             $stmt->bind_param("i", $user_id); | ||||
|             $stmt->execute(); | ||||
|             $stmt->close(); | ||||
|  | ||||
|             $stmt = $this->db->prepare(" | ||||
|                 UPDATE `vehicles`  | ||||
|                 SET `is_default` = 1  | ||||
|                 WHERE `id` = ? AND `user_id` = ? | ||||
|             "); | ||||
|             $stmt->bind_param("ii", $vehicle_id, $user_id); | ||||
|  | ||||
|             if ($stmt->execute()) { | ||||
|                 $this->db->commit(); | ||||
|                 return true; | ||||
|             } else { | ||||
|                 $this->db->rollback(); | ||||
|                 return "Error: " . $stmt->error; | ||||
|             } | ||||
|         } catch (mysqli_sql_exception $e) { | ||||
|             $this->db->rollback(); | ||||
|             return $e->getMessage(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public function delete($vehicle_id, $user_id) { | ||||
|         try { | ||||
|             $stmt = $this->db->prepare("SELECT id FROM vehicles WHERE id = ? AND user_id = ?"); | ||||
|             $stmt->bind_param("ii", $vehicle_id, $user_id); | ||||
|             $stmt->execute(); | ||||
|             $result = $stmt->get_result(); | ||||
|  | ||||
|             if ($result->num_rows === 0) { | ||||
|                 return "Error: Unauthorized action or vehicle not found."; | ||||
|             } | ||||
|  | ||||
|             $stmt = $this->db->prepare("DELETE FROM vehicles WHERE id = ?"); | ||||
|             $stmt->bind_param("i", $vehicle_id); | ||||
|  | ||||
|             if ($stmt->execute()) { | ||||
|                 return true; | ||||
|             } else { | ||||
|                 return "Error: " . $stmt->error; | ||||
|             } | ||||
|         } catch (mysqli_sql_exception $e) { | ||||
|             return $e->getMessage(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -2,8 +2,8 @@ | ||||
| <link rel="stylesheet" href="/css/form.css"> | ||||
| <section class="form signin"> | ||||
|     <div class="header-form"> | ||||
|         <img src="/img/logo.jpg" alt="Habit Tracker Logo"> | ||||
|         <h1>Sign in to Habit Tracker</h1> | ||||
|         <img src="/img/logo.jpg" alt="Fuel Stats Logo"> | ||||
|         <h1>Sign in to Fuel Stats</h1> | ||||
|     </div> | ||||
|  | ||||
|     <?php if ($this->get('error')): ?> | ||||
| @@ -27,7 +27,7 @@ | ||||
|     </form> | ||||
|  | ||||
|     <div class="bordered"> | ||||
|         <p>New to Habit Tracker?</p> | ||||
|         <p>New to Fuel Stats?</p> | ||||
|         <a href="/auth/signup">Create an account</a> | ||||
|     </div> | ||||
| </section> | ||||
|   | ||||
| @@ -2,8 +2,8 @@ | ||||
| <link rel="stylesheet" href="/css/form.css"> | ||||
| <section class="form signup"> | ||||
|     <div class="header-form"> | ||||
|         <img src="/img/logo.jpg" alt="Habit Tracker Logo"> | ||||
|         <h1>Sign up to Habit Tracker</h1> | ||||
|         <img src="/img/logo.jpg" alt="Fuel Stats Logo"> | ||||
|         <h1>Sign up to Fuel Stats</h1> | ||||
|     </div> | ||||
|  | ||||
|     <?php if ($this->get('error')): ?> | ||||
|   | ||||
| @@ -1,47 +1,122 @@ | ||||
| <link rel="stylesheet" href="/css/dashboard.css"> | ||||
|  | ||||
| <link rel="stylesheet" href="/css/form.css"> | ||||
| <link rel="stylesheet" href="/css/vehicle_create.css"> | ||||
|  | ||||
| <section class="dashboard"> | ||||
|     <h1>Welcome, <?= htmlspecialchars($_SESSION['user']['username']) ?>!</h1> | ||||
|     <div> | ||||
|         <a href="/habits/create" class="btn-green">Create new habit!</a> | ||||
|         <a href="/habits" class="btn-primary">List all habits</a> | ||||
|     <?php if(!isset($data['default_car'])): ?> | ||||
|      | ||||
|     <div id="intro"> | ||||
|         <a href="/vehicles/create">Create your first vehicle</a> | ||||
|     </div> | ||||
|     <?php elseif (isset($data['latest_record'])): ?> | ||||
|  | ||||
|     <div id="actions"> | ||||
|         <a href="/refuel/create" class="btn-green">Add new refuel record</a> | ||||
|         <a href="/vehicles" class="btn-primary">List all vehicles</a> | ||||
|         <a class="btn-warning" id="btn-offline-add">Add new offline refuel record</a> | ||||
|     </div> | ||||
|     <div class="card-wrapper"> | ||||
|         <section class="card upcoming"> | ||||
|             <h2>Upcoming</h2> | ||||
|             <div class="habit"> | ||||
|                 <b>Habit Title</b> | ||||
|                 <p>Frequency</p> | ||||
|                 <p>Reward points</p> | ||||
|         <section class="card latest"> | ||||
|             <h2>Latest fuel record</h2> | ||||
|             <hr> | ||||
|             <div> | ||||
|                 <b>Car:</b> | ||||
|                 <p><?= $data['latest_record']['vehicle_name'] ?></p> | ||||
|                 <b>Liters:</b> | ||||
|                 <p><?= $data['latest_record']['liters'] ?> liters</p> | ||||
|                 <b>Price per liter:</b> | ||||
|                 <p><?= $data['latest_record']['price_per_liter'] ?>,-/liter</p> | ||||
|                 <b>Total price:</b> | ||||
|                 <p><?= $data['latest_record']['total_price'] ?>,-</p> | ||||
|                 <b>Mileage:</b> | ||||
|                 <p><?= $data['latest_record']['mileage'] ?> km</p> | ||||
|                 <?php if (isset($data['latest_record']['note'])): ?> | ||||
|                 <b>Note:</b> | ||||
|                 <p><?= $data['latest_record']['note'] ?></p> | ||||
|                 <?php endif; ?> | ||||
|             </div> | ||||
|         </section> | ||||
|  | ||||
|         <section class="card recent"> | ||||
|             <h2>Recent</h2> | ||||
|             <div class="habit"> | ||||
|                 <b>Habit Title</b> | ||||
|                 <p>Frequency</p> | ||||
|                 <p>Reward points</p> | ||||
|             </div> | ||||
|         </section> | ||||
|  | ||||
|         <section class="card missed"> | ||||
|             <h2>Missed</h2> | ||||
|             <div class="habit"> | ||||
|                 <b>Habit Title</b> | ||||
|                 <p>Frequency</p> | ||||
|                 <p>Reward points</p> | ||||
|             </div> | ||||
|         <section class="card"> | ||||
|             <h2>Default car</h2> | ||||
|             <hr> | ||||
|             <b>Car</b> | ||||
|             <p><?= $data['default_car']['name'] ?></p> | ||||
|             <b>Registration plate</b> | ||||
|             <p><?= $data['default_car']['registration_plate'] ?></p> | ||||
|             <b>Fuel type</b> | ||||
|             <p><?= $data['default_car']['fuel_type'] ?></p> | ||||
|             <b>Note</b> | ||||
|             <p><?= $data['default_car']['note'] ?></p> | ||||
|         </section> | ||||
|  | ||||
|         <section class="card history-graph"> | ||||
|             <h2>Graph of History</h2> | ||||
|             <h2>Chart of Gas price</h2> | ||||
|             <hr> | ||||
|             <p><?= $data['default_car']['name'] . " | " . $data['default_car']['registration_plate']?></p> | ||||
|             <canvas id="chart-gas-price"></canvas> | ||||
|         </section> | ||||
|  | ||||
|         <section class="card streak"> | ||||
|             <h2>Streak</h2> | ||||
|             <p>You're current streak is 123 days</p> | ||||
|             <p>Good job!</p> | ||||
|         <section class="card history-graph"> | ||||
|             <h2>Average fuel consumption</h2> | ||||
|             <hr> | ||||
|             <p><?= $data['default_car']['name'] . " | " . $data['default_car']['registration_plate']?></p> | ||||
|             <b id="avg-fl-cnsmp"></b> | ||||
|         </section> | ||||
|     </div> | ||||
|     <?php else: ?> | ||||
|     <div id="actions"> | ||||
|         <a href="/refuel/create" class="btn-green">Add new refuel record</a> | ||||
|         <a href="/vehicles" class="btn-primary">List all vehicles</a> | ||||
|         <a class="btn-warning" id="btn-offline-add">Add new offline refuel record</a> | ||||
|     </div> | ||||
|     <div class="alert-warning"> | ||||
|         <p>Default vehicle <b><i><?= $data['default_car']['name'] ?></i></b> doesn't have any refuel record yet.</p> | ||||
|         <p>Select another vehicle or create first refuel record.</p> | ||||
|     </div> | ||||
|     <?php endif; ?> | ||||
| </section> | ||||
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | ||||
| <script> | ||||
|     const ctx = document.getElementById('chart-gas-price'); | ||||
|     const data = <?= json_encode($data['date_price_data']); ?>; | ||||
|  | ||||
|     new Chart(ctx, { | ||||
|         type: 'line', | ||||
|         data: { | ||||
|             labels: [...data['date']], | ||||
|             datasets: [{ | ||||
|                 label: 'Gas price history', | ||||
|                 data: data['price'], | ||||
|                 borderWidth: 1, | ||||
|             }] | ||||
|         }, | ||||
|     }); | ||||
| </script> | ||||
|  | ||||
| <script> | ||||
|     const data2 = <?= json_encode($data['date_price_data']); ?>; | ||||
|     let cnt_ltr = 0 | ||||
|     let cnt_km = 0 | ||||
|     let first_km = 0 | ||||
|  | ||||
|     console.log(data2) | ||||
|  | ||||
|     for(let i = 0; i < data2['liters'].length; i++) { | ||||
|         if(i == 0) { | ||||
|             first_km = data2['mileage'][i] | ||||
|         } | ||||
|         cnt_ltr += data2['liters'][i] | ||||
|         cnt_km =+ data2['mileage'][i] | ||||
|     } | ||||
|  | ||||
|     console.log("Liters", cnt_ltr, cnt_km, first_km) | ||||
|     console.log("Avg", (cnt_km - first_km) / cnt_ltr) | ||||
|  | ||||
|     document.querySelector("#avg-fl-cnsmp").textContent = Math.floor((cnt_km - first_km) / cnt_ltr) + " l/100km" | ||||
| </script> | ||||
|  | ||||
|  | ||||
| <script defer src="/js/offline-records.js"></script> | ||||
|   | ||||
| @@ -4,7 +4,7 @@ | ||||
| <head> | ||||
|     <meta charset="UTF-8"> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||||
|     <title>Habit Tracker | Error 494</title> | ||||
|     <title>Fuel Stats | Error 494</title> | ||||
| </head> | ||||
| <body> | ||||
|     <h1>Error 404 - Page not found</h1> | ||||
|   | ||||
| @@ -1,70 +0,0 @@ | ||||
| <link rel="stylesheet" href="/css/form.css"> | ||||
| <link rel="stylesheet" href="/css/habits_create.css"> | ||||
| <section class="form habit-create"> | ||||
|     <h1 class="header-form"><?= $this->get('title', 'Create Habit') ?></h1> | ||||
|  | ||||
|     <?php if ($this->get('error')): ?> | ||||
|         <div class="error" style="color: red; margin-bottom: 1rem;"> | ||||
|             <?= htmlspecialchars($this->get('error')) ?> | ||||
|         </div> | ||||
|     <?php endif; ?> | ||||
|  | ||||
|     <form method="POST" action="/habits/create"> | ||||
|         <label for="name">Habit Name</label> | ||||
|         <input type="text" name="name" id="name" required value="<?= htmlspecialchars($_POST['name'] ?? '') ?>"> | ||||
|  | ||||
|         <label for="frequency">Frequency</label> | ||||
|         <select name="frequency" id="frequency" onchange="toggleCustomFrequency(this.value)"> | ||||
|             <option value="Daily">Daily</option> | ||||
|             <option value="Weekly">Weekly</option> | ||||
|             <option value="Custom">Custom</option> | ||||
|         </select> | ||||
|  | ||||
|         <div id="custom-frequency" style="display: none;"> | ||||
|             <label id="lbl_dow">Days of the Week</label> | ||||
|             <div class="dow_chb_wrapper"> | ||||
|                 <label for="dow_mon">Monday</label> | ||||
|                 <input type="checkbox" name="days_of_week[]" id="dow_mon" value="1"> | ||||
|             </div> | ||||
|             <div class="dow_chb_wrapper"> | ||||
|                 <label for="dow_tue">Tuesday</label> | ||||
|                 <input type="checkbox" name="days_of_week[]" id="dow_tue" value="2"> | ||||
|             </div> | ||||
|             <div class="dow_chb_wrapper"> | ||||
|                 <label for="dow_wed">Wednesday</label> | ||||
|                 <input type="checkbox" name="days_of_week[]" id="dow_wed" value="3"> | ||||
|             </div> | ||||
|             <div class="dow_chb_wrapper"> | ||||
|                 <label for="dow_thu">Thursday</label> | ||||
|                 <input type="checkbox" name="days_of_week[]" id="dow_thu" value="4"> | ||||
|             </div> | ||||
|             <div class="dow_chb_wrapper"> | ||||
|                 <label for="dow_fri">Friday</label> | ||||
|                 <input type="checkbox" name="days_of_week[]" id="dow_fri" value="5"> | ||||
|             </div> | ||||
|             <div class="dow_chb_wrapper"> | ||||
|                 <label for="dow_sat">Saturday</label> | ||||
|                 <input type="checkbox" name="days_of_week[]" id="dow_sat" value="6"> | ||||
|             </div> | ||||
|             <div class="dow_chb_wrapper"> | ||||
|                 <label for="dow_sun">Sunday</label> | ||||
|                 <input type="checkbox" name="days_of_week[]" id="dow_sun" value="7"> | ||||
|             </div> | ||||
|  | ||||
|             <label for="days_of_month" id="lbl_dom">Days of the Month</label> | ||||
|             <input type="text" name="days_of_month" id="days_of_month" placeholder="1,15 (comma-separated)"> | ||||
|  | ||||
|             <label for="months">Months</label> | ||||
|             <input type="text" name="months" id="months" placeholder="1,7,12 (comma-separated)"> | ||||
|         </div> | ||||
|  | ||||
|         <input type="submit" value="Create Habit"> | ||||
|     </form> | ||||
| </section> | ||||
|  | ||||
| <script> | ||||
| function toggleCustomFrequency(value) { | ||||
|     const customFrequencyDiv = document.getElementById('custom-frequency'); | ||||
|     customFrequencyDiv.style.display = value === 'Custom' ? 'flex' : 'none'; | ||||
| } | ||||
| </script> | ||||
| @@ -1,24 +0,0 @@ | ||||
| <link rel="stylesheet" href="/css/habits_dashboard.css"> | ||||
| <section class="habits"> | ||||
|     <?php if (empty($this->get('habits'))): ?> | ||||
|         <p>No habits yet. <a href="/habits/create">Create your first habit</a>.</p> | ||||
|     <?php else: ?> | ||||
|         <div class="habits-wrapper"> | ||||
|         <?php foreach ($this->get('habits') as $habit): ?> | ||||
|             <div class="habit bordered"> | ||||
|                 <b><?= htmlspecialchars($habit['title']) ?></b> | ||||
|                 <p>Frequency: <?= htmlspecialchars($habit['frequency']) ?></p> | ||||
|                 <?php if (isset($habit['custom_frequency'])): ?> | ||||
|                     <p><?= htmlspecialchars($habit['custom_frequency'] ?? 'N/A') ?></p> | ||||
|                 <?php endif; ?> | ||||
|                     <p><?= htmlspecialchars($habit['reward_points']) ?></p> | ||||
|                     <p><?= htmlspecialchars($habit['created_at']) ?></p> | ||||
|                     <a href="/habits/done">Mark as done</a> | | ||||
|                     <a href="/habits/edit?id=<?= $habit['id'] ?>">Edit</a> | | ||||
|                     <a href="/habits/delete?id=<?= $habit['id'] ?>" onclick="return confirm('Are you sure you want to delete this habit?')">Delete</a> | ||||
|             </div> | ||||
|         <?php endforeach; ?> | ||||
|         </div> | ||||
|         <a href="/habits/create" class="btn-green">Create new habit!</a> | ||||
|     <?php endif; ?> | ||||
| </section> | ||||
| @@ -1,4 +1,4 @@ | ||||
| <div> | ||||
|     <h1>Welcome to Habit Tracker!</h1> | ||||
|     <p>Track your habits and achieve your goals.</p> | ||||
|     <h1>Welcome to Fuel Stats!</h1> | ||||
|     <p>Keep track of your refuels.</p> | ||||
| </div> | ||||
|   | ||||
| @@ -6,7 +6,7 @@ | ||||
|     <meta name="author" content="Filip Rojek | http://filiprojek.cz"> | ||||
|     <meta name="email" content="webmaster(@)fofrweb.com"> | ||||
|     <meta name="copyright" content="(c) filiprojek.cz"> | ||||
|     <title>Habit Tracker | <?= $data['title'] ?></title> | ||||
|     <title>Fuel Stats<?= isset($data['title']) ? " | " . $data['title'] : "" ?></title> | ||||
|     <link rel="stylesheet" href="/css/main.css"> | ||||
|     <link rel="stylesheet" href="/css/global.css"> | ||||
|     <link rel="stylesheet" href="/css/vars.css"> | ||||
| @@ -17,7 +17,7 @@ | ||||
|     <header> | ||||
|       <div id="hd-left"> | ||||
|         <a href="/"><img src="/img/logo.jpg" alt="home"></a> | ||||
|         <label><?= $data['title'] ?></label> | ||||
|         <label><?= isset($data['title']) ? $data['title'] : "" ?></label> | ||||
|       </div> | ||||
|       <div id="hd-right"> | ||||
|         <?php if (!isset($_SESSION['user'])): ?> | ||||
|   | ||||
| @@ -6,7 +6,7 @@ | ||||
|     <meta name="author" content="Filip Rojek | http://filiprojek.cz"> | ||||
|     <meta name="email" content="webmaster(@)fofrweb.com"> | ||||
|     <meta name="copyright" content="(c) filiprojek.cz"> | ||||
|     <title>Habit Tracker | <?= $data['title'] ?></title> | ||||
|     <title>Fuel Stats<?= isset($data['title']) ? " | " . $data['title'] : "" ?></title> | ||||
|     <link rel="stylesheet" href="/css/main.css"> | ||||
|     <link rel="stylesheet" href="/css/global.css"> | ||||
|     <link rel="stylesheet" href="/css/vars.css"> | ||||
|   | ||||
							
								
								
									
										125
									
								
								app/views/refuel/create.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								app/views/refuel/create.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,125 @@ | ||||
| <link rel="stylesheet" href="/css/form.css"> | ||||
| <link rel="stylesheet" href="/css/vehicle_create.css"> | ||||
| <section class="form"> | ||||
|     <h1 class="header-form"><?= $this->get('title') ?></h1> | ||||
|  | ||||
|     <?php if ($this->get('error')): ?> | ||||
|         <div class="error" style="color: red; margin-bottom: 1rem;"> | ||||
|             <?= htmlspecialchars($this->get('error')) ?> | ||||
|         </div> | ||||
|     <?php endif; ?> | ||||
|  | ||||
|     <form method="POST" action="/refuel/create"> | ||||
|         <label for="vehicle">Vehicle</label> | ||||
|         <select name="vehicle" id="vehicle"> | ||||
|         <?php foreach ($this->get('vehicles') as $vehicle): ?> | ||||
|             <option value="<?= $vehicle['id'] ?>"><?= $vehicle['name'] . " | " . $vehicle['registration_plate'] ?></option> | ||||
|         <?php endforeach; ?> | ||||
|         </select> | ||||
|         <?php if (isset($this->get('validationErrors')['vehicle'])): ?> | ||||
|             <small class="error"><?= $this->get('validationErrors')['vehicle'] ?></small> | ||||
|         <?php endif; ?> | ||||
|  | ||||
|         <label for="fuel_type">Fuel type</label> | ||||
|         <select name="fuel_type" id="fuel_type"> | ||||
|             <option value="Diesel">Diesel</option> | ||||
|             <option value="Gasoline 95">Gasoline 95</option> | ||||
|             <option value="Gasoline 98">Gasoline 98</option> | ||||
|             <option value="Premium Diesel">Premium Diesel</option> | ||||
|             <option value="Premium Gasoline 95">Premium Gasoline 95</option> | ||||
|             <option value="Premium Gasoline 98">Premium Gasoline 98</option> | ||||
|             <option value="Other">Other</option> | ||||
|         </select> | ||||
|         <?php if (isset($this->get('validationErrors')['fuel_type'])): ?> | ||||
|             <small class="error"><?= $this->get('validationErrors')['fuel_type'] ?></small> | ||||
|         <?php endif; ?> | ||||
|  | ||||
|         <label for="liters">Liters</label> | ||||
|         <input type="number" name="liters" id="liters" min="0" step=".01" value="<?= htmlspecialchars($_POST['liters'] ?? '0.0') ?>"> | ||||
|         <?php if (isset($this->get('validationErrors')['liters'])): ?> | ||||
|             <small class="error"><?= $this->get('validationErrors')['liters'] ?></small> | ||||
|         <?php endif; ?> | ||||
|  | ||||
|         <label for="price_per_liter">Price per liter</label> | ||||
|         <input type="number" name="price_per_liter" id="price_per_liter" min="0" step=".01" value="<?= htmlspecialchars($_POST['price_per_liter'] ?? '0.0') ?>"> | ||||
|         <?php if (isset($this->get('validationErrors')['price_per_liter'])): ?> | ||||
|             <small class="error"><?= $this->get('validationErrors')['price_per_liter'] ?></small> | ||||
|         <?php endif; ?> | ||||
|  | ||||
|         <label for="total_price">Total price</label> | ||||
|         <input type="number" name="total_price" id="total_price" min="0" step=".01" value="<?= htmlspecialchars($_POST['total_price'] ?? '0.0') ?>"> | ||||
|         <?php if (isset($this->get('validationErrors')['total_price'])): ?> | ||||
|             <small class="error"><?= $this->get('validationErrors')['total_price'] ?></small> | ||||
|         <?php endif; ?> | ||||
|  | ||||
|         <label for="mileage">Mileage</label> | ||||
|         <input type="number" name="mileage" id="mileage" min="0" step="1" value="<?= htmlspecialchars($_POST['mileage'] ?? '0') ?>"> | ||||
|         <?php if (isset($this->get('validationErrors')['mileage'])): ?> | ||||
|             <small class="error"><?= $this->get('validationErrors')['mileage'] ?></small> | ||||
|         <?php endif; ?> | ||||
|  | ||||
|         <label for="note">Note</label> | ||||
|         <input type="text" name="note" id="note" value="<?= htmlspecialchars($_POST['note'] ?? '') ?>"> | ||||
|         <?php if (isset($this->get('validationErrors')['note'])): ?> | ||||
|             <small class="error"><?= $this->get('validationErrors')['note'] ?></small> | ||||
|         <?php endif; ?> | ||||
|  | ||||
|         <input type="submit" value="Create fuel record"> | ||||
|     </form> | ||||
| </section> | ||||
|  | ||||
| <script> | ||||
|     const inp_lit = document.querySelector("input#liters")  | ||||
|     const inp_ppl = document.querySelector("input#price_per_liter") | ||||
|     const inp_tot = document.querySelector("input#total_price") | ||||
|  | ||||
|     const rnd = (num) => Math.round((num + Number.EPSILON) * 100) / 100 | ||||
|  | ||||
|     function calculate(){ | ||||
|         let liters = Number(inp_lit.value) | ||||
|         let price_per_liter = Number(inp_ppl.value) | ||||
|         let total_price = Number(inp_tot.value) | ||||
|  | ||||
|         if(price_per_liter > 0 && liters > 0) { | ||||
|             total_price = rnd(liters * price_per_liter) | ||||
|         } | ||||
|  | ||||
|         if(price_per_liter > 0 && total_price > 0) { | ||||
|             liters = rnd(total_price / price_per_liter) | ||||
|         } | ||||
|  | ||||
|         if(liters > 0 && total_price > 0) { | ||||
|             price_per_liter = rnd(total_price / liters) | ||||
|         } | ||||
|  | ||||
|         inp_lit.value = liters | ||||
|         inp_ppl.value = price_per_liter | ||||
|         inp_tot.value = total_price | ||||
|     } | ||||
|  | ||||
|     [inp_lit, inp_ppl, inp_tot].forEach(inp => { | ||||
|         inp.addEventListener("change", () => { | ||||
|             calculate() | ||||
|         }) | ||||
|     }) | ||||
|  | ||||
|     const vehicles = <?= json_encode($data['vehicles']); ?>; | ||||
|     const fuel_sel = document.querySelector("#fuel_type") | ||||
|     const vehic_sel = document.querySelector("#vehicle") | ||||
|  | ||||
|     function selectFuel() { | ||||
|         const veh_id = vehic_sel.value | ||||
|         vehicles.forEach(el => { | ||||
|             if(el.id == veh_id) { | ||||
|                 fuel_sel.value = el.fuel_type | ||||
|                 return | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     selectFuel() | ||||
|  | ||||
|     vehic_sel.addEventListener("change", () => { | ||||
|         selectFuel() | ||||
|     }) | ||||
| </script> | ||||
							
								
								
									
										35
									
								
								app/views/vehicles/create.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								app/views/vehicles/create.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| <link rel="stylesheet" href="/css/form.css"> | ||||
| <link rel="stylesheet" href="/css/vehicle_create.css"> | ||||
| <section class="form"> | ||||
|     <h1 class="header-form"><?= $this->get('title', 'Create Vehicle') ?></h1> | ||||
|  | ||||
|     <?php if ($this->get('error')): ?> | ||||
|         <div class="error" style="color: red; margin-bottom: 1rem;"> | ||||
|             <?= htmlspecialchars($this->get('error')) ?> | ||||
|         </div> | ||||
|     <?php endif; ?> | ||||
|  | ||||
|     <form method="POST" action="/vehicles/create"> | ||||
|         <label for="name">Vehicle name</label> | ||||
|         <input type="text" name="name" id="name" required value="<?= htmlspecialchars($_POST['name'] ?? '') ?>"> | ||||
|  | ||||
|         <label for="registration_plate">Registration plate</label> | ||||
|         <input type="text" name="registration_plate" id="registration_plate" maxlength="10" onkeypress="return event.charCode != 32" required value="<?= htmlspecialchars($_POST['registration_plate'] ?? '') ?>"> | ||||
|  | ||||
|         <label for="fuel_type">Fuel type</label> | ||||
|         <select name="fuel_type" id="fuel_type"> | ||||
|             <option value="Diesel">Diesel</option> | ||||
|             <option value="Gasoline 95">Gasoline 95</option> | ||||
|             <option value="Gasoline 98">Gasoline 98</option> | ||||
|             <option value="Premium Diesel">Premium Diesel</option> | ||||
|             <option value="Premium Gasoline 95">Premium Gasoline 95</option> | ||||
|             <option value="Premium Gasoline 98">Premium Gasoline 98</option> | ||||
|             <option value="Other">Other</option> | ||||
|         </select> | ||||
|  | ||||
|         <label for="note">Note</label> | ||||
|         <input type="text" name="note" id="note" value="<?= htmlspecialchars($_POST['note'] ?? '') ?>"> | ||||
|  | ||||
|         <input type="submit" value="Create vehicle"> | ||||
|     </form> | ||||
| </section> | ||||
							
								
								
									
										34
									
								
								app/views/vehicles/index.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								app/views/vehicles/index.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| <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"> | ||||
|                     <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> | ||||
|  | ||||
|                     <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> | ||||
|                 </div> | ||||
|             </div> | ||||
|         <?php endforeach; ?> | ||||
|         </div> | ||||
|     <?php endif; ?> | ||||
| </section> | ||||
| @@ -1,6 +1,6 @@ | ||||
| <?php | ||||
|  | ||||
| define('DB_HOST', '0.0.0.0'); | ||||
| define('DB_NAME', 'habit_tracker');  | ||||
| define('DB_NAME', 'fuel_stats');  | ||||
| define('DB_USER', 'username');  | ||||
| define('DB_PASS', 'password'); | ||||
|   | ||||
| @@ -57,13 +57,12 @@ class Database { | ||||
|      * Check and create required tables if they don't exist | ||||
|      */ | ||||
|     private function checkAndCreateTables() { | ||||
|         // Create users table | ||||
|         // Create tables | ||||
|         $usersTableQuery = "CREATE TABLE IF NOT EXISTS users ( | ||||
|             id INT AUTO_INCREMENT PRIMARY KEY, | ||||
|             username VARCHAR(50) NOT NULL, | ||||
|             email VARCHAR(100) NOT NULL UNIQUE, | ||||
|             password VARCHAR(255) NOT NULL, | ||||
|             points INT DEFAULT 0, | ||||
|             created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP | ||||
|         ) ENGINE=InnoDB;"; | ||||
|  | ||||
| @@ -71,36 +70,57 @@ class Database { | ||||
|             die("Failed to create users table: " . $this->connection->error); | ||||
|         } | ||||
|  | ||||
|         // Create progress table | ||||
|         $progressTableQuery = "CREATE TABLE IF NOT EXISTS progress ( | ||||
|             id INT AUTO_INCREMENT PRIMARY KEY, | ||||
|             user_id INT NOT NULL, | ||||
|             habit_id INT NOT NULL, | ||||
|             date DATE NOT NULL, | ||||
|             status ENUM('Done', 'Pending') DEFAULT 'Pending', | ||||
|             FOREIGN KEY (user_id) REFERENCES users(id) | ||||
|         ) ENGINE=InnoDB;"; | ||||
|         $vehiclesTableQuery = " | ||||
|             CREATE TABLE IF NOT EXISTS vehicles ( | ||||
|                 id INT AUTO_INCREMENT PRIMARY KEY, | ||||
|                 user_id INT NOT NULL, | ||||
|                 name VARCHAR(100) NOT NULL, | ||||
|                 registration_plate VARCHAR(50) NOT NULL UNIQUE, | ||||
|                 fuel_type ENUM('Diesel', 'Gasoline 95', 'Gasoline 98', 'Premium Diesel', 'Premium Gasoline 95', 'Premium Gasoline 98', 'Other') NOT NULL, | ||||
|                 note VARCHAR(150) NULL, | ||||
|                 is_default BOOLEAN DEFAULT FALSE NOT NULL, | ||||
|                 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||||
|                 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE | ||||
|             ) ENGINE=InnoDB; | ||||
|         "; | ||||
|  | ||||
|         if (!$this->connection->query($progressTableQuery)) { | ||||
|             die("Failed to create progress table: " . $this->connection->error); | ||||
|         if (!$this->connection->query($vehiclesTableQuery)) { | ||||
|             die("Failed to create vehicles table: " . $this->connection->error); | ||||
|         } | ||||
|  | ||||
|          | ||||
|         // Create habits table | ||||
|         $habitsTableQuery = "CREATE TABLE IF NOT EXISTS habits ( | ||||
|         $refuelingTableQuery = " | ||||
|             CREATE TABLE IF NOT EXISTS refueling_records ( | ||||
|                 id INT AUTO_INCREMENT PRIMARY KEY, | ||||
|                 user_id INT NOT NULL, | ||||
|                 vehicle_id INT NOT NULL, | ||||
|                 fuel_type ENUM('Diesel', 'Gasoline 95', 'Gasoline 98', 'Premium Diesel', 'Premium Gasoline 95', 'Premium Gasoline 98', 'Other') NOT NULL, | ||||
|                 note VARCHAR(150) NULL, | ||||
|                 liters DOUBLE(10, 2) NOT NULL, | ||||
|                 price_per_liter DOUBLE(10, 2) NOT NULL, | ||||
|                 total_price DOUBLE(10, 2) NOT NULL, | ||||
|                 mileage INT UNSIGNED NULL, | ||||
|                 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||||
|                 FOREIGN KEY (vehicle_id) REFERENCES vehicles(id) ON DELETE CASCADE, | ||||
|                 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE | ||||
|             ) ENGINE=InnoDB; | ||||
|         "; | ||||
|  | ||||
|         if (!$this->connection->query($refuelingTableQuery)) { | ||||
|             die("Failed to create refueling_records table: " . $this->connection->error); | ||||
|         } | ||||
|  | ||||
|         /* | ||||
|         // Create table_name table | ||||
|         $usersTableQuery = "CREATE TABLE IF NOT EXISTS table_name ( | ||||
|             id INT AUTO_INCREMENT PRIMARY KEY, | ||||
|             user_id INT NOT NULL, | ||||
|             title VARCHAR(100) NOT NULL, | ||||
|             frequency ENUM('Daily', 'Weekly', 'Custom') NOT NULL, | ||||
|             custom_frequency VARCHAR(255) DEFAULT NULL, -- Store crontab-like string | ||||
|             reward_points INT DEFAULT 1, | ||||
|             created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||||
|             FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE | ||||
|             username VARCHAR(50) NOT NULL, | ||||
|             created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP | ||||
|         ) ENGINE=InnoDB;"; | ||||
|  | ||||
|         if (!$this->connection->query($habitsTableQuery)) { | ||||
|             die("Failed to create habits table: " . $this->connection->error); | ||||
|         if (!$this->connection->query($usersTableQuery)) { | ||||
|             die("Failed to create table_name table: " . $this->connection->error); | ||||
|         } | ||||
|         */ | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -12,6 +12,15 @@ class Validator { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Check if a field contains numbers | ||||
|      */ | ||||
|     public function number($field, $value, $message = null) { | ||||
|         if(!is_numeric($value)) { | ||||
|             $this->errors[$field] = $message ?? "$field must be an number"; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Check if a field meets minimum length | ||||
|      */ | ||||
| @@ -46,6 +55,10 @@ class Validator { | ||||
|         return $this->errors; | ||||
|     } | ||||
|  | ||||
|     public function setErrors($errors) { | ||||
|         $this->errors = $errors; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Check if validation passed | ||||
|      */ | ||||
|   | ||||
| @@ -6,6 +6,8 @@ services: | ||||
|       MARIADB_ROOT_PASSWORD: root | ||||
|     ports: | ||||
|       - 3306:3306 | ||||
|     networks: | ||||
|       - fuelstats-network | ||||
|     profiles: ["prod", "dev"] | ||||
|  | ||||
|   phpmyadmin: | ||||
| @@ -15,9 +17,14 @@ services: | ||||
|       - 8080:80 | ||||
|     environment: | ||||
|       - PMA_ARBITRARY=1 | ||||
|       - PMA_HOST=mariadb | ||||
|     depends_on: | ||||
|       - mariadb | ||||
|     networks: | ||||
|       - fuelstats-network | ||||
|     profiles: ["dev"] | ||||
|  | ||||
|   habittracker: | ||||
|   fuelstats: | ||||
|     build: | ||||
|       context: . | ||||
|       dockerfile: Dockerfile | ||||
| @@ -28,4 +35,10 @@ services: | ||||
|     depends_on: | ||||
|       - mariadb | ||||
|     restart: on-failure:2 | ||||
|     networks: | ||||
|       - fuelstats-network | ||||
|     profiles: ["prod"] | ||||
|  | ||||
| networks: | ||||
|   fuelstats-network: | ||||
|     driver: bridge | ||||
|   | ||||
| @@ -9,13 +9,30 @@ | ||||
|   flex-wrap: wrap; | ||||
|   gap: 1rem; | ||||
|   justify-content: center; | ||||
|   margin-top: 2rem; | ||||
| } | ||||
|  | ||||
| .card { | ||||
|   background-color: var(--clr-secondary); | ||||
|   border-radius: var(--border-radious); | ||||
|   border: var(--borderWidth-thin) solid var(--clr-border); | ||||
|   min-width: 17rem; | ||||
|   width: 18rem; | ||||
|   padding: 1rem; | ||||
| } | ||||
|  | ||||
| #btn-offline-add { | ||||
|   display: none; | ||||
| } | ||||
|  | ||||
| .alert-warning { | ||||
|   background-color: var(--clr-warning-muted); | ||||
|   border: var(--borderWidth-thin) solid var(--clr-border-danger); | ||||
|   padding: 1rem; | ||||
|   border-radius: var(--border-radious); | ||||
| } | ||||
|  | ||||
| .alert-danger { | ||||
|   background-color: var(--clr-danger-muted); | ||||
|   border: var(--borderWidth-thin) solid var(--clr-border-danger); | ||||
|   padding: 1rem; | ||||
|   border-radius: var(--border-radious); | ||||
| } | ||||
|   | ||||
| @@ -20,13 +20,15 @@ h1 { | ||||
| .btn-secondary, | ||||
| .btn-tertiary, | ||||
| .btn-green, | ||||
| .btn-danger { | ||||
| .btn-danger, | ||||
| .btn-warning { | ||||
| 	background-color: var(--clr-primary); | ||||
| 	padding: .5rem; | ||||
| 	text-decoration: none; | ||||
| 	cursor: pointer; | ||||
| 	border-radius: var(--border-radious); | ||||
| 	border: var(--borderWidth-thin) solid var(--clr-border); | ||||
| 	color: white; | ||||
| } | ||||
|  | ||||
| .btn-secondary { | ||||
| @@ -45,3 +47,13 @@ h1 { | ||||
| 	background-color: var(--clr-danger-muted); | ||||
| 	border: var(--borderWidth-thin) solid var(--clr-border-danger); | ||||
| } | ||||
|  | ||||
| .btn-warning { | ||||
| 	background-color: var(--clr-warning-muted); | ||||
| 	border: var(--borderWidth-thin) solid var(--clr-border-danger); | ||||
| } | ||||
|  | ||||
| #actions { | ||||
| 	padding-top: 1rem; | ||||
| 	padding-bottom: 2rem; | ||||
| } | ||||
|   | ||||
| @@ -1,21 +0,0 @@ | ||||
| .form form .dow_chb_wrapper input[type="checkbox"] { | ||||
|   width: 1rem; | ||||
| } | ||||
|  | ||||
| .form form .dow_chb_wrapper { | ||||
|   display: flex; | ||||
|   justify-content: space-between; | ||||
| } | ||||
|  | ||||
| #lbl_dow { | ||||
|   margin-bottom: .5rem; | ||||
| } | ||||
|  | ||||
| #lbl_dom { | ||||
|   margin-top: .5rem; | ||||
| } | ||||
|  | ||||
| #custom-frequency { | ||||
|   flex-direction: column; | ||||
|   justify-content: space-between; | ||||
| } | ||||
| @@ -1,15 +0,0 @@ | ||||
| .habits-wrapper { | ||||
|   display: flex; | ||||
|   gap: 1rem; | ||||
|   flex-wrap: wrap; | ||||
|   justify-content: center; | ||||
| } | ||||
|  | ||||
| .habits .bordered { | ||||
|   border-radius: var(--border-radious); | ||||
|   border: var(--borderWidth-thin) solid var(--clr-border); | ||||
|   width: 17rem; | ||||
|   padding: 1rem; | ||||
|   margin-top: 1rem; | ||||
|   text-align: center; | ||||
| } | ||||
| @@ -5,6 +5,7 @@ | ||||
|   --clr-tertiary: #151b23; | ||||
|   --clr-green: #238636; | ||||
|   --clr-danger-muted: #f851491a; | ||||
|   --clr-warning-muted: #e08e455e; | ||||
|  | ||||
|   --clr-link-blue: #4493f8; | ||||
|   --clr-light-blue: #39a2ae; | ||||
|   | ||||
							
								
								
									
										3
									
								
								public/css/vehicle_create.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								public/css/vehicle_create.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| #registration_plate { | ||||
|   text-transform: uppercase; | ||||
| } | ||||
							
								
								
									
										29
									
								
								public/css/vehicles.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								public/css/vehicles.css
									
									
									
									
									
										Normal 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; | ||||
| } | ||||
| @@ -20,7 +20,8 @@ require_once '../core/Database.php'; | ||||
| require_once '../core/middlewares/RequireAuth.php'; | ||||
|  | ||||
| require_once models . 'User.php'; | ||||
| require_once models . 'Habit.php'; | ||||
| require_once models . 'Vehicle.php'; | ||||
| require_once models . 'Refuel.php'; | ||||
|  | ||||
| // Initialize router | ||||
| $router = new Router(); | ||||
| @@ -40,12 +41,22 @@ $router->group('/auth', [], function ($router) { | ||||
| // dashboard route | ||||
| $router->add('/dashboard', 'DashboardController@index', ['RequireAuth']); | ||||
|  | ||||
| // habits routes | ||||
| $router->group('/habits', ['RequireAuth'], function ($router) { | ||||
|     $router->add('', 'HabitController@index'); | ||||
|     $router->add('/create', 'HabitController@create'); | ||||
|     $router->add('/edit/{id}', 'HabitController@edit'); | ||||
|     $router->add('/delete/{id}', 'HabitController@delete'); | ||||
| // vehicle routes | ||||
| $router->group('/vehicles', ['RequireAuth'], function ($router) { | ||||
|     $router->add('', 'VehicleController@index'); | ||||
|     $router->add('/create', 'VehicleController@create'); | ||||
|     $router->add('/edit/{id}', 'VehicleController@edit'); | ||||
|     $router->add('/delete', 'VehicleController@delete'); | ||||
|     $router->add('/default', 'VehicleController@setDefault'); | ||||
| }); | ||||
|  | ||||
| $router->group('/refuel', ['RequireAuth'], function ($router) { | ||||
|     $router->add('/create', 'RefuelController@create'); | ||||
| }); | ||||
|  | ||||
| // API | ||||
| $router->group('/api/v1', ['RequireAuth'], function ($router) { | ||||
|     $router->add('/vehicles/get', 'VehicleController@api_get'); | ||||
| }); | ||||
|  | ||||
| $router->dispatch(); | ||||
|   | ||||
							
								
								
									
										247
									
								
								public/js/offline-records.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										247
									
								
								public/js/offline-records.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,247 @@ | ||||
| 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")) { | ||||
| 			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) { | ||||
| 				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> | ||||
|         <!-- <?php if ($this->get('error')): ?> --> | ||||
|         <!--     <div class="error" style="color: red; margin-bottom: 1rem;"> --> | ||||
|         <!--         <?= htmlspecialchars($this->get('error')) ?> --> | ||||
|         <!--     </div> --> | ||||
|         <!-- <?php endif; ?> --> | ||||
|         <form id="offline_refuel_add"> | ||||
|             <label for="vehicle">Vehicle</label> | ||||
|             <select name="vehicle" id="vehicle"> | ||||
|             ${vehicles | ||||
| 							.map( | ||||
| 								(el) => | ||||
| 									`<option value="${el.id}">${el.name} | ${el.registration_plate}</option>`, | ||||
| 							) | ||||
| 							.join("")} | ||||
|             <!-- <?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="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="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="0.0"> | ||||
|             <!-- <?php if (isset($this->get('validationErrors')['total_price'])): ?> --> | ||||
|             <!--     <small class="error"><?= $this->get('validationErrors')['total_price'] ?></small> --> | ||||
|             <!-- <?php endif; ?> --> | ||||
|  | ||||
|         		<label for="mileage">Mileage</label> | ||||
|         		<input type="number" name="mileage" id="mileage" min="0" step="1" value="0"> | ||||
|         		<!-- <?php if (isset($this->get('validationErrors')['mileage'])): ?> --> | ||||
|          		<!--    	<small class="error"><?= $this->get('validationErrors')['mileage'] ?></small> --> | ||||
|         		<!-- <?php endif; ?> --> | ||||
|  | ||||
|             <label for="note">Note</label> | ||||
|             <input type="text" name="note" id="note"> | ||||
|             <!-- <?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"> | ||||
|         </form> | ||||
|     </section> | ||||
|   `; | ||||
| 	document.querySelector("main").appendChild(offline); | ||||
| 	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(); | ||||
| 	}); | ||||
| }); | ||||
		Reference in New Issue
	
	Block a user