Compare commits

...

104 Commits

Author SHA1 Message Date
Hargata Softworks
96914b3db4 Merge pull request #39 from hargata/Hargata/upgradestab
Upgrades Tab
2024-01-08 19:04:57 -07:00
DESKTOP-GENO133\IvanPlex
7ad6d3369d updated field placeholders 2024-01-08 19:00:30 -07:00
DESKTOP-GENO133\IvanPlex
0ae28d436b added upgrade tab functionality. 2024-01-08 18:33:21 -07:00
DESKTOP-T0O5CDB\DESK-555BD
4575cf338f Added upgrades section backend. 2024-01-08 17:47:11 -07:00
Hargata Softworks
c526a9f207 Merge pull request #37 from hargata/Hargata/uk.mpg
Hargata/uk.mpg
2024-01-08 16:11:39 -07:00
DESKTOP-T0O5CDB\DESK-555BD
1a3282c1ef removed unnecessary lines. 2024-01-08 16:11:02 -07:00
DESKTOP-T0O5CDB\DESK-555BD
f52e5b49c7 rehashed logic. 2024-01-08 15:31:02 -07:00
DESKTOP-T0O5CDB\DESK-555BD
507fb6daf6 refreshed logic. 2024-01-08 15:30:15 -07:00
Hargata Softworks
9d3fbd05fc Merge pull request #35 from hargata/Hargata/uk.mpg
added support for UK MPG
2024-01-08 15:14:00 -07:00
DESKTOP-T0O5CDB\DESK-555BD
2a0f884e89 fixed unit cost for gas. 2024-01-08 15:13:26 -07:00
DESKTOP-T0O5CDB\DESK-555BD
26012bf27a added support for UK MPG 2024-01-08 15:11:23 -07:00
Hargata Softworks
3fa3dbaa8c Merge pull request #32 from hargata/Hargata/ghcr.multiplatform
removing arm64 support for now until I can figure out a workaround fo…
2024-01-08 13:49:12 -07:00
DESKTOP-T0O5CDB\DESK-555BD
95cac60c71 removing arm64 support for now until I can figure out a workaround for qemu. 2024-01-08 13:48:51 -07:00
Hargata Softworks
7878ce65ca Merge pull request #31 from hargata/Hargata/ghcr.multiplatform
...
2024-01-08 13:44:30 -07:00
DESKTOP-T0O5CDB\DESK-555BD
c6c57a5de6 ... 2024-01-08 13:44:08 -07:00
Hargata Softworks
2ece3cd113 Merge pull request #30 from hargata/Hargata/ghcr.multiplatform
Hargata/ghcr.multiplatform
2024-01-08 13:37:02 -07:00
DESKTOP-T0O5CDB\DESK-555BD
9504674933 tab 2024-01-08 13:36:52 -07:00
DESKTOP-T0O5CDB\DESK-555BD
3c0bdcea0e approach 2 2024-01-08 13:35:39 -07:00
DESKTOP-T0O5CDB\DESK-555BD
a4762d5b32 lil disappointment 2024-01-08 13:26:24 -07:00
Hargata Softworks
dc18bb5b8f Merge pull request #29 from hargata/Hargata/ghcr.multiplatform
lemme try this.
2024-01-08 13:15:54 -07:00
DESKTOP-T0O5CDB\DESK-555BD
452248e681 lemme try this. 2024-01-08 13:15:32 -07:00
Hargata Softworks
a7ea03014f Merge pull request #28 from hargata/Hargata/ghcr.multiplatform
Multi Platform Docker Image
2024-01-08 12:49:16 -07:00
DESKTOP-T0O5CDB\DESK-555BD
cb89ca3426 hm 2024-01-08 12:47:00 -07:00
DESKTOP-T0O5CDB\DESK-555BD
760f7e1888 I wondre if this works 2024-01-08 12:44:07 -07:00
Hargata Softworks
ba149568a5 Merge pull request #27 from hargata/Hargata/rename.uploadedfiles
Rename Uploaded Files
2024-01-08 11:25:50 -07:00
DESKTOP-GENO133\IvanPlex
2ef281bd0a added setting to hide zero costs with dashes 2024-01-08 11:24:51 -07:00
DESKTOP-GENO133\IvanPlex
859796cfe8 fixed file deletion button not working. 2024-01-08 11:00:19 -07:00
DESKTOP-GENO133\IvanPlex
f1e0254f95 added ability to rename uploaded files on tax records. 2024-01-08 10:53:44 -07:00
DESKTOP-GENO133\IvanPlex
70d0827432 added functionality to edit uploaded file names for gas records. 2024-01-08 10:50:41 -07:00
DESKTOP-GENO133\IvanPlex
851a7af287 added functionality to rename uploaded files in collision records. 2024-01-08 10:34:45 -07:00
DESKTOP-GENO133\IvanPlex
e46be7ba0a added ability to rename files on service record page. 2024-01-08 09:05:09 -07:00
Hargata Softworks
b4258dc11a Merge pull request #24 from hargata/fuelly.import
Fuelly.import
2024-01-07 23:14:06 -07:00
DESKTOP-GENO133\IvanPlex
0f35621846 config 2024-01-07 23:08:44 -07:00
DESKTOP-GENO133\IvanPlex
374f919296 added description to mapper 2024-01-07 23:04:20 -07:00
DESKTOP-GENO133\IvanPlex
33b824b316 fuelly import but can only import one csv per car. 2024-01-07 23:00:07 -07:00
Hargata Softworks
418e468f14 Update README.md 2024-01-07 16:42:52 -07:00
Hargata Softworks
0a1f6a569d Merge pull request #22 from hargata/electric.vehicle
fixed datepicker format not parsing correctly.
2024-01-07 16:12:40 -07:00
DESKTOP-GENO133\IvanPlex
2a842d1c8c fixed datepicker format not parsing correctly. 2024-01-07 16:12:16 -07:00
DESKTOP-T0O5CDB\DESK-555BD
4f76991840 persist dataprotection keys. 2024-01-07 15:10:32 -07:00
Hargata Softworks
0487feb35e Merge pull request #20 from hargata/electric.vehicle
dumb
2024-01-07 14:55:22 -07:00
DESKTOP-GENO133\IvanPlex
70308ed6eb dumb 2024-01-07 14:54:52 -07:00
Hargata Softworks
bfdef5d296 Merge pull request #19 from hargata/electric.vehicle
minor oversight.
2024-01-07 14:49:59 -07:00
DESKTOP-GENO133\IvanPlex
a236c4a151 minor oversight. 2024-01-07 14:49:14 -07:00
Hargata Softworks
54d20b5573 Merge pull request #18 from hargata/electric.vehicle
moved electric vehicle flag to vehicle level.
2024-01-07 14:32:59 -07:00
DESKTOP-GENO133\IvanPlex
ecd2b83cf0 moved electric vehicle flag to vehicle level. 2024-01-07 14:31:04 -07:00
Hargata Softworks
80504e71c9 Merge pull request #17 from hargata/Hargata/reminders
dirty fix for null date strings.
2024-01-07 12:09:48 -07:00
DESKTOP-GENO133\IvanPlex
fb272c9c40 dirty fix for null date strings. 2024-01-07 12:09:05 -07:00
Hargata Softworks
1c31477c37 Merge pull request #16 from hargata/Hargata/reminders
Hargata/reminders
2024-01-07 11:52:35 -07:00
DESKTOP-GENO133\IvanPlex
2ecd286aa1 quality of life improvement 2024-01-07 11:49:27 -07:00
DESKTOP-GENO133\IvanPlex
f28af456b3 Add Reminder function. 2024-01-07 11:28:17 -07:00
DESKTOP-GENO133\IvanPlex
c05b5e4c3d added reminder refresh and count 2024-01-07 10:47:36 -07:00
DESKTOP-GENO133\IvanPlex
0f70f8212b added past due urgency 2024-01-07 09:14:51 -07:00
DESKTOP-GENO133\IvanPlex
f805e311b0 Merge branch 'main' into Hargata/reminders
# Conflicts:
#	Controllers/HomeController.cs
2024-01-07 08:45:32 -07:00
Hargata Softworks
42f7bd298c Merge pull request #15 from hargata/Hargata/ghcr.auto
fixed short date pattern for datetimepicker.
2024-01-07 07:54:11 -07:00
DESKTOP-GENO133\IvanPlex
e64d4f75b5 fixed short date pattern for datetimepicker. 2024-01-07 07:52:45 -07:00
Hargata Softworks
b972d5b7e5 Merge pull request #14 from hargata/Hargata/ghcr.auto
Update docker compose file and move env to own file.
2024-01-07 07:30:05 -07:00
DESKTOP-GENO133\IvanPlex
f639c2c38b accidentally comitted my volume configs, fixed bug related to missing userconfig file. 2024-01-07 07:28:52 -07:00
DESKTOP-GENO133\IvanPlex
4e92155f5b Update docker compose file and move env to own file. 2024-01-07 07:13:06 -07:00
Hargata Softworks
78857c1b79 Merge pull request #11 from hargata/Hargata/ghcr.auto
auto publish to GHCR
2024-01-06 22:07:33 -07:00
DESKTOP-GENO133\IvanPlex
4e5d893850 removed pull request that forces a branch on main. 2024-01-06 22:07:07 -07:00
DESKTOP-GENO133\IvanPlex
a1fe446c2a quotes 2024-01-06 22:04:09 -07:00
DESKTOP-GENO133\IvanPlex
f126a309f0 try again 2024-01-06 22:01:43 -07:00
DESKTOP-GENO133\IvanPlex
b2b129389a test 2024-01-06 22:00:45 -07:00
DESKTOP-GENO133\IvanPlex
f2386fc9d8 weird. 2024-01-06 21:49:29 -07:00
DESKTOP-GENO133\IvanPlex
d625a91ed7 auto publish to GHCR 2024-01-06 21:46:29 -07:00
DESKTOP-GENO133\IvanPlex
b5f8d2d44e more partial views and logic for reminder. 2024-01-06 21:32:11 -07:00
DESKTOP-GENO133\IvanPlex
206b053d27 added enum to reminder metric. add reminder modal. 2024-01-06 18:57:23 -07:00
DESKTOP-GENO133\IvanPlex
a2d16d7643 inject reminderrecord data access into vehiclecontroller. 2024-01-06 16:04:06 -07:00
DESKTOP-GENO133\IvanPlex
89345fd8eb static helper for userconfig path. 2024-01-06 15:47:21 -07:00
DESKTOP-GENO133\IvanPlex
4abf7fbba2 added static helper and ReminderRecord models 2024-01-06 15:44:07 -07:00
DESKTOP-GENO133\IvanPlex
b5987927be data access for reminders. 2024-01-06 13:09:34 -07:00
Hargata Softworks
bfef9b9498 Create docker-image.yml 2024-01-06 13:02:22 -07:00
DESKTOP-GENO133\IvanPlex
6b4bbd8410 Updated readme and spacing for columns on gas table. 2024-01-06 12:23:16 -07:00
DESKTOP-GENO133\IvanPlex
05f89073cd added env. 2024-01-06 12:21:09 -07:00
Hargata Softworks
3776d6e11f Merge pull request #8 from florianschroen/cleanup-docker-compose
rename docker-compose files and remove unused network "app"
2024-01-06 11:07:30 -07:00
Florian Schroen
2e301760f4 rename docker-compose files and remove unused network "app"
the default docker compose file should be minimal and easy to read as docker-compose.yml

other docker-compose variants for typical setup should be named as:
 docker-compose.<variant>.yml

this way one could use e.g. `docker compose -f docker-compose.traefik.yml up` to
start the variant for traefik. (mind keeping the file extension)

and fixed a typo the README.md
2024-01-06 19:01:18 +01:00
DESKTOP-GENO133\IvanPlex
2817b5914f huh turns out docker compose build and docker compose up do different things. 2024-01-06 10:53:56 -07:00
DESKTOP-GENO133\IvanPlex
8276d8d720 g dnag it 2024-01-06 10:52:23 -07:00
DESKTOP-GENO133\IvanPlex
711c7c14f7 updated readme so it's laid out better 2024-01-06 10:51:53 -07:00
DESKTOP-GENO133\IvanPlex
f651f53ee6 added no traefik docker compose, removed old docker compose file. 2024-01-06 10:48:12 -07:00
Hargata Softworks
c00ea88f73 Merge pull request #6 from florianschroen/dockerize
Dockerize
2024-01-06 10:42:13 -07:00
DESKTOP-GENO133\IvanPlex
583c83643a added dashes when MPG is not available. 2024-01-06 10:30:25 -07:00
DESKTOP-GENO133\IvanPlex
0775ede344 folder name 2024-01-06 10:28:09 -07:00
DESKTOP-GENO133\IvanPlex
bf8ecdbe7a hm. 2024-01-06 10:13:28 -07:00
DESKTOP-GENO133\IvanPlex
ed81d53175 make user config persistable 2024-01-06 10:10:48 -07:00
DESKTOP-GENO133\IvanPlex
4543e56c61 added logic to overwrite consumption units with kwh 2024-01-06 09:57:08 -07:00
Florian Schroen
401724b66b add docker-compose.yml, refactor for docker volume usage
user defined data should not be mixed with static application data in
one directory.

therefore we need to move some files like db and userconfig to separate
directories, which can then be declared as docker volumes.
2024-01-06 17:56:10 +01:00
DESKTOP-GENO133\IvanPlex
b0773f5102 lol paths 2024-01-06 09:41:36 -07:00
DESKTOP-GENO133\IvanPlex
cff9289c39 update readme again 2024-01-06 09:31:25 -07:00
DESKTOP-GENO133\IvanPlex
69cfac1c6f added docker compose 2024-01-06 09:30:10 -07:00
DESKTOP-GENO133\IvanPlex
0b05315671 make data folder if not exist, updated Dockerfile so we no longer specify a specific port, we are now defaulting to 8080 for internal port 2024-01-06 09:14:28 -07:00
Hargata Softworks
af9c96c002 Merge pull request #5 from FFCoder/dockerSupport
dockerSupport: Added Basic Docker support to the application.
2024-01-06 09:01:39 -07:00
DESKTOP-GENO133\IvanPlex
f1d4973f59 resolve conflict 2024-01-06 09:00:35 -07:00
DESKTOP-GENO133\IvanPlex
25db16c47f added kwh setting 2024-01-06 08:53:45 -07:00
Jonathon Chambers
1286974fb6 dockerSupport: Added expose to the dockerfile. Can be used without but still nice to add. 2024-01-06 10:49:25 -05:00
Jonathon Chambers
997c045312 dockerSupport: Removed readme in data dir. Not needed 2024-01-06 10:46:20 -05:00
Jonathon Chambers
71c2e64daf dockerSupport: Added Basic Docker support to the application. 2024-01-06 10:42:14 -05:00
DESKTOP-GENO133\IvanPlex
1c2368f5a1 added option to mark gas record as not filled to full so that MPG calculations are deferred 2024-01-05 22:53:27 -07:00
DESKTOP-GENO133\IvanPlex
208eb22b8c updated license 2024-01-05 14:15:21 -07:00
DESKTOP-GENO133\IvanPlex
25e32589d4 disabled CSV imports by default. 2024-01-05 11:51:03 -07:00
DESKTOP-GENO133\IvanPlex
1727f0d193 placeholder for tax record modal 2024-01-05 11:50:15 -07:00
DESKTOP-GENO133\IvanPlex
0faffc2167 usability enhancements added placeholder text. 2024-01-05 11:22:44 -07:00
DESKTOP-GENO133\IvanPlex
999649a095 Usability enhancements: placeholders in vehicle and service record modals 2024-01-05 09:07:28 -07:00
DESKTOP-GENO133\IvanPlex
cfb8e6ea55 Usability enhancements: allow login on enter key, added icons on login and logout buttons, added placeholder on gas modal 2024-01-05 08:52:48 -07:00
75 changed files with 1897 additions and 295 deletions

2
.env Normal file
View File

@@ -0,0 +1,2 @@
LC_ALL=en_US.UTF-8
LANG=en_US.UTF-8

31
.github/workflows/docker-image.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
name: Docker Image CI
on:
push:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: "hargata"
password: "${{ secrets.GHCR_PAT }}"
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64
push: true
tags: |
ghcr.io/hargata/lubelogger:latest

1
.gitignore vendored
View File

@@ -3,6 +3,7 @@ bin/
obj/
wwwroot/images/
cartracker.db
data/cartracker.db
wwwroot/documents/
wwwroot/temp/
wwwroot/imports/

View File

