Compare commits

..

61 Commits

Author SHA1 Message Date
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
53 changed files with 1115 additions and 92 deletions

2
.env Normal file
View File

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

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

@@ -0,0 +1,19 @@
name: Docker Image CI
on:
push:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build and push the Docker image
run: |
docker login -u "hargata" -p "${{ secrets.GHCR_PAT }}" ghcr.io
docker build . --file Dockerfile --tag ghcr.io/hargata/lubelogger:latest
docker push ghcr.io/hargata/lubelogger:latest

1
.gitignore vendored
View File

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

View File

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

View File

@@ -6,6 +6,7 @@ using CarCareTracker.Helper;
using CsvHelper; using CsvHelper;
using System.Globalization; using System.Globalization;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using CarCareTracker.External.Implementations;
namespace CarCareTracker.Controllers namespace CarCareTracker.Controllers
{ {
@@ -19,19 +20,21 @@ namespace CarCareTracker.Controllers
private readonly IGasRecordDataAccess _gasRecordDataAccess; private readonly IGasRecordDataAccess _gasRecordDataAccess;
private readonly ICollisionRecordDataAccess _collisionRecordDataAccess; private readonly ICollisionRecordDataAccess _collisionRecordDataAccess;
private readonly ITaxRecordDataAccess _taxRecordDataAccess; private readonly ITaxRecordDataAccess _taxRecordDataAccess;
private readonly IReminderRecordDataAccess _reminderRecordDataAccess;
private readonly IWebHostEnvironment _webEnv; private readonly IWebHostEnvironment _webEnv;
private readonly bool _useDescending; private readonly bool _useDescending;
private readonly IConfiguration _config; private readonly IConfiguration _config;
private readonly IFileHelper _fileHelper; private readonly IFileHelper _fileHelper;
public VehicleController(ILogger<VehicleController> logger, public VehicleController(ILogger<VehicleController> logger,
IFileHelper fileHelper, IFileHelper fileHelper,
IVehicleDataAccess dataAccess, IVehicleDataAccess dataAccess,
INoteDataAccess noteDataAccess, INoteDataAccess noteDataAccess,
IServiceRecordDataAccess serviceRecordDataAccess, IServiceRecordDataAccess serviceRecordDataAccess,
IGasRecordDataAccess gasRecordDataAccess, IGasRecordDataAccess gasRecordDataAccess,
ICollisionRecordDataAccess collisionRecordDataAccess, ICollisionRecordDataAccess collisionRecordDataAccess,
ITaxRecordDataAccess taxRecordDataAccess, ITaxRecordDataAccess taxRecordDataAccess,
IReminderRecordDataAccess reminderRecordDataAccess,
IWebHostEnvironment webEnv, IWebHostEnvironment webEnv,
IConfiguration config) IConfiguration config)
{ {
@@ -43,6 +46,7 @@ namespace CarCareTracker.Controllers
_gasRecordDataAccess = gasRecordDataAccess; _gasRecordDataAccess = gasRecordDataAccess;
_collisionRecordDataAccess = collisionRecordDataAccess; _collisionRecordDataAccess = collisionRecordDataAccess;
_taxRecordDataAccess = taxRecordDataAccess; _taxRecordDataAccess = taxRecordDataAccess;
_reminderRecordDataAccess = reminderRecordDataAccess;
_webEnv = webEnv; _webEnv = webEnv;
_config = config; _config = config;
_useDescending = bool.Parse(config[nameof(UserConfig.UseDescending)]); _useDescending = bool.Parse(config[nameof(UserConfig.UseDescending)]);
@@ -90,6 +94,7 @@ namespace CarCareTracker.Controllers
_collisionRecordDataAccess.DeleteAllCollisionRecordsByVehicleId(vehicleId) && _collisionRecordDataAccess.DeleteAllCollisionRecordsByVehicleId(vehicleId) &&
_taxRecordDataAccess.DeleteAllTaxRecordsByVehicleId(vehicleId) && _taxRecordDataAccess.DeleteAllTaxRecordsByVehicleId(vehicleId) &&
_noteDataAccess.DeleteNoteByVehicleId(vehicleId) && _noteDataAccess.DeleteNoteByVehicleId(vehicleId) &&
_reminderRecordDataAccess.DeleteAllReminderRecordsByVehicleId(vehicleId) &&
_dataAccess.DeleteVehicle(vehicleId); _dataAccess.DeleteVehicle(vehicleId);
return Json(result); return Json(result);
} }
@@ -160,7 +165,8 @@ namespace CarCareTracker.Controllers
_gasRecordDataAccess.SaveGasRecordToVehicle(convertedRecord); _gasRecordDataAccess.SaveGasRecordToVehicle(convertedRecord);
} }
} }
} else if (mode == "servicerecord") }
else if (mode == "servicerecord")
{ {
var records = csv.GetRecords<ServiceRecordImport>().ToList(); var records = csv.GetRecords<ServiceRecordImport>().ToList();
if (records.Any()) if (records.Any())
@@ -179,7 +185,8 @@ namespace CarCareTracker.Controllers
_serviceRecordDataAccess.SaveServiceRecordToVehicle(convertedRecord); _serviceRecordDataAccess.SaveServiceRecordToVehicle(convertedRecord);
} }
} }
} else if (mode == "repairrecord") }
else if (mode == "repairrecord")
{ {
var records = csv.GetRecords<ServiceRecordImport>().ToList(); var records = csv.GetRecords<ServiceRecordImport>().ToList();
if (records.Any()) if (records.Any())
@@ -198,7 +205,8 @@ namespace CarCareTracker.Controllers
_collisionRecordDataAccess.SaveCollisionRecordToVehicle(convertedRecord); _collisionRecordDataAccess.SaveCollisionRecordToVehicle(convertedRecord);
} }
} }
} else if (mode == "taxrecord") }
else if (mode == "taxrecord")
{ {
var records = csv.GetRecords<TaxRecordImport>().ToList(); var records = csv.GetRecords<TaxRecordImport>().ToList();
if (records.Any()) if (records.Any())
@@ -239,14 +247,16 @@ namespace CarCareTracker.Controllers
bool useMPG = bool.Parse(_config[nameof(UserConfig.UseMPG)]); bool useMPG = bool.Parse(_config[nameof(UserConfig.UseMPG)]);
var computedResults = new List<GasRecordViewModel>(); var computedResults = new List<GasRecordViewModel>();
int previousMileage = 0; int previousMileage = 0;
decimal unFactoredConsumption = 0.00M;
int unFactoredMileage = 0;
//perform computation. //perform computation.
for(int i = 0; i < result.Count; i++) for (int i = 0; i < result.Count; i++)
{ {
if (i > 0) if (i > 0)
{ {
var currentObject = result[i]; var currentObject = result[i];
var deltaMileage = currentObject.Mileage - previousMileage; var deltaMileage = currentObject.Mileage - previousMileage;
computedResults.Add(new GasRecordViewModel() var gasRecordViewModel = new GasRecordViewModel()
{ {
Id = currentObject.Id, Id = currentObject.Id,
VehicleId = currentObject.VehicleId, VehicleId = currentObject.VehicleId,
@@ -255,10 +265,24 @@ namespace CarCareTracker.Controllers
Gallons = currentObject.Gallons, Gallons = currentObject.Gallons,
Cost = currentObject.Cost, Cost = currentObject.Cost,
DeltaMileage = deltaMileage, DeltaMileage = deltaMileage,
MilesPerGallon = useMPG ? (deltaMileage / currentObject.Gallons) : 100 / (deltaMileage / currentObject.Gallons),
CostPerGallon = (currentObject.Cost / currentObject.Gallons) CostPerGallon = (currentObject.Cost / currentObject.Gallons)
}); };
} else if (currentObject.IsFillToFull)
{
//if user filled to full.
gasRecordViewModel.MilesPerGallon = useMPG ? ((unFactoredMileage + deltaMileage) / (unFactoredConsumption + currentObject.Gallons)) : 100 / ((unFactoredMileage + deltaMileage) / (unFactoredConsumption + currentObject.Gallons));
//reset unFactored vars
unFactoredConsumption = 0;
unFactoredMileage = 0;
} else
{
unFactoredConsumption += currentObject.Gallons;
unFactoredMileage += deltaMileage;
gasRecordViewModel.MilesPerGallon = 0;
}
computedResults.Add(gasRecordViewModel);
}
else
{ {
computedResults.Add(new GasRecordViewModel() computedResults.Add(new GasRecordViewModel()
{ {
@@ -279,7 +303,13 @@ namespace CarCareTracker.Controllers
{ {
computedResults = computedResults.OrderByDescending(x => DateTime.Parse(x.Date)).ThenByDescending(x => x.Mileage).ToList(); 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] [HttpPost]
public IActionResult SaveGasRecordToVehicleId(GasRecordInput gasRecord) public IActionResult SaveGasRecordToVehicleId(GasRecordInput gasRecord)
@@ -291,7 +321,7 @@ namespace CarCareTracker.Controllers
[HttpGet] [HttpGet]
public IActionResult GetAddGasRecordPartialView() public IActionResult GetAddGasRecordPartialView()
{ {
return PartialView("_GasModal", new GasRecordInput()); return PartialView("_GasModal", new GasRecordInputContainer() { GasRecord = new GasRecordInput() });
} }
[HttpGet] [HttpGet]
public IActionResult GetGasRecordForEditById(int gasRecordId) public IActionResult GetGasRecordForEditById(int gasRecordId)
@@ -305,9 +335,16 @@ namespace CarCareTracker.Controllers
Cost = result.Cost, Cost = result.Cost,
Date = result.Date.ToShortDateString(), Date = result.Date.ToShortDateString(),
Files = result.Files, 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] [HttpPost]
public IActionResult DeleteGasRecordById(int gasRecordId) public IActionResult DeleteGasRecordById(int gasRecordId)
@@ -349,9 +386,11 @@ namespace CarCareTracker.Controllers
{ {
var result = _serviceRecordDataAccess.GetServiceRecordById(serviceRecordId); var result = _serviceRecordDataAccess.GetServiceRecordById(serviceRecordId);
//convert to Input object. //convert to Input object.
var convertedResult = new ServiceRecordInput { Id = result.Id, var convertedResult = new ServiceRecordInput
Cost = result.Cost, {
Date = result.Date.ToShortDateString(), Id = result.Id,
Cost = result.Cost,
Date = result.Date.ToShortDateString(),
Description = result.Description, Description = result.Description,
Mileage = result.Mileage, Mileage = result.Mileage,
Notes = result.Notes, Notes = result.Notes,
@@ -360,7 +399,7 @@ namespace CarCareTracker.Controllers
}; };
return PartialView("_ServiceRecordModal", convertedResult); return PartialView("_ServiceRecordModal", convertedResult);
} }
[HttpPost] [HttpPost]
public IActionResult DeleteServiceRecordById(int serviceRecordId) public IActionResult DeleteServiceRecordById(int serviceRecordId)
{ {
var result = _serviceRecordDataAccess.DeleteServiceRecordById(serviceRecordId); var result = _serviceRecordDataAccess.DeleteServiceRecordById(serviceRecordId);
@@ -508,12 +547,178 @@ namespace CarCareTracker.Controllers
{ {
gasRecords.RemoveAll(x => x.Date.Year != year); 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), MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
Cost = x.Sum(y=>y.Cost) Cost = x.Sum(y => y.Cost)
}).ToList(); }).ToList();
return PartialView("_GasCostByMonthReport", groupedGasRecord); return PartialView("_GasCostByMonthReport", groupedGasRecord);
} }
#endregion #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));
}
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
} }
} }

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"]

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

View File

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

View File

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

View File

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

View File

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

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 MIT License
Copyright (c) 2023 ivancheahhh Copyright (c) 2023 Hargata Softworks
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

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

View File

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

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

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

View File

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

View File

@@ -11,4 +11,49 @@ Because nobody should have to deal with a homemade spreadsheet or a shoebox full
- Bootstrap-DatePicker - Bootstrap-DatePicker
- SweetAlert2 - SweetAlert2
- CsvHelper - CsvHelper
- Chart.js - Chart.js
## Docker Setup (Recommended)
1. Install Docker
2. Clone this repo
3. CHECK culture in Dockerfile, 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

@@ -16,7 +16,7 @@
</div> </div>
<div class="form-check form-switch"> <div class="form-check form-switch">
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="useMPG" checked="@Model.UseMPG"> <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>
</div> </div>
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">

View File

@@ -3,6 +3,16 @@
@{ @{
var useDarkMode = bool.Parse(Configuration["UseDarkMode"]); var useDarkMode = bool.Parse(Configuration["UseDarkMode"]);
var enableCsvImports = bool.Parse(Configuration["EnableCsvImports"]); 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")"> <html lang="en" data-bs-theme="@(useDarkMode ? "dark" : "light")">
<head> <head>
@@ -28,6 +38,11 @@
enableCsvImport : "@enableCsvImports" == "True" enableCsvImport : "@enableCsvImports" == "True"
} }
} }
function getShortDatePattern() {
return {
pattern: "@shortDatePattern"
}
}
</script> </script>
@await RenderSectionAsync("Scripts", required: false) @await RenderSectionAsync("Scripts", required: false)
</head> </head>

