Compare commits

..

66 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
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
56 changed files with 1167 additions and 128 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/
wwwroot/images/
cartracker.db
data/cartracker.db
wwwroot/documents/
wwwroot/temp/
wwwroot/imports/

View File

@@ -54,7 +54,12 @@ namespace CarCareTracker.Controllers
{
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 +73,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,7 @@ using CarCareTracker.Helper;
using CsvHelper;
using System.Globalization;
using Microsoft.AspNetCore.Authorization;
using CarCareTracker.External.Implementations;
namespace CarCareTracker.Controllers
{
@@ -19,6 +20,7 @@ namespace CarCareTracker.Controllers
private readonly IGasRecordDataAccess _gasRecordDataAccess;
private readonly ICollisionRecordDataAccess _collisionRecordDataAccess;
private readonly ITaxRecordDataAccess _taxRecordDataAccess;
private readonly IReminderRecordDataAccess _reminderRecordDataAccess;
private readonly IWebHostEnvironment _webEnv;
private readonly bool _useDescending;
private readonly IConfiguration _config;
@@ -32,6 +34,7 @@ namespace CarCareTracker.Controllers
IGasRecordDataAccess gasRecordDataAccess,
ICollisionRecordDataAccess collisionRecordDataAccess,
ITaxRecordDataAccess taxRecordDataAccess,
IReminderRecordDataAccess reminderRecordDataAccess,
IWebHostEnvironment webEnv,
IConfiguration config)
{
@@ -43,6 +46,7 @@ namespace CarCareTracker.Controllers
_gasRecordDataAccess = gasRecordDataAccess;
_collisionRecordDataAccess = collisionRecordDataAccess;
_taxRecordDataAccess = taxRecordDataAccess;
_reminderRecordDataAccess = reminderRecordDataAccess;
_webEnv = webEnv;
_config = config;
_useDescending = bool.Parse(config[nameof(UserConfig.UseDescending)]);
@@ -90,6 +94,7 @@ namespace CarCareTracker.Controllers
_collisionRecordDataAccess.DeleteAllCollisionRecordsByVehicleId(vehicleId) &&
_taxRecordDataAccess.DeleteAllTaxRecordsByVehicleId(vehicleId) &&
_noteDataAccess.DeleteNoteByVehicleId(vehicleId) &&
_reminderRecordDataAccess.DeleteAllReminderRecordsByVehicleId(vehicleId) &&
_dataAccess.DeleteVehicle(vehicleId);
return Json(result);
}
@@ -160,7 +165,8 @@ namespace CarCareTracker.Controllers
_gasRecordDataAccess.SaveGasRecordToVehicle(convertedRecord);
}
}
} else if (mode == "servicerecord")
}
else if (mode == "servicerecord")
{
var records = csv.GetRecords<ServiceRecordImport>().ToList();
if (records.Any())
@@ -179,7 +185,8 @@ namespace CarCareTracker.Controllers
_serviceRecordDataAccess.SaveServiceRecordToVehicle(convertedRecord);
}
}
} else if (mode == "repairrecord")
}
else if (mode == "repairrecord")
{
var records = csv.GetRecords<ServiceRecordImport>().ToList();
if (records.Any())
@@ -198,7 +205,8 @@ namespace CarCareTracker.Controllers
_collisionRecordDataAccess.SaveCollisionRecordToVehicle(convertedRecord);
}
}
} else if (mode == "taxrecord")
}
else if (mode == "taxrecord")
{
var records = csv.GetRecords<TaxRecordImport>().ToList();
if (records.Any())
@@ -239,14 +247,16 @@ namespace CarCareTracker.Controllers
bool useMPG = bool.Parse(_config[nameof(UserConfig.UseMPG)]);
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++)
{
if (i > 0)
{
var currentObject = result[i];
var deltaMileage = currentObject.Mileage - previousMileage;
computedResults.Add(new GasRecordViewModel()
var gasRecordViewModel = new GasRecordViewModel()
{
Id = currentObject.Id,
VehicleId = currentObject.VehicleId,
@@ -255,10 +265,24 @@ namespace CarCareTracker.Controllers
Gallons = currentObject.Gallons,
Cost = currentObject.Cost,
DeltaMileage = deltaMileage,
MilesPerGallon = useMPG ? (deltaMileage / currentObject.Gallons) : 100 / (deltaMileage / 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()
{
@@ -279,7 +303,13 @@ namespace CarCareTracker.Controllers
{
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 +321,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 +335,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,7 +386,9 @@ namespace CarCareTracker.Controllers
{
var result = _serviceRecordDataAccess.GetServiceRecordById(serviceRecordId);
//convert to Input object.
var convertedResult = new ServiceRecordInput { Id = result.Id,
var convertedResult = new ServiceRecordInput
{
Id = result.Id,
Cost = result.Cost,
Date = result.Date.ToShortDateString(),
Description = result.Description,
@@ -508,12 +547,178 @@ 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));
}
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.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

@@ -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);
}
}

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

@@ -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>();
}
}

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 Model { 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<ICollisionRecordDataAccess, CollisionRecordDataAccess>();
builder.Services.AddSingleton<ITaxRecordDataAccess, TaxRecordDataAccess>();
builder.Services.AddSingleton<IReminderRecordDataAccess, ReminderRecordDataAccess>();
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

@@ -12,3 +12,48 @@ Because nobody should have to deal with a homemade spreadsheet or a shoebox full
- SweetAlert2
- CsvHelper
- 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

@@ -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,7 @@
</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>
<div class="col-12 col-md-6">

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,7 @@
<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="~/lib/chart-js/chart.umd.js"></script>
}
<div class="container">
@@ -35,6 +36,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,6 +69,7 @@
</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>
</div>
@@ -80,6 +85,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,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>
@@ -41,6 +44,15 @@
}
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 +62,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,21 +1,33 @@
@inject IConfiguration Configuration
@model GasRecordViewModelContainer
@{
var enableCsvImports = bool.Parse(Configuration[nameof(UserConfig.EnableCsvImports)]);
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="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)
{
@@ -40,22 +52,22 @@
<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">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">@gasRecord.Cost.ToString("C3")</td>
<td class="col-1">@gasRecord.CostPerGallon.ToString("C3")</td>
</tr>
}
</tbody>

View File

@@ -1,10 +1,21 @@
@inject IConfiguration Configuration
@model GasRecordInputContainer
@{
var useMPG = bool.Parse(Configuration[nameof(UserConfig.UseMPG)]);
var useKwh = Model.UseKwh;
var isNew = Model.GasRecord.Id == 0;
string consumptionUnit;
if (useKwh)
{
consumptionUnit = "kWh";
}
else
{
consumptionUnit = useMPG ? "gallons" : "liters";
}
}
@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,22 +26,26 @@
<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">
<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)
@foreach (UploadedFiles filesUploaded in Model.GasRecord.Files)
{
<div class="d-flex justify-content-between">
<a type="button" class="btn btn-link" href="@filesUploaded.Location" target="_blank">@filesUploaded.Name</a>
@@ -52,16 +67,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 +85,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>
@@ -41,6 +44,15 @@
}
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 +62,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,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>
@@ -39,6 +42,15 @@
}
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 +60,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,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,13 +5,6 @@
"Microsoft.AspNetCore": "Warning"
}
},
"Kestrel": {
"Endpoints": {
"http": {
"Url": "http://localhost:5000"
}
}
},
"AllowedHosts": "*",
"UseDarkMode": false,
"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":true,"UseMPG":true,"UseDescending":false,"EnableAuth":false,"UserNameHash":"","UserPasswordHash":""}