@@ -45,7 +45,9 @@ namespace CarCareTracker.Controllers
UseDarkMode = bool.Parse(_config[nameof(UserConfig.UseDarkMode)]),
UseMPG = bool.Parse(_config[nameof(UserConfig.UseMPG)]),
UseDescending = bool.Parse(_config[nameof(UserConfig.UseDescending)]),
EnableAuth = bool.Parse(_config[nameof(UserConfig.EnableAuth)])
EnableAuth = bool.Parse(_config[nameof(UserConfig.EnableAuth)]),
HideZero = bool.Parse(_config[nameof(UserConfig.HideZero)]),
UseUKMPG = bool.Parse(_config[nameof(UserConfig.UseUKMPG)])
};
return PartialView("_Settings", userConfig);
}
@@ -53,8 +55,13 @@ namespace CarCareTracker.Controllers
public IActionResult WriteToSettings(UserConfig userConfig)
{
try
{
var configFileContents = System.IO.File.ReadAllText("userConfig.json");
{
if (!System.IO.File.Exists(StaticHelper.UserConfigPath))
{
//if file doesn't exist it might be because it's running on a mounted volume in docker.
System.IO.File.WriteAllText(StaticHelper.UserConfigPath, System.Text.Json.JsonSerializer.Serialize(new UserConfig()));
}
var configFileContents = System.IO.File.ReadAllText(StaticHelper.UserConfigPath);
var existingUserConfig = System.Text.Json.JsonSerializer.Deserialize<UserConfig>(configFileContents);
if (existingUserConfig is not null)
{
@@ -68,7 +75,7 @@ namespace CarCareTracker.Controllers
userConfig.UserNameHash = string.Empty;
userConfig.UserPasswordHash = string.Empty;
}
System.IO.File.WriteAllText("userConfig.json", System.Text.Json.JsonSerializer.Serialize(userConfig));
System.IO.File.WriteAllText(StaticHelper.UserConfigPath, System.Text.Json.JsonSerializer.Serialize(userConfig));
return Json(true);
} catch (Exception ex)
{

View File

@@ -1,4 +1,5 @@
using CarCareTracker.Models;
using CarCareTracker.Helper;
using CarCareTracker.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Mvc;
@@ -36,7 +37,7 @@ namespace CarCareTracker.Controllers
//compare it against hashed credentials
try
{
var configFileContents = System.IO.File.ReadAllText("userConfig.json");
var configFileContents = System.IO.File.ReadAllText(StaticHelper.UserConfigPath);
var existingUserConfig = System.Text.Json.JsonSerializer.Deserialize<UserConfig>(configFileContents);
if (existingUserConfig is not null)
{
@@ -74,7 +75,7 @@ namespace CarCareTracker.Controllers
{
try
{
var configFileContents = System.IO.File.ReadAllText("userConfig.json");
var configFileContents = System.IO.File.ReadAllText(StaticHelper.UserConfigPath);
var existingUserConfig = JsonSerializer.Deserialize<UserConfig>(configFileContents);
if (existingUserConfig is not null)
{
@@ -86,7 +87,7 @@ namespace CarCareTracker.Controllers
existingUserConfig.UserNameHash = hashedUserName;
existingUserConfig.UserPasswordHash = hashedPassword;
}
System.IO.File.WriteAllText("userConfig.json", JsonSerializer.Serialize(existingUserConfig));
System.IO.File.WriteAllText(StaticHelper.UserConfigPath, JsonSerializer.Serialize(existingUserConfig));
return Json(true);
}
catch (Exception ex)
@@ -101,7 +102,7 @@ namespace CarCareTracker.Controllers
{
try
{
var configFileContents = System.IO.File.ReadAllText("userConfig.json");
var configFileContents = System.IO.File.ReadAllText(StaticHelper.UserConfigPath);
var existingUserConfig = JsonSerializer.Deserialize<UserConfig>(configFileContents);
if (existingUserConfig is not null)
{
@@ -110,7 +111,7 @@ namespace CarCareTracker.Controllers
existingUserConfig.UserNameHash = string.Empty;
existingUserConfig.UserPasswordHash = string.Empty;
}
System.IO.File.WriteAllText("userConfig.json", JsonSerializer.Serialize(existingUserConfig));
System.IO.File.WriteAllText(StaticHelper.UserConfigPath, JsonSerializer.Serialize(existingUserConfig));
//destroy any login cookies.
Response.Cookies.Delete("ACCESS_TOKEN");
return Json(true);

View File

@@ -6,6 +6,8 @@ using CarCareTracker.Helper;
using CsvHelper;
using System.Globalization;
using Microsoft.AspNetCore.Authorization;
using CarCareTracker.External.Implementations;
using CarCareTracker.MapProfile;
namespace CarCareTracker.Controllers
{
@@ -19,19 +21,23 @@ namespace CarCareTracker.Controllers
private readonly IGasRecordDataAccess _gasRecordDataAccess;
private readonly ICollisionRecordDataAccess _collisionRecordDataAccess;
private readonly ITaxRecordDataAccess _taxRecordDataAccess;
private readonly IReminderRecordDataAccess _reminderRecordDataAccess;
private readonly IUpgradeRecordDataAccess _upgradeRecordDataAccess;
private readonly IWebHostEnvironment _webEnv;
private readonly bool _useDescending;
private readonly IConfiguration _config;
private readonly IFileHelper _fileHelper;
public VehicleController(ILogger<VehicleController> logger,
IFileHelper fileHelper,
IVehicleDataAccess dataAccess,
INoteDataAccess noteDataAccess,
IServiceRecordDataAccess serviceRecordDataAccess,
public VehicleController(ILogger<VehicleController> logger,
IFileHelper fileHelper,
IVehicleDataAccess dataAccess,
INoteDataAccess noteDataAccess,
IServiceRecordDataAccess serviceRecordDataAccess,
IGasRecordDataAccess gasRecordDataAccess,
ICollisionRecordDataAccess collisionRecordDataAccess,
ITaxRecordDataAccess taxRecordDataAccess,
IReminderRecordDataAccess reminderRecordDataAccess,
IUpgradeRecordDataAccess upgradeRecordDataAccess,
IWebHostEnvironment webEnv,
IConfiguration config)
{
@@ -43,6 +49,8 @@ namespace CarCareTracker.Controllers
_gasRecordDataAccess = gasRecordDataAccess;
_collisionRecordDataAccess = collisionRecordDataAccess;
_taxRecordDataAccess = taxRecordDataAccess;
_reminderRecordDataAccess = reminderRecordDataAccess;
_upgradeRecordDataAccess = upgradeRecordDataAccess;
_webEnv = webEnv;
_config = config;
_useDescending = bool.Parse(config[nameof(UserConfig.UseDescending)]);
@@ -90,6 +98,8 @@ namespace CarCareTracker.Controllers
_collisionRecordDataAccess.DeleteAllCollisionRecordsByVehicleId(vehicleId) &&
_taxRecordDataAccess.DeleteAllTaxRecordsByVehicleId(vehicleId) &&
_noteDataAccess.DeleteNoteByVehicleId(vehicleId) &&
_reminderRecordDataAccess.DeleteAllReminderRecordsByVehicleId(vehicleId) &&
_upgradeRecordDataAccess.DeleteAllUpgradeRecordsByVehicleId(vehicleId) &&
_dataAccess.DeleteVehicle(vehicleId);
return Json(result);
}
@@ -117,12 +127,12 @@ namespace CarCareTracker.Controllers
}
#region "Bulk Imports"
[HttpGet]
public IActionResult GetBulkImportModalPartialView(string mode)
public IActionResult GetBulkImportModalPartialView(ImportMode mode)
{
return PartialView("_BulkDataImporter", mode);
}
[HttpPost]
public IActionResult ImportToVehicleIdFromCsv(int vehicleId, string mode, string fileName)
public IActionResult ImportToVehicleIdFromCsv(int vehicleId, ImportMode mode, string fileName)
{
if (vehicleId == default || string.IsNullOrWhiteSpace(fileName))
{
@@ -140,78 +150,85 @@ namespace CarCareTracker.Controllers
var config = new CsvHelper.Configuration.CsvConfiguration(System.Globalization.CultureInfo.InvariantCulture);
config.MissingFieldFound = null;
config.HeaderValidated = null;
config.PrepareHeaderForMatch = args => { return args.Header.Trim().ToLower(); };
using (var csv = new CsvReader(reader, config))
{
if (mode == "gasrecord")
csv.Context.RegisterClassMap<FuellyMapper>();
var records = csv.GetRecords<ImportModel>().ToList();
if (records.Any())
{
var records = csv.GetRecords<GasRecordImport>().ToList();
if (records.Any())
foreach (ImportModel importModel in records)
{
foreach (GasRecordImport recordToInsert in records)
if (mode == ImportMode.GasRecord)
{
//convert to gas model.
var convertedRecord = new GasRecord()
{
VehicleId = vehicleId,
Date = recordToInsert.Date,
Mileage = recordToInsert.Odometer,
Gallons = recordToInsert.FuelConsumed,
Cost = recordToInsert.Cost
Date = DateTime.Parse(importModel.Date),
Mileage = int.Parse(importModel.Odometer, NumberStyles.Any),
Gallons = decimal.Parse(importModel.FuelConsumed, NumberStyles.Any)
};
_gasRecordDataAccess.SaveGasRecordToVehicle(convertedRecord);
if (string.IsNullOrWhiteSpace(importModel.Cost) && !string.IsNullOrWhiteSpace(importModel.Price))
{
//cost was not given but price is.
//fuelly sometimes exports CSVs without total cost.
var parsedPrice = decimal.Parse(importModel.Price, NumberStyles.Any);
convertedRecord.Cost = convertedRecord.Gallons * parsedPrice;
} else
{
convertedRecord.Cost = decimal.Parse(importModel.Cost, NumberStyles.Any);
}
if (string.IsNullOrWhiteSpace(importModel.IsFillToFull) && !string.IsNullOrWhiteSpace(importModel.PartialFuelUp))
{
var parsedBool = importModel.PartialFuelUp.Trim() == "1";
convertedRecord.IsFillToFull = !parsedBool;
} else if (!string.IsNullOrWhiteSpace(importModel.IsFillToFull))
{
var parsedBool = importModel.IsFillToFull.Trim() == "1" || importModel.IsFillToFull.Trim() == "Full";
convertedRecord.IsFillToFull = parsedBool;
}
//insert record into db, check to make sure fuelconsumed is not zero so we don't get a divide by zero error.
if (convertedRecord.Gallons > 0)
{
_gasRecordDataAccess.SaveGasRecordToVehicle(convertedRecord);
}
}
}
} else if (mode == "servicerecord")
{
var records = csv.GetRecords<ServiceRecordImport>().ToList();
if (records.Any())
{
foreach (ServiceRecordImport recordToInsert in records)
else if (mode == ImportMode.ServiceRecord)
{
var convertedRecord = new ServiceRecord()
{
VehicleId = vehicleId,
Date = recordToInsert.Date,
Mileage = recordToInsert.Odometer,
Description = recordToInsert.Description,
Notes = recordToInsert.Notes,
Cost = recordToInsert.Cost
Date = DateTime.Parse(importModel.Date),
Mileage = int.Parse(importModel.Odometer, NumberStyles.Any),
Description = string.IsNullOrWhiteSpace(importModel.Description) ? $"Service Record on {importModel.Date}" : importModel.Description,
Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes,
Cost = decimal.Parse(importModel.Cost, NumberStyles.Any)
};
_serviceRecordDataAccess.SaveServiceRecordToVehicle(convertedRecord);
}
}
} else if (mode == "repairrecord")
{
var records = csv.GetRecords<ServiceRecordImport>().ToList();
if (records.Any())
{
foreach (ServiceRecordImport recordToInsert in records)
else if (mode == ImportMode.RepairRecord)
{
var convertedRecord = new CollisionRecord()
{
VehicleId = vehicleId,
Date = recordToInsert.Date,
Mileage = recordToInsert.Odometer,
Description = recordToInsert.Description,
Notes = recordToInsert.Notes,
Cost = recordToInsert.Cost
Date = DateTime.Parse(importModel.Date),
Mileage = int.Parse(importModel.Odometer, NumberStyles.Any),
Description = string.IsNullOrWhiteSpace(importModel.Description) ? $"Repair Record on {importModel.Date}" : importModel.Description,
Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes,
Cost = decimal.Parse(importModel.Cost, NumberStyles.Any)
};
_collisionRecordDataAccess.SaveCollisionRecordToVehicle(convertedRecord);
}
}
} else if (mode == "taxrecord")
{
var records = csv.GetRecords<TaxRecordImport>().ToList();
if (records.Any())
{
foreach (TaxRecordImport recordToInsert in records)
else if (mode == ImportMode.TaxRecord)
{
var convertedRecord = new TaxRecord()
{
VehicleId = vehicleId,
Date = recordToInsert.Date,
Description = recordToInsert.Description,
Notes = recordToInsert.Notes,
Cost = recordToInsert.Cost
Date = DateTime.Parse(importModel.Date),
Description = string.IsNullOrWhiteSpace(importModel.Description) ? $"Tax Record on {importModel.Date}" : importModel.Description,
Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes,
Cost = decimal.Parse(importModel.Cost, NumberStyles.Any)
};
_taxRecordDataAccess.SaveTaxRecordToVehicle(convertedRecord);
}
@@ -237,49 +254,83 @@ namespace CarCareTracker.Controllers
result = result.OrderBy(x => x.Date).ThenBy(x => x.Mileage).ToList();
//check if the user uses MPG or Liters per 100km.
bool useMPG = bool.Parse(_config[nameof(UserConfig.UseMPG)]);
bool useUKMPG = bool.Parse(_config[nameof(UserConfig.UseUKMPG)]);
var computedResults = new List<GasRecordViewModel>();
int previousMileage = 0;
decimal unFactoredConsumption = 0.00M;
int unFactoredMileage = 0;
//perform computation.
for(int i = 0; i < result.Count; i++)
for (int i = 0; i < result.Count; i++)
{
var currentObject = result[i];
decimal convertedConsumption;
if (useUKMPG && useMPG)
{
//if we're using UK MPG and the user wants imperial calculation insteace of l/100km
//if UK MPG is selected then the gas consumption are stored in liters but need to convert into UK gallons for computation.
convertedConsumption = currentObject.Gallons / 4.546M;
} else
{
convertedConsumption = currentObject.Gallons;
}
if (i > 0)
{
var currentObject = result[i];
var deltaMileage = currentObject.Mileage - previousMileage;
var gasRecordViewModel = new GasRecordViewModel()
{
Id = currentObject.Id,
VehicleId = currentObject.VehicleId,
Date = currentObject.Date.ToShortDateString(),
Mileage = currentObject.Mileage,
Gallons = convertedConsumption,
Cost = currentObject.Cost,
DeltaMileage = deltaMileage,
CostPerGallon = (currentObject.Cost / convertedConsumption)
};
if (currentObject.IsFillToFull)
{
//if user filled to full.
gasRecordViewModel.MilesPerGallon = useMPG ? ((unFactoredMileage + deltaMileage) / (unFactoredConsumption + convertedConsumption)) : 100 / ((unFactoredMileage + deltaMileage) / (unFactoredConsumption + convertedConsumption));
//reset unFactored vars
unFactoredConsumption = 0;
unFactoredMileage = 0;
}
else
{
unFactoredConsumption += convertedConsumption;
unFactoredMileage += deltaMileage;
gasRecordViewModel.MilesPerGallon = 0;
}
computedResults.Add(gasRecordViewModel);
}
else
{
computedResults.Add(new GasRecordViewModel()
{
Id = currentObject.Id,
VehicleId = currentObject.VehicleId,
Date = currentObject.Date.ToShortDateString(),
Mileage = currentObject.Mileage,
Gallons = currentObject.Gallons,
Gallons = convertedConsumption,
Cost = currentObject.Cost,
DeltaMileage = deltaMileage,
MilesPerGallon = useMPG ? (deltaMileage / currentObject.Gallons) : 100 / (deltaMileage / currentObject.Gallons),
CostPerGallon = (currentObject.Cost / currentObject.Gallons)
});
} else
{
computedResults.Add(new GasRecordViewModel()
{
Id = result[i].Id,
VehicleId = result[i].VehicleId,
Date = result[i].Date.ToShortDateString(),
Mileage = result[i].Mileage,
Gallons = result[i].Gallons,
Cost = result[i].Cost,
DeltaMileage = 0,
MilesPerGallon = 0,
CostPerGallon = (result[i].Cost / result[i].Gallons)
CostPerGallon = (currentObject.Cost / convertedConsumption)
});
}
previousMileage = result[i].Mileage;
previousMileage = currentObject.Mileage;
}
if (_useDescending)
{
computedResults = computedResults.OrderByDescending(x => DateTime.Parse(x.Date)).ThenByDescending(x => x.Mileage).ToList();
}
return PartialView("_Gas", computedResults);
var vehicleIsElectric = _dataAccess.GetVehicleById(vehicleId).IsElectric;
var viewModel = new GasRecordViewModelContainer()
{
UseKwh = vehicleIsElectric,
GasRecords = computedResults
};
return PartialView("_Gas", viewModel);
}
[HttpPost]
public IActionResult SaveGasRecordToVehicleId(GasRecordInput gasRecord)
@@ -291,7 +342,7 @@ namespace CarCareTracker.Controllers
[HttpGet]
public IActionResult GetAddGasRecordPartialView()
{
return PartialView("_GasModal", new GasRecordInput());
return PartialView("_GasModal", new GasRecordInputContainer() { GasRecord = new GasRecordInput() });
}
[HttpGet]
public IActionResult GetGasRecordForEditById(int gasRecordId)
@@ -305,9 +356,16 @@ namespace CarCareTracker.Controllers
Cost = result.Cost,
Date = result.Date.ToShortDateString(),
Files = result.Files,
Gallons = result.Gallons
Gallons = result.Gallons,
IsFillToFull = result.IsFillToFull
};
return PartialView("_GasModal", convertedResult);
var vehicleIsElectric = _dataAccess.GetVehicleById(convertedResult.VehicleId).IsElectric;
var viewModel = new GasRecordInputContainer()
{
UseKwh = vehicleIsElectric,
GasRecord = convertedResult
};
return PartialView("_GasModal", viewModel);
}
[HttpPost]
public IActionResult DeleteGasRecordById(int gasRecordId)
@@ -349,9 +407,11 @@ namespace CarCareTracker.Controllers
{
var result = _serviceRecordDataAccess.GetServiceRecordById(serviceRecordId);
//convert to Input object.
var convertedResult = new ServiceRecordInput { Id = result.Id,
Cost = result.Cost,
Date = result.Date.ToShortDateString(),
var convertedResult = new ServiceRecordInput
{
Id = result.Id,
Cost = result.Cost,
Date = result.Date.ToShortDateString(),
Description = result.Description,
Mileage = result.Mileage,
Notes = result.Notes,
@@ -360,7 +420,7 @@ namespace CarCareTracker.Controllers
};
return PartialView("_ServiceRecordModal", convertedResult);
}
[HttpPost]
[HttpPost]
public IActionResult DeleteServiceRecordById(int serviceRecordId)
{
var result = _serviceRecordDataAccess.DeleteServiceRecordById(serviceRecordId);
@@ -485,19 +545,22 @@ namespace CarCareTracker.Controllers
var gasRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
var collisionRecords = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId);
var taxRecords = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId);
var upgradeRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId);
if (year != default)
{
serviceRecords.RemoveAll(x => x.Date.Year != year);
gasRecords.RemoveAll(x => x.Date.Year != year);
collisionRecords.RemoveAll(x => x.Date.Year != year);
taxRecords.RemoveAll(x => x.Date.Year != year);
upgradeRecords.RemoveAll(x => x.Date.Year != year);
}
var viewModel = new CostMakeUpForVehicle
{
ServiceRecordSum = serviceRecords.Sum(x => x.Cost),
GasRecordSum = gasRecords.Sum(x => x.Cost),
CollisionRecordSum = collisionRecords.Sum(x => x.Cost),
TaxRecordSum = taxRecords.Sum(x => x.Cost)
TaxRecordSum = taxRecords.Sum(x => x.Cost),
UpgradeRecordSum = upgradeRecords.Sum(x=>x.Cost)
};
return PartialView("_CostMakeUpReport", viewModel);
}
@@ -508,12 +571,238 @@ namespace CarCareTracker.Controllers
{
gasRecords.RemoveAll(x => x.Date.Year != year);
}
var groupedGasRecord = gasRecords.GroupBy(x => x.Date.Month).OrderBy(x=>x.Key).Select(x => new GasCostForVehicleByMonth {
var groupedGasRecord = gasRecords.GroupBy(x => x.Date.Month).OrderBy(x => x.Key).Select(x => new GasCostForVehicleByMonth
{
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
Cost = x.Sum(y=>y.Cost)
Cost = x.Sum(y => y.Cost)
}).ToList();
return PartialView("_GasCostByMonthReport", groupedGasRecord);
}
#endregion
#region "Reminders"
private int GetMaxMileage(int vehicleId)
{
var numbersArray = new List<int>();
var serviceRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId);
if (serviceRecords.Any())
{
numbersArray.Add(serviceRecords.Max(x => x.Mileage));
}
var repairRecords = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId);
if (repairRecords.Any())
{
numbersArray.Add(repairRecords.Max(x => x.Mileage));
}
var gasRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
if (gasRecords.Any())
{
numbersArray.Add(gasRecords.Max(x => x.Mileage));
}
var upgradeRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId);
if (upgradeRecords.Any())
{
numbersArray.Add(upgradeRecords.Max(x => x.Mileage));
}
return numbersArray.Any() ? numbersArray.Max() : 0;
}
private List<ReminderRecordViewModel> GetRemindersAndUrgency(int vehicleId)
{
var currentMileage = GetMaxMileage(vehicleId);
var reminders = _reminderRecordDataAccess.GetReminderRecordsByVehicleId(vehicleId);
List<ReminderRecordViewModel> reminderViewModels = new List<ReminderRecordViewModel>();
foreach (var reminder in reminders)
{
var reminderViewModel = new ReminderRecordViewModel()
{
Id = reminder.Id,
VehicleId = reminder.VehicleId,
Date = reminder.Date,
Mileage = reminder.Mileage,
Description = reminder.Description,
Notes = reminder.Notes,
Metric = reminder.Metric
};
if (reminder.Metric == ReminderMetric.Both)
{
if (reminder.Date < DateTime.Now)
{
reminderViewModel.Urgency = ReminderUrgency.PastDue;
reminderViewModel.Metric = ReminderMetric.Date;
}
else if (reminder.Mileage < currentMileage)
{
reminderViewModel.Urgency = ReminderUrgency.PastDue;
reminderViewModel.Metric = ReminderMetric.Odometer;
}
else if (reminder.Date < DateTime.Now.AddDays(7))
{
//if less than a week from today or less than 50 miles from current mileage then very urgent.
reminderViewModel.Urgency = ReminderUrgency.VeryUrgent;
//have to specify by which metric this reminder is urgent.
reminderViewModel.Metric = ReminderMetric.Date;
}
else if (reminder.Mileage < currentMileage + 50)
{
reminderViewModel.Urgency = ReminderUrgency.VeryUrgent;
reminderViewModel.Metric = ReminderMetric.Odometer;
}
else if (reminder.Date < DateTime.Now.AddDays(30))
{
reminderViewModel.Urgency = ReminderUrgency.Urgent;
reminderViewModel.Metric = ReminderMetric.Date;
}
else if (reminder.Mileage < currentMileage + 100)
{
reminderViewModel.Urgency = ReminderUrgency.Urgent;
reminderViewModel.Metric = ReminderMetric.Odometer;
}
}
else if (reminder.Metric == ReminderMetric.Date)
{
if (reminder.Date < DateTime.Now)
{
reminderViewModel.Urgency = ReminderUrgency.PastDue;
}
else if (reminder.Date < DateTime.Now.AddDays(7))
{
reminderViewModel.Urgency = ReminderUrgency.VeryUrgent;
}
else if (reminder.Date < DateTime.Now.AddDays(30))
{
reminderViewModel.Urgency = ReminderUrgency.Urgent;
}
}
else if (reminder.Metric == ReminderMetric.Odometer)
{
if (reminder.Mileage < currentMileage)
{
reminderViewModel.Urgency = ReminderUrgency.PastDue;
reminderViewModel.Metric = ReminderMetric.Odometer;
}
else if (reminder.Mileage < currentMileage + 50)
{
reminderViewModel.Urgency = ReminderUrgency.VeryUrgent;
}
else if (reminder.Mileage < currentMileage + 100)
{
reminderViewModel.Urgency = ReminderUrgency.Urgent;
}
}
reminderViewModels.Add(reminderViewModel);
}
return reminderViewModels;
}
[HttpGet]
public IActionResult GetVehicleHaveUrgentOrPastDueReminders(int vehicleId)
{
var result = GetRemindersAndUrgency(vehicleId);
if (result.Where(x => x.Urgency == ReminderUrgency.VeryUrgent || x.Urgency == ReminderUrgency.PastDue).Any())
{
return Json(true);
}
return Json(false);
}
[HttpGet]
public IActionResult GetReminderRecordsByVehicleId(int vehicleId)
{
var result = GetRemindersAndUrgency(vehicleId);
result = result.OrderByDescending(x => x.Urgency).ToList();
return PartialView("_ReminderRecords", result);
}
[HttpPost]
public IActionResult SaveReminderRecordToVehicleId(ReminderRecordInput reminderRecord)
{
var result = _reminderRecordDataAccess.SaveReminderRecordToVehicle(reminderRecord.ToReminderRecord());
return Json(result);
}
[HttpPost]
public IActionResult GetAddReminderRecordPartialView(ReminderRecordInput? reminderModel)
{
if (reminderModel is not null)
{
return PartialView("_ReminderRecordModal", reminderModel);
}
else
{
return PartialView("_ReminderRecordModal", new ReminderRecordInput());
}
}
[HttpGet]
public IActionResult GetReminderRecordForEditById(int reminderRecordId)
{
var result = _reminderRecordDataAccess.GetReminderRecordById(reminderRecordId);
//convert to Input object.
var convertedResult = new ReminderRecordInput
{
Id = result.Id,
Date = result.Date.ToShortDateString(),
Description = result.Description,
Notes = result.Notes,
VehicleId = result.VehicleId,
Mileage = result.Mileage,
Metric = result.Metric
};
return PartialView("_ReminderRecordModal", convertedResult);
}
[HttpPost]
public IActionResult DeleteReminderRecordById(int reminderRecordId)
{
var result = _reminderRecordDataAccess.DeleteReminderRecordById(reminderRecordId);
return Json(result);
}
#endregion
#region "Upgrade Records"
[HttpGet]
public IActionResult GetUpgradeRecordsByVehicleId(int vehicleId)
{
var result = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId);
if (_useDescending)
{
result = result.OrderByDescending(x => x.Date).ThenByDescending(x => x.Mileage).ToList();
}
else
{
result = result.OrderBy(x => x.Date).ThenBy(x => x.Mileage).ToList();
}
return PartialView("_UpgradeRecords", result);
}
[HttpPost]
public IActionResult SaveUpgradeRecordToVehicleId(UpgradeRecordInput upgradeRecord)
{
//move files from temp.
upgradeRecord.Files = upgradeRecord.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList();
var result = _upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(upgradeRecord.ToUpgradeRecord());
return Json(result);
}
[HttpGet]
public IActionResult GetAddUpgradeRecordPartialView()
{
return PartialView("_UpgradeRecordModal", new UpgradeRecordInput());
}
[HttpGet]
public IActionResult GetUpgradeRecordForEditById(int upgradeRecordId)
{
var result = _upgradeRecordDataAccess.GetUpgradeRecordById(upgradeRecordId);
//convert to Input object.
var convertedResult = new UpgradeRecordInput
{
Id = result.Id,
Cost = result.Cost,
Date = result.Date.ToShortDateString(),
Description = result.Description,
Mileage = result.Mileage,
Notes = result.Notes,
VehicleId = result.VehicleId,
Files = result.Files
};
return PartialView("_UpgradeRecordModal", convertedResult);
}
[HttpPost]
public IActionResult DeleteUpgradeRecordById(int upgradeRecordId)
{
var result = _upgradeRecordDataAccess.DeleteUpgradeRecordById(upgradeRecordId);
return Json(result);
}
#endregion
}
}

12
Dockerfile Normal file
View File

@@ -0,0 +1,12 @@
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build-env
WORKDIR /App
COPY . ./
RUN dotnet restore
RUN dotnet publish -c Release -o out
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /App
COPY --from=build-env /App/out .
EXPOSE 8080
CMD ["./CarCareTracker"]

10
Enum/ImportMode.cs Normal file
View File

@@ -0,0 +1,10 @@
namespace CarCareTracker.Models
{
public enum ImportMode
{
ServiceRecord = 0,
RepairRecord = 1,
GasRecord = 2,
TaxRecord = 3
}
}

9
Enum/ReminderMetric.cs Normal file
View File

@@ -0,0 +1,9 @@
namespace CarCareTracker.Models
{
public enum ReminderMetric
{
Date = 0,
Odometer = 1,
Both = 2
}
}

10
Enum/ReminderUrgency.cs Normal file
View File

@@ -0,0 +1,10 @@
namespace CarCareTracker.Models
{
public enum ReminderUrgency
{
NotUrgent = 0,
Urgent = 1,
VeryUrgent = 2,
PastDue = 3
}
}

View File

@@ -1,4 +1,5 @@
using CarCareTracker.External.Interfaces;
using CarCareTracker.Helper;
using CarCareTracker.Models;
using LiteDB;
@@ -6,7 +7,7 @@ namespace CarCareTracker.External.Implementations
{
public class CollisionRecordDataAccess : ICollisionRecordDataAccess
{
private static string dbName = "cartracker.db";
private static string dbName = StaticHelper.DbName;
private static string tableName = "collisionrecords";
public List<CollisionRecord> GetCollisionRecordsByVehicleId(int vehicleId)
{

View File

@@ -1,4 +1,5 @@
using CarCareTracker.External.Interfaces;
using CarCareTracker.Helper;
using CarCareTracker.Models;
using LiteDB;
@@ -6,7 +7,7 @@ namespace CarCareTracker.External.Implementations
{
public class GasRecordDataAccess: IGasRecordDataAccess
{
private static string dbName = "cartracker.db";
private static string dbName = StaticHelper.DbName;
private static string tableName = "gasrecords";
public List<GasRecord> GetGasRecordsByVehicleId(int vehicleId)
{

View File

@@ -1,4 +1,5 @@
using CarCareTracker.External.Interfaces;
using CarCareTracker.Helper;
using CarCareTracker.Models;
using LiteDB;
@@ -6,7 +7,7 @@ namespace CarCareTracker.External.Implementations
{
public class NoteDataAccess: INoteDataAccess
{
private static string dbName = "cartracker.db";
private static string dbName = StaticHelper.DbName;
private static string tableName = "notes";
public Note GetNoteByVehicleId(int vehicleId)
{

View File

@@ -0,0 +1,57 @@
using CarCareTracker.External.Interfaces;
using CarCareTracker.Helper;
using CarCareTracker.Models;
using LiteDB;
namespace CarCareTracker.External.Implementations
{
public class ReminderRecordDataAccess : IReminderRecordDataAccess
{
private static string dbName = StaticHelper.DbName;
private static string tableName = "reminderrecords";
public List<ReminderRecord> GetReminderRecordsByVehicleId(int vehicleId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<ReminderRecord>(tableName);
var reminderRecords = table.Find(Query.EQ(nameof(ReminderRecord.VehicleId), vehicleId));
return reminderRecords.ToList() ?? new List<ReminderRecord>();
};
}
public ReminderRecord GetReminderRecordById(int reminderRecordId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<ReminderRecord>(tableName);
return table.FindById(reminderRecordId);
};
}
public bool DeleteReminderRecordById(int reminderRecordId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<ReminderRecord>(tableName);
table.Delete(reminderRecordId);
return true;
};
}
public bool SaveReminderRecordToVehicle(ReminderRecord reminderRecord)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<ReminderRecord>(tableName);
table.Upsert(reminderRecord);
return true;
};
}
public bool DeleteAllReminderRecordsByVehicleId(int vehicleId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<ReminderRecord>(tableName);
var reminderRecords = table.DeleteMany(Query.EQ(nameof(ReminderRecord.VehicleId), vehicleId));
return true;
};
}
}
}

View File

@@ -1,4 +1,5 @@
using CarCareTracker.External.Interfaces;
using CarCareTracker.Helper;
using CarCareTracker.Models;
using LiteDB;
@@ -6,7 +7,7 @@ namespace CarCareTracker.External.Implementations
{
public class ServiceRecordDataAccess: IServiceRecordDataAccess
{
private static string dbName = "cartracker.db";
private static string dbName = StaticHelper.DbName;
private static string tableName = "servicerecords";
public List<ServiceRecord> GetServiceRecordsByVehicleId(int vehicleId)
{

View File

@@ -1,4 +1,5 @@
using CarCareTracker.External.Interfaces;
using CarCareTracker.Helper;
using CarCareTracker.Models;
using LiteDB;
@@ -6,7 +7,7 @@ namespace CarCareTracker.External.Implementations
{
public class TaxRecordDataAccess : ITaxRecordDataAccess
{
private static string dbName = "cartracker.db";
private static string dbName = StaticHelper.DbName;
private static string tableName = "taxrecords";
public List<TaxRecord> GetTaxRecordsByVehicleId(int vehicleId)
{

View File

@@ -0,0 +1,57 @@
using CarCareTracker.External.Interfaces;
using CarCareTracker.Helper;
using CarCareTracker.Models;
using LiteDB;
namespace CarCareTracker.External.Implementations
{
public class UpgradeRecordDataAccess : IUpgradeRecordDataAccess
{
private static string dbName = StaticHelper.DbName;
private static string tableName = "upgraderecords";
public List<UpgradeRecord> GetUpgradeRecordsByVehicleId(int vehicleId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<UpgradeRecord>(tableName);
var upgradeRecords = table.Find(Query.EQ(nameof(UpgradeRecord.VehicleId), vehicleId));
return upgradeRecords.ToList() ?? new List<UpgradeRecord>();
};
}
public UpgradeRecord GetUpgradeRecordById(int upgradeRecordId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<UpgradeRecord>(tableName);
return table.FindById(upgradeRecordId);
};
}
public bool DeleteUpgradeRecordById(int upgradeRecordId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<UpgradeRecord>(tableName);
table.Delete(upgradeRecordId);
return true;
};
}
public bool SaveUpgradeRecordToVehicle(UpgradeRecord upgradeRecord)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<UpgradeRecord>(tableName);
table.Upsert(upgradeRecord);
return true;
};
}
public bool DeleteAllUpgradeRecordsByVehicleId(int vehicleId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<UpgradeRecord>(tableName);
var upgradeRecords = table.DeleteMany(Query.EQ(nameof(UpgradeRecord.VehicleId), vehicleId));
return true;
};
}
}
}

View File

@@ -1,4 +1,5 @@
using CarCareTracker.External.Interfaces;
using CarCareTracker.Helper;
using CarCareTracker.Models;
using LiteDB;
@@ -6,7 +7,7 @@ namespace CarCareTracker.External.Implementations
{
public class VehicleDataAccess: IVehicleDataAccess
{
private static string dbName = "cartracker.db";
private static string dbName = StaticHelper.DbName;
private static string tableName = "vehicles";
public bool SaveVehicle(Vehicle vehicle)
{

View File

@@ -0,0 +1,13 @@
using CarCareTracker.Models;
namespace CarCareTracker.External.Interfaces
{
public interface IReminderRecordDataAccess
{
public List<ReminderRecord> GetReminderRecordsByVehicleId(int vehicleId);
public ReminderRecord GetReminderRecordById(int reminderRecordId);
public bool DeleteReminderRecordById(int reminderRecordId);
public bool SaveReminderRecordToVehicle(ReminderRecord reminderRecord);
public bool DeleteAllReminderRecordsByVehicleId(int vehicleId);
}
}

View File

@@ -0,0 +1,13 @@
using CarCareTracker.Models;
namespace CarCareTracker.External.Interfaces
{
public interface IUpgradeRecordDataAccess
{
public List<UpgradeRecord> GetUpgradeRecordsByVehicleId(int vehicleId);
public UpgradeRecord GetUpgradeRecordById(int upgradeRecordId);
public bool DeleteUpgradeRecordById(int upgradeRecordId);
public bool SaveUpgradeRecordToVehicle(UpgradeRecord upgradeRecord);
public bool DeleteAllUpgradeRecordsByVehicleId(int vehicleId);
}
}

11
Helper/StaticHelper.cs Normal file
View File

@@ -0,0 +1,11 @@
namespace CarCareTracker.Helper
{
/// <summary>
/// helper method for static vars
/// </summary>
public static class StaticHelper
{
public static string DbName = "data/cartracker.db";
public static string UserConfigPath = "config/userConfig.json";
}
}

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2023 ivancheahhh
Copyright (c) 2023 Hargata Softworks
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -0,0 +1,21 @@
using CarCareTracker.Models;
using CsvHelper.Configuration;
namespace CarCareTracker.MapProfile
{
public class FuellyMapper: ClassMap<ImportModel>
{
public FuellyMapper()
{
Map(m => m.Date).Name(["date", "fuelup_date"]);
Map(m => m.Odometer).Name(["odometer"]);
Map(m => m.FuelConsumed).Name(["gallons", "liters", "litres", "consumption", "fuelconsumed"]);
Map(m => m.Cost).Name(["cost", "total cost", "totalcost", "total price"]);
Map(m => m.Notes).Name("notes", "note");
Map(m => m.Price).Name(["price"]);
Map(m => m.PartialFuelUp).Name(["partial_fuelup"]);
Map(m => m.IsFillToFull).Name(["isfilltofull", "filled up"]);
Map(m => m.Description).Name(["description"]);
}
}
}

View File

@@ -14,6 +14,7 @@
/// </summary>
public decimal Gallons { get; set; }
public decimal Cost { get; set; }
public bool IsFillToFull { get; set; } = true;
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
}
}

View File

@@ -14,7 +14,17 @@
/// </summary>
public decimal Gallons { get; set; }
public decimal Cost { get; set; }
public bool IsFillToFull { get; set; } = true;
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
public GasRecord ToGasRecord() { return new GasRecord { Id = Id, Cost = Cost, Date = DateTime.Parse(Date), Gallons = Gallons, Mileage = Mileage, VehicleId = VehicleId, Files = Files }; }
public GasRecord ToGasRecord() { return new GasRecord {
Id = Id,
Cost = Cost,
Date = DateTime.Parse(Date),
Gallons = Gallons,
Mileage = Mileage,
VehicleId = VehicleId,
Files = Files,
IsFillToFull = IsFillToFull
}; }
}
}

View File

@@ -0,0 +1,8 @@
namespace CarCareTracker.Models
{
public class GasRecordInputContainer
{
public bool UseKwh { get; set; }
public GasRecordInput GasRecord { get; set; }
}
}

View File

@@ -0,0 +1,8 @@
namespace CarCareTracker.Models
{
public class GasRecordViewModelContainer
{
public bool UseKwh { get; set; }
public List<GasRecordViewModel> GasRecords { get; set; } = new List<GasRecordViewModel>();
}
}

18
Models/ImportModel.cs Normal file
View File

@@ -0,0 +1,18 @@
namespace CarCareTracker.Models
{
/// <summary>
/// Import model used for importing Gas records.
/// </summary>
public class ImportModel
{
public string Date { get; set; }
public string Odometer { get; set; }
public string Description { get; set; }
public string Notes { get; set; }
public string FuelConsumed { get; set; }
public string Cost { get; set; }
public string Price { get; set; }
public string PartialFuelUp { get; set; }
public string IsFillToFull { get; set; }
}
}

View File

@@ -1,34 +0,0 @@
namespace CarCareTracker.Models
{
/// <summary>
/// Import model used for importing Gas records.
/// </summary>
public class GasRecordImport
{
public DateTime Date { get; set; }
public int Odometer { get; set; }
public decimal FuelConsumed { get; set; }
public decimal Cost { get; set; }
}
/// <summary>
/// Import model used for importing Service and Repair records.
/// </summary>
public class ServiceRecordImport
{
public DateTime Date { get; set; }
public int Odometer { get; set; }
public string Description { get; set; }
public string Notes { get; set; }
public decimal Cost { get; set; }
}
/// <summary>
/// Import model used for importing tax records.
/// </summary>
public class TaxRecordImport
{
public DateTime Date { get; set; }
public string Description { get; set; }
public string Notes { get; set; }
public decimal Cost { get; set; }
}
}

View File

@@ -0,0 +1,13 @@
namespace CarCareTracker.Models
{
public class ReminderRecord
{
public int Id { get; set; }
public int VehicleId { get; set; }
public DateTime Date { get; set; }
public int Mileage { get; set; }
public string Description { get; set; }
public string Notes { get; set; }
public ReminderMetric Metric { get; set; } = ReminderMetric.Date;
}
}

View File

@@ -0,0 +1,21 @@
namespace CarCareTracker.Models
{
public class ReminderRecordInput
{
public int Id { get; set; }
public int VehicleId { get; set; }
public string Date { get; set; } = DateTime.Now.AddDays(1).ToShortDateString();
public int Mileage { get; set; }
public string Description { get; set; }
public string Notes { get; set; }
public ReminderMetric Metric { get; set; } = ReminderMetric.Date;
public ReminderRecord ToReminderRecord() { return new ReminderRecord {
Id = Id,
VehicleId = VehicleId,
Date = DateTime.Parse(string.IsNullOrWhiteSpace(Date) ? DateTime.Now.AddDays(1).ToShortDateString() : Date),
Mileage = Mileage,
Description = Description,
Metric = Metric,
Notes = Notes }; }
}
}

View File

@@ -0,0 +1,17 @@
namespace CarCareTracker.Models
{
public class ReminderRecordViewModel
{
public int Id { get; set; }
public int VehicleId { get; set; }
public DateTime Date { get; set; }
public int Mileage { get; set; }
public string Description { get; set; }
public string Notes { get; set; }
/// <summary>
/// Reason why this reminder is urgent
/// </summary>
public ReminderMetric Metric { get; set; } = ReminderMetric.Date;
public ReminderUrgency Urgency { get; set; } = ReminderUrgency.NotUrgent;
}
}

View File

@@ -6,5 +6,6 @@
public decimal GasRecordSum { get; set; }
public decimal TaxRecordSum { get; set; }
public decimal CollisionRecordSum { get; set; }
public decimal UpgradeRecordSum { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
namespace CarCareTracker.Models
{
public class UpgradeRecord
{
public int Id { get; set; }
public int VehicleId { get; set; }
public DateTime Date { get; set; }
public int Mileage { get; set; }
public string Description { get; set; }
public decimal Cost { get; set; }
public string Notes { get; set; }
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
}
}

View File

@@ -0,0 +1,15 @@
namespace CarCareTracker.Models
{
public class UpgradeRecordInput
{
public int Id { get; set; }
public int VehicleId { get; set; }
public string Date { get; set; }
public int Mileage { get; set; }
public string Description { get; set; }
public decimal Cost { get; set; }
public string Notes { get; set; }
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
public UpgradeRecord ToUpgradeRecord() { return new UpgradeRecord { Id = Id, VehicleId = VehicleId, Date = DateTime.Parse(Date), Cost = Cost, Mileage = Mileage, Description = Description, Notes = Notes, Files = Files }; }
}
}

View File

@@ -7,6 +7,8 @@
public bool UseMPG { get; set; }
public bool UseDescending { get; set; }
public bool EnableAuth { get; set; }
public bool HideZero { get; set; }
public bool UseUKMPG {get;set;}
public string UserNameHash { get; set; }
public string UserPasswordHash { get; set;}
}

View File

@@ -8,5 +8,6 @@
public string Make { get; set; }
public string Model { get; set; }
public string LicensePlate { get; set; }
public bool IsElectric { get; set; } = false;
}
}

View File

@@ -15,10 +15,17 @@ builder.Services.AddSingleton<IServiceRecordDataAccess, ServiceRecordDataAccess>
builder.Services.AddSingleton<IGasRecordDataAccess, GasRecordDataAccess>();
builder.Services.AddSingleton<ICollisionRecordDataAccess, CollisionRecordDataAccess>();
builder.Services.AddSingleton<ITaxRecordDataAccess, TaxRecordDataAccess>();
builder.Services.AddSingleton<IReminderRecordDataAccess, ReminderRecordDataAccess>();
builder.Services.AddSingleton<IUpgradeRecordDataAccess, UpgradeRecordDataAccess>();
builder.Services.AddSingleton<IFileHelper, FileHelper>();
if (!Directory.Exists("data"))
{
Directory.CreateDirectory("data");
}
//Additional JsonFile
builder.Configuration.AddJsonFile("userConfig.json", optional: true, reloadOnChange: true);
builder.Configuration.AddJsonFile(StaticHelper.UserConfigPath, optional: true, reloadOnChange: true);
//Configure Auth
builder.Services.AddDataProtection();

View File

@@ -11,4 +11,56 @@ Because nobody should have to deal with a homemade spreadsheet or a shoebox full
- Bootstrap-DatePicker
- SweetAlert2
- CsvHelper
- Chart.js
- Chart.js
## Docker Setup (GHCR)
1. Install Docker
2. Run `docker pull ghcr.io/hargata/lubelogger:latest`
3. CHECK culture in .env file, default is en_US, this will change the currency and date formats.
4. If not using traefik, use docker-compose-notraefik.yml
5. Run `docker-compose up`
## Docker Setup (Manual Build)
1. Install Docker
2. Clone this repo
3. CHECK culture in .env file, default is en_US
4. Run `docker build -t lubelogger -f Dockerfile .`
5. CHECK docker-compose.yml and make sure the mounting directories look correct.
6. If not using traefik, use docker-compose-notraefik.yml
7. Run `docker-compose up`
## Additional Docker Instructions
### manual
- build
```
docker build -t hargata/lubelog:latest .
```
- run
```
docker run -d hargata/lubelog:latest
```
add `-v` for persistent volumes as needed. Have a look at the docker-compose.yml for examples.
## docker-compose
- build image
```
docker compose build
```
- run
```
docker compose up
# or variant with traefik labels:
docker compose -f docker-compose.traefik.yml up
```

View File

@@ -26,7 +26,7 @@
@if (enableAuth)
{
<li class="nav-item">
<button class="nav-link" onclick="performLogOut()">Logout</button>
<button class="nav-link" onclick="performLogOut()"><i class="bi bi-box-arrow-right me-2"></i>Logout</button>
</li>
}
</ul>

View File

@@ -16,7 +16,11 @@
</div>
<div class="form-check form-switch">
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="useMPG" checked="@Model.UseMPG">
<label class="form-check-label" for="useMPG">Use Imperial Units for Fuel Economy Calculations(Miles, Gallons)</label>
<label class="form-check-label" for="useMPG">Use Imperial Calculation for Fuel Economy Calculations(MPG)<br /><small class="text-body-secondary">This Will Also Change Units to Miles and Gallons</small></label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="useUKMPG" checked="@Model.UseUKMPG">
<label class="form-check-label" for="useUKMPG">Use UK MPG Calculation<br /><small class="text-body-secondary">Input Gas Consumption in Liters, it will be converted to UK Gals for MPG Calculation</small></label>
</div>
</div>
<div class="col-12 col-md-6">
@@ -24,6 +28,10 @@
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="useDescending" checked="@Model.UseDescending">
<label class="form-check-label" for="useDescending">Sort lists in Descending Order(Newest to Oldest)</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="hideZero" checked="@Model.HideZero">
<label class="form-check-label" for="hideZero">Replace @(0.ToString("C")) Costs with ---</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" onChange="enableAuthCheckChanged()" type="checkbox" role="switch" id="enableAuth" checked="@Model.EnableAuth">
<label class="form-check-label" for="enableAuth">Enable Authentication</label>
@@ -76,7 +84,9 @@
useDarkMode: $("#enableDarkMode").is(':checked'),
enableCsvImports: $("#enableCsvImports").is(':checked'),
useMPG: $("#useMPG").is(':checked'),
useDescending: $("#useDescending").is(':checked')
useDescending: $("#useDescending").is(':checked'),
hideZero: $("#hideZero").is(":checked"),
useUKMpg: $("#useUKMPG").is(":checked")
}
$.post('/Home/WriteToSettings', { userConfig: userConfigObject}, function(data){
if (data) {

View File

@@ -14,14 +14,14 @@
</div>
<div class="form-group">
<label for="inputUserPassword">Password</label>
<input type="password" id="inputUserPassword" class="form-control">
<input type="password" id="inputUserPassword" onkeyup="handlePasswordKeyPress(event)" class="form-control">
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="inputPersistent">
<label class="form-check-label" for="inputPersistent">Remember Me</label>
</div>
<div class="d-grid">
<button type="button" class="btn btn-warning mt-2" onclick="performLogin()">Login</button>
<button type="button" class="btn btn-warning mt-2" onclick="performLogin()"><i class="bi bi-box-arrow-in-right me-2"></i>Login</button>
</div>
</div>
</div>

View File

@@ -3,6 +3,16 @@
@{
var useDarkMode = bool.Parse(Configuration["UseDarkMode"]);
var enableCsvImports = bool.Parse(Configuration["EnableCsvImports"]);
var shortDatePattern = System.Globalization.CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern;
shortDatePattern = shortDatePattern.ToLower();
if (!shortDatePattern.Contains("dd"))
{
shortDatePattern = shortDatePattern.Replace("d", "dd");
}
if (!shortDatePattern.Contains("mm"))
{
shortDatePattern = shortDatePattern.Replace("m", "mm");
}
}
<html lang="en" data-bs-theme="@(useDarkMode ? "dark" : "light")">
<head>
@@ -28,6 +38,11 @@
enableCsvImport : "@enableCsvImports" == "True"
}
}
function getShortDatePattern() {
return {
pattern: "@shortDatePattern"
}
}
</script>
@await RenderSectionAsync("Scripts", required: false)
</head>

View File

@@ -8,6 +8,8 @@
<script src="~/js/gasrecord.js" asp-append-version="true"></script>
<script src="~/js/collisionrecord.js" asp-append-version="true"></script>
<script src="~/js/taxrecord.js" asp-append-version="true"></script>
<script src="~/js/reminderrecord.js" asp-append-version="true"></script>
<script src="~/js/upgraderecord.js" asp-append-version="true"></script>
<script src="~/lib/chart-js/chart.umd.js"></script>
}
<div class="container">
@@ -26,6 +28,9 @@
<li class="nav-item" role="presentation">
<button class="nav-link" id="accident-tab" data-bs-toggle="tab" data-bs-target="#accident-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-exclamation-octagon me-2"></i>Repairs</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="upgrade-tab" data-bs-toggle="tab" data-bs-target="#upgrade-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-wrench-adjustable me-2"></i>Upgrades</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="gas-tab" data-bs-toggle="tab" data-bs-target="#gas-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-fuel-pump me-2"></i>Fuel</button>
</li>
@@ -35,6 +40,9 @@
<li class="nav-item" role="presentation">
<button class="nav-link" id="notes-tab" data-bs-toggle="tab" data-bs-target="#notes-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-journal-bookmark me-2"></i>Notes</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="reminder-tab" data-bs-toggle="tab" data-bs-target="#reminder-tab-pane" type="button" role="tab" aria-selected="false"><div id="reminderBellDiv" style="display:inline-flex;"><i id="reminderBell" class="bi bi-bell me-2"></i></div>Reminders</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="report-tab" data-bs-toggle="tab" data-bs-target="#report-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-file-bar-graph me-2"></i>Reports</button>
</li>
@@ -65,7 +73,9 @@
</div>
</div>
<div class="tab-pane fade" id="accident-tab-pane" role="tabpanel" tabindex="0"></div>
<div class="tab-pane fade" id="reminder-tab-pane" role="tabpanel" tabindex="0"></div>
<div class="tab-pane fade" id="report-tab-pane" role="tabpanel" tabindex="0"></div>
<div class="tab-pane fade" id="upgrade-tab-pane" role="tabpanel" tabindex="0"></div>
</div>
</div>
<div class="modal fade" id="editVehicleModal" tabindex="-1" role="dialog">
@@ -80,6 +90,12 @@
</div>
</div>
</div>
<div class="modal fade" id="reminderRecordModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content" id="reminderRecordModalContent">
</div>
</div>
</div>
<script>
function GetVehicleId() {
return { vehicleId: @Model.Id};

View File

@@ -1,4 +1,4 @@
@model string
@model ImportMode
<div class="modal-header">
<h5 class="modal-title">Import Data from CSV</h5>
<button type="button" class="btn-close" onclick="hideBulkImportModal()" aria-label="Close"></button>
@@ -16,13 +16,13 @@
<div class="alert alert-danger" role="alert">
Failure to format the data correctly can cause data corruption. Please make sure you make a copy of the local database before proceeding.
</div>
@if (Model == "gasrecord")
@if (Model == ImportMode.GasRecord)
{
<a class="btn btn-link" href="/defaults/gassample.csv" target="_blank">Download Sample</a>
} else if (Model == "servicerecord" || Model == "repairrecord")
} else if (Model == ImportMode.ServiceRecord || Model == ImportMode.RepairRecord)
{
<a class="btn btn-link" href="/defaults/servicerecordsample.csv" target="_blank">Download Sample</a>
} else if (Model == "taxrecord")
} else if (Model == ImportMode.TaxRecord)
{
<a class="btn btn-link" href="/defaults/taxrecordsample.csv" target="_blank">Download Sample</a>
}
@@ -52,13 +52,13 @@
if (data) {
successToast("Data Imported Successfully");
hideBulkImportModal();
if (mode == "gasrecord") {
if (mode == "GasRecord") {
getVehicleGasRecords(vehicleId);
} else if (mode == "servicerecord") {
} else if (mode == "ServiceRecord") {
getVehicleServiceRecords(vehicleId);
} else if (mode == "repairrecord") {
} else if (mode == "RepairRecord") {
getVehicleCollisionRecords(vehicleId);
} else if (mode == "taxrecord") {
} else if (mode == "TaxRecord") {
getVehicleTaxRecords(vehicleId);
}
} else {

View File

@@ -1,6 +1,9 @@
@model CollisionRecordInput
@{
var isNew = Model.Id == 0;
}
<div class="modal-header">
<h5 class="modal-title">@(Model.Id == 0 ? "Add New Repair Record" : "Edit Repair Record")</h5>
<h5 class="modal-title">@(isNew ? "Add New Repair Record" : "Edit Repair Record")</h5>
<button type="button" class="btn-close" onclick="hideAddCollisionRecordModal()" aria-label="Close"></button>
</div>
<div class="modal-body">
@@ -11,15 +14,15 @@
<input type="text" id="workAroundInput" style="height:0px; width:0px; display:none;">
<label for="collisionRecordDate">Date</label>
<div class="input-group">
<input type="text" id="collisionRecordDate" class="form-control" value="@Model.Date">
<input type="text" id="collisionRecordDate" class="form-control" placeholder="Date repair was performed" value="@Model.Date">
<span class="input-group-text"><i class="bi bi-calendar-event"></i></span>
</div>
<label for="collisionRecordMileage">Odometer</label>
<input type="number" id="collisionRecordMileage" class="form-control" value="@Model.Mileage">
<input type="number" id="collisionRecordMileage" class="form-control" placeholder="Odometer reading when repaired" value="@(isNew ? "" : Model.Mileage)">
<label for="collisionRecordDescription">Description</label>
<input type="text" id="collisionRecordDescription" class="form-control" value="@Model.Description">
<input type="text" id="collisionRecordDescription" class="form-control" placeholder="Description of item(s) repaired(i.e. Alternator)" value="@Model.Description">
<label for="collisionRecordCost">Cost</label>
<input type="number" id="collisionRecordCost" class="form-control" value="@Model.Cost">
<input type="number" id="collisionRecordCost" class="form-control" placeholder="Cost of the repair" value="@(isNew ? "" : Model.Cost)">
</div>
<div class="col-md-6 col-12">
<label for="collisionRecordNotes">Notes(optional)</label>
@@ -27,20 +30,22 @@
@if (Model.Files.Any())
{
<div>
<label>Uploaded Documents</label>
@foreach (UploadedFiles filesUploaded in Model.Files)
{
<div class="d-flex justify-content-between">
<a type="button" class="btn btn-link" href="@filesUploaded.Location" target="_blank">@filesUploaded.Name</a>
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteCollisionRecordFile('@filesUploaded.Location', this)"><i class="bi bi-trash"></i></button>
</div>
}
@await Html.PartialAsync("_UploadedFiles", Model.Files)
<label for="collisionRecordFiles">Upload more documents</label>
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="collisionRecordFiles">
</div>
}
else
{
@if (isNew)
{
<div class="form-check">
<input class="form-check-input" type="checkbox" value="" id="addReminderCheck">
<label class="form-check-label" for="addReminderCheck">
Add Reminder
</label>
</div>
}
<label for="collisionRecordFiles">Upload documents(optional)</label>
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="collisionRecordFiles">
}
@@ -50,16 +55,16 @@
</form>
</div>
<div class="modal-footer">
@if (Model.Id > 0)
@if (!isNew)
{
<button type="button" class="btn btn-danger" onclick="deleteCollisionRecord(@Model.Id)" style="margin-right:auto;">Delete</button>
}
<button type="button" class="btn btn-secondary" onclick="hideAddCollisionRecordModal()">Cancel</button>
@if (Model.Id == 0)
@if (isNew)
{
<button type="button" class="btn btn-primary" onclick="saveCollisionRecordToVehicle()">Add New Repair Record</button>
}
else if (Model.Id > 0)
else if (!isNew)
{
<button type="button" class="btn btn-primary" onclick="saveCollisionRecordToVehicle(true)">Edit Repair Record</button>
}

View File

@@ -1,6 +1,7 @@
@inject IConfiguration Configuration
@{
var enableCsvImports = bool.Parse(Configuration[nameof(UserConfig.EnableCsvImports)]);
var hideZero = bool.Parse(Configuration[nameof(UserConfig.HideZero)]);
}
@model List<CollisionRecord>
<div class="row">
@@ -18,7 +19,7 @@
<span class="visually-hidden">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" onclick="showBulkImportModal('repairrecord')">Import via CSV</a></li>
<li><a class="dropdown-item" href="#" onclick="showBulkImportModal('RepairRecord')">Import via CSV</a></li>
</ul>
</div>
}
@@ -48,7 +49,7 @@
<td class="col-1">@collisionRecord.Date.ToShortDateString()</td>
<td class="col-2">@collisionRecord.Mileage</td>
<td class="col-4">@collisionRecord.Description</td>
<td class="col-2">@collisionRecord.Cost.ToString("C")</td>
<td class="col-2">@((hideZero && collisionRecord.Cost == default) ? "---" : collisionRecord.Cost.ToString("C"))</td>
<td class="col-3 text-truncate">@collisionRecord.Notes</td>
</tr>
}
@@ -58,7 +59,7 @@
</div>
<div class="modal fade" id="collisionRecordModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal fade" data-bs-focus="false" id="collisionRecordModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content" id="collisionRecordModalContent">
</div>

View File

@@ -1,5 +1,5 @@
@model CostMakeUpForVehicle
@if (Model.CollisionRecordSum + Model.ServiceRecordSum + Model.GasRecordSum + Model.TaxRecordSum > 0)
@if (Model.CollisionRecordSum + Model.ServiceRecordSum + Model.GasRecordSum + Model.TaxRecordSum + Model.UpgradeRecordSum > 0)
{
<canvas id="pie-chart"></canvas>
<script>
@@ -9,17 +9,18 @@
new Chart($("#pie-chart"), {
type: 'pie',
data: {
labels: ["Planned Maintenance(Service Records)", "Unplanned Maintenance(Repairs)", "Tax", "Fuel"],
labels: ["Planned Maintenance(Service Records)", "Unplanned Maintenance(Repairs)", "Upgrades", "Tax", "Fuel"],
datasets: [
{
label: "Expenses by Category",
backgroundColor: ["#003f5c", "#58508d", "#bc5090", "#ff6361"],
backgroundColor: ["#003f5c", "#58508d", "#bc5090", "#ff6361", "#ffa600"],
data: [
@Model.ServiceRecordSum,
@Model.CollisionRecordSum,
@Model.UpgradeRecordSum,
@Model.TaxRecordSum,
@Model.GasRecordSum
]
]
}
]
},
@@ -41,7 +42,8 @@
});
}
</script>
} else
}
else
{
<h1>No data found or all records have zero sums, insert records with non-zero sums to see visualizations here.</h1>
}