View File

@@ -8,6 +8,7 @@
<script src="~/js/gasrecord.js" asp-append-version="true"></script> <script src="~/js/gasrecord.js" asp-append-version="true"></script>
<script src="~/js/collisionrecord.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/taxrecord.js" asp-append-version="true"></script>
<script src="~/js/reminderrecord.js" asp-append-version="true"></script>
<script src="~/lib/chart-js/chart.umd.js"></script> <script src="~/lib/chart-js/chart.umd.js"></script>
} }
<div class="container"> <div class="container">
@@ -35,6 +36,9 @@
<li class="nav-item" role="presentation"> <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> <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>
<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"> <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> <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> </li>
@@ -65,6 +69,7 @@
</div> </div>
</div> </div>
<div class="tab-pane fade" id="accident-tab-pane" role="tabpanel" tabindex="0"></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="report-tab-pane" role="tabpanel" tabindex="0"></div>
</div> </div>
</div> </div>
@@ -80,6 +85,12 @@
</div> </div>
</div> </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> <script>
function GetVehicleId() { function GetVehicleId() {
return { vehicleId: @Model.Id}; return { vehicleId: @Model.Id};

View File

@@ -44,6 +44,15 @@
} }
else 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> <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"> <input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="collisionRecordFiles">
} }

View File