View File

@@ -56,3 +56,47 @@ html {
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,7 +118,8 @@ function getAndValidateCollisionRecordValues() {
description: collisionDescription,
cost: collisionCost,
notes: collisionNotes,
files: uploadedFiles
files: uploadedFiles,
addReminderRecord: addReminderRecord
}
}
function deleteCollisionRecordFile(fileLocation, event) {

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,7 +113,8 @@ function getAndValidateGasRecordValues() {
mileage: gasMileage,
gallons: gasGallons,
cost: gasCost,
files: uploadedFiles
files: uploadedFiles,
isFillToFull: gasIsFillToFull
}
}
function deleteGasRecordFile(fileLocation, event) {

View File

@@ -10,3 +10,8 @@
}
})
}
function handlePasswordKeyPress(event) {
if (event.keyCode == 13) {
performLogin();
}
}

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);
//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,7 +118,8 @@ function getAndValidateServiceRecordValues() {
description: serviceDescription,
cost: serviceCost,
notes: serviceNotes,
files: uploadedFiles
files: uploadedFiles,
addReminderRecord: addReminderRecord
}
}
function deleteServiceRecordFile(fileLocation, event) {

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,7 +110,8 @@ function getAndValidateTaxRecordValues() {
description: taxDescription,
cost: taxCost,
notes: taxNotes,
files: uploadedFiles
files: uploadedFiles,
addReminderRecord: addReminderRecord
}
}
function deleteTaxRecordFile(fileLocation, event) {

View File

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