View File

@@ -1,21 +1,47 @@
@inject IConfiguration Configuration
@model GasRecordViewModelContainer
@{
var enableCsvImports = bool.Parse(Configuration[nameof(UserConfig.EnableCsvImports)]);
var useMPG = bool.Parse(Configuration[nameof(UserConfig.UseMPG)]);
var useUKMPG = bool.Parse(Configuration[nameof(UserConfig.UseUKMPG)]);
var hideZero = bool.Parse(Configuration[nameof(UserConfig.HideZero)]);
var useKwh = Model.UseKwh;
string consumptionUnit;
string fuelEconomyUnit;
string distanceUnit = useMPG ? "mi." : "km";
if (useKwh)
{
consumptionUnit = "kWh";
fuelEconomyUnit = useMPG ? "mi./kWh" : "kWh/100km";
}
else if (useMPG && useUKMPG)
{
consumptionUnit = "imp gal";
fuelEconomyUnit = "mpg";
} else if (useUKMPG)
{
fuelEconomyUnit = "l/100mi.";
consumptionUnit = "l";
distanceUnit = "mi.";
}
else
{
consumptionUnit = useMPG ? "US gal" : "l";
fuelEconomyUnit = useMPG ? "mpg" : "l/100km";
}
}
@model List<GasRecordViewModel>
<div class="row">
<div class="d-flex justify-content-between">
<div class="d-flex align-items-center flex-wrap">
<span class="ms-2 badge bg-success">@($"# of Gas Records: {Model.Count()}")</span>
@if (Model.Count() > 1)
<span class="ms-2 badge bg-success">@($"# of Gas Records: {Model.GasRecords.Count()}")</span>
@if (Model.GasRecords.Where(x => x.MilesPerGallon > 0).Any())
{
<span class="ms-2 badge bg-primary">@($"Average Fuel Economy: {Model.Where(y => y.MilesPerGallon > 0)?.Average(x => x.MilesPerGallon).ToString("F") ?? "0"}")</span>
<span class="ms-2 badge bg-primary">@($"Min Fuel Economy: {Model.Where(y => y.MilesPerGallon > 0)?.Min(x => x.MilesPerGallon).ToString("F") ?? "0"}")</span>
<span class="ms-2 badge bg-primary">@($"Max Fuel Economy: {Model.Max(x => x.MilesPerGallon).ToString("F") ?? "0"}")</span>
<span class="ms-2 badge bg-primary">@($"Average Fuel Economy: {Model.GasRecords.Where(y => y.MilesPerGallon > 0)?.Average(x => x.MilesPerGallon).ToString("F") ?? "0"}")</span>
<span class="ms-2 badge bg-primary">@($"Min Fuel Economy: {Model.GasRecords.Where(y => y.MilesPerGallon > 0)?.Min(x => x.MilesPerGallon).ToString("F") ?? "0"}")</span>
<span class="ms-2 badge bg-primary">@($"Max Fuel Economy: {Model.GasRecords.Max(x => x.MilesPerGallon).ToString("F") ?? "0"}")</span>
}
<span class="ms-2 badge bg-success">@($"Total Fuel Consumed: {Model.Sum(x=>x.Gallons).ToString("F")}")</span>
<span class="ms-2 badge bg-success">@($"Total Cost: {Model.Sum(x => x.Cost).ToString("C")}")</span>
<span class="ms-2 badge bg-success">@($"Total Fuel Consumed: {Model.GasRecords.Sum(x => x.Gallons).ToString("F")}")</span>
<span class="ms-2 badge bg-success">@($"Total Cost: {Model.GasRecords.Sum(x => x.Cost).ToString("C3")}")</span>
</div>
@if (enableCsvImports)
{
@@ -25,7 +51,7 @@
<span class="visually-hidden">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" onclick="showBulkImportModal('gasrecord')">Import via CSV</a></li>
<li><a class="dropdown-item" href="#" onclick="showBulkImportModal('GasRecord')">Import via CSV</a></li>
</ul>
</div>
} else {
@@ -39,23 +65,23 @@
<thead>
<tr class="d-flex">
<th scope="col" class="col-2">Date Refueled</th>
<th scope="col" class="col-2">Odometer(@(useMPG ? "mi." : "km"))</th>
<th scope="col" class="col-2">Consumption(@(useMPG ? "gal" : "l"))</th>
<th scope="col" class="col-2">Fuel Economy(@(useMPG ? "mpg" : "l/100km"))</th>
<th scope="col" class="col-2">Cost</th>
<th scope="col" class="col-2">Unit Cost</th>
<th scope="col" class="col-2">Odometer(@(distanceUnit))</th>
<th scope="col" class="col-2">Consumption(@(consumptionUnit))</th>
<th scope="col" class="col-4">Fuel Economy(@(fuelEconomyUnit))</th>
<th scope="col" class="col-1">Cost</th>
<th scope="col" class="col-1">Unit Cost</th>
</tr>
</thead>
<tbody>
@foreach (GasRecordViewModel gasRecord in Model)
@foreach (GasRecordViewModel gasRecord in Model.GasRecords)
{
<tr class="d-flex" style="cursor:pointer;" onclick="showEditGasRecordModal(@gasRecord.Id)">
<td class="col-2">@gasRecord.Date</td>
<td class="col-2">@gasRecord.Mileage</td>
<td class="col-2">@gasRecord.Gallons.ToString("F")</td>
<td class="col-2">@gasRecord.MilesPerGallon.ToString("F")</td>
<td class="col-2">@gasRecord.Cost.ToString("C")</td>
<td class="col-2">@gasRecord.CostPerGallon.ToString("C")</td>
<td class="col-4">@(gasRecord.MilesPerGallon == 0 ? "---" : gasRecord.MilesPerGallon.ToString("F"))</td>
<td class="col-1">@((hideZero && gasRecord.Cost == default) ? "---" : gasRecord.Cost.ToString("C3"))</td>
<td class="col-1">@((hideZero && gasRecord.CostPerGallon == default) ? "---" : gasRecord.CostPerGallon.ToString("C3"))</td>
</tr>
}
</tbody>
@@ -64,7 +90,7 @@
</div>
<div class="modal fade" id="gasRecordModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal fade" data-bs-focus="false" id="gasRecordModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content" id="gasRecordModalContent">

View File

@@ -1,10 +1,34 @@
@inject IConfiguration Configuration
@model GasRecordInputContainer
@{
var useMPG = bool.Parse(Configuration[nameof(UserConfig.UseMPG)]);
var useUKMPG = bool.Parse(Configuration[nameof(UserConfig.UseUKMPG)]);
var useKwh = Model.UseKwh;
var isNew = Model.GasRecord.Id == 0;
string consumptionUnit;
string distanceUnit;
if (useKwh)
{
consumptionUnit = "kWh";
} else if (useUKMPG)
{
consumptionUnit = "liters";
}
else
{
consumptionUnit = useMPG ? "gallons" : "liters";
}
if (useUKMPG)
{
distanceUnit = "miles";
}
else
{
distanceUnit = useMPG ? "miles" : "kilometers";
}
}
@model GasRecordInput
<div class="modal-header">
<h5 class="modal-title">@(Model.Id == 0 ? "Add New Gas Record" : "Edit Gas Record")</h5>
<h5 class="modal-title">@(isNew ? "Add New Gas Record" : "Edit Gas Record")</h5>
<button type="button" class="btn-close" onclick="hideAddGasRecordModal()" aria-label="Close"></button>
</div>
<div class="modal-body">
@@ -15,28 +39,25 @@
<input type="text" id="workAroundInput" style="height:0px; width:0px; display:none;">
<label for="gasRecordDate">Date</label>
<div class="input-group">
<input type="text" id="gasRecordDate" class="form-control" value="@Model.Date">
<input type="text" id="gasRecordDate" placeholder="Date refueled" class="form-control" value="@Model.GasRecord.Date">
<span class="input-group-text"><i class="bi bi-calendar-event"></i></span>
</div>
<label for="gasRecordMileage">Odometer Reading(@(useMPG ? "miles" : "kilometers"))</label>
<input type="number" id="gasRecordMileage" class="form-control" value="@Model.Mileage">
<label for="gasRecordGallons">Fuel Consumption(@(useMPG ? "gallons" : "liters"))</label>
<input type="text" id="gasRecordGallons" class="form-control" value="@Model.Gallons">
<label for="gasRecordMileage">Odometer Reading(@distanceUnit)</label>
<input type="number" id="gasRecordMileage" class="form-control" placeholder="Odometer reading when refueled" value="@(isNew ? "" : Model.GasRecord.Mileage)">
<label for="gasRecordGallons">Fuel Consumption(@(consumptionUnit))</label>
<input type="text" id="gasRecordGallons" class="form-control" placeholder="Amount of gas refueled" value="@(isNew ? "" : Model.GasRecord.Gallons)">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="gasIsFillToFull" checked="@Model.GasRecord.IsFillToFull">
<label class="form-check-label" for="gasIsFillToFull">Is Filled To Full</label>
</div>
<label for="GasRecordCost">Cost</label>
<input type="number" id="gasRecordCost" class="form-control" value="@Model.Cost">
<input type="number" id="gasRecordCost" class="form-control" placeholder="Cost of gas refueled" value="@(isNew ? "" : Model.GasRecord.Cost)">
</div>
<div class="col-md-6 col-12">
@if (Model.Files.Any())
@if (Model.GasRecord.Files.Any())
{
<div>
<label>Uploaded Documents</label>
@foreach (UploadedFiles filesUploaded in Model.Files)
{
<div class="d-flex justify-content-between">
<a type="button" class="btn btn-link" href="@filesUploaded.Location" target="_blank">@filesUploaded.Name</a>
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteGasRecordFile('@filesUploaded.Location', this)"><i class="bi bi-trash"></i></button>
</div>
}
@await Html.PartialAsync("_UploadedFiles", Model.GasRecord.Files)
<label for="gasRecordFiles">Upload more documents</label>
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="gasRecordFiles">
</div>
@@ -52,16 +73,16 @@
</form>
</div>
<div class="modal-footer">
@if (Model.Id > 0)
@if (!isNew)
{
<button type="button" class="btn btn-danger" onclick="deleteGasRecord(@Model.Id)" style="margin-right:auto;">Delete</button>
<button type="button" class="btn btn-danger" onclick="deleteGasRecord(@Model.GasRecord.Id)" style="margin-right:auto;">Delete</button>
}
<button type="button" class="btn btn-secondary" onclick="hideAddGasRecordModal()">Cancel</button>
@if (Model.Id == 0)
@if (isNew)
{
<button type="button" class="btn btn-primary" onclick="saveGasRecordToVehicle()">Add New Gas Record</button>
}
else if (Model.Id > 0)
else if (!isNew)
{
<button type="button" class="btn btn-primary" onclick="saveGasRecordToVehicle(true)">Edit Gas Record</button>
}
@@ -70,12 +91,12 @@
var uploadedFiles = [];
getUploadedFilesFromModel();
function getUploadedFilesFromModel() {
@foreach (UploadedFiles filesUploaded in Model.Files)
@foreach (UploadedFiles filesUploaded in Model.GasRecord.Files)
{
@:uploadedFiles.push({ name: "@filesUploaded.Name", location: "@filesUploaded.Location" });
}
}
function getGasRecordModelData(){
return {id: @Model.Id}
return { id: @Model.GasRecord.Id}
}
</script>

View File

@@ -0,0 +1,68 @@
@model ReminderRecordInput
@{
var isNew = Model.Id == 0;
}
<div class="modal-header">
<h5 class="modal-title">@(isNew ? "Add New Reminder" : "Edit Reminder")</h5>
<button type="button" class="btn-close" onclick="hideAddReminderRecordModal()" aria-label="Close"></button>
</div>
<div class="modal-body">
<form>
<div class="form-group">
<div class="row">
<div class="col-md-6 col-12" id="reminderOptions">
<input type="text" id="workAroundInput" style="height:0px; width:0px; display:none;">
<label for="reminderDescription">Description</label>
<input type="text" id="reminderDescription" class="form-control" placeholder="Reminder Description" value="@Model.Description">
<label>Remind me on:</label>
<div class="form-check">
<input class="form-check-input" type="radio" name="reminderMetricOptions" id="reminderMetricDate" value="@(ReminderMetric.Date)" checked="@(Model.Metric == ReminderMetric.Date)">
<label class="form-check-label" for="reminderMetricDate">Date</label>
</div>
<div class="input-group">
<input type="text" id="reminderDate" class="form-control" placeholder="Future Date" value="@Model.Date">
<span class="input-group-text"><i class="bi bi-calendar-event"></i></span>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="reminderMetricOptions" id="reminderMetricOdometer" value="@(ReminderMetric.Odometer)" checked="@(Model.Metric == ReminderMetric.Odometer)">
<label class="form-check-label" for="reminderMetricOdometer">Odometer</label>
</div>
<div class="input-group">
<input type="number" id="reminderMileage" class="form-control" placeholder="Future Odometer Reading" value="@(isNew ? "" : Model.Mileage)">
<div class="input-group-text">
<button type="button" class="btn btn-sm btn-primary" onclick="appendMileageToOdometer(500)">+500</button>
</div>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="reminderMetricOptions" id="reminderMetricBoth" value="@(ReminderMetric.Both)" checked="@(Model.Metric == ReminderMetric.Both)">
<label class="form-check-label" for="reminderMetricBoth">Whichever comes first</label>
</div>
</div>
<div class="col-md-6 col-12">
<label for="reminderNotes">Notes(optional)</label>
<textarea id="reminderNotes" class="form-control" rows="5">@Model.Notes</textarea>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
@if (!isNew)
{
<button type="button" class="btn btn-danger" onclick="deleteReminderRecord(@Model.Id)" style="margin-right:auto;">Delete</button>
}
<button type="button" class="btn btn-secondary" onclick="hideAddReminderRecordModal()">Cancel</button>
@if (isNew)
{
<button type="button" class="btn btn-primary" onclick="saveReminderRecordToVehicle()">Add New Reminder</button>
}
else if (!isNew)
{
<button type="button" class="btn btn-primary" onclick="saveReminderRecordToVehicle(true)">Edit Reminder</button>
}
</div>
<script>
function getReminderRecordModelData() {
return { id: @Model.Id}
}
</script>

View File

@@ -0,0 +1,70 @@
@model List<ReminderRecordViewModel>
<div class="row">
<div class="d-flex justify-content-between">
<div class="d-flex align-items-center flex-wrap">
<span class="ms-2 badge bg-success">@($"# of Reminders: {Model.Count()}")</span>
<span class="ms-2 badge bg-secondary">@($"Past Due: {Model.Where(x => x.Urgency == ReminderUrgency.PastDue).Count()}")</span>
<span class="ms-2 badge bg-danger">@($"Very Urgent: {Model.Where(x=>x.Urgency == ReminderUrgency.VeryUrgent).Count()}")</span>
<span class="ms-2 badge bg-warning">@($"Urgent: {Model.Where(x => x.Urgency == ReminderUrgency.Urgent).Count()}")</span>
<span class="ms-2 badge bg-success">@($"Not Urgent: {Model.Where(x => x.Urgency == ReminderUrgency.NotUrgent).Count()}")</span>
</div>
<div>
<button onclick="showAddReminderModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>Add Reminder</button>
</div>
</div>
</div>
<div class="row vehicleDetailTabContainer">
<div class="col-12">
<table class="table table-hover">
<thead>
<tr class="d-flex">
<th scope="col" class="col-1">Urgency</th>
<th scope="col" class="col-2">Metric</th>
<th scope="col" class="col-5">Description</th>
<th scope="col" class="col-3">Notes</th>
<th scope="col" class="col-1">Delete</th>
</tr>
</thead>
<tbody>
@foreach (ReminderRecordViewModel reminderRecord in Model)
{
<tr class="d-flex" style="cursor:pointer;" onclick="showEditReminderRecordModal(@reminderRecord.Id)">
@if (reminderRecord.Urgency == ReminderUrgency.VeryUrgent)
{
<td class="col-1"><span class="badge text-bg-danger">Very Urgent</span></td>
}
else if (reminderRecord.Urgency == ReminderUrgency.Urgent)
{
<td class="col-1"><span class="badge text-bg-warning">Urgent</span></td>
}
else if (reminderRecord.Urgency == ReminderUrgency.PastDue)
{
<td class="col-1"><span class="badge text-bg-secondary">Past Due</span></td>
}
else
{
<td class="col-1"><span class="badge text-bg-success">Not Urgent</span></td>
}
@if (reminderRecord.Metric == ReminderMetric.Date)
{
<td class="col-2">@reminderRecord.Date.ToShortDateString()</td>
}
else if (reminderRecord.Metric == ReminderMetric.Odometer)
{
<td class="col-2">@reminderRecord.Mileage</td>
}
else
{
<td class="col-2">@reminderRecord.Metric</td>
}
<td class="col-5">@reminderRecord.Description</td>
<td class="col-3 text-truncate">@reminderRecord.Notes</td>
<td class="col-1 text-truncate">
<button type="button" class="btn btn-danger" onclick="deleteReminderRecord(@reminderRecord.Id, this)"><i class="bi bi-trash"></i></button>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>

View File

@@ -1,6 +1,9 @@
@model ServiceRecordInput
@{
var isNew = Model.Id == 0;
}
<div class="modal-header">
<h5 class="modal-title">@(Model.Id == 0 ? "Add New Service Record" : "Edit Service Record")</h5>
<h5 class="modal-title">@(isNew ? "Add New Service Record" : "Edit Service Record")</h5>
<button type="button" class="btn-close" onclick="hideAddServiceRecordModal()" aria-label="Close"></button>
</div>
<div class="modal-body">
@@ -11,15 +14,15 @@
<input type="text" id="workAroundInput" style="height:0px; width:0px; display:none;">
<label for="serviceRecordDate">Date</label>
<div class="input-group">
<input type="text" id="serviceRecordDate" class="form-control" value="@Model.Date">
<input type="text" id="serviceRecordDate" class="form-control" placeholder="Date service was performed" value="@Model.Date">
<span class="input-group-text"><i class="bi bi-calendar-event"></i></span>
</div>
<label for="serviceRecordMileage">Odometer</label>
<input type="number" id="serviceRecordMileage" class="form-control" value="@Model.Mileage">
<input type="number" id="serviceRecordMileage" class="form-control" placeholder="Odometer reading when serviced" value="@(isNew ? "" : Model.Mileage)">
<label for="serviceRecordDescription">Description</label>
<input type="text" id="serviceRecordDescription" class="form-control" value="@Model.Description">
<input type="text" id="serviceRecordDescription" class="form-control" placeholder="Description of item(s) serviced(i.e. Oil Change)" value="@Model.Description">
<label for="serviceRecordCost">Cost</label>
<input type="number" id="serviceRecordCost" class="form-control" value="@Model.Cost">
<input type="number" id="serviceRecordCost" class="form-control" placeholder="Cost of the service" value="@(isNew ? "" : Model.Cost)">
</div>
<div class="col-md-6 col-12">
<label for="serviceRecordNotes">Notes(optional)</label>
@@ -27,20 +30,22 @@
@if (Model.Files.Any())
{
<div>
<label>Uploaded Documents</label>
@foreach (UploadedFiles filesUploaded in Model.Files)
{
<div class="d-flex justify-content-between">
<a type="button" class="btn btn-link" href="@filesUploaded.Location" target="_blank">@filesUploaded.Name</a>
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteServiceRecordFile('@filesUploaded.Location', this)"><i class="bi bi-trash"></i></button>
</div>
}
@await Html.PartialAsync("_UploadedFiles", Model.Files)
<label for="serviceRecordFiles">Upload more documents</label>
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="serviceRecordFiles">
</div>
}
else
{
@if (isNew)
{
<div class="form-check">
<input class="form-check-input" type="checkbox" value="" id="addReminderCheck">
<label class="form-check-label" for="addReminderCheck">
Add Reminder
</label>
</div>
}
<label for="serviceRecordFiles">Upload documents(optional)</label>
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="serviceRecordFiles">
}
@@ -50,16 +55,16 @@
</form>
</div>
<div class="modal-footer">
@if (Model.Id > 0)
@if (!isNew)
{
<button type="button" class="btn btn-danger" onclick="deleteServiceRecord(@Model.Id)" style="margin-right:auto;">Delete</button>
}
<button type="button" class="btn btn-secondary" onclick="hideAddServiceRecordModal()">Cancel</button>
@if (Model.Id == 0)
@if (isNew)
{
<button type="button" class="btn btn-primary" onclick="saveServiceRecordToVehicle()">Add New Service Record</button>
}
else if (Model.Id > 0)
else if (!isNew)
{
<button type="button" class="btn btn-primary" onclick="saveServiceRecordToVehicle(true)">Edit Service Record</button>
}

View File

@@ -1,6 +1,7 @@
@inject IConfiguration Configuration
@{
var enableCsvImports = bool.Parse(Configuration[nameof(UserConfig.EnableCsvImports)]);
var hideZero = bool.Parse(Configuration[nameof(UserConfig.HideZero)]);
}
@model List<ServiceRecord>
<div class="row">
@@ -18,7 +19,7 @@
<span class="visually-hidden">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" onclick="showBulkImportModal('servicerecord')">Import via CSV</a></li>
<li><a class="dropdown-item" href="#" onclick="showBulkImportModal('ServiceRecord')">Import via CSV</a></li>
</ul>
</div>
}
@@ -48,7 +49,7 @@
<td class="col-1">@serviceRecord.Date.ToShortDateString()</td>
<td class="col-2">@serviceRecord.Mileage</td>
<td class="col-4">@serviceRecord.Description</td>
<td class="col-2">@serviceRecord.Cost.ToString("C")</td>
<td class="col-2">@((hideZero && serviceRecord.Cost == default) ? "---" : serviceRecord.Cost.ToString("C"))</td>
<td class="col-3 text-truncate">@serviceRecord.Notes</td>
</tr>
}
@@ -58,7 +59,7 @@
</div>
<div class="modal fade" id="serviceRecordModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal fade" data-bs-focus="false" id="serviceRecordModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content" id="serviceRecordModalContent">

View File

@@ -1,6 +1,9 @@
@model TaxRecordInput
@{
var isNew = Model.Id == 0;
}
<div class="modal-header">
<h5 class="modal-title">@(Model.Id == 0 ? "Add New Tax Record" : "Edit Tax Record")</h5>
<h5 class="modal-title">@(isNew ? "Add New Tax Record" : "Edit Tax Record")</h5>
<button type="button" class="btn-close" onclick="hideAddTaxRecordModal()" aria-label="Close"></button>
</div>
<div class="modal-body">
@@ -11,13 +14,13 @@
<input type="text" id="workAroundInput" style="height:0px; width:0px; display:none;">
<label for="taxRecordDate">Date</label>
<div class="input-group">
<input type="text" id="taxRecordDate" class="form-control" value="@Model.Date">
<input type="text" id="taxRecordDate" class="form-control" placeholder="Date tax was paid" value="@Model.Date">
<span class="input-group-text"><i class="bi bi-calendar-event"></i></span>
</div>
<label for="taxRecordDescription">Description</label>
<input type="text" id="taxRecordDescription" class="form-control" value="@Model.Description">
<input type="text" id="taxRecordDescription" class="form-control" placeholder="Description of tax paid(i.e. Registration)" value="@Model.Description">
<label for="taxRecordCost">Cost</label>
<input type="number" id="taxRecordCost" class="form-control" value="@Model.Cost">
<input type="number" id="taxRecordCost" class="form-control" placeholder="Cost of tax paid" value="@(isNew? "" : Model.Cost)">
</div>
<div class="col-md-6 col-12">
<label for="taxRecordNotes">Notes(optional)</label>
@@ -25,20 +28,22 @@
@if (Model.Files.Any())
{
<div>
<label>Uploaded Documents</label>
@foreach (UploadedFiles filesUploaded in Model.Files)
{
<div class="d-flex justify-content-between">
<a type="button" class="btn btn-link" href="@filesUploaded.Location" target="_blank">@filesUploaded.Name</a>
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteTaxRecordFile('@filesUploaded.Location', this)"><i class="bi bi-trash"></i></button>
</div>
}
@await Html.PartialAsync("_UploadedFiles", Model.Files)
<label for="taxRecordFiles">Upload more documents</label>
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="taxRecordFiles">
</div>
}
else
{
@if (isNew)
{
<div class="form-check">
<input class="form-check-input" type="checkbox" value="" id="addReminderCheck">
<label class="form-check-label" for="addReminderCheck">
Add Reminder
</label>
</div>
}
<label for="taxRecordFiles">Upload documents(optional)</label>
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="taxRecordFiles">
}
@@ -48,16 +53,16 @@
</form>
</div>
<div class="modal-footer">
@if (Model.Id > 0)
@if (!isNew)
{
<button type="button" class="btn btn-danger" onclick="deleteTaxRecord(@Model.Id)" style="margin-right:auto;">Delete</button>
}
<button type="button" class="btn btn-secondary" onclick="hideAddTaxRecordModal()">Cancel</button>
@if (Model.Id == 0)
@if (isNew)
{
<button type="button" class="btn btn-primary" onclick="saveTaxRecordToVehicle()">Add New Tax Record</button>
}
else if (Model.Id > 0)
else if (!isNew)
{
<button type="button" class="btn btn-primary" onclick="saveTaxRecordToVehicle(true)">Edit Tax Record</button>
}

View File

@@ -1,6 +1,7 @@
@inject IConfiguration Configuration
@{
var enableCsvImports = bool.Parse(Configuration[nameof(UserConfig.EnableCsvImports)]);
var hideZero = bool.Parse(Configuration[nameof(UserConfig.HideZero)]);
}
@model List<TaxRecord>
<div class="row">
@@ -46,7 +47,7 @@
<tr class="d-flex" style="cursor:pointer;" onclick="showEditTaxRecordModal(@taxRecord.Id)">
<td class="col-1">@taxRecord.Date.ToShortDateString()</td>
<td class="col-6">@taxRecord.Description</td>
<td class="col-2">@taxRecord.Cost.ToString("C")</td>
<td class="col-2">@((hideZero && taxRecord.Cost == default) ? "---" : taxRecord.Cost.ToString("C"))</td>
<td class="col-3 text-truncate">@taxRecord.Notes</td>
</tr>
}
@@ -56,7 +57,7 @@
</div>
<div class="modal fade" id="taxRecordModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal fade" data-bs-focus="false" id="taxRecordModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content" id="taxRecordModalContent">
</div>