@@ -1,21 +1,33 @@
@inject IConfiguration Configuration @inject IConfiguration Configuration
@model GasRecordViewModelContainer
@{ @{
var enableCsvImports = bool.Parse(Configuration[nameof(UserConfig.EnableCsvImports)]); var enableCsvImports = bool.Parse(Configuration[nameof(UserConfig.EnableCsvImports)]);
var useMPG = bool.Parse(Configuration[nameof(UserConfig.UseMPG)]); var useMPG = bool.Parse(Configuration[nameof(UserConfig.UseMPG)]);
var useKwh = Model.UseKwh;
string consumptionUnit;
string fuelEconomyUnit;
if (useKwh)
{
consumptionUnit = "kWh";
fuelEconomyUnit = useMPG ? "mi/kWh" : "kWh/100km";
} else
{
consumptionUnit = useMPG ? "gal" : "l";
fuelEconomyUnit = useMPG ? "mpg" : "l/100km";
}
} }
@model List<GasRecordViewModel>
<div class="row"> <div class="row">
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<div class="d-flex align-items-center flex-wrap"> <div class="d-flex align-items-center flex-wrap">
<span class="ms-2 badge bg-success">@($"# of Gas Records: {Model.Count()}")</span> <span class="ms-2 badge bg-success">@($"# of Gas Records: {Model.GasRecords.Count()}")</span>
@if (Model.Count() > 1) @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">@($"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.Where(y => y.MilesPerGallon > 0)?.Min(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.Max(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 Fuel Consumed: {Model.GasRecords.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 Cost: {Model.GasRecords.Sum(x => x.Cost).ToString("C3")}")</span>
</div> </div>
@if (enableCsvImports) @if (enableCsvImports)
{ {
@@ -40,22 +52,22 @@
<tr class="d-flex"> <tr class="d-flex">
<th scope="col" class="col-2">Date Refueled</th> <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">Odometer(@(useMPG ? "mi." : "km"))</th>
<th scope="col" class="col-2">Consumption(@(useMPG ? "gal" : "l"))</th> <th scope="col" class="col-2">Consumption(@(consumptionUnit))</th>
<th scope="col" class="col-2">Fuel Economy(@(useMPG ? "mpg" : "l/100km"))</th> <th scope="col" class="col-4">Fuel Economy(@(fuelEconomyUnit))</th>
<th scope="col" class="col-2">Cost</th> <th scope="col" class="col-1">Cost</th>
<th scope="col" class="col-2">Unit Cost</th> <th scope="col" class="col-1">Unit Cost</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@foreach (GasRecordViewModel gasRecord in Model) @foreach (GasRecordViewModel gasRecord in Model.GasRecords)
{ {
<tr class="d-flex" style="cursor:pointer;" onclick="showEditGasRecordModal(@gasRecord.Id)"> <tr class="d-flex" style="cursor:pointer;" onclick="showEditGasRecordModal(@gasRecord.Id)">
<td class="col-2">@gasRecord.Date</td> <td class="col-2">@gasRecord.Date</td>
<td class="col-2">@gasRecord.Mileage</td> <td class="col-2">@gasRecord.Mileage</td>
<td class="col-2">@gasRecord.Gallons.ToString("F")</td> <td class="col-2">@gasRecord.Gallons.ToString("F")</td>
<td class="col-2">@gasRecord.MilesPerGallon.ToString("F")</td> <td class="col-4">@(gasRecord.MilesPerGallon == 0 ? "---" : gasRecord.MilesPerGallon.ToString("F"))</td>
<td class="col-2">@gasRecord.Cost.ToString("C")</td> <td class="col-1">@gasRecord.Cost.ToString("C3")</td>
<td class="col-2">@gasRecord.CostPerGallon.ToString("C")</td> <td class="col-1">@gasRecord.CostPerGallon.ToString("C3")</td>
</tr> </tr>
} }
</tbody> </tbody>

View File

@@ -1,8 +1,18 @@
@inject IConfiguration Configuration @inject IConfiguration Configuration
@model GasRecordInput @model GasRecordInputContainer
@{ @{
var useMPG = bool.Parse(Configuration[nameof(UserConfig.UseMPG)]); var useMPG = bool.Parse(Configuration[nameof(UserConfig.UseMPG)]);
var isNew = Model.Id == 0; var useKwh = Model.UseKwh;
var isNew = Model.GasRecord.Id == 0;
string consumptionUnit;
if (useKwh)
{
consumptionUnit = "kWh";
}
else
{
consumptionUnit = useMPG ? "gallons" : "liters";
}
} }
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">@(isNew ? "Add New Gas Record" : "Edit Gas Record")</h5> <h5 class="modal-title">@(isNew ? "Add New Gas Record" : "Edit Gas Record")</h5>
@@ -16,22 +26,26 @@
<input type="text" id="workAroundInput" style="height:0px; width:0px; display:none;"> <input type="text" id="workAroundInput" style="height:0px; width:0px; display:none;">
<label for="gasRecordDate">Date</label> <label for="gasRecordDate">Date</label>
<div class="input-group"> <div class="input-group">
<input type="text" id="gasRecordDate" placeholder="Date refueled" 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> <span class="input-group-text"><i class="bi bi-calendar-event"></i></span>
</div> </div>
<label for="gasRecordMileage">Odometer Reading(@(useMPG ? "miles" : "kilometers"))</label> <label for="gasRecordMileage">Odometer Reading(@(useMPG ? "miles" : "kilometers"))</label>
<input type="number" id="gasRecordMileage" class="form-control" placeholder="Odometer reading when refueled" value="@(isNew ? "" : Model.Mileage)"> <input type="number" id="gasRecordMileage" class="form-control" placeholder="Odometer reading when refueled" value="@(isNew ? "" : Model.GasRecord.Mileage)">
<label for="gasRecordGallons">Fuel Consumption(@(useMPG ? "gallons" : "liters"))</label> <label for="gasRecordGallons">Fuel Consumption(@(consumptionUnit))</label>
<input type="text" id="gasRecordGallons" class="form-control" placeholder="Amount of gas it takes to fill back up to full" value="@(isNew ? "" : Model.Gallons)"> <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> <label for="GasRecordCost">Cost</label>
<input type="number" id="gasRecordCost" class="form-control" placeholder="Cost of gas it takes to fill back up to full" value="@(isNew ? "" : Model.Cost)"> <input type="number" id="gasRecordCost" class="form-control" placeholder="Cost of gas refueled" value="@(isNew ? "" : Model.GasRecord.Cost)">
</div> </div>
<div class="col-md-6 col-12"> <div class="col-md-6 col-12">
@if (Model.Files.Any()) @if (Model.GasRecord.Files.Any())
{ {
<div> <div>
<label>Uploaded Documents</label> <label>Uploaded Documents</label>
@foreach (UploadedFiles filesUploaded in Model.Files) @foreach (UploadedFiles filesUploaded in Model.GasRecord.Files)
{ {
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<a type="button" class="btn btn-link" href="@filesUploaded.Location" target="_blank">@filesUploaded.Name</a> <a type="button" class="btn btn-link" href="@filesUploaded.Location" target="_blank">@filesUploaded.Name</a>
@@ -55,7 +69,7 @@
<div class="modal-footer"> <div class="modal-footer">
@if (!isNew) @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> <button type="button" class="btn btn-secondary" onclick="hideAddGasRecordModal()">Cancel</button>
@if (isNew) @if (isNew)
@@ -71,12 +85,12 @@
var uploadedFiles = []; var uploadedFiles = [];
getUploadedFilesFromModel(); getUploadedFilesFromModel();
function getUploadedFilesFromModel() { function getUploadedFilesFromModel() {
@foreach (UploadedFiles filesUploaded in Model.Files) @foreach (UploadedFiles filesUploaded in Model.GasRecord.Files)
{ {
@:uploadedFiles.push({ name: "@filesUploaded.Name", location: "@filesUploaded.Location" }); @:uploadedFiles.push({ name: "@filesUploaded.Name", location: "@filesUploaded.Location" });
} }
} }
function getGasRecordModelData(){ function getGasRecordModelData(){
return {id: @Model.Id} return { id: @Model.GasRecord.Id}
} }
</script> </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

@@ -44,6 +44,15 @@
} }
else 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> <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"> <input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="serviceRecordFiles">
} }

View File

@@ -42,6 +42,15 @@
} }
else 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> <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"> <input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="taxRecordFiles">
} }

View File

@@ -20,6 +20,10 @@
<input type="text" id="inputModel" class="form-control" placeholder="Model" value="@Model.Model"> <input type="text" id="inputModel" class="form-control" placeholder="Model" value="@Model.Model">
<label for="inputLicensePlate">License Plate</label> <label for="inputLicensePlate">License Plate</label>
<input type="text" id="inputLicensePlate" class="form-control" placeholder="License Plate" 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)) @if (!string.IsNullOrWhiteSpace(Model.ImageLocation))
{ {
<label for="inputImage">Replace picture(optional)</label> <label for="inputImage">Replace picture(optional)</label>

View File

@@ -5,13 +5,6 @@
"Microsoft.AspNetCore": "Warning" "Microsoft.AspNetCore": "Warning"
} }
}, },
"Kestrel": {
"Endpoints": {
"http": {
"Url": "http://localhost:5000"
}
}
},
"AllowedHosts": "*", "AllowedHosts": "*",
"UseDarkMode": false, "UseDarkMode": false,
"EnableCsvImports": true, "EnableCsvImports": true,

