Compare commits
135 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20190642a8 | ||
|
|
5d2068bd78 | ||
|
|
3a01944e30 | ||
|
|
014b6bce8d | ||
|
|
8f69399f1b | ||
|
|
200aa79dac | ||
|
|
dfb6f69d69 | ||
|
|
cd2fb2dd21 | ||
|
|
a396eb4f72 | ||
|
|
46675c944f | ||
|
|
ff7ad04f0b | ||
|
|
d69ede1447 | ||
|
|
b477cf9d51 | ||
|
|
ad8c27a2e6 | ||
|
|
52f9ae7ea1 | ||
|
|
ed1066662e | ||
|
|
e3bf6f03d7 | ||
|
|
8cc2ceecc6 | ||
|
|
9837172774 | ||
|
|
fba2a6dc68 | ||
|
|
a467ba08c6 | ||
|
|
7bfbb49efb | ||
|
|
7e4f5807d2 | ||
|
|
d5aee08f69 | ||
|
|
080d50c6bc | ||
|
|
3105622d63 | ||
|
|
eac181ff20 | ||
|
|
2a732cb343 | ||
|
|
da5e97143e | ||
|
|
9bdce69a2a | ||
|
|
8b1db34860 | ||
|
|
3c67d37e12 | ||
|
|
b73c2e979f | ||
|
|
a52fc29067 | ||
|
|
c6774d4be7 | ||
|
|
1271b3517d | ||
|
|
96914b3db4 | ||
|
|
7ad6d3369d | ||
|
|
0ae28d436b | ||
|
|
4575cf338f | ||
|
|
c526a9f207 | ||
|
|
1a3282c1ef | ||
|
|
f52e5b49c7 | ||
|
|
507fb6daf6 | ||
|
|
9d3fbd05fc | ||
|
|
2a0f884e89 | ||
|
|
26012bf27a | ||
|
|
3fa3dbaa8c | ||
|
|
95cac60c71 | ||
|
|
7878ce65ca | ||
|
|
c6c57a5de6 | ||
|
|
2ece3cd113 | ||
|
|
9504674933 | ||
|
|
3c0bdcea0e | ||
|
|
a4762d5b32 | ||
|
|
dc18bb5b8f | ||
|
|
452248e681 | ||
|
|
a7ea03014f | ||
|
|
cb89ca3426 | ||
|
|
760f7e1888 | ||
|
|
ba149568a5 | ||
|
|
2ef281bd0a | ||
|
|
859796cfe8 | ||
|
|
f1e0254f95 | ||
|
|
70d0827432 | ||
|
|
851a7af287 | ||
|
|
e46be7ba0a | ||
|
|
b4258dc11a | ||
|
|
0f35621846 | ||
|
|
374f919296 | ||
|
|
33b824b316 | ||
|
|
418e468f14 | ||
|
|
0a1f6a569d | ||
|
|
2a842d1c8c | ||
|
|
4f76991840 | ||
|
|
0487feb35e | ||
|
|
70308ed6eb | ||
|
|
bfdef5d296 | ||
|
|
a236c4a151 | ||
|
|
54d20b5573 | ||
|
|
ecd2b83cf0 | ||
|
|
80504e71c9 | ||
|
|
fb272c9c40 | ||
|
|
1c31477c37 | ||
|
|
2ecd286aa1 | ||
|
|
f28af456b3 | ||
|
|
c05b5e4c3d | ||
|
|
0f70f8212b | ||
|
|
f805e311b0 | ||
|
|
42f7bd298c | ||
|
|
e64d4f75b5 | ||
|
|
b972d5b7e5 | ||
|
|
f639c2c38b | ||
|
|
4e92155f5b | ||
|
|
78857c1b79 | ||
|
|
4e5d893850 | ||
|
|
a1fe446c2a | ||
|
|
f126a309f0 | ||
|
|
b2b129389a | ||
|
|
f2386fc9d8 | ||
|
|
d625a91ed7 | ||
|
|
b5f8d2d44e | ||
|
|
206b053d27 | ||
|
|
a2d16d7643 | ||
|
|
89345fd8eb | ||
|
|
4abf7fbba2 | ||
|
|
b5987927be | ||
|
|
bfef9b9498 | ||
|
|
6b4bbd8410 | ||
|
|
05f89073cd | ||
|
|
3776d6e11f | ||
|
|
2e301760f4 | ||
|
|
2817b5914f | ||
|
|
8276d8d720 | ||
|
|
711c7c14f7 | ||
|
|
f651f53ee6 | ||
|
|
c00ea88f73 | ||
|
|
583c83643a | ||
|
|
0775ede344 | ||
|
|
bf8ecdbe7a | ||
|
|
ed81d53175 | ||
|
|
4543e56c61 | ||
|
|
401724b66b | ||
|
|
b0773f5102 | ||
|
|
cff9289c39 | ||
|
|
69cfac1c6f | ||
|
|
0b05315671 | ||
|
|
af9c96c002 | ||
|
|
f1d4973f59 | ||
|
|
25db16c47f | ||
|
|
1286974fb6 | ||
|
|
997c045312 | ||
|
|
71c2e64daf | ||
|
|
1c2368f5a1 | ||
|
|
208eb22b8c |
31
.github/workflows/docker-image.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
name: Docker Image CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: "hargata"
|
||||
password: "${{ secrets.GHCR_PAT }}"
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: |
|
||||
ghcr.io/hargata/lubelogger:latest
|
||||
1
.gitignore
vendored
@@ -3,6 +3,7 @@ bin/
|
||||
obj/
|
||||
wwwroot/images/
|
||||
cartracker.db
|
||||
data/cartracker.db
|
||||
wwwroot/documents/
|
||||
wwwroot/temp/
|
||||
wwwroot/imports/
|
||||
|
||||
130
Controllers/APIController.cs
Normal file
@@ -0,0 +1,130 @@
|
||||
using CarCareTracker.External.Interfaces;
|
||||
using CarCareTracker.Helper;
|
||||
using CarCareTracker.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace CarCareTracker.Controllers
|
||||
{
|
||||
[Authorize]
|
||||
public class APIController : Controller
|
||||
{
|
||||
private readonly IVehicleDataAccess _dataAccess;
|
||||
private readonly INoteDataAccess _noteDataAccess;
|
||||
private readonly IServiceRecordDataAccess _serviceRecordDataAccess;
|
||||
private readonly IGasRecordDataAccess _gasRecordDataAccess;
|
||||
private readonly ICollisionRecordDataAccess _collisionRecordDataAccess;
|
||||
private readonly ITaxRecordDataAccess _taxRecordDataAccess;
|
||||
private readonly IReminderRecordDataAccess _reminderRecordDataAccess;
|
||||
private readonly IUpgradeRecordDataAccess _upgradeRecordDataAccess;
|
||||
private readonly IReminderHelper _reminderHelper;
|
||||
private readonly IGasHelper _gasHelper;
|
||||
public APIController(IVehicleDataAccess dataAccess,
|
||||
IGasHelper gasHelper,
|
||||
IReminderHelper reminderHelper,
|
||||
INoteDataAccess noteDataAccess,
|
||||
IServiceRecordDataAccess serviceRecordDataAccess,
|
||||
IGasRecordDataAccess gasRecordDataAccess,
|
||||
ICollisionRecordDataAccess collisionRecordDataAccess,
|
||||
ITaxRecordDataAccess taxRecordDataAccess,
|
||||
IReminderRecordDataAccess reminderRecordDataAccess,
|
||||
IUpgradeRecordDataAccess upgradeRecordDataAccess)
|
||||
{
|
||||
_dataAccess = dataAccess;
|
||||
_noteDataAccess = noteDataAccess;
|
||||
_serviceRecordDataAccess = serviceRecordDataAccess;
|
||||
_gasRecordDataAccess = gasRecordDataAccess;
|
||||
_collisionRecordDataAccess = collisionRecordDataAccess;
|
||||
_taxRecordDataAccess = taxRecordDataAccess;
|
||||
_reminderRecordDataAccess = reminderRecordDataAccess;
|
||||
_upgradeRecordDataAccess = upgradeRecordDataAccess;
|
||||
_gasHelper = gasHelper;
|
||||
_reminderHelper = reminderHelper;
|
||||
}
|
||||
public IActionResult Index()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("/api/vehicles")]
|
||||
public IActionResult Vehicles()
|
||||
{
|
||||
var result = _dataAccess.GetVehicles();
|
||||
return Json(result);
|
||||
}
|
||||
[HttpGet]
|
||||
[Route("/api/vehicle/servicerecords")]
|
||||
public IActionResult ServiceRecords(int vehicleId)
|
||||
{
|
||||
var vehicleRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId);
|
||||
var result = vehicleRecords.Select(x => new ServiceRecordExportModel { Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString(), Notes = x.Notes, Odometer = x.Mileage.ToString() });
|
||||
return Json(result);
|
||||
}
|
||||
[HttpGet]
|
||||
[Route("/api/vehicle/repairrecords")]
|
||||
public IActionResult RepairRecords(int vehicleId)
|
||||
{
|
||||
var vehicleRecords = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId);
|
||||
var result = vehicleRecords.Select(x => new ServiceRecordExportModel { Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString(), Notes = x.Notes, Odometer = x.Mileage.ToString() });
|
||||
return Json(result);
|
||||
}
|
||||
[HttpGet]
|
||||
[Route("/api/vehicle/upgraderecords")]
|
||||
public IActionResult UpgradeRecords(int vehicleId)
|
||||
{
|
||||
var vehicleRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId);
|
||||
var result = vehicleRecords.Select(x => new ServiceRecordExportModel { Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString(), Notes = x.Notes, Odometer = x.Mileage.ToString() });
|
||||
return Json(result);
|
||||
}
|
||||
[HttpGet]
|
||||
[Route("/api/vehicle/taxrecords")]
|
||||
public IActionResult TaxRecords(int vehicleId)
|
||||
{
|
||||
var result = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId);
|
||||
return Json(result);
|
||||
}
|
||||
[HttpGet]
|
||||
[Route("/api/vehicle/gasrecords")]
|
||||
public IActionResult GasRecords(int vehicleId, bool useMPG, bool useUKMPG)
|
||||
{
|
||||
var vehicleRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
|
||||
var result = _gasHelper.GetGasRecordViewModels(vehicleRecords, useMPG, useUKMPG).Select(x => new GasRecordExportModel { Date = x.Date, Odometer = x.Mileage.ToString(), Cost = x.Cost.ToString(), FuelConsumed = x.Gallons.ToString(), FuelEconomy = x.MilesPerGallon.ToString()});
|
||||
return Json(result);
|
||||
}
|
||||
[HttpGet]
|
||||
[Route("/api/vehicle/reminders")]
|
||||
public IActionResult Reminders(int vehicleId)
|
||||
{
|
||||
var currentMileage = GetMaxMileage(vehicleId);
|
||||
var reminders = _reminderRecordDataAccess.GetReminderRecordsByVehicleId(vehicleId);
|
||||
var results = _reminderHelper.GetReminderRecordViewModels(reminders, currentMileage, DateTime.Now).Select(x=> new ReminderExportModel { Description = x.Description, Urgency = x.Urgency.ToString(), Metric = x.Metric.ToString(), Notes = x.Notes});
|
||||
return Json(results);
|
||||
}
|
||||
private int GetMaxMileage(int vehicleId)
|
||||
{
|
||||
var numbersArray = new List<int>();
|
||||
var serviceRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId);
|
||||
if (serviceRecords.Any())
|
||||
{
|
||||
numbersArray.Add(serviceRecords.Max(x => x.Mileage));
|
||||
}
|
||||
var repairRecords = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId);
|
||||
if (repairRecords.Any())
|
||||
{
|
||||
numbersArray.Add(repairRecords.Max(x => x.Mileage));
|
||||
}
|
||||
var gasRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
|
||||
if (gasRecords.Any())
|
||||
{
|
||||
numbersArray.Add(gasRecords.Max(x => x.Mileage));
|
||||
}
|
||||
var upgradeRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId);
|
||||
if (upgradeRecords.Any())
|
||||
{
|
||||
numbersArray.Add(upgradeRecords.Max(x => x.Mileage));
|
||||
}
|
||||
return numbersArray.Any() ? numbersArray.Max() : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,9 @@ namespace CarCareTracker.Controllers
|
||||
UseDarkMode = bool.Parse(_config[nameof(UserConfig.UseDarkMode)]),
|
||||
UseMPG = bool.Parse(_config[nameof(UserConfig.UseMPG)]),
|
||||
UseDescending = bool.Parse(_config[nameof(UserConfig.UseDescending)]),
|
||||
EnableAuth = bool.Parse(_config[nameof(UserConfig.EnableAuth)])
|
||||
EnableAuth = bool.Parse(_config[nameof(UserConfig.EnableAuth)]),
|
||||
HideZero = bool.Parse(_config[nameof(UserConfig.HideZero)]),
|
||||
UseUKMPG = bool.Parse(_config[nameof(UserConfig.UseUKMPG)])
|
||||
};
|
||||
return PartialView("_Settings", userConfig);
|
||||
}
|
||||
@@ -54,7 +56,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 +75,7 @@ namespace CarCareTracker.Controllers
|
||||
userConfig.UserNameHash = string.Empty;
|
||||
userConfig.UserPasswordHash = string.Empty;
|
||||
}
|
||||
System.IO.File.WriteAllText("userConfig.json", System.Text.Json.JsonSerializer.Serialize(userConfig));
|
||||
System.IO.File.WriteAllText(StaticHelper.UserConfigPath, System.Text.Json.JsonSerializer.Serialize(userConfig));
|
||||
return Json(true);
|
||||
} catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
@@ -12,14 +13,17 @@ namespace CarCareTracker.Controllers
|
||||
public class LoginController : Controller
|
||||
{
|
||||
private IDataProtector _dataProtector;
|
||||
private ILoginHelper _loginHelper;
|
||||
private readonly ILogger<LoginController> _logger;
|
||||
public LoginController(
|
||||
ILogger<LoginController> logger,
|
||||
IDataProtectionProvider securityProvider
|
||||
IDataProtectionProvider securityProvider,
|
||||
ILoginHelper loginHelper
|
||||
)
|
||||
{
|
||||
_dataProtector = securityProvider.CreateProtector("login");
|
||||
_logger = logger;
|
||||
_loginHelper = loginHelper;
|
||||
}
|
||||
public IActionResult Index()
|
||||
{
|
||||
@@ -36,19 +40,9 @@ namespace CarCareTracker.Controllers
|
||||
//compare it against hashed credentials
|
||||
try
|
||||
{
|
||||
var configFileContents = System.IO.File.ReadAllText("userConfig.json");
|
||||
var existingUserConfig = System.Text.Json.JsonSerializer.Deserialize<UserConfig>(configFileContents);
|
||||
if (existingUserConfig is not null)
|
||||
var loginIsValid = _loginHelper.ValidateUserCredentials(credentials);
|
||||
if (loginIsValid)
|
||||
{
|
||||
//create hashes of the login credentials.
|
||||
var hashedUserName = Sha256_hash(credentials.UserName);
|
||||
var hashedPassword = Sha256_hash(credentials.Password);
|
||||
//compare against stored hash.
|
||||
if (hashedUserName == existingUserConfig.UserNameHash &&
|
||||
hashedPassword == existingUserConfig.UserPasswordHash)
|
||||
{
|
||||
//auth success, create auth cookie
|
||||
//encrypt stuff.
|
||||
AuthCookie authCookie = new AuthCookie
|
||||
{
|
||||
Id = 1, //this is hardcoded for now
|
||||
@@ -61,7 +55,6 @@ namespace CarCareTracker.Controllers
|
||||
return Json(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error on saving config file.");
|
||||
@@ -74,7 +67,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 +79,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 +94,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 +103,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);
|
||||
|
||||
@@ -6,6 +6,7 @@ using CarCareTracker.Helper;
|
||||
using CsvHelper;
|
||||
using System.Globalization;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using CarCareTracker.MapProfile;
|
||||
|
||||
namespace CarCareTracker.Controllers
|
||||
{
|
||||
@@ -19,19 +20,29 @@ namespace CarCareTracker.Controllers
|
||||
private readonly IGasRecordDataAccess _gasRecordDataAccess;
|
||||
private readonly ICollisionRecordDataAccess _collisionRecordDataAccess;
|
||||
private readonly ITaxRecordDataAccess _taxRecordDataAccess;
|
||||
private readonly IReminderRecordDataAccess _reminderRecordDataAccess;
|
||||
private readonly IUpgradeRecordDataAccess _upgradeRecordDataAccess;
|
||||
private readonly IWebHostEnvironment _webEnv;
|
||||
private readonly bool _useDescending;
|
||||
private readonly IConfiguration _config;
|
||||
private readonly IFileHelper _fileHelper;
|
||||
private readonly IGasHelper _gasHelper;
|
||||
private readonly IReminderHelper _reminderHelper;
|
||||
private readonly IReportHelper _reportHelper;
|
||||
|
||||
public VehicleController(ILogger<VehicleController> logger,
|
||||
IFileHelper fileHelper,
|
||||
IGasHelper gasHelper,
|
||||
IReminderHelper reminderHelper,
|
||||
IReportHelper reportHelper,
|
||||
IVehicleDataAccess dataAccess,
|
||||
INoteDataAccess noteDataAccess,
|
||||
IServiceRecordDataAccess serviceRecordDataAccess,
|
||||
IGasRecordDataAccess gasRecordDataAccess,
|
||||
ICollisionRecordDataAccess collisionRecordDataAccess,
|
||||
ITaxRecordDataAccess taxRecordDataAccess,
|
||||
IReminderRecordDataAccess reminderRecordDataAccess,
|
||||
IUpgradeRecordDataAccess upgradeRecordDataAccess,
|
||||
IWebHostEnvironment webEnv,
|
||||
IConfiguration config)
|
||||
{
|
||||
@@ -39,10 +50,15 @@ namespace CarCareTracker.Controllers
|
||||
_dataAccess = dataAccess;
|
||||
_noteDataAccess = noteDataAccess;
|
||||
_fileHelper = fileHelper;
|
||||
_gasHelper = gasHelper;
|
||||
_reminderHelper = reminderHelper;
|
||||
_reportHelper = reportHelper;
|
||||
_serviceRecordDataAccess = serviceRecordDataAccess;
|
||||
_gasRecordDataAccess = gasRecordDataAccess;
|
||||
_collisionRecordDataAccess = collisionRecordDataAccess;
|
||||
_taxRecordDataAccess = taxRecordDataAccess;
|
||||
_reminderRecordDataAccess = reminderRecordDataAccess;
|
||||
_upgradeRecordDataAccess = upgradeRecordDataAccess;
|
||||
_webEnv = webEnv;
|
||||
_config = config;
|
||||
_useDescending = bool.Parse(config[nameof(UserConfig.UseDescending)]);
|
||||
@@ -89,40 +105,123 @@ namespace CarCareTracker.Controllers
|
||||
_serviceRecordDataAccess.DeleteAllServiceRecordsByVehicleId(vehicleId) &&
|
||||
_collisionRecordDataAccess.DeleteAllCollisionRecordsByVehicleId(vehicleId) &&
|
||||
_taxRecordDataAccess.DeleteAllTaxRecordsByVehicleId(vehicleId) &&
|
||||
_noteDataAccess.DeleteNoteByVehicleId(vehicleId) &&
|
||||
_noteDataAccess.DeleteAllNotesByVehicleId(vehicleId) &&
|
||||
_reminderRecordDataAccess.DeleteAllReminderRecordsByVehicleId(vehicleId) &&
|
||||
_upgradeRecordDataAccess.DeleteAllUpgradeRecordsByVehicleId(vehicleId) &&
|
||||
_dataAccess.DeleteVehicle(vehicleId);
|
||||
return Json(result);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult SaveNoteToVehicle(Note newNote)
|
||||
{
|
||||
//check if there is already an existing note for this vehicle.
|
||||
var existingNote = _noteDataAccess.GetNoteByVehicleId(newNote.VehicleId);
|
||||
if (existingNote.Id != default)
|
||||
{
|
||||
newNote.Id = existingNote.Id;
|
||||
}
|
||||
var result = _noteDataAccess.SaveNoteToVehicleId(newNote);
|
||||
return Json(result);
|
||||
}
|
||||
[HttpGet]
|
||||
public IActionResult GetNoteByVehicleId(int vehicleId)
|
||||
{
|
||||
var existingNote = _noteDataAccess.GetNoteByVehicleId(vehicleId);
|
||||
if (existingNote.Id != default)
|
||||
{
|
||||
return Json(existingNote.NoteText);
|
||||
}
|
||||
return Json("");
|
||||
}
|
||||
#region "Bulk Imports"
|
||||
[HttpGet]
|
||||
public IActionResult GetBulkImportModalPartialView(string mode)
|
||||
public IActionResult GetBulkImportModalPartialView(ImportMode mode)
|
||||
{
|
||||
return PartialView("_BulkDataImporter", mode);
|
||||
}
|
||||
[HttpGet]
|
||||
public IActionResult ExportFromVehicleToCsv(int vehicleId, ImportMode mode)
|
||||
{
|
||||
if (vehicleId == default)
|
||||
{
|
||||
return Json(false);
|
||||
}
|
||||
string uploadDirectory = "temp/";
|
||||
string uploadPath = Path.Combine(_webEnv.WebRootPath, uploadDirectory);
|
||||
if (!Directory.Exists(uploadPath))
|
||||
Directory.CreateDirectory(uploadPath);
|
||||
if (mode == ImportMode.ServiceRecord)
|
||||
{
|
||||
var fileNameToExport = $"temp/{Guid.NewGuid()}.csv";
|
||||
var fullExportFilePath = _fileHelper.GetFullFilePath(fileNameToExport, false);
|
||||
var vehicleRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId);
|
||||
if (vehicleRecords.Any())
|
||||
{
|
||||
var exportData = vehicleRecords.Select(x => new ServiceRecordExportModel { Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString("C"), Notes = x.Notes, Odometer = x.Mileage.ToString() });
|
||||
using (var writer = new StreamWriter(fullExportFilePath))
|
||||
{
|
||||
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
|
||||
{
|
||||
csv.WriteRecords(exportData);
|
||||
}
|
||||
}
|
||||
return Json($"/{fileNameToExport}");
|
||||
}
|
||||
}
|
||||
else if (mode == ImportMode.RepairRecord)
|
||||
{
|
||||
var fileNameToExport = $"temp/{Guid.NewGuid()}.csv";
|
||||
var fullExportFilePath = _fileHelper.GetFullFilePath(fileNameToExport, false);
|
||||
var vehicleRecords = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId);
|
||||
if (vehicleRecords.Any())
|
||||
{
|
||||
var exportData = vehicleRecords.Select(x => new ServiceRecordExportModel { Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString("C"), Notes = x.Notes, Odometer = x.Mileage.ToString() });
|
||||
using (var writer = new StreamWriter(fullExportFilePath))
|
||||
{
|
||||
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
|
||||
{
|
||||
csv.WriteRecords(exportData);
|
||||
}
|
||||
}
|
||||
return Json($"/{fileNameToExport}");
|
||||
}
|
||||
}
|
||||
else if (mode == ImportMode.UpgradeRecord)
|
||||
{
|
||||
var fileNameToExport = $"temp/{Guid.NewGuid()}.csv";
|
||||
var fullExportFilePath = _fileHelper.GetFullFilePath(fileNameToExport, false);
|
||||
var vehicleRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId);
|
||||
if (vehicleRecords.Any())
|
||||
{
|
||||
var exportData = vehicleRecords.Select(x => new ServiceRecordExportModel { Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString("C"), Notes = x.Notes, Odometer = x.Mileage.ToString() });
|
||||
using (var writer = new StreamWriter(fullExportFilePath))
|
||||
{
|
||||
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
|
||||
{
|
||||
csv.WriteRecords(exportData);
|
||||
}
|
||||
}
|
||||
return Json($"/{fileNameToExport}");
|
||||
}
|
||||
}
|
||||
else if (mode == ImportMode.TaxRecord) {
|
||||
var fileNameToExport = $"temp/{Guid.NewGuid()}.csv";
|
||||
var fullExportFilePath = _fileHelper.GetFullFilePath(fileNameToExport, false);
|
||||
var vehicleRecords = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId);
|
||||
if (vehicleRecords.Any())
|
||||
{
|
||||
var exportData = vehicleRecords.Select(x => new TaxRecordExportModel { Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString("C"), Notes = x.Notes });
|
||||
using (var writer = new StreamWriter(fullExportFilePath))
|
||||
{
|
||||
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
|
||||
{
|
||||
csv.WriteRecords(exportData);
|
||||
}
|
||||
}
|
||||
return Json($"/{fileNameToExport}");
|
||||
}
|
||||
}
|
||||
else if (mode == ImportMode.GasRecord)
|
||||
{
|
||||
var fileNameToExport = $"temp/{Guid.NewGuid()}.csv";
|
||||
var fullExportFilePath = _fileHelper.GetFullFilePath(fileNameToExport, false);
|
||||
var vehicleRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
|
||||
bool useMPG = bool.Parse(_config[nameof(UserConfig.UseMPG)]);
|
||||
bool useUKMPG = bool.Parse(_config[nameof(UserConfig.UseUKMPG)]);
|
||||
vehicleRecords = vehicleRecords.OrderBy(x => x.Date).ThenBy(x => x.Mileage).ToList();
|
||||
var convertedRecords = _gasHelper.GetGasRecordViewModels(vehicleRecords, useMPG, useUKMPG);
|
||||
var exportData = convertedRecords.Select(x => new GasRecordExportModel { Date = x.Date.ToString(), Cost = x.Cost.ToString(), FuelConsumed = x.Gallons.ToString(), FuelEconomy = x.MilesPerGallon.ToString(), Odometer = x.Mileage.ToString() });
|
||||
using (var writer = new StreamWriter(fullExportFilePath))
|
||||
{
|
||||
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
|
||||
{
|
||||
csv.WriteRecords(exportData);
|
||||
}
|
||||
}
|
||||
return Json($"/{fileNameToExport}");
|
||||
}
|
||||
return Json(false);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult ImportToVehicleIdFromCsv(int vehicleId, string mode, string fileName)
|
||||
public IActionResult ImportToVehicleIdFromCsv(int vehicleId, ImportMode mode, string fileName)
|
||||
{
|
||||
if (vehicleId == default || string.IsNullOrWhiteSpace(fileName))
|
||||
{
|
||||
@@ -140,78 +239,103 @@ namespace CarCareTracker.Controllers
|
||||
var config = new CsvHelper.Configuration.CsvConfiguration(System.Globalization.CultureInfo.InvariantCulture);
|
||||
config.MissingFieldFound = null;
|
||||
config.HeaderValidated = null;
|
||||
config.PrepareHeaderForMatch = args => { return args.Header.Trim().ToLower(); };
|
||||
using (var csv = new CsvReader(reader, config))
|
||||
{
|
||||
if (mode == "gasrecord")
|
||||
{
|
||||
var records = csv.GetRecords<GasRecordImport>().ToList();
|
||||
csv.Context.RegisterClassMap<FuellyMapper>();
|
||||
var records = csv.GetRecords<ImportModel>().ToList();
|
||||
if (records.Any())
|
||||
{
|
||||
foreach (GasRecordImport recordToInsert in records)
|
||||
foreach (ImportModel importModel in records)
|
||||
{
|
||||
if (mode == ImportMode.GasRecord)
|
||||
{
|
||||
//convert to gas model.
|
||||
var convertedRecord = new GasRecord()
|
||||
{
|
||||
VehicleId = vehicleId,
|
||||
Date = recordToInsert.Date,
|
||||
Mileage = recordToInsert.Odometer,
|
||||
Gallons = recordToInsert.FuelConsumed,
|
||||
Cost = recordToInsert.Cost
|
||||
Date = DateTime.Parse(importModel.Date),
|
||||
Mileage = int.Parse(importModel.Odometer, NumberStyles.Any),
|
||||
Gallons = decimal.Parse(importModel.FuelConsumed, NumberStyles.Any)
|
||||
};
|
||||
if (string.IsNullOrWhiteSpace(importModel.Cost) && !string.IsNullOrWhiteSpace(importModel.Price))
|
||||
{
|
||||
//cost was not given but price is.
|
||||
//fuelly sometimes exports CSVs without total cost.
|
||||
var parsedPrice = decimal.Parse(importModel.Price, NumberStyles.Any);
|
||||
convertedRecord.Cost = convertedRecord.Gallons * parsedPrice;
|
||||
} else
|
||||
{
|
||||
convertedRecord.Cost = decimal.Parse(importModel.Cost, NumberStyles.Any);
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(importModel.IsFillToFull) && !string.IsNullOrWhiteSpace(importModel.PartialFuelUp))
|
||||
{
|
||||
var parsedBool = importModel.PartialFuelUp.Trim() == "1";
|
||||
convertedRecord.IsFillToFull = !parsedBool;
|
||||
} else if (!string.IsNullOrWhiteSpace(importModel.IsFillToFull))
|
||||
{
|
||||
var parsedBool = importModel.IsFillToFull.Trim() == "1" || importModel.IsFillToFull.Trim() == "Full";
|
||||
convertedRecord.IsFillToFull = parsedBool;
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(importModel.MissedFuelUp))
|
||||
{
|
||||
var parsedBool = importModel.MissedFuelUp.Trim() == "1";
|
||||
convertedRecord.MissedFuelUp = parsedBool;
|
||||
}
|
||||
//insert record into db, check to make sure fuelconsumed is not zero so we don't get a divide by zero error.
|
||||
if (convertedRecord.Gallons > 0)
|
||||
{
|
||||
_gasRecordDataAccess.SaveGasRecordToVehicle(convertedRecord);
|
||||
}
|
||||
}
|
||||
} else if (mode == "servicerecord")
|
||||
{
|
||||
var records = csv.GetRecords<ServiceRecordImport>().ToList();
|
||||
if (records.Any())
|
||||
{
|
||||
foreach (ServiceRecordImport recordToInsert in records)
|
||||
else if (mode == ImportMode.ServiceRecord)
|
||||
{
|
||||
var convertedRecord = new ServiceRecord()
|
||||
{
|
||||
VehicleId = vehicleId,
|
||||
Date = recordToInsert.Date,
|
||||
Mileage = recordToInsert.Odometer,
|
||||
Description = recordToInsert.Description,
|
||||
Notes = recordToInsert.Notes,
|
||||
Cost = recordToInsert.Cost
|
||||
Date = DateTime.Parse(importModel.Date),
|
||||
Mileage = int.Parse(importModel.Odometer, NumberStyles.Any),
|
||||
Description = string.IsNullOrWhiteSpace(importModel.Description) ? $"Service Record on {importModel.Date}" : importModel.Description,
|
||||
Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes,
|
||||
Cost = decimal.Parse(importModel.Cost, NumberStyles.Any)
|
||||
};
|
||||
_serviceRecordDataAccess.SaveServiceRecordToVehicle(convertedRecord);
|
||||
}
|
||||
}
|
||||
} else if (mode == "repairrecord")
|
||||
{
|
||||
var records = csv.GetRecords<ServiceRecordImport>().ToList();
|
||||
if (records.Any())
|
||||
{
|
||||
foreach (ServiceRecordImport recordToInsert in records)
|
||||
else if (mode == ImportMode.RepairRecord)
|
||||
{
|
||||
var convertedRecord = new CollisionRecord()
|
||||
{
|
||||
VehicleId = vehicleId,
|
||||
Date = recordToInsert.Date,
|
||||
Mileage = recordToInsert.Odometer,
|
||||
Description = recordToInsert.Description,
|
||||
Notes = recordToInsert.Notes,
|
||||
Cost = recordToInsert.Cost
|
||||
Date = DateTime.Parse(importModel.Date),
|
||||
Mileage = int.Parse(importModel.Odometer, NumberStyles.Any),
|
||||
Description = string.IsNullOrWhiteSpace(importModel.Description) ? $"Repair Record on {importModel.Date}" : importModel.Description,
|
||||
Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes,
|
||||
Cost = decimal.Parse(importModel.Cost, NumberStyles.Any)
|
||||
};
|
||||
_collisionRecordDataAccess.SaveCollisionRecordToVehicle(convertedRecord);
|
||||
}
|
||||
else if (mode == ImportMode.UpgradeRecord)
|
||||
{
|
||||
var convertedRecord = new UpgradeRecord()
|
||||
{
|
||||
VehicleId = vehicleId,
|
||||
Date = DateTime.Parse(importModel.Date),
|
||||
Mileage = int.Parse(importModel.Odometer, NumberStyles.Any),
|
||||
Description = string.IsNullOrWhiteSpace(importModel.Description) ? $"Upgrade Record on {importModel.Date}" : importModel.Description,
|
||||
Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes,
|
||||
Cost = decimal.Parse(importModel.Cost, NumberStyles.Any)
|
||||
};
|
||||
_upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(convertedRecord);
|
||||
}
|
||||
} else if (mode == "taxrecord")
|
||||
{
|
||||
var records = csv.GetRecords<TaxRecordImport>().ToList();
|
||||
if (records.Any())
|
||||
{
|
||||
foreach (TaxRecordImport recordToInsert in records)
|
||||
else if (mode == ImportMode.TaxRecord)
|
||||
{
|
||||
var convertedRecord = new TaxRecord()
|
||||
{
|
||||
VehicleId = vehicleId,
|
||||
Date = recordToInsert.Date,
|
||||
Description = recordToInsert.Description,
|
||||
Notes = recordToInsert.Notes,
|
||||
Cost = recordToInsert.Cost
|
||||
Date = DateTime.Parse(importModel.Date),
|
||||
Description = string.IsNullOrWhiteSpace(importModel.Description) ? $"Tax Record on {importModel.Date}" : importModel.Description,
|
||||
Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes,
|
||||
Cost = decimal.Parse(importModel.Cost, NumberStyles.Any)
|
||||
};
|
||||
_taxRecordDataAccess.SaveTaxRecordToVehicle(convertedRecord);
|
||||
}
|
||||
@@ -237,49 +361,19 @@ namespace CarCareTracker.Controllers
|
||||
result = result.OrderBy(x => x.Date).ThenBy(x => x.Mileage).ToList();
|
||||
//check if the user uses MPG or Liters per 100km.
|
||||
bool useMPG = bool.Parse(_config[nameof(UserConfig.UseMPG)]);
|
||||
var computedResults = new List<GasRecordViewModel>();
|
||||
int previousMileage = 0;
|
||||
//perform computation.
|
||||
for(int i = 0; i < result.Count; i++)
|
||||
{
|
||||
if (i > 0)
|
||||
{
|
||||
var currentObject = result[i];
|
||||
var deltaMileage = currentObject.Mileage - previousMileage;
|
||||
computedResults.Add(new GasRecordViewModel()
|
||||
{
|
||||
Id = currentObject.Id,
|
||||
VehicleId = currentObject.VehicleId,
|
||||
Date = currentObject.Date.ToShortDateString(),
|
||||
Mileage = currentObject.Mileage,
|
||||
Gallons = currentObject.Gallons,
|
||||
Cost = currentObject.Cost,
|
||||
DeltaMileage = deltaMileage,
|
||||
MilesPerGallon = useMPG ? (deltaMileage / currentObject.Gallons) : 100 / (deltaMileage / currentObject.Gallons),
|
||||
CostPerGallon = (currentObject.Cost / currentObject.Gallons)
|
||||
});
|
||||
} else
|
||||
{
|
||||
computedResults.Add(new GasRecordViewModel()
|
||||
{
|
||||
Id = result[i].Id,
|
||||
VehicleId = result[i].VehicleId,
|
||||
Date = result[i].Date.ToShortDateString(),
|
||||
Mileage = result[i].Mileage,
|
||||
Gallons = result[i].Gallons,
|
||||
Cost = result[i].Cost,
|
||||
DeltaMileage = 0,
|
||||
MilesPerGallon = 0,
|
||||
CostPerGallon = (result[i].Cost / result[i].Gallons)
|
||||
});
|
||||
}
|
||||
previousMileage = result[i].Mileage;
|
||||
}
|
||||
bool useUKMPG = bool.Parse(_config[nameof(UserConfig.UseUKMPG)]);
|
||||
var computedResults = _gasHelper.GetGasRecordViewModels(result, useMPG, useUKMPG);
|
||||
if (_useDescending)
|
||||
{
|
||||
computedResults = computedResults.OrderByDescending(x => DateTime.Parse(x.Date)).ThenByDescending(x => x.Mileage).ToList();
|
||||
}
|
||||
return PartialView("_Gas", computedResults);
|
||||
var vehicleIsElectric = _dataAccess.GetVehicleById(vehicleId).IsElectric;
|
||||
var viewModel = new GasRecordViewModelContainer()
|
||||
{
|
||||
UseKwh = vehicleIsElectric,
|
||||
GasRecords = computedResults
|
||||
};
|
||||
return PartialView("_Gas", viewModel);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult SaveGasRecordToVehicleId(GasRecordInput gasRecord)
|
||||
@@ -291,7 +385,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 +399,17 @@ namespace CarCareTracker.Controllers
|
||||
Cost = result.Cost,
|
||||
Date = result.Date.ToShortDateString(),
|
||||
Files = result.Files,
|
||||
Gallons = result.Gallons
|
||||
Gallons = result.Gallons,
|
||||
IsFillToFull = result.IsFillToFull,
|
||||
MissedFuelUp = result.MissedFuelUp
|
||||
};
|
||||
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 +451,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,
|
||||
@@ -474,9 +578,71 @@ namespace CarCareTracker.Controllers
|
||||
#endregion
|
||||
#region "Reports"
|
||||
[HttpGet]
|
||||
public IActionResult GetReportPartialView()
|
||||
public IActionResult GetReportPartialView(int vehicleId)
|
||||
{
|
||||
return PartialView("_Report");
|
||||
//get records
|
||||
var serviceRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId);
|
||||
var gasRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
|
||||
var collisionRecords = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId);
|
||||
var taxRecords = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId);
|
||||
var upgradeRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId);
|
||||
var viewModel = new ReportViewModel();
|
||||
//get totalCostMakeUp
|
||||
viewModel.CostMakeUpForVehicle = new CostMakeUpForVehicle
|
||||
{
|
||||
ServiceRecordSum = serviceRecords.Sum(x => x.Cost),
|
||||
GasRecordSum = gasRecords.Sum(x => x.Cost),
|
||||
CollisionRecordSum = collisionRecords.Sum(x => x.Cost),
|
||||
TaxRecordSum = taxRecords.Sum(x => x.Cost),
|
||||
UpgradeRecordSum = upgradeRecords.Sum(x => x.Cost)
|
||||
};
|
||||
//get costbymonth
|
||||
List<CostForVehicleByMonth> allCosts = new List<CostForVehicleByMonth>();
|
||||
allCosts.AddRange(_reportHelper.GetServiceRecordSum(serviceRecords, 0));
|
||||
allCosts.AddRange(_reportHelper.GetRepairRecordSum(collisionRecords, 0));
|
||||
allCosts.AddRange(_reportHelper.GetUpgradeRecordSum(upgradeRecords, 0));
|
||||
allCosts.AddRange(_reportHelper.GetUpgradeRecordSum(upgradeRecords, 0));
|
||||
allCosts.AddRange(_reportHelper.GetGasRecordSum(gasRecords, 0));
|
||||
allCosts.AddRange(_reportHelper.GetTaxRecordSum(taxRecords, 0));
|
||||
viewModel.CostForVehicleByMonth = allCosts.GroupBy(x => x.MonthName).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth
|
||||
{
|
||||
MonthName = x.Key,
|
||||
Cost = x.Sum(y => y.Cost)
|
||||
}).ToList();
|
||||
//get reminders
|
||||
var reminders = GetRemindersAndUrgency(vehicleId, DateTime.Now);
|
||||
viewModel.ReminderMakeUpForVehicle = new ReminderMakeUpForVehicle
|
||||
{
|
||||
NotUrgentCount = reminders.Where(x => x.Urgency == ReminderUrgency.NotUrgent).Count(),
|
||||
UrgentCount = reminders.Where(x => x.Urgency == ReminderUrgency.Urgent).Count(),
|
||||
VeryUrgentCount = reminders.Where(x => x.Urgency == ReminderUrgency.VeryUrgent).Count(),
|
||||
PastDueCount = reminders.Where(x => x.Urgency == ReminderUrgency.PastDue).Count()
|
||||
};
|
||||
//populate year dropdown.
|
||||
var numbersArray = new List<int>();
|
||||
if (serviceRecords.Any())
|
||||
{
|
||||
numbersArray.Add(serviceRecords.Min(x => x.Date.Year));
|
||||
}
|
||||
if (collisionRecords.Any())
|
||||
{
|
||||
numbersArray.Add(collisionRecords.Min(x => x.Date.Year));
|
||||
}
|
||||
if (gasRecords.Any())
|
||||
{
|
||||
numbersArray.Add(gasRecords.Min(x => x.Date.Year));
|
||||
}
|
||||
if (upgradeRecords.Any())
|
||||
{
|
||||
numbersArray.Add(upgradeRecords.Min(x => x.Date.Year));
|
||||
}
|
||||
var minYear = numbersArray.Any() ? numbersArray.Min() : DateTime.Now.AddYears(-5).Year;
|
||||
var yearDifference = DateTime.Now.Year - minYear + 1;
|
||||
for(int i = 0; i < yearDifference; i++)
|
||||
{
|
||||
viewModel.Years.Add(DateTime.Now.AddYears(i * -1).Year);
|
||||
}
|
||||
return PartialView("_Report", viewModel);
|
||||
}
|
||||
[HttpGet]
|
||||
public IActionResult GetCostMakeUpForVehicle(int vehicleId, int year = 0)
|
||||
@@ -485,34 +651,248 @@ namespace CarCareTracker.Controllers
|
||||
var gasRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
|
||||
var collisionRecords = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId);
|
||||
var taxRecords = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId);
|
||||
var upgradeRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId);
|
||||
if (year != default)
|
||||
{
|
||||
serviceRecords.RemoveAll(x => x.Date.Year != year);
|
||||
gasRecords.RemoveAll(x => x.Date.Year != year);
|
||||
collisionRecords.RemoveAll(x => x.Date.Year != year);
|
||||
taxRecords.RemoveAll(x => x.Date.Year != year);
|
||||
upgradeRecords.RemoveAll(x => x.Date.Year != year);
|
||||
}
|
||||
var viewModel = new CostMakeUpForVehicle
|
||||
{
|
||||
ServiceRecordSum = serviceRecords.Sum(x => x.Cost),
|
||||
GasRecordSum = gasRecords.Sum(x => x.Cost),
|
||||
CollisionRecordSum = collisionRecords.Sum(x => x.Cost),
|
||||
TaxRecordSum = taxRecords.Sum(x => x.Cost)
|
||||
TaxRecordSum = taxRecords.Sum(x => x.Cost),
|
||||
UpgradeRecordSum = upgradeRecords.Sum(x=>x.Cost)
|
||||
};
|
||||
return PartialView("_CostMakeUpReport", viewModel);
|
||||
}
|
||||
public IActionResult GetFuelCostByMonthByVehicle(int vehicleId, int year = 0)
|
||||
public IActionResult GetReminderMakeUpByVehicle(int vehicleId, int daysToAdd)
|
||||
{
|
||||
var reminders = GetRemindersAndUrgency(vehicleId, DateTime.Now.AddDays(daysToAdd));
|
||||
var viewModel = new ReminderMakeUpForVehicle
|
||||
{
|
||||
NotUrgentCount = reminders.Where(x => x.Urgency == ReminderUrgency.NotUrgent).Count(),
|
||||
UrgentCount = reminders.Where(x => x.Urgency == ReminderUrgency.Urgent).Count(),
|
||||
VeryUrgentCount = reminders.Where(x => x.Urgency == ReminderUrgency.VeryUrgent).Count(),
|
||||
PastDueCount = reminders.Where(x => x.Urgency == ReminderUrgency.PastDue).Count()
|
||||
};
|
||||
return PartialView("_ReminderMakeUpReport", viewModel);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult GetCostByMonthByVehicle(int vehicleId, List<ImportMode> selectedMetrics, int year = 0)
|
||||
{
|
||||
List<CostForVehicleByMonth> allCosts = new List<CostForVehicleByMonth>();
|
||||
if (selectedMetrics.Contains(ImportMode.ServiceRecord))
|
||||
{
|
||||
var serviceRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId);
|
||||
allCosts.AddRange(_reportHelper.GetServiceRecordSum(serviceRecords, year));
|
||||
}
|
||||
if (selectedMetrics.Contains(ImportMode.RepairRecord))
|
||||
{
|
||||
var repairRecords = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId);
|
||||
allCosts.AddRange(_reportHelper.GetRepairRecordSum(repairRecords, year));
|
||||
}
|
||||
if (selectedMetrics.Contains(ImportMode.UpgradeRecord))
|
||||
{
|
||||
var upgradeRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId);
|
||||
allCosts.AddRange(_reportHelper.GetUpgradeRecordSum(upgradeRecords, year));
|
||||
}
|
||||
if (selectedMetrics.Contains(ImportMode.GasRecord))
|
||||
{
|
||||
var gasRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
|
||||
if (year != default)
|
||||
{
|
||||
gasRecords.RemoveAll(x => x.Date.Year != year);
|
||||
allCosts.AddRange(_reportHelper.GetGasRecordSum(gasRecords, year));
|
||||
}
|
||||
var groupedGasRecord = gasRecords.GroupBy(x => x.Date.Month).OrderBy(x=>x.Key).Select(x => new GasCostForVehicleByMonth {
|
||||
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
|
||||
if (selectedMetrics.Contains(ImportMode.TaxRecord))
|
||||
{
|
||||
var taxRecords = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId);
|
||||
allCosts.AddRange(_reportHelper.GetTaxRecordSum(taxRecords, year));
|
||||
}
|
||||
var groupedRecord = allCosts.GroupBy(x => x.MonthName).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth
|
||||
{
|
||||
MonthName = x.Key,
|
||||
Cost = x.Sum(y => y.Cost)
|
||||
}).ToList();
|
||||
return PartialView("_GasCostByMonthReport", groupedGasRecord);
|
||||
return PartialView("_GasCostByMonthReport", groupedRecord);
|
||||
}
|
||||
#endregion
|
||||
#region "Reminders"
|
||||
private int GetMaxMileage(int vehicleId)
|
||||
{
|
||||
var numbersArray = new List<int>();
|
||||
var serviceRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId);
|
||||
if (serviceRecords.Any())
|
||||
{
|
||||
numbersArray.Add(serviceRecords.Max(x => x.Mileage));
|
||||
}
|
||||
var repairRecords = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId);
|
||||
if (repairRecords.Any())
|
||||
{
|
||||
numbersArray.Add(repairRecords.Max(x => x.Mileage));
|
||||
}
|
||||
var gasRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
|
||||
if (gasRecords.Any())
|
||||
{
|
||||
numbersArray.Add(gasRecords.Max(x => x.Mileage));
|
||||
}
|
||||
var upgradeRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId);
|
||||
if (upgradeRecords.Any())
|
||||
{
|
||||
numbersArray.Add(upgradeRecords.Max(x => x.Mileage));
|
||||
}
|
||||
return numbersArray.Any() ? numbersArray.Max() : 0;
|
||||
}
|
||||
private List<ReminderRecordViewModel> GetRemindersAndUrgency(int vehicleId, DateTime dateCompare)
|
||||
{
|
||||
var currentMileage = GetMaxMileage(vehicleId);
|
||||
var reminders = _reminderRecordDataAccess.GetReminderRecordsByVehicleId(vehicleId);
|
||||
List<ReminderRecordViewModel> results = _reminderHelper.GetReminderRecordViewModels(reminders, currentMileage, dateCompare);
|
||||
return results;
|
||||
}
|
||||
[HttpGet]
|
||||
public IActionResult GetVehicleHaveUrgentOrPastDueReminders(int vehicleId)
|
||||
{
|
||||
var result = GetRemindersAndUrgency(vehicleId, DateTime.Now);
|
||||
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, DateTime.Now);
|
||||
result = result.OrderByDescending(x => x.Urgency).ToList();
|
||||
return PartialView("_ReminderRecords", result);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult SaveReminderRecordToVehicleId(ReminderRecordInput reminderRecord)
|
||||
{
|
||||
var result = _reminderRecordDataAccess.SaveReminderRecordToVehicle(reminderRecord.ToReminderRecord());
|
||||
return Json(result);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult GetAddReminderRecordPartialView(ReminderRecordInput? reminderModel)
|
||||
{
|
||||
if (reminderModel is not null)
|
||||
{
|
||||
return PartialView("_ReminderRecordModal", reminderModel);
|
||||
}
|
||||
else
|
||||
{
|
||||
return PartialView("_ReminderRecordModal", new ReminderRecordInput());
|
||||
}
|
||||
}
|
||||
[HttpGet]
|
||||
public IActionResult GetReminderRecordForEditById(int reminderRecordId)
|
||||
{
|
||||
var result = _reminderRecordDataAccess.GetReminderRecordById(reminderRecordId);
|
||||
//convert to Input object.
|
||||
var convertedResult = new ReminderRecordInput
|
||||
{
|
||||
Id = result.Id,
|
||||
Date = result.Date.ToShortDateString(),
|
||||
Description = result.Description,
|
||||
Notes = result.Notes,
|
||||
VehicleId = result.VehicleId,
|
||||
Mileage = result.Mileage,
|
||||
Metric = result.Metric
|
||||
};
|
||||
return PartialView("_ReminderRecordModal", convertedResult);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult DeleteReminderRecordById(int reminderRecordId)
|
||||
{
|
||||
var result = _reminderRecordDataAccess.DeleteReminderRecordById(reminderRecordId);
|
||||
return Json(result);
|
||||
}
|
||||
#endregion
|
||||
#region "Upgrade Records"
|
||||
[HttpGet]
|
||||
public IActionResult GetUpgradeRecordsByVehicleId(int vehicleId)
|
||||
{
|
||||
var result = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId);
|
||||
if (_useDescending)
|
||||
{
|
||||
result = result.OrderByDescending(x => x.Date).ThenByDescending(x => x.Mileage).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
result = result.OrderBy(x => x.Date).ThenBy(x => x.Mileage).ToList();
|
||||
}
|
||||
return PartialView("_UpgradeRecords", result);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult SaveUpgradeRecordToVehicleId(UpgradeRecordInput upgradeRecord)
|
||||
{
|
||||
//move files from temp.
|
||||
upgradeRecord.Files = upgradeRecord.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList();
|
||||
var result = _upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(upgradeRecord.ToUpgradeRecord());
|
||||
return Json(result);
|
||||
}
|
||||
[HttpGet]
|
||||
public IActionResult GetAddUpgradeRecordPartialView()
|
||||
{
|
||||
return PartialView("_UpgradeRecordModal", new UpgradeRecordInput());
|
||||
}
|
||||
[HttpGet]
|
||||
public IActionResult GetUpgradeRecordForEditById(int upgradeRecordId)
|
||||
{
|
||||
var result = _upgradeRecordDataAccess.GetUpgradeRecordById(upgradeRecordId);
|
||||
//convert to Input object.
|
||||
var convertedResult = new UpgradeRecordInput
|
||||
{
|
||||
Id = result.Id,
|
||||
Cost = result.Cost,
|
||||
Date = result.Date.ToShortDateString(),
|
||||
Description = result.Description,
|
||||
Mileage = result.Mileage,
|
||||
Notes = result.Notes,
|
||||
VehicleId = result.VehicleId,
|
||||
Files = result.Files
|
||||
};
|
||||
return PartialView("_UpgradeRecordModal", convertedResult);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult DeleteUpgradeRecordById(int upgradeRecordId)
|
||||
{
|
||||
var result = _upgradeRecordDataAccess.DeleteUpgradeRecordById(upgradeRecordId);
|
||||
return Json(result);
|
||||
}
|
||||
#endregion
|
||||
#region "Notes"
|
||||
[HttpGet]
|
||||
public IActionResult GetNotesByVehicleId(int vehicleId)
|
||||
{
|
||||
var result = _noteDataAccess.GetNotesByVehicleId(vehicleId);
|
||||
return PartialView("_Notes", result);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult SaveNoteToVehicleId(Note note)
|
||||
{
|
||||
var result = _noteDataAccess.SaveNoteToVehicle(note);
|
||||
return Json(result);
|
||||
}
|
||||
[HttpGet]
|
||||
public IActionResult GetAddNotePartialView()
|
||||
{
|
||||
return PartialView("_NoteModal", new Note());
|
||||
}
|
||||
[HttpGet]
|
||||
public IActionResult GetNoteForEditById(int noteId)
|
||||
{
|
||||
var result = _noteDataAccess.GetNoteById(noteId);
|
||||
return PartialView("_NoteModal", result);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult DeleteNoteById(int noteId)
|
||||
{
|
||||
var result = _noteDataAccess.DeleteNoteById(noteId);
|
||||
return Json(result);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
12
Dockerfile
Normal 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"]
|
||||
11
Enum/ImportMode.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace CarCareTracker.Models
|
||||
{
|
||||
public enum ImportMode
|
||||
{
|
||||
ServiceRecord = 0,
|
||||
RepairRecord = 1,
|
||||
GasRecord = 2,
|
||||
TaxRecord = 3,
|
||||
UpgradeRecord = 4
|
||||
}
|
||||
}
|
||||
9
Enum/ReminderMetric.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace CarCareTracker.Models
|
||||
{
|
||||
public enum ReminderMetric
|
||||
{
|
||||
Date = 0,
|
||||
Odometer = 1,
|
||||
Both = 2
|
||||
}
|
||||
}
|
||||
10
Enum/ReminderUrgency.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace CarCareTracker.Models
|
||||
{
|
||||
public enum ReminderUrgency
|
||||
{
|
||||
NotUrgent = 0,
|
||||
Urgent = 1,
|
||||
VeryUrgent = 2,
|
||||
PastDue = 3
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
32
External/Implementations/NoteDataAccess.cs
vendored
@@ -1,4 +1,5 @@
|
||||
using CarCareTracker.External.Interfaces;
|
||||
using CarCareTracker.Helper;
|
||||
using CarCareTracker.Models;
|
||||
using LiteDB;
|
||||
|
||||
@@ -6,18 +7,26 @@ 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)
|
||||
public List<Note> GetNotesByVehicleId(int vehicleId)
|
||||
{
|
||||
using (var db = new LiteDatabase(dbName))
|
||||
{
|
||||
var table = db.GetCollection<Note>(tableName);
|
||||
var noteToReturn = table.FindOne(Query.EQ(nameof(Note.VehicleId), vehicleId));
|
||||
return noteToReturn ?? new Note();
|
||||
var noteToReturn = table.Find(Query.EQ(nameof(Note.VehicleId), vehicleId));
|
||||
return noteToReturn.ToList() ?? new List<Note>();
|
||||
};
|
||||
}
|
||||
public bool SaveNoteToVehicleId(Note note)
|
||||
public Note GetNoteById(int noteId)
|
||||
{
|
||||
using (var db = new LiteDatabase(dbName))
|
||||
{
|
||||
var table = db.GetCollection<Note>(tableName);
|
||||
return table.FindById(noteId);
|
||||
};
|
||||
}
|
||||
public bool SaveNoteToVehicle(Note note)
|
||||
{
|
||||
using (var db = new LiteDatabase(dbName))
|
||||
{
|
||||
@@ -26,12 +35,21 @@ namespace CarCareTracker.External.Implementations
|
||||
return true;
|
||||
};
|
||||
}
|
||||
public bool DeleteNoteByVehicleId(int vehicleId)
|
||||
public bool DeleteNoteById(int noteId)
|
||||
{
|
||||
using (var db = new LiteDatabase(dbName))
|
||||
{
|
||||
var table = db.GetCollection<Note>(tableName);
|
||||
table.DeleteMany(Query.EQ(nameof(Note.VehicleId), vehicleId));
|
||||
table.Delete(noteId);
|
||||
return true;
|
||||
};
|
||||
}
|
||||
public bool DeleteAllNotesByVehicleId(int vehicleId)
|
||||
{
|
||||
using (var db = new LiteDatabase(dbName))
|
||||
{
|
||||
var table = db.GetCollection<Note>(tableName);
|
||||
var notes = table.DeleteMany(Query.EQ(nameof(Note.VehicleId), vehicleId));
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
57
External/Implementations/ReminderRecordDataAccess.cs
vendored
Normal 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;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
57
External/Implementations/UpgradeRecordDataAccess.cs
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
using CarCareTracker.External.Interfaces;
|
||||
using CarCareTracker.Helper;
|
||||
using CarCareTracker.Models;
|
||||
using LiteDB;
|
||||
|
||||
namespace CarCareTracker.External.Implementations
|
||||
{
|
||||
public class UpgradeRecordDataAccess : IUpgradeRecordDataAccess
|
||||
{
|
||||
private static string dbName = StaticHelper.DbName;
|
||||
private static string tableName = "upgraderecords";
|
||||
public List<UpgradeRecord> GetUpgradeRecordsByVehicleId(int vehicleId)
|
||||
{
|
||||
using (var db = new LiteDatabase(dbName))
|
||||
{
|
||||
var table = db.GetCollection<UpgradeRecord>(tableName);
|
||||
var upgradeRecords = table.Find(Query.EQ(nameof(UpgradeRecord.VehicleId), vehicleId));
|
||||
return upgradeRecords.ToList() ?? new List<UpgradeRecord>();
|
||||
};
|
||||
}
|
||||
public UpgradeRecord GetUpgradeRecordById(int upgradeRecordId)
|
||||
{
|
||||
using (var db = new LiteDatabase(dbName))
|
||||
{
|
||||
var table = db.GetCollection<UpgradeRecord>(tableName);
|
||||
return table.FindById(upgradeRecordId);
|
||||
};
|
||||
}
|
||||
public bool DeleteUpgradeRecordById(int upgradeRecordId)
|
||||
{
|
||||
using (var db = new LiteDatabase(dbName))
|
||||
{
|
||||
var table = db.GetCollection<UpgradeRecord>(tableName);
|
||||
table.Delete(upgradeRecordId);
|
||||
return true;
|
||||
};
|
||||
}
|
||||
public bool SaveUpgradeRecordToVehicle(UpgradeRecord upgradeRecord)
|
||||
{
|
||||
using (var db = new LiteDatabase(dbName))
|
||||
{
|
||||
var table = db.GetCollection<UpgradeRecord>(tableName);
|
||||
table.Upsert(upgradeRecord);
|
||||
return true;
|
||||
};
|
||||
}
|
||||
public bool DeleteAllUpgradeRecordsByVehicleId(int vehicleId)
|
||||
{
|
||||
using (var db = new LiteDatabase(dbName))
|
||||
{
|
||||
var table = db.GetCollection<UpgradeRecord>(tableName);
|
||||
var upgradeRecords = table.DeleteMany(Query.EQ(nameof(UpgradeRecord.VehicleId), vehicleId));
|
||||
return true;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
8
External/Interfaces/INoteDataAccess.cs
vendored
@@ -4,8 +4,10 @@ namespace CarCareTracker.External.Interfaces
|
||||
{
|
||||
public interface INoteDataAccess
|
||||
{
|
||||
public Note GetNoteByVehicleId(int vehicleId);
|
||||
public bool SaveNoteToVehicleId(Note note);
|
||||
bool DeleteNoteByVehicleId(int vehicleId);
|
||||
public List<Note> GetNotesByVehicleId(int vehicleId);
|
||||
public Note GetNoteById(int noteId);
|
||||
public bool SaveNoteToVehicle(Note note);
|
||||
public bool DeleteNoteById(int noteId);
|
||||
public bool DeleteAllNotesByVehicleId(int vehicleId);
|
||||
}
|
||||
}
|
||||
|
||||
13
External/Interfaces/IReminderRecordDataAccess.cs
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
13
External/Interfaces/IUpgradeRecordDataAccess.cs
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
using CarCareTracker.Models;
|
||||
|
||||
namespace CarCareTracker.External.Interfaces
|
||||
{
|
||||
public interface IUpgradeRecordDataAccess
|
||||
{
|
||||
public List<UpgradeRecord> GetUpgradeRecordsByVehicleId(int vehicleId);
|
||||
public UpgradeRecord GetUpgradeRecordById(int upgradeRecordId);
|
||||
public bool DeleteUpgradeRecordById(int upgradeRecordId);
|
||||
public bool SaveUpgradeRecordToVehicle(UpgradeRecord upgradeRecord);
|
||||
public bool DeleteAllUpgradeRecordsByVehicleId(int vehicleId);
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,9 @@
|
||||
{
|
||||
public interface IFileHelper
|
||||
{
|
||||
string GetFullFilePath(string currentFilePath);
|
||||
public string MoveFileFromTemp(string currentFilePath, string newFolder);
|
||||
public bool DeleteFile(string currentFilePath);
|
||||
string GetFullFilePath(string currentFilePath, bool mustExist = true);
|
||||
string MoveFileFromTemp(string currentFilePath, string newFolder);
|
||||
bool DeleteFile(string currentFilePath);
|
||||
}
|
||||
public class FileHelper: IFileHelper
|
||||
{
|
||||
@@ -13,7 +13,7 @@
|
||||
{
|
||||
_webEnv = webEnv;
|
||||
}
|
||||
public string GetFullFilePath(string currentFilePath)
|
||||
public string GetFullFilePath(string currentFilePath, bool mustExist = true)
|
||||
{
|
||||
if (currentFilePath.StartsWith("/"))
|
||||
{
|
||||
@@ -23,7 +23,10 @@
|
||||
if (File.Exists(oldFilePath))
|
||||
{
|
||||
return oldFilePath;
|
||||
} else
|
||||
} else if (!mustExist)
|
||||
{
|
||||
return oldFilePath;
|
||||
}
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
90
Helper/GasHelper.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
using CarCareTracker.Models;
|
||||
|
||||
namespace CarCareTracker.Helper
|
||||
{
|
||||
public interface IGasHelper
|
||||
{
|
||||
List<GasRecordViewModel> GetGasRecordViewModels(List<GasRecord> result, bool useMPG, bool useUKMPG);
|
||||
}
|
||||
public class GasHelper : IGasHelper
|
||||
{
|
||||
public List<GasRecordViewModel> GetGasRecordViewModels(List<GasRecord> result, bool useMPG, bool useUKMPG)
|
||||
{
|
||||
var computedResults = new List<GasRecordViewModel>();
|
||||
int previousMileage = 0;
|
||||
decimal unFactoredConsumption = 0.00M;
|
||||
int unFactoredMileage = 0;
|
||||
//perform computation.
|
||||
for (int i = 0; i < result.Count; i++)
|
||||
{
|
||||
var currentObject = result[i];
|
||||
decimal convertedConsumption;
|
||||
if (useUKMPG && useMPG)
|
||||
{
|
||||
//if we're using UK MPG and the user wants imperial calculation insteace of l/100km
|
||||
//if UK MPG is selected then the gas consumption are stored in liters but need to convert into UK gallons for computation.
|
||||
convertedConsumption = currentObject.Gallons / 4.546M;
|
||||
}
|
||||
else
|
||||
{
|
||||
convertedConsumption = currentObject.Gallons;
|
||||
}
|
||||
if (i > 0)
|
||||
{
|
||||
var deltaMileage = currentObject.Mileage - previousMileage;
|
||||
var gasRecordViewModel = new GasRecordViewModel()
|
||||
{
|
||||
Id = currentObject.Id,
|
||||
VehicleId = currentObject.VehicleId,
|
||||
Date = currentObject.Date.ToShortDateString(),
|
||||
Mileage = currentObject.Mileage,
|
||||
Gallons = convertedConsumption,
|
||||
Cost = currentObject.Cost,
|
||||
DeltaMileage = deltaMileage,
|
||||
CostPerGallon = currentObject.Cost / convertedConsumption
|
||||
};
|
||||
if (currentObject.MissedFuelUp)
|
||||
{
|
||||
//if they missed a fuel up, we skip MPG calculation.
|
||||
gasRecordViewModel.MilesPerGallon = 0;
|
||||
//reset unFactored vars for missed fuel up because the numbers wont be reliable.
|
||||
unFactoredConsumption = 0;
|
||||
unFactoredMileage = 0;
|
||||
}
|
||||
else if (currentObject.IsFillToFull)
|
||||
{
|
||||
//if user filled to full.
|
||||
gasRecordViewModel.MilesPerGallon = useMPG ? (unFactoredMileage + deltaMileage) / (unFactoredConsumption + convertedConsumption) : 100 / ((unFactoredMileage + deltaMileage) / (unFactoredConsumption + convertedConsumption));
|
||||
//reset unFactored vars
|
||||
unFactoredConsumption = 0;
|
||||
unFactoredMileage = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
unFactoredConsumption += convertedConsumption;
|
||||
unFactoredMileage += deltaMileage;
|
||||
gasRecordViewModel.MilesPerGallon = 0;
|
||||
}
|
||||
computedResults.Add(gasRecordViewModel);
|
||||
}
|
||||
else
|
||||
{
|
||||
computedResults.Add(new GasRecordViewModel()
|
||||
{
|
||||
Id = currentObject.Id,
|
||||
VehicleId = currentObject.VehicleId,
|
||||
Date = currentObject.Date.ToShortDateString(),
|
||||
Mileage = currentObject.Mileage,
|
||||
Gallons = convertedConsumption,
|
||||
Cost = currentObject.Cost,
|
||||
DeltaMileage = 0,
|
||||
MilesPerGallon = 0,
|
||||
CostPerGallon = currentObject.Cost / convertedConsumption
|
||||
});
|
||||
}
|
||||
previousMileage = currentObject.Mileage;
|
||||
}
|
||||
return computedResults;
|
||||
}
|
||||
}
|
||||
}
|
||||
47
Helper/LoginHelper.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using CarCareTracker.Models;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace CarCareTracker.Helper
|
||||
{
|
||||
public interface ILoginHelper
|
||||
{
|
||||
bool ValidateUserCredentials(LoginModel credentials);
|
||||
}
|
||||
public class LoginHelper: ILoginHelper
|
||||
{
|
||||
public bool ValidateUserCredentials(LoginModel credentials)
|
||||
{
|
||||
var configFileContents = System.IO.File.ReadAllText(StaticHelper.UserConfigPath);
|
||||
var existingUserConfig = System.Text.Json.JsonSerializer.Deserialize<UserConfig>(configFileContents);
|
||||
if (existingUserConfig is not null)
|
||||
{
|
||||
//create hashes of the login credentials.
|
||||
var hashedUserName = Sha256_hash(credentials.UserName);
|
||||
var hashedPassword = Sha256_hash(credentials.Password);
|
||||
//compare against stored hash.
|
||||
if (hashedUserName == existingUserConfig.UserNameHash &&
|
||||
hashedPassword == existingUserConfig.UserPasswordHash)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
private static string Sha256_hash(string value)
|
||||
{
|
||||
StringBuilder Sb = new StringBuilder();
|
||||
|
||||
using (var hash = SHA256.Create())
|
||||
{
|
||||
Encoding enc = Encoding.UTF8;
|
||||
byte[] result = hash.ComputeHash(enc.GetBytes(value));
|
||||
|
||||
foreach (byte b in result)
|
||||
Sb.Append(b.ToString("x2"));
|
||||
}
|
||||
|
||||
return Sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
97
Helper/ReminderHelper.cs
Normal file
@@ -0,0 +1,97 @@
|
||||
using CarCareTracker.Models;
|
||||
|
||||
namespace CarCareTracker.Helper
|
||||
{
|
||||
public interface IReminderHelper
|
||||
{
|
||||
List<ReminderRecordViewModel> GetReminderRecordViewModels(List<ReminderRecord> reminders, int currentMileage, DateTime dateCompare);
|
||||
}
|
||||
public class ReminderHelper: IReminderHelper
|
||||
{
|
||||
public List<ReminderRecordViewModel> GetReminderRecordViewModels(List<ReminderRecord> reminders, int currentMileage, DateTime dateCompare)
|
||||
{
|
||||
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 < dateCompare)
|
||||
{
|
||||
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 < dateCompare.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 < dateCompare.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 < dateCompare)
|
||||
{
|
||||
reminderViewModel.Urgency = ReminderUrgency.PastDue;
|
||||
}
|
||||
else if (reminder.Date < dateCompare.AddDays(7))
|
||||
{
|
||||
reminderViewModel.Urgency = ReminderUrgency.VeryUrgent;
|
||||
}
|
||||
else if (reminder.Date < dateCompare.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
77
Helper/ReportHelper.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
using CarCareTracker.Models;
|
||||
using System.Globalization;
|
||||
|
||||
namespace CarCareTracker.Helper
|
||||
{
|
||||
public interface IReportHelper
|
||||
{
|
||||
IEnumerable<CostForVehicleByMonth> GetServiceRecordSum(List<ServiceRecord> serviceRecords, int year = 0);
|
||||
IEnumerable<CostForVehicleByMonth> GetRepairRecordSum(List<CollisionRecord> repairRecords, int year = 0);
|
||||
IEnumerable<CostForVehicleByMonth> GetUpgradeRecordSum(List<UpgradeRecord> upgradeRecords, int year = 0);
|
||||
IEnumerable<CostForVehicleByMonth> GetGasRecordSum(List<GasRecord> gasRecords, int year = 0);
|
||||
IEnumerable<CostForVehicleByMonth> GetTaxRecordSum(List<TaxRecord> taxRecords, int year = 0);
|
||||
}
|
||||
public class ReportHelper: IReportHelper
|
||||
{
|
||||
public IEnumerable<CostForVehicleByMonth> GetServiceRecordSum(List<ServiceRecord> serviceRecords, int year = 0)
|
||||
{
|
||||
if (year != default)
|
||||
{
|
||||
serviceRecords.RemoveAll(x => x.Date.Year != year);
|
||||
}
|
||||
return serviceRecords.GroupBy(x => x.Date.Month).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth
|
||||
{
|
||||
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
|
||||
Cost = x.Sum(y => y.Cost)
|
||||
});
|
||||
}
|
||||
public IEnumerable<CostForVehicleByMonth> GetRepairRecordSum(List<CollisionRecord> repairRecords, int year = 0)
|
||||
{
|
||||
if (year != default)
|
||||
{
|
||||
repairRecords.RemoveAll(x => x.Date.Year != year);
|
||||
}
|
||||
return repairRecords.GroupBy(x => x.Date.Month).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth
|
||||
{
|
||||
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
|
||||
Cost = x.Sum(y => y.Cost)
|
||||
});
|
||||
}
|
||||
public IEnumerable<CostForVehicleByMonth> GetUpgradeRecordSum(List<UpgradeRecord> upgradeRecords, int year = 0)
|
||||
{
|
||||
if (year != default)
|
||||
{
|
||||
upgradeRecords.RemoveAll(x => x.Date.Year != year);
|
||||
}
|
||||
return upgradeRecords.GroupBy(x => x.Date.Month).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth
|
||||
{
|
||||
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
|
||||
Cost = x.Sum(y => y.Cost)
|
||||
});
|
||||
}
|
||||
public IEnumerable<CostForVehicleByMonth> GetGasRecordSum(List<GasRecord> gasRecords, int year = 0)
|
||||
{
|
||||
if (year != default)
|
||||
{
|
||||
gasRecords.RemoveAll(x => x.Date.Year != year);
|
||||
}
|
||||
return gasRecords.GroupBy(x => x.Date.Month).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth
|
||||
{
|
||||
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
|
||||
Cost = x.Sum(y => y.Cost)
|
||||
});
|
||||
}
|
||||
public IEnumerable<CostForVehicleByMonth> GetTaxRecordSum(List<TaxRecord> taxRecords, int year = 0)
|
||||
{
|
||||
if (year != default)
|
||||
{
|
||||
taxRecords.RemoveAll(x => x.Date.Year != year);
|
||||
}
|
||||
return taxRecords.GroupBy(x => x.Date.Month).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth
|
||||
{
|
||||
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
|
||||
Cost = x.Sum(y => y.Cost)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
26
Helper/StaticHelper.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
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";
|
||||
|
||||
public static string TruncateStrings(string input, int maxLength = 25)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
if (input.Length > maxLength)
|
||||
{
|
||||
return (input.Substring(0, maxLength) + "...");
|
||||
} else
|
||||
{
|
||||
return input;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
LICENSE
@@ -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
|
||||
|
||||
22
MapProfile/FuellyMappers.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using CarCareTracker.Models;
|
||||
using CsvHelper.Configuration;
|
||||
|
||||
namespace CarCareTracker.MapProfile
|
||||
{
|
||||
public class FuellyMapper: ClassMap<ImportModel>
|
||||
{
|
||||
public FuellyMapper()
|
||||
{
|
||||
Map(m => m.Date).Name(["date", "fuelup_date"]);
|
||||
Map(m => m.Odometer).Name(["odometer"]);
|
||||
Map(m => m.FuelConsumed).Name(["gallons", "liters", "litres", "consumption", "quantity", "fueleconomy"]);
|
||||
Map(m => m.Cost).Name(["cost", "total cost", "totalcost", "total price"]);
|
||||
Map(m => m.Notes).Name("notes", "note");
|
||||
Map(m => m.Price).Name(["price"]);
|
||||
Map(m => m.PartialFuelUp).Name(["partial_fuelup"]);
|
||||
Map(m => m.IsFillToFull).Name(["isfilltofull", "filled up"]);
|
||||
Map(m => m.Description).Name(["description"]);
|
||||
Map(m => m.MissedFuelUp).Name(["missed_fuelup"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using CarCareTracker.Models;
|
||||
using CarCareTracker.Helper;
|
||||
using CarCareTracker.Models;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
@@ -14,17 +15,20 @@ namespace CarCareTracker.Middleware
|
||||
{
|
||||
private IHttpContextAccessor _httpContext;
|
||||
private IDataProtector _dataProtector;
|
||||
private ILoginHelper _loginHelper;
|
||||
private bool enableAuth;
|
||||
public Authen(
|
||||
IOptionsMonitor<AuthenticationSchemeOptions> options,
|
||||
UrlEncoder encoder,
|
||||
ILoggerFactory logger,
|
||||
IConfiguration configuration,
|
||||
ILoginHelper loginHelper,
|
||||
IDataProtectionProvider securityProvider,
|
||||
IHttpContextAccessor httpContext) : base(options, logger, encoder)
|
||||
{
|
||||
_httpContext = httpContext;
|
||||
_dataProtector = securityProvider.CreateProtector("login");
|
||||
_loginHelper = loginHelper;
|
||||
enableAuth = bool.Parse(configuration["EnableAuth"]);
|
||||
}
|
||||
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
@@ -45,11 +49,38 @@ namespace CarCareTracker.Middleware
|
||||
{
|
||||
//auth is enabled by user, we will have to authenticate the user via a ticket retrieved from the auth cookie.
|
||||
var access_token = _httpContext.HttpContext.Request.Cookies["ACCESS_TOKEN"];
|
||||
if (string.IsNullOrWhiteSpace(access_token))
|
||||
//auth using Basic Auth for API.
|
||||
var request_header = _httpContext.HttpContext.Request.Headers["Authorization"];
|
||||
if (string.IsNullOrWhiteSpace(access_token) && string.IsNullOrWhiteSpace(request_header))
|
||||
{
|
||||
return AuthenticateResult.Fail("Cookie is invalid or does not exist.");
|
||||
}
|
||||
else
|
||||
else if (!string.IsNullOrWhiteSpace(request_header))
|
||||
{
|
||||
var cleanedHeader = request_header.ToString().Replace("Basic ", "").Trim();
|
||||
byte[] data = Convert.FromBase64String(cleanedHeader);
|
||||
string decodedString = System.Text.Encoding.UTF8.GetString(data);
|
||||
var splitString = decodedString.Split(":");
|
||||
if (splitString.Count() != 2)
|
||||
{
|
||||
return AuthenticateResult.Fail("Invalid credentials");
|
||||
} else
|
||||
{
|
||||
var validUser = _loginHelper.ValidateUserCredentials(new LoginModel { UserName = splitString[0], Password = splitString[1] });
|
||||
if (validUser)
|
||||
{
|
||||
var appIdentity = new ClaimsIdentity("Custom");
|
||||
var userIdentity = new List<Claim>
|
||||
{
|
||||
new(ClaimTypes.Name, splitString[0])
|
||||
};
|
||||
appIdentity.AddClaims(userIdentity);
|
||||
AuthenticationTicket ticket = new AuthenticationTicket(new ClaimsPrincipal(appIdentity), this.Scheme.Name);
|
||||
return AuthenticateResult.Success(ticket);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(access_token))
|
||||
{
|
||||
//decrypt the access token.
|
||||
var decryptedCookie = _dataProtector.Unprotect(access_token);
|
||||
@@ -84,6 +115,14 @@ namespace CarCareTracker.Middleware
|
||||
}
|
||||
protected override Task HandleChallengeAsync(AuthenticationProperties properties)
|
||||
{
|
||||
if (Request.RouteValues.TryGetValue("controller", out object value))
|
||||
{
|
||||
if (value.ToString().ToLower() == "api")
|
||||
{
|
||||
Response.StatusCode = 401;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
Response.Redirect("/Login/Index");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
/// </summary>
|
||||
public decimal Gallons { get; set; }
|
||||
public decimal Cost { get; set; }
|
||||
public bool IsFillToFull { get; set; } = true;
|
||||
public bool MissedFuelUp { get; set; } = false;
|
||||
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,19 @@
|
||||
/// </summary>
|
||||
public decimal Gallons { get; set; }
|
||||
public decimal Cost { get; set; }
|
||||
public bool IsFillToFull { get; set; } = true;
|
||||
public bool MissedFuelUp { get; set; } = false;
|
||||
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,
|
||||
MissedFuelUp = MissedFuelUp
|
||||
}; }
|
||||
}
|
||||
}
|
||||
|
||||
8
Models/GasRecord/GasRecordInputContainer.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace CarCareTracker.Models
|
||||
{
|
||||
public class GasRecordInputContainer
|
||||
{
|
||||
public bool UseKwh { get; set; }
|
||||
public GasRecordInput GasRecord { get; set; }
|
||||
}
|
||||
}
|
||||
8
Models/GasRecord/GasRecordViewModelContainer.cs
Normal 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>();
|
||||
}
|
||||
}
|
||||
50
Models/ImportModel.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
namespace CarCareTracker.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Import model used for importing Gas records.
|
||||
/// </summary>
|
||||
public class ImportModel
|
||||
{
|
||||
public string Date { get; set; }
|
||||
public string Odometer { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Notes { get; set; }
|
||||
public string FuelConsumed { get; set; }
|
||||
public string Cost { get; set; }
|
||||
public string Price { get; set; }
|
||||
public string PartialFuelUp { get; set; }
|
||||
public string IsFillToFull { get; set; }
|
||||
public string MissedFuelUp { get; set; }
|
||||
}
|
||||
public class ServiceRecordExportModel
|
||||
{
|
||||
public string Date { get; set; }
|
||||
public string Odometer { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Notes { get; set; }
|
||||
public string Cost { get; set; }
|
||||
}
|
||||
public class TaxRecordExportModel
|
||||
{
|
||||
public string Date { get; set; }
|
||||
public string Odometer { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Notes { get; set; }
|
||||
public string Cost { get; set; }
|
||||
}
|
||||
public class GasRecordExportModel
|
||||
{
|
||||
public string Date { get; set; }
|
||||
public string Odometer { get; set; }
|
||||
public string FuelConsumed { get; set; }
|
||||
public string Cost { get; set; }
|
||||
public string FuelEconomy { get; set; }
|
||||
}
|
||||
public class ReminderExportModel
|
||||
{
|
||||
public string Description { get; set; }
|
||||
public string Urgency { get; set; }
|
||||
public string Metric { get; set; }
|
||||
public string Notes { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
namespace CarCareTracker.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Import model used for importing Gas records.
|
||||
/// </summary>
|
||||
public class GasRecordImport
|
||||
{
|
||||
public DateTime Date { get; set; }
|
||||
public int Odometer { get; set; }
|
||||
public decimal FuelConsumed { get; set; }
|
||||
public decimal Cost { get; set; }
|
||||
}
|
||||
/// <summary>
|
||||
/// Import model used for importing Service and Repair records.
|
||||
/// </summary>
|
||||
public class ServiceRecordImport
|
||||
{
|
||||
public DateTime Date { get; set; }
|
||||
public int Odometer { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Notes { get; set; }
|
||||
public decimal Cost { get; set; }
|
||||
}
|
||||
/// <summary>
|
||||
/// Import model used for importing tax records.
|
||||
/// </summary>
|
||||
public class TaxRecordImport
|
||||
{
|
||||
public DateTime Date { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Notes { get; set; }
|
||||
public decimal Cost { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int VehicleId { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string NoteText { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
13
Models/Reminder/ReminderRecord.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
21
Models/Reminder/ReminderRecordInput.cs
Normal 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 }; }
|
||||
}
|
||||
}
|
||||
17
Models/Reminder/ReminderRecordViewModel.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace CarCareTracker.Models
|
||||
{
|
||||
public class GasCostForVehicleByMonth
|
||||
public class CostForVehicleByMonth
|
||||
{
|
||||
public string MonthName { get; set; }
|
||||
public decimal Cost { get; set; }
|
||||
@@ -6,5 +6,6 @@
|
||||
public decimal GasRecordSum { get; set; }
|
||||
public decimal TaxRecordSum { get; set; }
|
||||
public decimal CollisionRecordSum { get; set; }
|
||||
public decimal UpgradeRecordSum { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
10
Models/Report/ReminderMakeUpForVehicle.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace CarCareTracker.Models
|
||||
{
|
||||
public class ReminderMakeUpForVehicle
|
||||
{
|
||||
public int NotUrgentCount { get; set; }
|
||||
public int UrgentCount { get; set; }
|
||||
public int VeryUrgentCount { get; set; }
|
||||
public int PastDueCount { get; set; }
|
||||
}
|
||||
}
|
||||
10
Models/Report/ReportViewModel.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace CarCareTracker.Models
|
||||
{
|
||||
public class ReportViewModel
|
||||
{
|
||||
public List<CostForVehicleByMonth> CostForVehicleByMonth { get; set; } = new List<CostForVehicleByMonth>();
|
||||
public CostMakeUpForVehicle CostMakeUpForVehicle { get; set; } = new CostMakeUpForVehicle();
|
||||
public ReminderMakeUpForVehicle ReminderMakeUpForVehicle { get; set; } = new ReminderMakeUpForVehicle();
|
||||
public List<int> Years { get; set; } = new List<int>();
|
||||
}
|
||||
}
|
||||
14
Models/Upgrades/UpgradeRecord.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace CarCareTracker.Models
|
||||
{
|
||||
public class UpgradeRecord
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int VehicleId { get; set; }
|
||||
public DateTime Date { get; set; }
|
||||
public int Mileage { get; set; }
|
||||
public string Description { get; set; }
|
||||
public decimal Cost { get; set; }
|
||||
public string Notes { get; set; }
|
||||
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
|
||||
}
|
||||
}
|
||||
15
Models/Upgrades/UpgradeReportInput.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace CarCareTracker.Models
|
||||
{
|
||||
public class UpgradeRecordInput
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int VehicleId { get; set; }
|
||||
public string Date { get; set; }
|
||||
public int Mileage { get; set; }
|
||||
public string Description { get; set; }
|
||||
public decimal Cost { get; set; }
|
||||
public string Notes { get; set; }
|
||||
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
|
||||
public UpgradeRecord ToUpgradeRecord() { return new UpgradeRecord { Id = Id, VehicleId = VehicleId, Date = DateTime.Parse(Date), Cost = Cost, Mileage = Mileage, Description = Description, Notes = Notes, Files = Files }; }
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,8 @@
|
||||
public bool UseMPG { get; set; }
|
||||
public bool UseDescending { get; set; }
|
||||
public bool EnableAuth { get; set; }
|
||||
public bool HideZero { get; set; }
|
||||
public bool UseUKMPG {get;set;}
|
||||
public string UserNameHash { get; set; }
|
||||
public string UserPasswordHash { get; set;}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
15
Program.cs
@@ -15,10 +15,23 @@ builder.Services.AddSingleton<IServiceRecordDataAccess, ServiceRecordDataAccess>
|
||||
builder.Services.AddSingleton<IGasRecordDataAccess, GasRecordDataAccess>();
|
||||
builder.Services.AddSingleton<ICollisionRecordDataAccess, CollisionRecordDataAccess>();
|
||||
builder.Services.AddSingleton<ITaxRecordDataAccess, TaxRecordDataAccess>();
|
||||
builder.Services.AddSingleton<IReminderRecordDataAccess, ReminderRecordDataAccess>();
|
||||
builder.Services.AddSingleton<IUpgradeRecordDataAccess, UpgradeRecordDataAccess>();
|
||||
|
||||
//configure helpers
|
||||
builder.Services.AddSingleton<IFileHelper, FileHelper>();
|
||||
builder.Services.AddSingleton<IGasHelper, GasHelper>();
|
||||
builder.Services.AddSingleton<IReminderHelper, ReminderHelper>();
|
||||
builder.Services.AddSingleton<ILoginHelper, LoginHelper>();
|
||||
builder.Services.AddSingleton<IReportHelper, ReportHelper>();
|
||||
|
||||
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();
|
||||
|
||||
52
README.md
@@ -12,3 +12,55 @@ Because nobody should have to deal with a homemade spreadsheet or a shoebox full
|
||||
- SweetAlert2
|
||||
- CsvHelper
|
||||
- Chart.js
|
||||
|
||||
## Docker Setup (GHCR)
|
||||
1. Install Docker
|
||||
2. Run `docker pull ghcr.io/hargata/lubelogger:latest`
|
||||
3. CHECK culture in .env file, default is en_US, this will change the currency and date formats.
|
||||
4. If not using traefik, use docker-compose-notraefik.yml
|
||||
5. Run `docker-compose up`
|
||||
|
||||
## Docker Setup (Manual Build)
|
||||
1. Install Docker
|
||||
2. Clone this repo
|
||||
3. CHECK culture in .env file, default is en_US
|
||||
4. Run `docker build -t lubelogger -f Dockerfile .`
|
||||
5. CHECK docker-compose.yml and make sure the mounting directories look correct.
|
||||
6. If not using traefik, use docker-compose-notraefik.yml
|
||||
7. Run `docker-compose up`
|
||||
|
||||
## Additional Docker Instructions
|
||||
|
||||
### manual
|
||||
|
||||
- build
|
||||
|
||||
```
|
||||
docker build -t hargata/lubelog:latest .
|
||||
```
|
||||
|
||||
- run
|
||||
|
||||
```
|
||||
docker run -d hargata/lubelog:latest
|
||||
```
|
||||
|
||||
add `-v` for persistent volumes as needed. Have a look at the docker-compose.yml for examples.
|
||||
|
||||
## docker-compose
|
||||
|
||||
- build image
|
||||
|
||||
```
|
||||
docker compose build
|
||||
```
|
||||
|
||||
- run
|
||||
|
||||
```
|
||||
docker compose up
|
||||
|
||||
# or variant with traefik labels:
|
||||
|
||||
docker compose -f docker-compose.traefik.yml up
|
||||
```
|
||||
|
||||
127
Views/API/Index.cshtml
Normal file
@@ -0,0 +1,127 @@
|
||||
<div class="row">
|
||||
<div class="d-flex justify-content-center">
|
||||
<h6 class="display-6 mt-2">API</h6>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="d-flex justify-content-center">
|
||||
<p class="lead">If authentication is enabled, use the credentials of the user for Basic Auth(RFC2617)</p>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col-1">
|
||||
<h6>Method</h6>
|
||||
</div>
|
||||
<div class="col-5">
|
||||
<h6>Endpoint</h6>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<h6>Description</h6>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<h6>Parameters</h6>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-1">
|
||||
GET
|
||||
</div>
|
||||
<div class="col-5">
|
||||
<code>/api/vehicles</code>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
Returns a list of vehicles
|
||||
</div>
|
||||
<div class="col-3">
|
||||
No Params
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-1">
|
||||
GET
|
||||
</div>
|
||||
<div class="col-5">
|
||||
<code>/api/vehicle/servicerecords</code>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
Returns a list of service records for the vehicle
|
||||
</div>
|
||||
<div class="col-3">
|
||||
vehicleId - Id of Vehicle
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-1">
|
||||
GET
|
||||
</div>
|
||||
<div class="col-5">
|
||||
<code>/api/vehicle/repairrecords</code>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
Returns a list of repair records for the vehicle
|
||||
</div>
|
||||
<div class="col-3">
|
||||
vehicleId - Id of Vehicle
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-1">
|
||||
GET
|
||||
</div>
|
||||
<div class="col-5">
|
||||
<code>/api/vehicle/upgraderecords</code>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
Returns a list of upgrade records for the vehicle
|
||||
</div>
|
||||
<div class="col-3">
|
||||
vehicleId - Id of Vehicle
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-1">
|
||||
GET
|
||||
</div>
|
||||
<div class="col-5">
|
||||
<code>/api/vehicle/taxrecords</code>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
Returns a list of tax records for the vehicle
|
||||
</div>
|
||||
<div class="col-3">
|
||||
vehicleId - Id of Vehicle
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-1">
|
||||
GET
|
||||
</div>
|
||||
<div class="col-5">
|
||||
<code>/api/vehicle/gasrecords</code>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
Returns a list of gas records for the vehicle
|
||||
</div>
|
||||
<div class="col-3">
|
||||
vehicleId - Id of Vehicle
|
||||
<br />
|
||||
useMPG(bool) - Use Imperial Units and Calculation
|
||||
<br />
|
||||
useUKMPG(bool) - Use UK Imperial Calculation
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-1">
|
||||
GET
|
||||
</div>
|
||||
<div class="col-5">
|
||||
<code>/api/vehicle/reminders</code>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
Returns a list of reminders for the vehicle
|
||||
</div>
|
||||
<div class="col-3">
|
||||
vehicleId - Id of Vehicle
|
||||
</div>
|
||||
</div>
|
||||
@@ -16,7 +16,11 @@
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="useMPG" checked="@Model.UseMPG">
|
||||
<label class="form-check-label" for="useMPG">Use Imperial Units for Fuel Economy Calculations(Miles, Gallons)</label>
|
||||
<label class="form-check-label" for="useMPG">Use Imperial Calculation for Fuel Economy Calculations(MPG)<br /><small class="text-body-secondary">This Will Also Change Units to Miles and Gallons</small></label>
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="useUKMPG" checked="@Model.UseUKMPG">
|
||||
<label class="form-check-label" for="useUKMPG">Use UK MPG Calculation<br /><small class="text-body-secondary">Input Gas Consumption in Liters, it will be converted to UK Gals for MPG Calculation</small></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
@@ -24,6 +28,10 @@
|
||||
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="useDescending" checked="@Model.UseDescending">
|
||||
<label class="form-check-label" for="useDescending">Sort lists in Descending Order(Newest to Oldest)</label>
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="hideZero" checked="@Model.HideZero">
|
||||
<label class="form-check-label" for="hideZero">Replace @(0.ToString("C")) Costs with ---</label>
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" onChange="enableAuthCheckChanged()" type="checkbox" role="switch" id="enableAuth" checked="@Model.EnableAuth">
|
||||
<label class="form-check-label" for="enableAuth">Enable Authentication</label>
|
||||
@@ -76,7 +84,9 @@
|
||||
useDarkMode: $("#enableDarkMode").is(':checked'),
|
||||
enableCsvImports: $("#enableCsvImports").is(':checked'),
|
||||
useMPG: $("#useMPG").is(':checked'),
|
||||
useDescending: $("#useDescending").is(':checked')
|
||||
useDescending: $("#useDescending").is(':checked'),
|
||||
hideZero: $("#hideZero").is(":checked"),
|
||||
useUKMpg: $("#useUKMPG").is(":checked")
|
||||
}
|
||||
$.post('/Home/WriteToSettings', { userConfig: userConfigObject}, function(data){
|
||||
if (data) {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
@section Scripts {
|
||||
<script src="~/js/login.js" asp-append-version="true"></script>
|
||||
}
|
||||
<div class="container chartContainer d-flex align-items-center justify-content-center">
|
||||
<div class="container d-flex align-items-center justify-content-center" style="height:100vh">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<img src="/defaults/lubelogger_logo.png" />
|
||||
|
||||
@@ -3,11 +3,24 @@
|
||||
@{
|
||||
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>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="apple-mobile-web-app-title" content="LubeLogger" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<title>@ViewData["Title"] - CarCareTracker</title>
|
||||
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap-icons.css" />
|
||||
@@ -15,6 +28,12 @@
|
||||
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
|
||||
<link rel="stylesheet" href="~/css/loader.css" asp-append-version="true" />
|
||||
<link rel="stylesheet" href="~/sweetalert/sweetalert2.min.css" asp-append-version="true" />
|
||||
<link rel="icon" sizes="192x192" href="~/defaults/lubelogger_icon_192.png" />
|
||||
<link rel="icon" sizes="128x128" href="~/defaults/lubelogger_icon_128.png" />
|
||||
<link rel="apple-touch-icon" sizes="128x128" href="~/defaults/lubelogger_icon_128.png" />
|
||||
<link rel="apple-touch-icon-precomposed" sizes="128x128" href="~/defaults/lubelogger_icon_128.png" />
|
||||
<link rel="apple-touch-startup-image" href="~/defaults/lubelogger_launch.png" />
|
||||
<link rel="manifest" href="~/manifest.json">
|
||||
<script src="~/lib/jquery/dist/jquery.min.js"></script>
|
||||
<script src="~/js/shared.js"></script>
|
||||
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
|
||||
@@ -28,6 +47,11 @@
|
||||
enableCsvImport : "@enableCsvImports" == "True"
|
||||
}
|
||||
}
|
||||
function getShortDatePattern() {
|
||||
return {
|
||||
pattern: "@shortDatePattern"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@await RenderSectionAsync("Scripts", required: false)
|
||||
</head>
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
<script src="~/js/gasrecord.js" asp-append-version="true"></script>
|
||||
<script src="~/js/collisionrecord.js" asp-append-version="true"></script>
|
||||
<script src="~/js/taxrecord.js" asp-append-version="true"></script>
|
||||
<script src="~/js/reminderrecord.js" asp-append-version="true"></script>
|
||||
<script src="~/js/upgraderecord.js" asp-append-version="true"></script>
|
||||
<script src="~/js/note.js" asp-append-version="true"></script>
|
||||
<script src="~/lib/chart-js/chart.umd.js"></script>
|
||||
}
|
||||
<div class="container">
|
||||
@@ -26,6 +29,9 @@
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="accident-tab" data-bs-toggle="tab" data-bs-target="#accident-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-exclamation-octagon me-2"></i>Repairs</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="upgrade-tab" data-bs-toggle="tab" data-bs-target="#upgrade-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-wrench-adjustable me-2"></i>Upgrades</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="gas-tab" data-bs-toggle="tab" data-bs-target="#gas-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-fuel-pump me-2"></i>Fuel</button>
|
||||
</li>
|
||||
@@ -35,6 +41,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>
|
||||
@@ -49,23 +58,11 @@
|
||||
<div class="tab-pane fade show active" id="servicerecord-tab-pane" role="tabpanel" tabindex="0"></div>
|
||||
<div class="tab-pane fade" id="gas-tab-pane" role="tabpanel" tabindex="0"></div>
|
||||
<div class="tab-pane fade" id="tax-tab-pane" role="tabpanel" tabindex="0"></div>
|
||||
<div class="tab-pane fade" id="notes-tab-pane" role="tabpanel" tabindex="0">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<label for="noteTextArea" class="form-label">This is where you can store notes related to the vehicle such as tire size, oil filter size, oil types, etc.</label>
|
||||
<textarea class="form-control vehicleNoteContainer" id="noteTextArea"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="d-flex flex-row-reverse">
|
||||
<div>
|
||||
<button onclick="saveVehicleNote(@Model.Id)" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square"></i>Save Note</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="notes-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="upgrade-tab-pane" role="tabpanel" tabindex="0"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" id="editVehicleModal" tabindex="-1" role="dialog">
|
||||
@@ -80,6 +77,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};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@model string
|
||||
@model ImportMode
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Import Data from CSV</h5>
|
||||
<button type="button" class="btn-close" onclick="hideBulkImportModal()" aria-label="Close"></button>
|
||||
@@ -16,13 +16,13 @@
|
||||
<div class="alert alert-danger" role="alert">
|
||||
Failure to format the data correctly can cause data corruption. Please make sure you make a copy of the local database before proceeding.
|
||||
</div>
|
||||
@if (Model == "gasrecord")
|
||||
@if (Model == ImportMode.GasRecord)
|
||||
{
|
||||
<a class="btn btn-link" href="/defaults/gassample.csv" target="_blank">Download Sample</a>
|
||||
} else if (Model == "servicerecord" || Model == "repairrecord")
|
||||
} else if (Model == ImportMode.ServiceRecord || Model == ImportMode.RepairRecord || Model == ImportMode.UpgradeRecord)
|
||||
{
|
||||
<a class="btn btn-link" href="/defaults/servicerecordsample.csv" target="_blank">Download Sample</a>
|
||||
} else if (Model == "taxrecord")
|
||||
} else if (Model == ImportMode.TaxRecord)
|
||||
{
|
||||
<a class="btn btn-link" href="/defaults/taxrecordsample.csv" target="_blank">Download Sample</a>
|
||||
}
|
||||
@@ -52,14 +52,16 @@
|
||||
if (data) {
|
||||
successToast("Data Imported Successfully");
|
||||
hideBulkImportModal();
|
||||
if (mode == "gasrecord") {
|
||||
if (mode == "GasRecord") {
|
||||
getVehicleGasRecords(vehicleId);
|
||||
} else if (mode == "servicerecord") {
|
||||
} else if (mode == "ServiceRecord") {
|
||||
getVehicleServiceRecords(vehicleId);
|
||||
} else if (mode == "repairrecord") {
|
||||
} else if (mode == "RepairRecord") {
|
||||
getVehicleCollisionRecords(vehicleId);
|
||||
} else if (mode == "taxrecord") {
|
||||
} else if (mode == "TaxRecord") {
|
||||
getVehicleTaxRecords(vehicleId);
|
||||
} else if (mode == "UpgradeRecord") {
|
||||
getVehicleUpgradeRecords(vehicleId);
|
||||
}
|
||||
} else {
|
||||
errorToast("An error has occurred, please double check the data and try again.");
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<label for="collisionRecordDescription">Description</label>
|
||||
<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" placeholder="Cost of the repair" value="@(isNew ? "" : Model.Cost)">
|
||||
<input type="text" 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>
|
||||
@@ -30,20 +30,22 @@
|
||||
@if (Model.Files.Any())
|
||||
{
|
||||
<div>
|
||||
<label>Uploaded Documents</label>
|
||||
@foreach (UploadedFiles filesUploaded in Model.Files)
|
||||
{
|
||||
<div class="d-flex justify-content-between">
|
||||
<a type="button" class="btn btn-link" href="@filesUploaded.Location" target="_blank">@filesUploaded.Name</a>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteCollisionRecordFile('@filesUploaded.Location', this)"><i class="bi bi-trash"></i></button>
|
||||
</div>
|
||||
}
|
||||
@await Html.PartialAsync("_UploadedFiles", Model.Files)
|
||||
<label for="collisionRecordFiles">Upload more documents</label>
|
||||
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="collisionRecordFiles">
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
@if (isNew)
|
||||
{
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" value="" id="addReminderCheck">
|
||||
<label class="form-check-label" for="addReminderCheck">
|
||||
Add Reminder
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
<label for="collisionRecordFiles">Upload documents(optional)</label>
|
||||
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="collisionRecordFiles">
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@inject IConfiguration Configuration
|
||||
@{
|
||||
var enableCsvImports = bool.Parse(Configuration[nameof(UserConfig.EnableCsvImports)]);
|
||||
var hideZero = bool.Parse(Configuration[nameof(UserConfig.HideZero)]);
|
||||
}
|
||||
@model List<CollisionRecord>
|
||||
<div class="row">
|
||||
@@ -18,7 +19,8 @@
|
||||
<span class="visually-hidden">Toggle Dropdown</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="#" onclick="showBulkImportModal('repairrecord')">Import via CSV</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="showBulkImportModal('RepairRecord')">Import via CSV</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="exportVehicleData('RepairRecord')">Export to CSV</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
@@ -48,8 +50,8 @@
|
||||
<td class="col-1">@collisionRecord.Date.ToShortDateString()</td>
|
||||
<td class="col-2">@collisionRecord.Mileage</td>
|
||||
<td class="col-4">@collisionRecord.Description</td>
|
||||
<td class="col-2">@collisionRecord.Cost.ToString("C")</td>
|
||||
<td class="col-3 text-truncate">@collisionRecord.Notes</td>
|
||||
<td class="col-2">@((hideZero && collisionRecord.Cost == default) ? "---" : collisionRecord.Cost.ToString("C"))</td>
|
||||
<td class="col-3 text-truncate">@CarCareTracker.Helper.StaticHelper.TruncateStrings(collisionRecord.Notes)</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
@@ -58,7 +60,7 @@
|
||||
</div>
|
||||
|
||||
|
||||
<div class="modal fade" id="collisionRecordModal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal fade" data-bs-focus="false" id="collisionRecordModal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content" id="collisionRecordModalContent">
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@model CostMakeUpForVehicle
|
||||
@if (Model.CollisionRecordSum + Model.ServiceRecordSum + Model.GasRecordSum + Model.TaxRecordSum > 0)
|
||||
@if (Model.CollisionRecordSum + Model.ServiceRecordSum + Model.GasRecordSum + Model.TaxRecordSum + Model.UpgradeRecordSum > 0)
|
||||
{
|
||||
<canvas id="pie-chart"></canvas>
|
||||
<script>
|
||||
@@ -9,14 +9,15 @@
|
||||
new Chart($("#pie-chart"), {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: ["Planned Maintenance(Service Records)", "Unplanned Maintenance(Repairs)", "Tax", "Fuel"],
|
||||
labels: ["Planned Maintenance(Service Records)", "Unplanned Maintenance(Repairs)", "Upgrades", "Tax", "Fuel"],
|
||||
datasets: [
|
||||
{
|
||||
label: "Expenses by Category",
|
||||
backgroundColor: ["#003f5c", "#58508d", "#bc5090", "#ff6361"],
|
||||
backgroundColor: ["#003f5c", "#58508d", "#bc5090", "#ff6361", "#ffa600"],
|
||||
data: [
|
||||
@Model.ServiceRecordSum,
|
||||
@Model.CollisionRecordSum,
|
||||
@Model.UpgradeRecordSum,
|
||||
@Model.TaxRecordSum,
|
||||
@Model.GasRecordSum
|
||||
]
|
||||
@@ -41,7 +42,10 @@
|
||||
});
|
||||
}
|
||||
</script>
|
||||
} else
|
||||
{
|
||||
<h1>No data found or all records have zero sums, insert records with non-zero sums to see visualizations here.</h1>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="text-center">
|
||||
<h4>No data found or all records have zero sums, insert records with non-zero sums to see visualizations here.</h4>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,21 +1,47 @@
|
||||
@inject IConfiguration Configuration
|
||||
@model GasRecordViewModelContainer
|
||||
@{
|
||||
var enableCsvImports = bool.Parse(Configuration[nameof(UserConfig.EnableCsvImports)]);
|
||||
var useMPG = bool.Parse(Configuration[nameof(UserConfig.UseMPG)]);
|
||||
var useUKMPG = bool.Parse(Configuration[nameof(UserConfig.UseUKMPG)]);
|
||||
var hideZero = bool.Parse(Configuration[nameof(UserConfig.HideZero)]);
|
||||
var useKwh = Model.UseKwh;
|
||||
string consumptionUnit;
|
||||
string fuelEconomyUnit;
|
||||
string distanceUnit = useMPG ? "mi." : "km";
|
||||
if (useKwh)
|
||||
{
|
||||
consumptionUnit = "kWh";
|
||||
fuelEconomyUnit = useMPG ? "mi./kWh" : "kWh/100km";
|
||||
}
|
||||
else if (useMPG && useUKMPG)
|
||||
{
|
||||
consumptionUnit = "imp gal";
|
||||
fuelEconomyUnit = "mpg";
|
||||
} else if (useUKMPG)
|
||||
{
|
||||
fuelEconomyUnit = "l/100mi.";
|
||||
consumptionUnit = "l";
|
||||
distanceUnit = "mi.";
|
||||
}
|
||||
else
|
||||
{
|
||||
consumptionUnit = useMPG ? "US gal" : "l";
|
||||
fuelEconomyUnit = useMPG ? "mpg" : "l/100km";
|
||||
}
|
||||
}
|
||||
@model List<GasRecordViewModel>
|
||||
<div class="row">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div class="d-flex align-items-center flex-wrap">
|
||||
<span class="ms-2 badge bg-success">@($"# of Gas Records: {Model.Count()}")</span>
|
||||
@if (Model.Count() > 1)
|
||||
<span class="ms-2 badge bg-success">@($"# of Gas Records: {Model.GasRecords.Count()}")</span>
|
||||
@if (Model.GasRecords.Where(x => x.MilesPerGallon > 0).Any())
|
||||
{
|
||||
<span class="ms-2 badge bg-primary">@($"Average Fuel Economy: {Model.Where(y => y.MilesPerGallon > 0)?.Average(x => x.MilesPerGallon).ToString("F") ?? "0"}")</span>
|
||||
<span class="ms-2 badge bg-primary">@($"Min Fuel Economy: {Model.Where(y => y.MilesPerGallon > 0)?.Min(x => x.MilesPerGallon).ToString("F") ?? "0"}")</span>
|
||||
<span class="ms-2 badge bg-primary">@($"Max Fuel Economy: {Model.Max(x => x.MilesPerGallon).ToString("F") ?? "0"}")</span>
|
||||
<span class="ms-2 badge bg-primary">@($"Average Fuel Economy: {Model.GasRecords.Where(y => y.MilesPerGallon > 0)?.Average(x => x.MilesPerGallon).ToString("F") ?? "0"}")</span>
|
||||
<span class="ms-2 badge bg-primary">@($"Min Fuel Economy: {Model.GasRecords.Where(y => y.MilesPerGallon > 0)?.Min(x => x.MilesPerGallon).ToString("F") ?? "0"}")</span>
|
||||
<span class="ms-2 badge bg-primary">@($"Max Fuel Economy: {Model.GasRecords.Max(x => x.MilesPerGallon).ToString("F") ?? "0"}")</span>
|
||||
}
|
||||
<span class="ms-2 badge bg-success">@($"Total Fuel Consumed: {Model.Sum(x=>x.Gallons).ToString("F")}")</span>
|
||||
<span class="ms-2 badge bg-success">@($"Total Cost: {Model.Sum(x => x.Cost).ToString("C")}")</span>
|
||||
<span class="ms-2 badge bg-success">@($"Total Fuel Consumed: {Model.GasRecords.Sum(x => x.Gallons).ToString("F")}")</span>
|
||||
<span class="ms-2 badge bg-success">@($"Total Cost: {Model.GasRecords.Sum(x => x.Cost).ToString("C3")}")</span>
|
||||
</div>
|
||||
@if (enableCsvImports)
|
||||
{
|
||||
@@ -25,7 +51,8 @@
|
||||
<span class="visually-hidden">Toggle Dropdown</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="#" onclick="showBulkImportModal('gasrecord')">Import via CSV</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="showBulkImportModal('GasRecord')">Import via CSV</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="exportVehicleData('GasRecord')">Export to CSV</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
} else {
|
||||
@@ -39,23 +66,23 @@
|
||||
<thead>
|
||||
<tr class="d-flex">
|
||||
<th scope="col" class="col-2">Date Refueled</th>
|
||||
<th scope="col" class="col-2">Odometer(@(useMPG ? "mi." : "km"))</th>
|
||||
<th scope="col" class="col-2">Consumption(@(useMPG ? "gal" : "l"))</th>
|
||||
<th scope="col" class="col-2">Fuel Economy(@(useMPG ? "mpg" : "l/100km"))</th>
|
||||
<th scope="col" class="col-2">Cost</th>
|
||||
<th scope="col" class="col-2">Unit Cost</th>
|
||||
<th scope="col" class="col-2">Odometer(@(distanceUnit))</th>
|
||||
<th scope="col" class="col-2">Consumption(@(consumptionUnit))</th>
|
||||
<th scope="col" class="col-4">Fuel Economy(@(fuelEconomyUnit))</th>
|
||||
<th scope="col" class="col-1">Cost</th>
|
||||
<th scope="col" class="col-1">Unit Cost</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (GasRecordViewModel gasRecord in Model)
|
||||
@foreach (GasRecordViewModel gasRecord in Model.GasRecords)
|
||||
{
|
||||
<tr class="d-flex" style="cursor:pointer;" onclick="showEditGasRecordModal(@gasRecord.Id)">
|
||||
<td class="col-2">@gasRecord.Date</td>
|
||||
<td class="col-2">@gasRecord.Mileage</td>
|
||||
<td class="col-2">@gasRecord.Gallons.ToString("F")</td>
|
||||
<td class="col-2">@gasRecord.MilesPerGallon.ToString("F")</td>
|
||||
<td class="col-2">@gasRecord.Cost.ToString("C")</td>
|
||||
<td class="col-2">@gasRecord.CostPerGallon.ToString("C")</td>
|
||||
<td class="col-4">@(gasRecord.MilesPerGallon == 0 ? "---" : gasRecord.MilesPerGallon.ToString("F"))</td>
|
||||
<td class="col-1">@((hideZero && gasRecord.Cost == default) ? "---" : gasRecord.Cost.ToString("C3"))</td>
|
||||
<td class="col-1">@((hideZero && gasRecord.CostPerGallon == default) ? "---" : gasRecord.CostPerGallon.ToString("C3"))</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
@@ -64,7 +91,7 @@
|
||||
</div>
|
||||
|
||||
|
||||
<div class="modal fade" id="gasRecordModal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal fade" data-bs-focus="false" id="gasRecordModal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content" id="gasRecordModalContent">
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@model List<GasCostForVehicleByMonth>
|
||||
@model List<CostForVehicleByMonth>
|
||||
@if (Model.Any())
|
||||
{
|
||||
<canvas id="bar-chart" class="vehicleDetailTabContainer"></canvas>
|
||||
@@ -8,7 +8,7 @@
|
||||
var barGraphLabels = [];
|
||||
var barGraphData = [];
|
||||
var useDarkMode = getGlobalConfig().useDarkMode;
|
||||
@foreach (GasCostForVehicleByMonth gasCost in Model)
|
||||
@foreach (CostForVehicleByMonth gasCost in Model)
|
||||
{
|
||||
@:barGraphLabels.push("@gasCost.MonthName");
|
||||
@:barGraphData.push(@gasCost.Cost);
|
||||
@@ -19,7 +19,7 @@
|
||||
labels: barGraphLabels,
|
||||
datasets: [
|
||||
{
|
||||
label: "Gas Expenses by Month",
|
||||
label: "Expenses by Month",
|
||||
backgroundColor: ["#00876c", "#43956e", "#67a371", "#89b177", "#a9be80", "#c8cb8b", "#e6d79b", "#e4c281", "#e3ab6b", "#e2925b", "#e07952", "#db5d4f"],
|
||||
data: barGraphData
|
||||
}
|
||||
@@ -52,5 +52,7 @@
|
||||
</script>
|
||||
} else
|
||||
{
|
||||
<h1>No data found, insert some gas data to see visualizations here.</h1>
|
||||
<div class="text-center">
|
||||
<h4>No data found, insert/select some data to see visualizations here.</h4>
|
||||
</div>
|
||||
}
|
||||
@@ -1,8 +1,31 @@
|
||||
@inject IConfiguration Configuration
|
||||
@model GasRecordInput
|
||||
@model GasRecordInputContainer
|
||||
@{
|
||||
var useMPG = bool.Parse(Configuration[nameof(UserConfig.UseMPG)]);
|
||||
var isNew = Model.Id == 0;
|
||||
var useUKMPG = bool.Parse(Configuration[nameof(UserConfig.UseUKMPG)]);
|
||||
var useKwh = Model.UseKwh;
|
||||
var isNew = Model.GasRecord.Id == 0;
|
||||
string consumptionUnit;
|
||||
string distanceUnit;
|
||||
if (useKwh)
|
||||
{
|
||||
consumptionUnit = "kWh";
|
||||
} else if (useUKMPG)
|
||||
{
|
||||
consumptionUnit = "liters";
|
||||
}
|
||||
else
|
||||
{
|
||||
consumptionUnit = useMPG ? "gallons" : "liters";
|
||||
}
|
||||
if (useUKMPG)
|
||||
{
|
||||
distanceUnit = "miles";
|
||||
}
|
||||
else
|
||||
{
|
||||
distanceUnit = useMPG ? "miles" : "kilometers";
|
||||
}
|
||||
}
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">@(isNew ? "Add New Gas Record" : "Edit Gas Record")</h5>
|
||||
@@ -16,28 +39,29 @@
|
||||
<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" 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>
|
||||
</div>
|
||||
<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)">
|
||||
<label for="gasRecordGallons">Fuel Consumption(@(useMPG ? "gallons" : "liters"))</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)">
|
||||
<label for="gasRecordMileage">Odometer Reading(@distanceUnit)</label>
|
||||
<input type="number" id="gasRecordMileage" class="form-control" placeholder="Odometer reading when refueled" value="@(isNew ? "" : Model.GasRecord.Mileage)">
|
||||
<label for="gasRecordGallons">Fuel Consumption(@(consumptionUnit))</label>
|
||||
<input type="text" id="gasRecordGallons" class="form-control" placeholder="Amount of gas refueled" value="@(isNew ? "" : Model.GasRecord.Gallons)">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="gasIsFillToFull" checked="@Model.GasRecord.IsFillToFull">
|
||||
<label class="form-check-label" for="gasIsFillToFull">Is Filled To Full</label>
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="gasIsMissed" checked="@Model.GasRecord.MissedFuelUp">
|
||||
<label class="form-check-label" for="gasIsMissed">Missed Fuel Up(Skip MPG Calculation)</label>
|
||||
</div>
|
||||
<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="text" id="gasRecordCost" class="form-control" placeholder="Cost of gas refueled" value="@(isNew ? "" : Model.GasRecord.Cost)">
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
@if (Model.Files.Any())
|
||||
@if (Model.GasRecord.Files.Any())
|
||||
{
|
||||
<div>
|
||||
<label>Uploaded Documents</label>
|
||||
@foreach (UploadedFiles filesUploaded in Model.Files)
|
||||
{
|
||||
<div class="d-flex justify-content-between">
|
||||
<a type="button" class="btn btn-link" href="@filesUploaded.Location" target="_blank">@filesUploaded.Name</a>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteGasRecordFile('@filesUploaded.Location', this)"><i class="bi bi-trash"></i></button>
|
||||
</div>
|
||||
}
|
||||
@await Html.PartialAsync("_UploadedFiles", Model.GasRecord.Files)
|
||||
<label for="gasRecordFiles">Upload more documents</label>
|
||||
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="gasRecordFiles">
|
||||
</div>
|
||||
@@ -55,7 +79,7 @@
|
||||
<div class="modal-footer">
|
||||
@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 (isNew)
|
||||
@@ -71,12 +95,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>
|
||||
45
Views/Vehicle/_NoteModal.cshtml
Normal file
@@ -0,0 +1,45 @@
|
||||
@model Note
|
||||
@{
|
||||
var isNew = Model.Id == 0;
|
||||
}
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">@(isNew ? "Add New Note" : "Edit Note")</h5>
|
||||
<button type="button" class="btn-close" onclick="hideAddNoteModal()" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<input type="text" id="workAroundInput" style="height:0px; width:0px; display:none;">
|
||||
<label for="noteDescription">Description</label>
|
||||
<input type="text" id="noteDescription" class="form-control" placeholder="Description of the note" value="@(isNew ? "" : Model.Description)">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label for="noteTextArea">Notes</label>
|
||||
<textarea class="form-control vehicleNoteContainer" id="noteTextArea">@Model.NoteText</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@if (!isNew)
|
||||
{
|
||||
<button type="button" class="btn btn-danger" onclick="deleteNote(@Model.Id)" style="margin-right:auto;">Delete</button>
|
||||
}
|
||||
<button type="button" class="btn btn-secondary" onclick="hideAddNoteModal()">Cancel</button>
|
||||
@if (isNew)
|
||||
{
|
||||
<button type="button" class="btn btn-primary" onclick="saveNoteToVehicle()">Add New Note</button>
|
||||
}
|
||||
else if (!isNew)
|
||||
{
|
||||
<button type="button" class="btn btn-primary" onclick="saveNoteToVehicle(true)">Edit Note</button>
|
||||
}
|
||||
</div>
|
||||
<script>
|
||||
function getNoteModelData(){
|
||||
return { id: @Model.Id}
|
||||
}
|
||||
</script>
|
||||
40
Views/Vehicle/_Notes.cshtml
Normal file
@@ -0,0 +1,40 @@
|
||||
@model List<Note>
|
||||
<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 Notes: {Model.Count()}")</span>
|
||||
</div>
|
||||
<div>
|
||||
<button onclick="showAddNoteModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>Add Note</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-3">Description</th>
|
||||
<th scope="col" class="col-9">Note</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (Note note in Model)
|
||||
{
|
||||
<tr class="d-flex" style="cursor:pointer;" onclick="showEditNoteModal(@note.Id)">
|
||||
<td class="col-3">@note.Description</td>
|
||||
<td class="col-9 text-truncate">@CarCareTracker.Helper.StaticHelper.TruncateStrings(note.NoteText, 100)</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="modal fade" data-bs-focus="false" id="noteModal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content" id="noteModalContent">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
50
Views/Vehicle/_ReminderMakeUpReport.cshtml
Normal file
@@ -0,0 +1,50 @@
|
||||
@model ReminderMakeUpForVehicle
|
||||
@if (Model.UrgentCount + Model.VeryUrgentCount + Model.NotUrgentCount + Model.PastDueCount > 0)
|
||||
{
|
||||
<canvas id="donut-chart"></canvas>
|
||||
<script>
|
||||
renderChart();
|
||||
function renderChart() {
|
||||
var useDarkMode = getGlobalConfig().useDarkMode;
|
||||
new Chart($("#donut-chart"), {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: ["Not Urgent", "Urgent", "Very Urgent", "Past Due"],
|
||||
datasets: [
|
||||
{
|
||||
label: "Reminders by Category",
|
||||
backgroundColor: ["#488f31", "#ffa600", "#de425b", "#cccccc"],
|
||||
data: [
|
||||
@Model.NotUrgentCount,
|
||||
@Model.UrgentCount,
|
||||
@Model.VeryUrgentCount,
|
||||
@Model.PastDueCount
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
plugins: {
|
||||
legend: {
|
||||
position: "bottom",
|
||||
labels: {
|
||||
color: useDarkMode ? "#fff" : "#000"
|
||||
}
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: "Reminders by Urgency",
|
||||
color: useDarkMode ? "#fff" : "#000"
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="text-center">
|
||||
<h4>No data found, create reminders to see visualizations here.</h4>
|
||||
</div>
|
||||
}
|
||||
68
Views/Vehicle/_ReminderRecordModal.cshtml
Normal 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>
|
||||
70
Views/Vehicle/_ReminderRecords.cshtml
Normal 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">@CarCareTracker.Helper.StaticHelper.TruncateStrings(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>
|
||||
@@ -1,36 +1,133 @@
|
||||
<div class="row">
|
||||
<div class="col-sm-6 col-12">
|
||||
@model ReportViewModel
|
||||
<div class="row">
|
||||
<div class="col-md-3 col-12 mt-2">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
|
||||
<select class="form-select" id="yearOption" onchange="yearUpdated()">
|
||||
<option value="0">All Time</option>
|
||||
@foreach (int year in Model.Years)
|
||||
{
|
||||
<option value="@year">@year</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="d-flex justify-content-center align-items-center col-12 chartContainer" id="costMakeUpReportContent">
|
||||
@await Html.PartialAsync("_CostMakeUpReport", Model.CostMakeUpForVehicle)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-12">
|
||||
<div class="col-md-6 col-12 mt-2">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
|
||||
<div class="col-12 d-flex justify-content-center reportsCheckBoxContainer">
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" onChange="updateCheck(this)" type="checkbox" id="serviceExpenseCheck" value="1" checked>
|
||||
<label class="form-check-label" for="serviceExpenseCheck">Service</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" onChange="updateCheck(this)" type="checkbox" id="repairExpenseCheck" value="2" checked>
|
||||
<label class="form-check-label" for="repairExpenseCheck">Repairs</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" onChange="updateCheck(this)" type="checkbox" id="upgradeExpenseCheck" value="3" checked>
|
||||
<label class="form-check-label" for="upgradeExpenseCheck">Upgrades</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" onChange="updateCheck(this)" type="checkbox" id="gasExpenseCheck" value="4" checked>
|
||||
<label class="form-check-label" for="gasExpenseCheck">Gas</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" onChange="updateCheck(this)" type="checkbox" id="taxExpenseCheck" value="5" checked>
|
||||
<label class="form-check-label" for="taxExpenseCheck">Tax</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="d-flex justify-content-center align-items-center col-12 chartContainer" id="gasCostByMonthReportContent">
|
||||
@await Html.PartialAsync("_GasCostByMonthReport", Model.CostForVehicleByMonth)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-12 mt-2">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<select class="form-select" onchange="updateReminderPie()" id="reminderOption">
|
||||
<option value="0">As of Today</option>
|
||||
<option value="30">+30 Days</option>
|
||||
<option value="60">+60 Days</option>
|
||||
<option value="90">+90 Days</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="d-flex justify-content-center align-items-center col-12 chartContainer" id="reminderMakeUpReportContent">
|
||||
@await Html.PartialAsync("_ReminderMakeUpReport", Model.ReminderMakeUpForVehicle)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="row">
|
||||
</div>
|
||||
<script>
|
||||
initiateChart();
|
||||
function initiateChart() {
|
||||
function getYear() {
|
||||
return $("#yearOption").val();
|
||||
}
|
||||
var debounce = null;
|
||||
function updateCheck(sender) {
|
||||
clearTimeout(debounce);
|
||||
debounce = setTimeout(function () {
|
||||
refreshBarChart();
|
||||
}, 1000);
|
||||
}
|
||||
function refreshBarChart(callBack) {
|
||||
var selectedMetrics = [];
|
||||
var vehicleId = GetVehicleId().vehicleId;
|
||||
$.get(`/Vehicle/GetCostMakeUpForVehicle?vehicleId=${vehicleId}`, function (data) {
|
||||
$("#costMakeUpReportContent").html(data);
|
||||
$.get(`/Vehicle/GetFuelCostByMonthByVehicle?vehicleId=${vehicleId}`, function (data) {
|
||||
var year = getYear();
|
||||
|
||||
if ($("#serviceExpenseCheck").is(":checked")) {
|
||||
selectedMetrics.push('ServiceRecord');
|
||||
}
|
||||
if ($("#repairExpenseCheck").is(":checked")) {
|
||||
selectedMetrics.push('RepairRecord');
|
||||
}
|
||||
if ($("#upgradeExpenseCheck").is(":checked")) {
|
||||
selectedMetrics.push('UpgradeRecord');
|
||||
}
|
||||
if ($("#gasExpenseCheck").is(":checked")) {
|
||||
selectedMetrics.push('GasRecord');
|
||||
}
|
||||
if ($("#taxExpenseCheck").is(":checked")) {
|
||||
selectedMetrics.push('TaxRecord');
|
||||
}
|
||||
|
||||
$.post('/Vehicle/GetCostByMonthByVehicle',
|
||||
{
|
||||
vehicleId: vehicleId,
|
||||
selectedMetrics: selectedMetrics,
|
||||
year: year
|
||||
}, function (data) {
|
||||
$("#gasCostByMonthReportContent").html(data);
|
||||
})
|
||||
if (callBack != undefined) {
|
||||
callBack();
|
||||
}
|
||||
});
|
||||
}
|
||||
function updateReminderPie() {
|
||||
var vehicleId = GetVehicleId().vehicleId;
|
||||
var daysToAdd = $("#reminderOption").val();
|
||||
$.get(`/Vehicle/GetReminderMakeUpByVehicle?vehicleId=${vehicleId}`, { daysToAdd: daysToAdd }, function (data) {
|
||||
$("#reminderMakeUpReportContent").html(data);
|
||||
});
|
||||
}
|
||||
//called when year selected is changed.
|
||||
function yearUpdated() {
|
||||
var vehicleId = GetVehicleId().vehicleId;
|
||||
var year = getYear();
|
||||
$.get(`/Vehicle/GetCostMakeUpForVehicle?vehicleId=${vehicleId}`, { year: year }, function (data) {
|
||||
$("#costMakeUpReportContent").html(data);
|
||||
refreshBarChart();
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -22,7 +22,7 @@
|
||||
<label for="serviceRecordDescription">Description</label>
|
||||
<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" placeholder="Cost of the service" value="@(isNew ? "" : Model.Cost)">
|
||||
<input type="text" 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>
|
||||
@@ -30,20 +30,22 @@
|
||||
@if (Model.Files.Any())
|
||||
{
|
||||
<div>
|
||||
<label>Uploaded Documents</label>
|
||||
@foreach (UploadedFiles filesUploaded in Model.Files)
|
||||
{
|
||||
<div class="d-flex justify-content-between">
|
||||
<a type="button" class="btn btn-link" href="@filesUploaded.Location" target="_blank">@filesUploaded.Name</a>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteServiceRecordFile('@filesUploaded.Location', this)"><i class="bi bi-trash"></i></button>
|
||||
</div>
|
||||
}
|
||||
@await Html.PartialAsync("_UploadedFiles", Model.Files)
|
||||
<label for="serviceRecordFiles">Upload more documents</label>
|
||||
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="serviceRecordFiles">
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
@if (isNew)
|
||||
{
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" value="" id="addReminderCheck">
|
||||
<label class="form-check-label" for="addReminderCheck">
|
||||
Add Reminder
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
<label for="serviceRecordFiles">Upload documents(optional)</label>
|
||||
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="serviceRecordFiles">
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@inject IConfiguration Configuration
|
||||
@{
|
||||
var enableCsvImports = bool.Parse(Configuration[nameof(UserConfig.EnableCsvImports)]);
|
||||
var hideZero = bool.Parse(Configuration[nameof(UserConfig.HideZero)]);
|
||||
}
|
||||
@model List<ServiceRecord>
|
||||
<div class="row">
|
||||
@@ -18,7 +19,8 @@
|
||||
<span class="visually-hidden">Toggle Dropdown</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="#" onclick="showBulkImportModal('servicerecord')">Import via CSV</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="showBulkImportModal('ServiceRecord')">Import via CSV</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="exportVehicleData('ServiceRecord')">Export to CSV</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
@@ -48,8 +50,8 @@
|
||||
<td class="col-1">@serviceRecord.Date.ToShortDateString()</td>
|
||||
<td class="col-2">@serviceRecord.Mileage</td>
|
||||
<td class="col-4">@serviceRecord.Description</td>
|
||||
<td class="col-2">@serviceRecord.Cost.ToString("C")</td>
|
||||
<td class="col-3 text-truncate">@serviceRecord.Notes</td>
|
||||
<td class="col-2">@((hideZero && serviceRecord.Cost == default) ? "---" : serviceRecord.Cost.ToString("C"))</td>
|
||||
<td class="col-3 text-truncate">@CarCareTracker.Helper.StaticHelper.TruncateStrings(serviceRecord.Notes)</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
@@ -58,7 +60,7 @@
|
||||
</div>
|
||||
|
||||
|
||||
<div class="modal fade" id="serviceRecordModal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal fade" data-bs-focus="false" id="serviceRecordModal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content" id="serviceRecordModalContent">
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<label for="taxRecordDescription">Description</label>
|
||||
<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" placeholder="Cost of tax paid" value="@(isNew? "" : Model.Cost)">
|
||||
<input type="text" 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>
|
||||
@@ -28,20 +28,22 @@
|
||||
@if (Model.Files.Any())
|
||||
{
|
||||
<div>
|
||||
<label>Uploaded Documents</label>
|
||||
@foreach (UploadedFiles filesUploaded in Model.Files)
|
||||
{
|
||||
<div class="d-flex justify-content-between">
|
||||
<a type="button" class="btn btn-link" href="@filesUploaded.Location" target="_blank">@filesUploaded.Name</a>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteTaxRecordFile('@filesUploaded.Location', this)"><i class="bi bi-trash"></i></button>
|
||||
</div>
|
||||
}
|
||||
@await Html.PartialAsync("_UploadedFiles", Model.Files)
|
||||
<label for="taxRecordFiles">Upload more documents</label>
|
||||
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="taxRecordFiles">
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
@if (isNew)
|
||||
{
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" value="" id="addReminderCheck">
|
||||
<label class="form-check-label" for="addReminderCheck">
|
||||
Add Reminder
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
<label for="taxRecordFiles">Upload documents(optional)</label>
|
||||
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="taxRecordFiles">
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@inject IConfiguration Configuration
|
||||
@{
|
||||
var enableCsvImports = bool.Parse(Configuration[nameof(UserConfig.EnableCsvImports)]);
|
||||
var hideZero = bool.Parse(Configuration[nameof(UserConfig.HideZero)]);
|
||||
}
|
||||
@model List<TaxRecord>
|
||||
<div class="row">
|
||||
@@ -18,7 +19,8 @@
|
||||
<span class="visually-hidden">Toggle Dropdown</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="#" onclick="showBulkImportModal('taxrecord')">Import via CSV</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="showBulkImportModal('TaxRecord')">Import via CSV</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="exportVehicleData('TaxRecord')">Export to CSV</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
@@ -46,8 +48,8 @@
|
||||
<tr class="d-flex" style="cursor:pointer;" onclick="showEditTaxRecordModal(@taxRecord.Id)">
|
||||
<td class="col-1">@taxRecord.Date.ToShortDateString()</td>
|
||||
<td class="col-6">@taxRecord.Description</td>
|
||||
<td class="col-2">@taxRecord.Cost.ToString("C")</td>
|
||||
<td class="col-3 text-truncate">@taxRecord.Notes</td>
|
||||
<td class="col-2">@((hideZero && taxRecord.Cost == default) ? "---" : taxRecord.Cost.ToString("C"))</td>
|
||||
<td class="col-3 text-truncate">@CarCareTracker.Helper.StaticHelper.TruncateStrings(taxRecord.Notes)</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
@@ -56,7 +58,7 @@
|
||||
</div>
|
||||
|
||||
|
||||
<div class="modal fade" id="taxRecordModal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal fade" data-bs-focus="false" id="taxRecordModal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content" id="taxRecordModalContent">
|
||||
</div>
|
||||
|
||||
84
Views/Vehicle/_UpgradeRecordModal.cshtml
Normal file
@@ -0,0 +1,84 @@
|
||||
@model UpgradeRecordInput
|
||||
@{
|
||||
var isNew = Model.Id == 0;
|
||||
}
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">@(isNew ? "Add New Upgrade Record" : "Edit Upgrade Record")</h5>
|
||||
<button type="button" class="btn-close" onclick="hideAddUpgradeRecordModal()" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-12">
|
||||
<input type="text" id="workAroundInput" style="height:0px; width:0px; display:none;">
|
||||
<label for="upgradeRecordDate">Date</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="upgradeRecordDate" class="form-control" placeholder="Date upgrade/mods was installed" value="@Model.Date">
|
||||
<span class="input-group-text"><i class="bi bi-calendar-event"></i></span>
|
||||
</div>
|
||||
<label for="upgradeRecordMileage">Odometer</label>
|
||||
<input type="number" id="upgradeRecordMileage" class="form-control" placeholder="Odometer reading when upgraded/modded" value="@(isNew ? "" : Model.Mileage)">
|
||||
<label for="upgradeRecordDescription">Description</label>
|
||||
<input type="text" id="upgradeRecordDescription" class="form-control" placeholder="Description of item(s) upgraded/modded" value="@Model.Description">
|
||||
<label for="upgradeRecordCost">Cost</label>
|
||||
<input type="text" id="upgradeRecordCost" class="form-control" placeholder="Cost of the upgrade/mods" value="@(isNew ? "" : Model.Cost)">
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<label for="upgradeRecordNotes">Notes(optional)</label>
|
||||
<textarea id="upgradeRecordNotes" class="form-control" rows="5">@Model.Notes</textarea>
|
||||
@if (Model.Files.Any())
|
||||
{
|
||||
<div>
|
||||
@await Html.PartialAsync("_UploadedFiles", Model.Files)
|
||||
<label for="upgradeRecordFiles">Upload more documents</label>
|
||||
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="upgradeRecordFiles">
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
@if (isNew)
|
||||
{
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" value="" id="addReminderCheck">
|
||||
<label class="form-check-label" for="addReminderCheck">
|
||||
Add Reminder
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
<label for="upgradeRecordFiles">Upload documents(optional)</label>
|
||||
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept=".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx" class="form-control-file" id="upgradeRecordFiles">
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@if (!isNew)
|
||||
{
|
||||
<button type="button" class="btn btn-danger" onclick="deleteUpgradeRecord(@Model.Id)" style="margin-right:auto;">Delete</button>
|
||||
}
|
||||
<button type="button" class="btn btn-secondary" onclick="hideAddUpgradeRecordModal()">Cancel</button>
|
||||
@if (isNew)
|
||||
{
|
||||
<button type="button" class="btn btn-primary" onclick="saveUpgradeRecordToVehicle()">Add New Upgrade Record</button>
|
||||
}
|
||||
else if (!isNew)
|
||||
{
|
||||
<button type="button" class="btn btn-primary" onclick="saveUpgradeRecordToVehicle(true)">Edit Upgrade Record</button>
|
||||
}
|
||||
</div>
|
||||
<script>
|
||||
var uploadedFiles = [];
|
||||
getUploadedFilesFromModel();
|
||||
function getUploadedFilesFromModel() {
|
||||
@foreach (UploadedFiles filesUploaded in Model.Files)
|
||||
{
|
||||
@:uploadedFiles.push({ name: "@filesUploaded.Name", location: "@filesUploaded.Location" });
|
||||
}
|
||||
}
|
||||
function getUpgradeRecordModelData() {
|
||||
return { id: @Model.Id}
|
||||
}
|
||||
</script>
|
||||
67
Views/Vehicle/_UpgradeRecords.cshtml
Normal file
@@ -0,0 +1,67 @@
|
||||
@inject IConfiguration Configuration
|
||||
@{
|
||||
var enableCsvImports = bool.Parse(Configuration[nameof(UserConfig.EnableCsvImports)]);
|
||||
var hideZero = bool.Parse(Configuration[nameof(UserConfig.HideZero)]);
|
||||
}
|
||||
@model List<UpgradeRecord>
|
||||
<div class="row">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div class="d-flex align-items-center flex-wrap">
|
||||
<span class="ms-2 badge bg-success">@($"# of Upgrade Records: {Model.Count()}")</span>
|
||||
<span class="ms-2 badge bg-primary">@($"Total: {Model.Sum(x => x.Cost).ToString("C")}")</span>
|
||||
</div>
|
||||
<div>
|
||||
@if (enableCsvImports)
|
||||
{
|
||||
<div class="btn-group">
|
||||
<button onclick="showAddUpgradeRecordModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>Add Upgrade Record</button>
|
||||
<button type="button" class="btn btn-md btn-primary btn-md mt-1 mb-1 dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<span class="visually-hidden">Toggle Dropdown</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="#" onclick="showBulkImportModal('UpgradeRecord')">Import via CSV</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="exportVehicleData('UpgradeRecord')">Export to CSV</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button onclick="showAddUpgradeRecordModal()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>Add Upgrade Record</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row vehicleDetailTabContainer">
|
||||
<div class="col-12">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr class="d-flex">
|
||||
<th scope="col" class="col-1">Date</th>
|
||||
<th scope="col" class="col-2">Odometer</th>
|
||||
<th scope="col" class="col-4">Description</th>
|
||||
<th scope="col" class="col-2">Cost</th>
|
||||
<th scope="col" class="col-3">Notes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (UpgradeRecord upgradeRecord in Model)
|
||||
{
|
||||
<tr class="d-flex" style="cursor:pointer;" onclick="showEditUpgradeRecordModal(@upgradeRecord.Id)">
|
||||
<td class="col-1">@upgradeRecord.Date.ToShortDateString()</td>
|
||||
<td class="col-2">@upgradeRecord.Mileage</td>
|
||||
<td class="col-4">@upgradeRecord.Description</td>
|
||||
<td class="col-2">@((hideZero && upgradeRecord.Cost == default) ? "---" : upgradeRecord.Cost.ToString("C"))</td>
|
||||
<td class="col-3 text-truncate">@CarCareTracker.Helper.StaticHelper.TruncateStrings(upgradeRecord.Notes)</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" data-bs-focus="false" id="upgradeRecordModal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content" id="upgradeRecordModalContent">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
16
Views/Vehicle/_UploadedFiles.cshtml
Normal file
@@ -0,0 +1,16 @@
|
||||
@model List<UploadedFiles>
|
||||
<label>Uploaded Documents</label>
|
||||
<ul class="list-group">
|
||||
@foreach (UploadedFiles filesUploaded in Model)
|
||||
{
|
||||
<li class="list-group-item">
|
||||
<div class="d-flex justify-content-between">
|
||||
<a type="button" class="btn btn-link" href="@filesUploaded.Location" target="_blank">@filesUploaded.Name</a>
|
||||
<div class="d-flex align-items-center">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary me-2" onclick="editFileName('@filesUploaded.Location', this)"><i class="bi bi-pencil"></i></button>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteFileFromUploadedFiles('@filesUploaded.Location', this)"><i class="bi bi-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
@@ -20,6 +20,10 @@
|
||||
<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" 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>
|
||||
|
||||
@@ -5,19 +5,14 @@
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"Kestrel": {
|
||||
"Endpoints": {
|
||||
"http": {
|
||||
"Url": "http://localhost:5000"
|
||||
}
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"UseDarkMode": false,
|
||||
"EnableCsvImports": true,
|
||||
"UseMPG": true,
|
||||
"UseDescending": false,
|
||||
"EnableAuth": false,
|
||||
"HideZero": false,
|
||||
"UseUKMPG": false,
|
||||
"UserNameHash": "",
|
||||
"UserPasswordHash": ""
|
||||
}
|
||||
|
||||
50
docker-compose.traefik.yml
Normal 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
@@ -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:
|
||||
BIN
docs/fuelmileage.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
docs/garage.png
Normal file
|
After Width: | Height: | Size: 109 KiB |
167
docs/index.html
Normal file
@@ -0,0 +1,167 @@
|
||||
<html lang="en" data-bs-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>LubeLogger</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
|
||||
<style>
|
||||
.customCarouselCaption{
|
||||
background-image: linear-gradient(to bottom, rgba(255,0,0,0), #000);
|
||||
padding-top: 3rem;
|
||||
color: #fff !important
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container" style="height:85vh;">
|
||||
<main role="main">
|
||||
<div class="row mt-2">
|
||||
<div class="d-flex justify-content-center">
|
||||
<img src="lubelogger_logo.png"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 d-flex justify-content-center">
|
||||
<h6 class="display-6 text-center">Self-Hosted, Open-Source, Unconventionally-Named Vehicle Maintenance Records and Fuel Mileage Tracker</h6>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="row">
|
||||
<div class="col-12 d-flex justify-content-center">
|
||||
<h6 class="display-6 text-center">Showcase</h6>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div id="carouselGallery" class="carousel slide">
|
||||
<div class="carousel-indicators">
|
||||
<button type="button" data-bs-target="#carouselGallery" data-bs-slide-to="0" class="active" aria-current="true"></button>
|
||||
<button type="button" data-bs-target="#carouselGallery" data-bs-slide-to="1"></button>
|
||||
<button type="button" data-bs-target="#carouselGallery" data-bs-slide-to="2"></button>
|
||||
<button type="button" data-bs-target="#carouselGallery" data-bs-slide-to="3"></button>
|
||||
</div>
|
||||
<div class="carousel-inner">
|
||||
<div class="carousel-item active">
|
||||
<img src="garage.png" class="d-block w-100" alt="Gallery View">
|
||||
<div class="carousel-caption d-none d-md-block customCarouselCaption">
|
||||
<h5>Garage</h5>
|
||||
<p>All of your vehicles conveniently displayed in one place</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="carousel-item">
|
||||
<img src="servicerecord.png" class="d-block w-100" alt="...">
|
||||
<div class="carousel-caption d-none d-md-block customCarouselCaption">
|
||||
<h5>Service, Repair, and Upgrade Records</h5>
|
||||
<p>Track all of the work done on your vehicles</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="carousel-item">
|
||||
<img src="fuelmileage.png" class="d-block w-100" alt="...">
|
||||
<div class="carousel-caption d-none d-md-block customCarouselCaption">
|
||||
<h5>Fuel Mileage</h5>
|
||||
<p>Keeps track of the fuel economy of your vehicles</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="carousel-item">
|
||||
<img src="reminder.png" class="d-block w-100" alt="...">
|
||||
<div class="carousel-caption d-none d-md-block customCarouselCaption">
|
||||
<h5>Reminders</h5>
|
||||
<p>Set up reminders so you never miss another scheduled maintenance</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="carousel-control-prev" type="button" data-bs-target="#carouselGallery" data-bs-slide="prev">
|
||||
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
|
||||
<span class="visually-hidden">Previous</span>
|
||||
</button>
|
||||
<button class="carousel-control-next" type="button" data-bs-target="#carouselGallery" data-bs-slide="next">
|
||||
<span class="carousel-control-next-icon" aria-hidden="true"></span>
|
||||
<span class="visually-hidden">Next</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="row">
|
||||
<div class="col-12 d-flex justify-content-center">
|
||||
<h6 class="display-6 text-center">Features</h6>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6">
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item">Keeps track of all your maintenance, repair, and upgrade records</li>
|
||||
<li class="list-group-item">Keeps track of your fuel economy(supports MPG, UK MPG, and L/100KM)</li>
|
||||
<li class="list-group-item">Keeps track of taxes(registration, going fast tax, etc)</li>
|
||||
<li class="list-group-item">No limit on how many vehicles you have in your garage</li>
|
||||
<li class="list-group-item">Import existing records from CSV(supports imports from Fuelly for Fuel Records)</li>
|
||||
<li class="list-group-item">Attach documents for each record(receipts, invoices, etc)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item">Set reminders so you never miss another scheduled maintenance</li>
|
||||
<li class="list-group-item">Dark Mode</li>
|
||||
<li class="list-group-item">Mobile/Small screen support</li>
|
||||
<li class="list-group-item">Basic Authentication for security</li>
|
||||
<li class="list-group-item">Coming Soon(API Endpoints)</li>
|
||||
<li class="list-group-item">Coming Soon(Consolidated Report Export - Just like CarFax)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="row">
|
||||
<div class="col-12 d-flex justify-content-center">
|
||||
<h6 class="display-6 text-center">Where to Download</h6>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 d-flex justify-content-center">
|
||||
<p class="lead">
|
||||
LubeLogger is released exclusively as a Docker Image via the GitHub Container Repository(GHCR)
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-12 d-flex justify-content-center">
|
||||
<p class="lead">
|
||||
To pull down the Docker Image, run
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-12 d-flex justify-content-center">
|
||||
<p><code>docker pull ghcr.io/hargata/lubelogger:latest</code></p>
|
||||
</div>
|
||||
<div class="col-12 d-flex justify-content-center">
|
||||
<p class="lead">
|
||||
Check the <a href="https://github.com/hargata/lubelog/" target="_blank">GitHub repository</a> and clone the "docker-compose.yml" and ".env" file onto your computer.
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-12 d-flex justify-content-center">
|
||||
<p class="lead">
|
||||
Navigate to the directory where those two files are located, then run
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-12 d-flex justify-content-center">
|
||||
<p><code>docker-compose up</code></p>
|
||||
</div>
|
||||
<div class="col-12 d-flex justify-content-center">
|
||||
<p class="lead">
|
||||
Navigate to the URL the service is listening on, the default is
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-12 d-flex justify-content-center">
|
||||
<p><code>http://localhost:8080</code></p>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="row">
|
||||
<div class="col-12 d-flex justify-content-center">
|
||||
<p class="lead">LubeLogger is proudly developed in Price Utah by Hargata Softworks
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-12 d-flex justify-content-center">
|
||||
<p class="lead">For more information regarding Price Utah, click <a href="https://www.visitutah.com/Places-To-Go/Cities-and-Towns/Price" target="_blank">here</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
docs/lubelogger_logo.png
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
docs/reminder.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
docs/servicerecord.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
@@ -36,8 +36,36 @@ html {
|
||||
overflow-x:auto;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.vehicleDetailTabContainer {
|
||||
background-color: #fff;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
color: #000 !important;
|
||||
overflow: visible;
|
||||
}
|
||||
td {
|
||||
color: #000 !important;
|
||||
}
|
||||
td.col-1{
|
||||
width:10%;
|
||||
}
|
||||
th.col-1 {
|
||||
width: 10%;
|
||||
}
|
||||
th {
|
||||
color: #000 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.chartContainer{
|
||||
height:65vh;
|
||||
height:30vh;
|
||||
overflow:auto;
|
||||
}
|
||||
|
||||
@@ -56,3 +84,52 @@ html {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.reportsCheckBoxContainer {
|
||||
padding: 0.375rem 2.25rem 0.375rem 0.75rem;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
BIN
wwwroot/defaults/garage.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
wwwroot/defaults/garage_narrow.png
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
wwwroot/defaults/lubelogger_icon_128.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
wwwroot/defaults/lubelogger_icon_144.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
wwwroot/defaults/lubelogger_icon_192.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
wwwroot/defaults/lubelogger_icon_72.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
wwwroot/defaults/lubelogger_launch.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 4.2 KiB |
@@ -3,9 +3,7 @@
|
||||
if (data) {
|
||||
$("#collisionRecordModalContent").html(data);
|
||||
//initiate datepicker
|
||||
$('#collisionRecordDate').datepicker({
|
||||
endDate: "+0d"
|
||||
});
|
||||
initDatePicker($('#collisionRecordDate'));
|
||||
$('#collisionRecordModal').modal('show');
|
||||
}
|
||||
});
|
||||
@@ -15,9 +13,7 @@ function showEditCollisionRecordModal(collisionRecordId) {
|
||||
if (data) {
|
||||
$("#collisionRecordModalContent").html(data);
|
||||
//initiate datepicker
|
||||
$('#collisionRecordDate').datepicker({
|
||||
endDate: "+0d"
|
||||
});
|
||||
initDatePicker($('#collisionRecordDate'));
|
||||
$('#collisionRecordModal').modal('show');
|
||||
}
|
||||
});
|
||||
@@ -64,6 +60,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 +76,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.
|
||||
@@ -97,7 +97,7 @@ function getAndValidateCollisionRecordValues() {
|
||||
} else {
|
||||
$("#collisionRecordDescription").removeClass("is-invalid");
|
||||
}
|
||||
if (collisionCost.trim() == '') {
|
||||
if (collisionCost.trim() == '' || !isValidMoney(collisionCost)) {
|
||||
hasError = true;
|
||||
$("#collisionRecordCost").addClass("is-invalid");
|
||||
} else {
|
||||
@@ -112,10 +112,7 @@ function getAndValidateCollisionRecordValues() {
|
||||
description: collisionDescription,
|
||||
cost: collisionCost,
|
||||
notes: collisionNotes,
|
||||
files: uploadedFiles
|
||||
files: uploadedFiles,
|
||||
addReminderRecord: addReminderRecord
|
||||
}
|
||||
}
|
||||
function deleteCollisionRecordFile(fileLocation, event) {
|
||||
event.parentElement.remove();
|
||||
uploadedFiles = uploadedFiles.filter(x => x.location != fileLocation);
|
||||
}
|
||||
@@ -3,9 +3,7 @@
|
||||
if (data) {
|
||||
$("#gasRecordModalContent").html(data);
|
||||
//initiate datepicker
|
||||
$('#gasRecordDate').datepicker({
|
||||
endDate: "+0d"
|
||||
});
|
||||
initDatePicker($('#gasRecordDate'));
|
||||
$('#gasRecordModal').modal('show');
|
||||
}
|
||||
});
|
||||
@@ -15,9 +13,7 @@ function showEditGasRecordModal(gasRecordId) {
|
||||
if (data) {
|
||||
$("#gasRecordModalContent").html(data);
|
||||
//initiate datepicker
|
||||
$('#gasRecordDate').datepicker({
|
||||
endDate: "+0d"
|
||||
});
|
||||
initDatePicker($('#gasRecordDate'));
|
||||
$('#gasRecordModal').modal('show');
|
||||
}
|
||||
});
|
||||
@@ -74,6 +70,8 @@ function getAndValidateGasRecordValues() {
|
||||
var gasMileage = $("#gasRecordMileage").val();
|
||||
var gasGallons = $("#gasRecordGallons").val();
|
||||
var gasCost = $("#gasRecordCost").val();
|
||||
var gasIsFillToFull = $("#gasIsFillToFull").is(":checked");
|
||||
var gasIsMissed = $("#gasIsMissed").is(":checked");
|
||||
var vehicleId = GetVehicleId().vehicleId;
|
||||
var gasRecordId = getGasRecordModelData().id;
|
||||
//validation
|
||||
@@ -96,7 +94,7 @@ function getAndValidateGasRecordValues() {
|
||||
} else {
|
||||
$("#gasRecordGallons").removeClass("is-invalid");
|
||||
}
|
||||
if (gasCost.trim() == '') {
|
||||
if (gasCost.trim() == '' || !isValidMoney(gasCost)) {
|
||||
hasError = true;
|
||||
$("#gasRecordCost").addClass("is-invalid");
|
||||
} else {
|
||||
@@ -110,10 +108,8 @@ function getAndValidateGasRecordValues() {
|
||||
mileage: gasMileage,
|
||||
gallons: gasGallons,
|
||||
cost: gasCost,
|
||||
files: uploadedFiles
|
||||
files: uploadedFiles,
|
||||
isFillToFull: gasIsFillToFull,
|
||||
missedFuelUp: gasIsMissed
|
||||
}
|
||||
}
|
||||
function deleteGasRecordFile(fileLocation, event) {
|
||||
event.parentElement.remove();
|
||||
uploadedFiles = uploadedFiles.filter(x => x.location != fileLocation);
|
||||
}
|
||||
90
wwwroot/js/note.js
Normal file
@@ -0,0 +1,90 @@
|
||||
function showAddNoteModal() {
|
||||
$.get('/Vehicle/GetAddNotePartialView', function (data) {
|
||||
if (data) {
|
||||
$("#noteModalContent").html(data);
|
||||
$('#noteModal').modal('show');
|
||||
}
|
||||
});
|
||||
}
|
||||
function showEditNoteModal(noteId) {
|
||||
$.get(`/Vehicle/GetNoteForEditById?noteId=${noteId}`, function (data) {
|
||||
if (data) {
|
||||
$("#noteModalContent").html(data);
|
||||
$('#noteModal').modal('show');
|
||||
}
|
||||
});
|
||||
}
|
||||
function hideAddNoteModal() {
|
||||
$('#noteModal').modal('hide');
|
||||
}
|
||||
function deleteNote(noteId) {
|
||||
$("#workAroundInput").show();
|
||||
Swal.fire({
|
||||
title: "Confirm Deletion?",
|
||||
text: "Deleted Notes cannot be restored.",
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "Delete",
|
||||
confirmButtonColor: "#dc3545"
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
$.post(`/Vehicle/DeleteNoteById?noteId=${noteId}`, function (data) {
|
||||
if (data) {
|
||||
hideAddNoteModal();
|
||||
successToast("Note Deleted");
|
||||
var vehicleId = GetVehicleId().vehicleId;
|
||||
getVehicleNotes(vehicleId);
|
||||
} else {
|
||||
errorToast("An error has occurred, please try again later.");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
$("#workAroundInput").hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
function saveNoteToVehicle(isEdit) {
|
||||
//get values
|
||||
var formValues = getAndValidateNoteValues();
|
||||
//validate
|
||||
if (formValues.hasError) {
|
||||
errorToast("Please check the form data");
|
||||
return;
|
||||
}
|
||||
//save to db.
|
||||
$.post('/Vehicle/SaveNoteToVehicleId', { note: formValues }, function (data) {
|
||||
if (data) {
|
||||
successToast(isEdit ? "Note Updated" : "Note Added.");
|
||||
hideAddNoteModal();
|
||||
getVehicleNotes(formValues.vehicleId);
|
||||
} else {
|
||||
errorToast("An error has occurred, please try again later.");
|
||||
}
|
||||
})
|
||||
}
|
||||
function getAndValidateNoteValues() {
|
||||
var noteDescription = $("#noteDescription").val();
|
||||
var noteText = $("#noteTextArea").val();
|
||||
var vehicleId = GetVehicleId().vehicleId;
|
||||
var noteId = getNoteModelData().id;
|
||||
//validation
|
||||
var hasError = false;
|
||||
if (noteDescription.trim() == '') { //eliminates whitespace.
|
||||
hasError = true;
|
||||
$("#noteDescription").addClass("is-invalid");
|
||||
} else {
|
||||
$("#noteDescription").removeClass("is-invalid");
|
||||
}
|
||||
if (noteText.trim() == '') {
|
||||
hasError = true;
|
||||
$("#noteTextArea").addClass("is-invalid");
|
||||
} else {
|
||||
$("#noteTextArea").removeClass("is-invalid");
|
||||
}
|
||||
return {
|
||||
id: noteId,
|
||||
hasError: hasError,
|
||||
vehicleId: vehicleId,
|
||||
description: noteDescription,
|
||||
noteText: noteText
|
||||
}
|
||||
}
|
||||
122
wwwroot/js/reminderrecord.js
Normal file
@@ -0,0 +1,122 @@
|
||||
function showEditReminderRecordModal(reminderId) {
|
||||
$.get(`/Vehicle/GetReminderRecordForEditById?reminderRecordId=${reminderId}`, function (data) {
|
||||
if (data) {
|
||||
$("#reminderRecordModalContent").html(data);
|
||||
initDatePicker($('#reminderDate'), true);
|
||||
$("#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
|
||||
}
|
||||
}
|
||||