View File

@@ -0,0 +1,84 @@
@model UpgradeRecordInput
@{
var isNew = Model.Id == 0;
}
<div class="modal-header">
<h5 class="modal-title">@(isNew ? "Add New Upgrade Record" : "Edit Upgrade Record")</h5>
<button type="button" class="btn-close" onclick="hideAddUpgradeRecordModal()" aria-label="Close"></button>
</div>
<div class="modal-body">
<form>
<div class="form-group">
<div class="row">
<div class="col-md-6 col-12">
<input type="text" id="workAroundInput" style="height:0px; width:0px; display:none;">
<label for="upgradeRecordDate">Date</label>
<div class="input-group">
<input type="text" id="upgradeRecordDate" class="form-control" placeholder="Date upgrade/mods was installed" value="@Model.Date">
<span class="input-group-text"><i class="bi bi-calendar-event"></i></span>
</div>
<label for="upgradeRecordMileage">Odometer</label>
<input type="number" id="upgradeRecordMileage" class="form-control" placeholder="Odometer reading when upgraded/modded" value="@(isNew ? "" : Model.Mileage)">
<label for="upgradeRecordDescription">Description</label>
<input type="text" id="upgradeRecordDescription" class="form-control" placeholder="Description of item(s) upgraded/modded" value="@Model.Description">
<label for="upgradeRecordCost">Cost</label>
<input type="number" id="upgradeRecordCost" class="form-control" placeholder="Cost of the upgrade/mods" value="@(isNew ? "" : Model.Cost)">
</div>
<div class="col-md-6 col-12">
<label for="upgradeRecordNotes">Notes(optional)</label>
<textarea id="upgradeRecordNotes" class="form-control" rows="5">@Model.Notes</textarea>
@if (Model.Files.Any())
{
<div>
@await Html.PartialAsync("_UploadedFiles", Model.Files)
<label for="upgradeRecordFiles">Upload more documents</label>
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="upgradeRecordFiles">
</div>
}
else
{
@if (isNew)
{
<div class="form-check">
<input class="form-check-input" type="checkbox" value="" id="addReminderCheck">
<label class="form-check-label" for="addReminderCheck">
Add Reminder
</label>
</div>
}
<label for="upgradeRecordFiles">Upload documents(optional)</label>
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="upgradeRecordFiles">
}
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
@if (!isNew)
{
<button type="button" class="btn btn-danger" onclick="deleteUpgradeRecord(@Model.Id)" style="margin-right:auto;">Delete</button>
}
<button type="button" class="btn btn-secondary" onclick="hideAddUpgradeRecordModal()">Cancel</button>
@if (isNew)
{
<button type="button" class="btn btn-primary" onclick="saveUpgradeRecordToVehicle()">Add New Upgrade Record</button>
}
else if (!isNew)
{
<button type="button" class="btn btn-primary" onclick="saveUpgradeRecordToVehicle(true)">Edit Upgrade Record</button>
}
</div>
<script>
var uploadedFiles = [];
getUploadedFilesFromModel();
function getUploadedFilesFromModel() {
@foreach (UploadedFiles filesUploaded in Model.Files)
{
@:uploadedFiles.push({ name: "@filesUploaded.Name", location: "@filesUploaded.Location" });
}
}
function getUpgradeRecordModelData() {
return { id: @Model.Id}
}
</script>