1
config/userConfig.json Normal file
View File

@@ -0,0 +1 @@
{"UseDarkMode":true,"UsekWh":false,"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":false,"UseMPG":true,"UseDescending":false,"EnableAuth":false,"UserNameHash":"","UserPasswordHash":""}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -34,6 +34,9 @@ $(document).ready(function () {
case "report-tab": case "report-tab":
getVehicleReport(); getVehicleReport();
break; break;
case "reminder-tab":
getVehicleReminders(vehicleId);
break;
} }
switch (e.relatedTarget.id) { //clear out previous tabs with grids in them to help with performance switch (e.relatedTarget.id) { //clear out previous tabs with grids in them to help with performance
case "servicerecord-tab": case "servicerecord-tab":
@@ -51,6 +54,9 @@ $(document).ready(function () {
case "report-tab": case "report-tab":
$("#report-tab-pane").html(""); $("#report-tab-pane").html("");
break; break;
case "reminder-tab":
$("#reminder-tab-pane").html("");
break;
} }
}); });
getVehicleServiceRecords(vehicleId); getVehicleServiceRecords(vehicleId);
@@ -67,6 +73,7 @@ function getVehicleServiceRecords(vehicleId) {
$.get(`/Vehicle/GetServiceRecordsByVehicleId?vehicleId=${vehicleId}`, function (data) { $.get(`/Vehicle/GetServiceRecordsByVehicleId?vehicleId=${vehicleId}`, function (data) {
if (data) { if (data) {
$("#servicerecord-tab-pane").html(data); $("#servicerecord-tab-pane").html(data);
getVehicleHaveImportantReminders(vehicleId);
} }
}) })
} }
@@ -74,6 +81,7 @@ function getVehicleGasRecords(vehicleId) {
$.get(`/Vehicle/GetGasRecordsByVehicleId?vehicleId=${vehicleId}`, function (data) { $.get(`/Vehicle/GetGasRecordsByVehicleId?vehicleId=${vehicleId}`, function (data) {
if (data) { if (data) {
$("#gas-tab-pane").html(data); $("#gas-tab-pane").html(data);
getVehicleHaveImportantReminders(vehicleId);
} }
}); });
} }
@@ -81,6 +89,7 @@ function getVehicleCollisionRecords(vehicleId) {
$.get(`/Vehicle/GetCollisionRecordsByVehicleId?vehicleId=${vehicleId}`, function (data) { $.get(`/Vehicle/GetCollisionRecordsByVehicleId?vehicleId=${vehicleId}`, function (data) {
if (data) { if (data) {
$("#accident-tab-pane").html(data); $("#accident-tab-pane").html(data);
getVehicleHaveImportantReminders(vehicleId);
} }
}); });
} }
@@ -88,6 +97,15 @@ function getVehicleTaxRecords(vehicleId) {
$.get(`/Vehicle/GetTaxRecordsByVehicleId?vehicleId=${vehicleId}`, function (data) { $.get(`/Vehicle/GetTaxRecordsByVehicleId?vehicleId=${vehicleId}`, function (data) {
if (data) { if (data) {
$("#tax-tab-pane").html(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 +176,40 @@ 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"
});
$("#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);
} }