View File

@@ -0,0 +1,66 @@
@inject IConfiguration Configuration
@{
var enableCsvImports = bool.Parse(Configuration[nameof(UserConfig.EnableCsvImports)]);
var hideZero = bool.Parse(Configuration[nameof(UserConfig.HideZero)]);
}
@model List<UpgradeRecord>
<div class="row">
<div class="d-flex justify-content-between">
<div class="d-flex align-items-center flex-wrap">
<span class="ms-2 badge bg-success">@($"# of Upgrade Records: {Model.Count()}")</span>
<span class="ms-2 badge bg-primary">@($"Total: {Model.Sum(x => x.Cost).ToString("C")}")</span>
</div>
<div>
@if (enableCsvImports)
{
<div class="btn-group">
<button onclick="showAddUpgradeRecordModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>Add Upgrade Record</button>
<button type="button" class="btn btn-md btn-primary btn-md mt-1 mb-1 dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
<span class="visually-hidden">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" onclick="showBulkImportModal('UpgradeRecord')">Import via CSV</a></li>
</ul>
</div>
}
else
{
<button onclick="showAddUpgradeRecordModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>Add Upgrade Record</button>
}
</div>
</div>
</div>
<div class="row vehicleDetailTabContainer">
<div class="col-12">
<table class="table table-hover">
<thead>
<tr class="d-flex">
<th scope="col" class="col-1">Date</th>
<th scope="col" class="col-2">Odometer</th>
<th scope="col" class="col-4">Description</th>
<th scope="col" class="col-2">Cost</th>
<th scope="col" class="col-3">Notes</th>
</tr>
</thead>
<tbody>
@foreach (UpgradeRecord upgradeRecord in Model)
{
<tr class="d-flex" style="cursor:pointer;" onclick="showEditUpgradeRecordModal(@upgradeRecord.Id)">
<td class="col-1">@upgradeRecord.Date.ToShortDateString()</td>
<td class="col-2">@upgradeRecord.Mileage</td>
<td class="col-4">@upgradeRecord.Description</td>
<td class="col-2">@((hideZero && upgradeRecord.Cost == default) ? "---" : upgradeRecord.Cost.ToString("C"))</td>
<td class="col-3 text-truncate">@upgradeRecord.Notes</td>
</tr>
}
</tbody>
</table>
</div>
</div>
<div class="modal fade" data-bs-focus="false" id="upgradeRecordModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content" id="upgradeRecordModalContent">
</div>
</div>
</div>

View File

@@ -0,0 +1,16 @@
@model List<UploadedFiles>
<label>Uploaded Documents</label>
<ul class="list-group">
@foreach (UploadedFiles filesUploaded in Model)
{
<li class="list-group-item">
<div class="d-flex justify-content-between">
<a type="button" class="btn btn-link" href="@filesUploaded.Location" target="_blank">@filesUploaded.Name</a>
<div class="d-flex align-items-center">
<button type="button" class="btn btn-sm btn-outline-secondary me-2" onclick="editFileName('@filesUploaded.Location', this)"><i class="bi bi-pencil"></i></button>
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteFileFromUploadedFiles('@filesUploaded.Location', this)"><i class="bi bi-trash"></i></button>
</div>
</div>
</li>
}
</ul>

View File

@@ -1,24 +1,29 @@
@model Vehicle
@{
var isNew = Model.Id == 0;
if (Model.ImageLocation == "/defaults/noimage.png")
{
Model.ImageLocation = "";
}
}
<div class="modal-header">
<h5 class="modal-title" id="addVehicleModalLabel">@(Model.Id == 0 ? "Add New Vehicle" : "Edit Vehicle")</h5>
<h5 class="modal-title" id="addVehicleModalLabel">@(isNew ? "Add New Vehicle" : "Edit Vehicle")</h5>
</div>
<div class="modal-body">
<form class="form-inline">
<div class="form-group">
<label for="inputYear">Year</label>
<input type="number" id="inputYear" class="form-control" value="@(Model.Year > 0 ? Model.Year : "")">
<input type="number" id="inputYear" class="form-control" placeholder="Year(must be after 1900)" value="@(isNew ? "" : Model.Year)">
<label for="inputMake">Make</label>
<input type="text" id="inputMake" class="form-control" value="@Model.Make">
<input type="text" id="inputMake" class="form-control" placeholder="Make" value="@Model.Make">
<label for="inputModel">Model</label>
<input type="text" id="inputModel" class="form-control" value="@Model.Model">
<input type="text" id="inputModel" class="form-control" placeholder="Model" value="@Model.Model">
<label for="inputLicensePlate">License Plate</label>
<input type="text" id="inputLicensePlate" class="form-control" value="@Model.LicensePlate">
<input type="text" id="inputLicensePlate" class="form-control" placeholder="License Plate" value="@Model.LicensePlate">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="inputIsElectric" checked="@Model.IsElectric">
<label class="form-check-label" for="inputIsElectric">Electric Vehicle</label>
</div>
@if (!string.IsNullOrWhiteSpace(Model.ImageLocation))
{
<label for="inputImage">Replace picture(optional)</label>
@@ -32,11 +37,11 @@
</form>
</div>
<div class="modal-footer">
@if (Model.Id == 0)
@if (isNew)
{
<button type="button" class="btn btn-secondary" onclick="hideAddVehicleModal()">Cancel</button>
<button type="button" onclick="saveVehicle(false)" class="btn btn-primary">Add New Vehicle</button>
} else if (Model.Id > 0)
} else if (!isNew)
{
<button type="button" class="btn btn-secondary" onclick="hideEditVehicleModal()">Cancel</button>
<button type="button" onclick="saveVehicle(true)" class="btn btn-primary">Save Vehicle</button>

View File

@@ -5,19 +5,14 @@
"Microsoft.AspNetCore": "Warning"
}
},
"Kestrel": {
"Endpoints": {
"http": {
"Url": "http://localhost:5000"
}
}
},
"AllowedHosts": "*",
"UseDarkMode": false,
"EnableCsvImports": true,
"UseMPG": true,
"UseDescending": false,
"EnableAuth": false,
"HideZero": false,
"UseUKMPG": false,
"UserNameHash": "",
"UserPasswordHash": ""
}

1
config/userConfig.json Normal file
View File

@@ -0,0 +1 @@
{"UseDarkMode":true,"EnableCsvImports":false,"UseMPG":true,"UseDescending":false,"EnableAuth":false,"UserNameHash":"","UserPasswordHash":""}

View File

@@ -0,0 +1,50 @@
---
version: "3.4"
services:
app:
image: ghcr.io/hargata/lubelogger:latest
build: .
restart: unless-stopped
# volumes used to keep data persistent
volumes:
- config:/App/config
- data:/App/data
- documents:/App/wwwroot/documents
- images:/App/wwwroot/images
- log:/App/log
- keys:/root/.aspnet/DataProtection-Keys
# expose port and/or use serving via traefik
ports:
- 8080:8080
env_file:
- .env
# traefik configurations, including networks can be commented out if not needed
networks:
- traefik-ingress
labels:
## Traefik General
# We set 'enable by default' to false, so this tells Traefik we want it to connect here
traefik.enable: true
# define network for traefik<>app communication
traefik.docker.network: traefik-ingress
## HTTP Routers
traefik.http.routers.whoami.entrypoints: https
traefik.http.routers.whoami.rule: Host(`lubelog.mydomain.tld`)
## Middlewares
#traefik.http.routers.whoami.middlewares: authentik@docker
# none
## HTTP Services
traefik.http.services.whoami.loadbalancer.server.port: 5000
volumes:
config:
data:
documents:
images:
log:
keys:
networks:
traefik-ingress:
external: true

29
docker-compose.yml Normal file
View File

@@ -0,0 +1,29 @@
---
version: "3.4"
services:
app:
image: ghcr.io/hargata/lubelogger:latest
build: .
restart: unless-stopped
# volumes used to keep data persistent
volumes:
- config:/App/config
- data:/App/data
- documents:/App/wwwroot/documents
- images:/App/wwwroot/images
- log:/App/log
- keys:/root/.aspnet/DataProtection-Keys
# expose port and/or use serving via traefik
ports:
- 8080:8080
env_file:
- .env
volumes:
config:
data:
documents:
images:
log:
keys:

View File

@@ -1 +0,0 @@
{"UseDarkMode":true,"EnableCsvImports":true,"UseMPG":true,"UseDescending":false,"EnableAuth":false,"UserNameHash":"","UserPasswordHash":""}

View File

@@ -55,4 +55,48 @@ html {
.display-7 {
font-size: 2rem;
}
}
.bell-shake {
animation: bellshake .5s;
backface-visibility: hidden;
transform-origin: top center;
}
@keyframes bellshake {
0% {
transform: rotate(0);
}
15% {
transform: rotate(5deg);
}
30% {
transform: rotate(-5deg);
}
45% {
transform: rotate(4deg);
}
60% {
transform: rotate(-4deg);
}
75% {
transform: rotate(2deg);
}
85% {
transform: rotate(-2deg);
}
92% {
transform: rotate(1deg);
}
100% {
transform: rotate(0);
}
}

View File

@@ -4,7 +4,8 @@
$("#collisionRecordModalContent").html(data);
//initiate datepicker
$('#collisionRecordDate').datepicker({
endDate: "+0d"
endDate: "+0d",
format: getShortDatePattern().pattern
});
$('#collisionRecordModal').modal('show');
}
@@ -16,7 +17,8 @@ function showEditCollisionRecordModal(collisionRecordId) {
$("#collisionRecordModalContent").html(data);
//initiate datepicker
$('#collisionRecordDate').datepicker({
endDate: "+0d"
endDate: "+0d",
format: getShortDatePattern().pattern
});
$('#collisionRecordModal').modal('show');
}
@@ -64,6 +66,9 @@ function saveCollisionRecordToVehicle(isEdit) {
successToast(isEdit ? "Repair Record Updated" : "Repair Record Added.");
hideAddCollisionRecordModal();
getVehicleCollisionRecords(formValues.vehicleId);
if (formValues.addReminderRecord) {
setTimeout(function () { showAddReminderModal(formValues); }, 500);
}
} else {
errorToast("An error has occurred, please try again later.");
}
@@ -77,6 +82,7 @@ function getAndValidateCollisionRecordValues() {
var collisionNotes = $("#collisionRecordNotes").val();
var vehicleId = GetVehicleId().vehicleId;
var collisionRecordId = getCollisionRecordModelData().id;
var addReminderRecord = $("#addReminderCheck").is(":checked");
//validation
var hasError = false;
if (collisionDate.trim() == '') { //eliminates whitespace.
@@ -112,10 +118,7 @@ function getAndValidateCollisionRecordValues() {
description: collisionDescription,
cost: collisionCost,
notes: collisionNotes,
files: uploadedFiles
files: uploadedFiles,
addReminderRecord: addReminderRecord
}
}
function deleteCollisionRecordFile(fileLocation, event) {
event.parentElement.remove();
uploadedFiles = uploadedFiles.filter(x => x.location != fileLocation);
}

View File

@@ -4,7 +4,8 @@
$("#gasRecordModalContent").html(data);
//initiate datepicker
$('#gasRecordDate').datepicker({
endDate: "+0d"
endDate: "+0d",
format: getShortDatePattern().pattern
});
$('#gasRecordModal').modal('show');
}
@@ -16,7 +17,8 @@ function showEditGasRecordModal(gasRecordId) {
$("#gasRecordModalContent").html(data);
//initiate datepicker
$('#gasRecordDate').datepicker({
endDate: "+0d"
endDate: "+0d",
format: getShortDatePattern().pattern
});
$('#gasRecordModal').modal('show');
}
@@ -74,6 +76,7 @@ function getAndValidateGasRecordValues() {
var gasMileage = $("#gasRecordMileage").val();
var gasGallons = $("#gasRecordGallons").val();
var gasCost = $("#gasRecordCost").val();
var gasIsFillToFull = $("#gasIsFillToFull").is(":checked");
var vehicleId = GetVehicleId().vehicleId;
var gasRecordId = getGasRecordModelData().id;
//validation
@@ -110,10 +113,7 @@ function getAndValidateGasRecordValues() {
mileage: gasMileage,
gallons: gasGallons,
cost: gasCost,
files: uploadedFiles
files: uploadedFiles,
isFillToFull: gasIsFillToFull
}
}
function deleteGasRecordFile(fileLocation, event) {
event.parentElement.remove();
uploadedFiles = uploadedFiles.filter(x => x.location != fileLocation);
}

View File

@@ -9,4 +9,9 @@
errorToast("Invalid Login Credentials, please try again.");
}
})
}
function handlePasswordKeyPress(event) {
if (event.keyCode == 13) {
performLogin();
}
}

View File

@@ -0,0 +1,125 @@
function showEditReminderRecordModal(reminderId) {
$.get(`/Vehicle/GetReminderRecordForEditById?reminderRecordId=${reminderId}`, function (data) {
if (data) {
$("#reminderRecordModalContent").html(data);
$('#reminderDate').datepicker({
startDate: "+0d",
format: getShortDatePattern().pattern
});
$("#reminderRecordModal").modal("show");
}
});
}
function hideAddReminderRecordModal() {
$('#reminderRecordModal').modal('hide');
}
function deleteReminderRecord(reminderRecordId, e) {
if (e != undefined) {
event.stopPropagation();
}
$("#workAroundInput").show();
Swal.fire({
title: "Confirm Deletion?",
text: "Deleted Reminders cannot be restored.",
showCancelButton: true,
confirmButtonText: "Delete",
confirmButtonColor: "#dc3545"
}).then((result) => {
if (result.isConfirmed) {
$.post(`/Vehicle/DeleteReminderRecordById?reminderRecordId=${reminderRecordId}`, function (data) {
if (data) {
hideAddReminderRecordModal();
successToast("Reminder Deleted");
var vehicleId = GetVehicleId().vehicleId;
getVehicleReminders(vehicleId);
} else {
errorToast("An error has occurred, please try again later.");
}
});
} else {
$("#workAroundInput").hide();
}
});
}
function saveReminderRecordToVehicle(isEdit) {
//get values
var formValues = getAndValidateReminderRecordValues();
//validate
if (formValues.hasError) {
errorToast("Please check the form data");
return;
}
//save to db.
$.post('/Vehicle/SaveReminderRecordToVehicleId', { reminderRecord: formValues }, function (data) {
if (data) {
successToast(isEdit ? "Reminder Updated" : "Reminder Added.");
hideAddReminderRecordModal();
getVehicleReminders(formValues.vehicleId);
} else {
errorToast("An error has occurred, please try again later.");
}
})
}
function appendMileageToOdometer(increment) {
var reminderMileage = $("#reminderMileage").val();
var reminderMileageIsInvalid = reminderMileage.trim() == '' || parseInt(reminderMileage) < 0;
if (reminderMileageIsInvalid) {
reminderMileage = 0;
} else {
reminderMileage = parseInt(reminderMileage);
}
reminderMileage += increment;
$("#reminderMileage").val(reminderMileage);
}
function getAndValidateReminderRecordValues() {
var reminderDate = $("#reminderDate").val();
var reminderMileage = $("#reminderMileage").val();
var reminderDescription = $("#reminderDescription").val();
var reminderNotes = $("#reminderNotes").val();
var reminderOption = $('#reminderOptions input:radio:checked').val();
var vehicleId = GetVehicleId().vehicleId;
var reminderId = getReminderRecordModelData().id;
//validation
var hasError = false;
var reminderDateIsInvalid = reminderDate.trim() == ''; //eliminates whitespace.
var reminderMileageIsInvalid = reminderMileage.trim() == '' || parseInt(reminderMileage) < 0;
if ((reminderOption == "Both" || reminderOption == "Date") && reminderDateIsInvalid) {
hasError = true;
$("#reminderDate").addClass("is-invalid");
} else if (reminderOption == "Date") {
$("#reminderDate").removeClass("is-invalid");
}
if ((reminderOption == "Both" || reminderOption == "Odometer") && reminderMileageIsInvalid) {
hasError = true;
$("#reminderMileage").addClass("is-invalid");
} else if (reminderOption == "Odometer") {
$("#reminderMileage").removeClass("is-invalid");
}
if (reminderDescription.trim() == '') {
hasError = true;
$("#reminderDescription").addClass("is-invalid");
} else {
$("#reminderDescription").removeClass("is-invalid");
}
if (reminderOption == undefined) {
hasError = true;
$("#reminderMetricDate").addClass("is-invalid");
$("#reminderMetricOdometer").addClass("is-invalid");
$("#reminderMetricBoth").addClass("is-invalid");
} else {
$("#reminderMetricDate").removeClass("is-invalid");
$("#reminderMetricOdometer").removeClass("is-invalid");
$("#reminderMetricBoth").removeClass("is-invalid");
}
return {
id: reminderId,
hasError: hasError,
vehicleId: vehicleId,
date: reminderDate,
mileage: reminderMileage,
description: reminderDescription,
notes: reminderNotes,
metric: reminderOption
}
}

View File

@@ -4,7 +4,8 @@
$("#serviceRecordModalContent").html(data);
//initiate datepicker
$('#serviceRecordDate').datepicker({
endDate: "+0d"
endDate: "+0d",
format: getShortDatePattern().pattern
});
$('#serviceRecordModal').modal('show');
}
@@ -16,7 +17,8 @@ function showEditServiceRecordModal(serviceRecordId) {
$("#serviceRecordModalContent").html(data);
//initiate datepicker
$('#serviceRecordDate').datepicker({
endDate: "+0d"
endDate: "+0d",
format: getShortDatePattern().pattern
});
$('#serviceRecordModal').modal('show');
}
@@ -64,6 +66,9 @@ function saveServiceRecordToVehicle(isEdit) {
successToast(isEdit ? "Service Record Updated" : "Service Record Added.");
hideAddServiceRecordModal();
getVehicleServiceRecords(formValues.vehicleId);
if (formValues.addReminderRecord) {
setTimeout(function () { showAddReminderModal(formValues); }, 500);
}
} else {
errorToast("An error has occurred, please try again later.");
}
@@ -77,6 +82,7 @@ function getAndValidateServiceRecordValues() {
var serviceNotes = $("#serviceRecordNotes").val();
var vehicleId = GetVehicleId().vehicleId;
var serviceRecordId = getServiceRecordModelData().id;
var addReminderRecord = $("#addReminderCheck").is(":checked");
//validation
var hasError = false;
if (serviceDate.trim() == '') { //eliminates whitespace.
@@ -112,10 +118,7 @@ function getAndValidateServiceRecordValues() {
description: serviceDescription,
cost: serviceCost,
notes: serviceNotes,
files: uploadedFiles
files: uploadedFiles,
addReminderRecord: addReminderRecord
}
}
function deleteServiceRecordFile(fileLocation, event) {
event.parentElement.remove();
uploadedFiles = uploadedFiles.filter(x => x.location != fileLocation);
}

View File

@@ -37,6 +37,7 @@ function saveVehicle(isEdit) {
var vehicleMake = $("#inputMake").val();
var vehicleModel = $("#inputModel").val();
var vehicleLicensePlate = $("#inputLicensePlate").val();
var vehicleIsElectric = $("#inputIsElectric").is(":checked");
//validate
var hasError = false;
if (vehicleYear.trim() == '' || parseInt(vehicleYear) < 1900) {
@@ -72,7 +73,8 @@ function saveVehicle(isEdit) {
year: vehicleYear,
make: vehicleMake,
model: vehicleModel,
licensePlate: vehicleLicensePlate
licensePlate: vehicleLicensePlate,
isElectric: vehicleIsElectric
}, function (data) {
if (data) {
if (!isEdit) {

View File

@@ -4,7 +4,8 @@
$("#taxRecordModalContent").html(data);
//initiate datepicker
$('#taxRecordDate').datepicker({
endDate: "+0d"
endDate: "+0d",
format: getShortDatePattern().pattern
});
$('#taxRecordModal').modal('show');
}
@@ -16,7 +17,8 @@ function showEditTaxRecordModal(taxRecordId) {
$("#taxRecordModalContent").html(data);
//initiate datepicker
$('#taxRecordDate').datepicker({
endDate: "+0d"
endDate: "+0d",
format: getShortDatePattern().pattern
});
$('#taxRecordModal').modal('show');
}
@@ -64,6 +66,9 @@ function saveTaxRecordToVehicle(isEdit) {
successToast(isEdit ? "Tax Record Updated" : "Tax Record Added.");
hideAddTaxRecordModal();
getVehicleTaxRecords(formValues.vehicleId);
if (formValues.addReminderRecord) {
setTimeout(function () { showAddReminderModal(formValues); }, 500);
}
} else {
errorToast("An error has occurred, please try again later.");
}
@@ -76,6 +81,7 @@ function getAndValidateTaxRecordValues() {
var taxNotes = $("#taxRecordNotes").val();
var vehicleId = GetVehicleId().vehicleId;
var taxRecordId = getTaxRecordModelData().id;
var addReminderRecord = $("#addReminderCheck").is(":checked");
//validation
var hasError = false;
if (taxDate.trim() == '') { //eliminates whitespace.
@@ -104,10 +110,7 @@ function getAndValidateTaxRecordValues() {
description: taxDescription,
cost: taxCost,
notes: taxNotes,
files: uploadedFiles
files: uploadedFiles,
addReminderRecord: addReminderRecord
}
}
function deleteTaxRecordFile(fileLocation, event) {
event.parentElement.remove();
uploadedFiles = uploadedFiles.filter(x => x.location != fileLocation);
}

124
wwwroot/js/upgraderecord.js Normal file
View File

@@ -0,0 +1,124 @@
function showAddUpgradeRecordModal() {
$.get('/Vehicle/GetAddUpgradeRecordPartialView', function (data) {
if (data) {
$("#upgradeRecordModalContent").html(data);
//initiate datepicker
$('#upgradeRecordDate').datepicker({
endDate: "+0d",
format: getShortDatePattern().pattern
});
$('#upgradeRecordModal').modal('show');
}
});
}
function showEditUpgradeRecordModal(upgradeRecordId) {
$.get(`/Vehicle/GetUpgradeRecordForEditById?upgradeRecordId=${upgradeRecordId}`, function (data) {
if (data) {
$("#upgradeRecordModalContent").html(data);
//initiate datepicker
$('#upgradeRecordDate').datepicker({
endDate: "+0d",
format: getShortDatePattern().pattern
});
$('#upgradeRecordModal').modal('show');
}
});
}
function hideAddUpgradeRecordModal() {
$('#upgradeRecordModal').modal('hide');
}
function deleteUpgradeRecord(upgradeRecordId) {
$("#workAroundInput").show();
Swal.fire({
title: "Confirm Deletion?",
text: "Deleted Upgrade Records cannot be restored.",
showCancelButton: true,
confirmButtonText: "Delete",
confirmButtonColor: "#dc3545"
}).then((result) => {
if (result.isConfirmed) {
$.post(`/Vehicle/DeleteUpgradeRecordById?upgradeRecordId=${upgradeRecordId}`, function (data) {
if (data) {
hideAddUpgradeRecordModal();
successToast("Upgrade Record Deleted");
var vehicleId = GetVehicleId().vehicleId;
getVehicleUpgradeRecords(vehicleId);
} else {
errorToast("An error has occurred, please try again later.");
}
});
} else {
$("#workAroundInput").hide();
}
});
}
function saveUpgradeRecordToVehicle(isEdit) {
//get values
var formValues = getAndValidateUpgradeRecordValues();
//validate
if (formValues.hasError) {
errorToast("Please check the form data");
return;
}
//save to db.
$.post('/Vehicle/SaveUpgradeRecordToVehicleId', { upgradeRecord: formValues }, function (data) {
if (data) {
successToast(isEdit ? "Upgrade Record Updated" : "Upgrade Record Added.");
hideAddUpgradeRecordModal();
getVehicleUpgradeRecords(formValues.vehicleId);
if (formValues.addReminderRecord) {
setTimeout(function () { showAddReminderModal(formValues); }, 500);
}
} else {
errorToast("An error has occurred, please try again later.");
}
})
}
function getAndValidateUpgradeRecordValues() {
var serviceDate = $("#upgradeRecordDate").val();
var serviceMileage = $("#upgradeRecordMileage").val();
var serviceDescription = $("#upgradeRecordDescription").val();
var serviceCost = $("#upgradeRecordCost").val();
var serviceNotes = $("#upgradeRecordNotes").val();
var vehicleId = GetVehicleId().vehicleId;
var upgradeRecordId = getUpgradeRecordModelData().id;
var addReminderRecord = $("#addReminderCheck").is(":checked");
//validation
var hasError = false;
if (serviceDate.trim() == '') { //eliminates whitespace.
hasError = true;
$("#upgradeRecordDate").addClass("is-invalid");
} else {
$("#upgradeRecordDate").removeClass("is-invalid");
}
if (serviceMileage.trim() == '' || parseInt(serviceMileage) < 0) {
hasError = true;
$("#upgradeRecordMileage").addClass("is-invalid");
} else {
$("#upgradeRecordMileage").removeClass("is-invalid");
}
if (serviceDescription.trim() == '') {
hasError = true;
$("#upgradeRecordDescription").addClass("is-invalid");
} else {
$("#upgradeRecordDescription").removeClass("is-invalid");
}
if (serviceCost.trim() == '') {
hasError = true;
$("#upgradeRecordCost").addClass("is-invalid");
} else {
$("#upgradeRecordCost").removeClass("is-invalid");
}
return {
id: upgradeRecordId,
hasError: hasError,
vehicleId: vehicleId,
date: serviceDate,
mileage: serviceMileage,
description: serviceDescription,
cost: serviceCost,
notes: serviceNotes,
files: uploadedFiles,
addReminderRecord: addReminderRecord
}
}

View File

@@ -34,6 +34,12 @@ $(document).ready(function () {
case "report-tab":
getVehicleReport();
break;
case "reminder-tab":
getVehicleReminders(vehicleId);
break;
case "upgrade-tab":
getVehicleUpgradeRecords(vehicleId);
break;
}
switch (e.relatedTarget.id) { //clear out previous tabs with grids in them to help with performance
case "servicerecord-tab":
@@ -51,6 +57,12 @@ $(document).ready(function () {
case "report-tab":
$("#report-tab-pane").html("");
break;
case "reminder-tab":
$("#reminder-tab-pane").html("");
break;
case "upgrade-tab":
$("#upgrade-tab-pane").html("");
break;
}
});
getVehicleServiceRecords(vehicleId);
@@ -67,13 +79,23 @@ function getVehicleServiceRecords(vehicleId) {
$.get(`/Vehicle/GetServiceRecordsByVehicleId?vehicleId=${vehicleId}`, function (data) {
if (data) {
$("#servicerecord-tab-pane").html(data);
getVehicleHaveImportantReminders(vehicleId);
}
})
});
}
function getVehicleUpgradeRecords(vehicleId) {
$.get(`/Vehicle/GetUpgradeRecordsByVehicleId?vehicleId=${vehicleId}`, function (data) {
if (data) {
$("#upgrade-tab-pane").html(data);
getVehicleHaveImportantReminders(vehicleId);
}
});
}
function getVehicleGasRecords(vehicleId) {
$.get(`/Vehicle/GetGasRecordsByVehicleId?vehicleId=${vehicleId}`, function (data) {
if (data) {
$("#gas-tab-pane").html(data);
getVehicleHaveImportantReminders(vehicleId);
}
});
}
@@ -81,6 +103,7 @@ function getVehicleCollisionRecords(vehicleId) {
$.get(`/Vehicle/GetCollisionRecordsByVehicleId?vehicleId=${vehicleId}`, function (data) {
if (data) {
$("#accident-tab-pane").html(data);
getVehicleHaveImportantReminders(vehicleId);
}
});
}
@@ -88,6 +111,15 @@ function getVehicleTaxRecords(vehicleId) {
$.get(`/Vehicle/GetTaxRecordsByVehicleId?vehicleId=${vehicleId}`, function (data) {
if (data) {
$("#tax-tab-pane").html(data);
getVehicleHaveImportantReminders(vehicleId);
}
});
}
function getVehicleReminders(vehicleId) {
$.get(`/Vehicle/GetReminderRecordsByVehicleId?vehicleId=${vehicleId}`, function (data) {
if (data) {
$("#reminder-tab-pane").html(data);
getVehicleHaveImportantReminders(vehicleId);
}
});
}
@@ -158,4 +190,70 @@ function uploadVehicleFilesAsync(event) {
}
}
});
}
function showAddReminderModal(reminderModalInput) {
if (reminderModalInput != undefined) {
$.post('/Vehicle/GetAddReminderRecordPartialView', {reminderModel: reminderModalInput}, function (data) {
$("#reminderRecordModalContent").html(data);
$('#reminderDate').datepicker({
startDate: "+0d"
});
$("#reminderRecordModal").modal("show");
});
} else {
$.post('/Vehicle/GetAddReminderRecordPartialView', function (data) {
$("#reminderRecordModalContent").html(data);
$('#reminderDate').datepicker({
startDate: "+0d",
format: getShortDatePattern().pattern
});
$("#reminderRecordModal").modal("show");
});
}
}
function getVehicleHaveImportantReminders(vehicleId) {
setTimeout(function () {
$.get(`/Vehicle/GetVehicleHaveUrgentOrPastDueReminders?vehicleId=${vehicleId}`, function (data) {
if (data) {
$("#reminderBell").removeClass("bi-bell");
$("#reminderBell").addClass("bi-bell-fill");
$("#reminderBell").addClass("text-warning");
$("#reminderBellDiv").addClass("bell-shake");
} else {
$("#reminderBellDiv").removeClass("bell-shake");
$("#reminderBell").removeClass("bi-bell-fill");
$("#reminderBell").addClass("bi-bell");
$("#reminderBell").removeClass("text-warning");
}
});
}, 500);
}
function deleteFileFromUploadedFiles(fileLocation, event) {
event.parentElement.parentElement.parentElement.remove();
uploadedFiles = uploadedFiles.filter(x => x.location != fileLocation);
}
function editFileName(fileLocation, event) {
Swal.fire({
title: 'Rename File',
html: `
<input type="text" id="newFileName" class="swal2-input" placeholder="New File Name">
`,
confirmButtonText: 'Rename',
focusConfirm: false,
preConfirm: () => {
const newFileName = $("#newFileName").val();
if (!newFileName) {
Swal.showValidationMessage(`Please enter a valid file name`)
}
return { newFileName }
},
}).then(function (result) {
if (result.isConfirmed) {
var linkDisplayObject = $(event.parentElement.parentElement).find('a')[0];
linkDisplayObject.text = result.value.newFileName;
var editFileIndex = uploadedFiles.findIndex(x => x.location == fileLocation);
uploadedFiles[editFileIndex].name = result.value.newFileName;
}
});
}