Compare commits

..

74 Commits

Author SHA1 Message Date
DESKTOP-T0O5CDB\DESK-555BD
20190642a8 fixed login viewport height. 2024-01-11 14:34:43 -07:00
Hargata Softworks
5d2068bd78 Merge pull request #64 from hargata/Hargata/pwa
Add PWA Manifests.
2024-01-11 14:20:49 -07:00
DESKTOP-T0O5CDB\DESK-555BD
3a01944e30 resized screenshot 2024-01-11 14:14:05 -07:00
DESKTOP-T0O5CDB\DESK-555BD
014b6bce8d added pwa for android and iphone. 2024-01-11 13:53:02 -07:00
Hargata Softworks
8f69399f1b Merge pull request #63 from hargata/Hargata/reports.improvement
Hargata/reports.improvement
2024-01-11 11:38:19 -07:00
DESKTOP-T0O5CDB\DESK-555BD
200aa79dac Merge branch 'Hargata/consolidated.report' into Hargata/reports.improvement 2024-01-11 11:37:00 -07:00
DESKTOP-T0O5CDB\DESK-555BD
dfb6f69d69 removed "All" checkbox on reports page. 2024-01-11 11:26:47 -07:00
DESKTOP-T0O5CDB\DESK-555BD
cd2fb2dd21 quick fix. 2024-01-11 09:27:31 -07:00
DESKTOP-T0O5CDB\DESK-555BD
a396eb4f72 Updated colors again. 2024-01-11 09:22:57 -07:00
Hargata Softworks
46675c944f Merge pull request #57 from hargata/Hargata/consolidated.report
Updated Chart Colors.
2024-01-10 20:17:00 -07:00
Hargata Softworks
ff7ad04f0b Merge pull request #56 from hargata/Hargata/missed.fuelupimport
add missed_fuelup column to csv imports.
2024-01-10 20:15:43 -07:00
DESKTOP-GENO133\IvanPlex
d69ede1447 add missed_fuelup column to csv imports. 2024-01-10 20:14:59 -07:00
DESKTOP-T0O5CDB\DESK-555BD
b477cf9d51 Updated Chart Colors. 2024-01-10 14:54:46 -07:00
Hargata Softworks
ad8c27a2e6 Merge pull request #55 from hargata/Hargata/consolidated.report
fixed for real.
2024-01-10 14:43:31 -07:00
DESKTOP-T0O5CDB\DESK-555BD
52f9ae7ea1 fixed for real. 2024-01-10 14:42:58 -07:00
Hargata Softworks
ed1066662e Merge pull request #54 from hargata/Hargata/consolidated.report
Hargata/consolidated.report
2024-01-10 14:30:14 -07:00
DESKTOP-T0O5CDB\DESK-555BD
e3bf6f03d7 Make charts responsive to params. 2024-01-10 14:04:37 -07:00
DESKTOP-T0O5CDB\DESK-555BD
8cc2ceecc6 Merge branch 'main' into Hargata/consolidated.report 2024-01-10 09:23:21 -07:00
Hargata Softworks
9837172774 Merge pull request #52 from hargata/Hargata/missed.fuelup
added missed fuel up feature.
2024-01-10 05:37:44 -07:00
DESKTOP-GENO133\IvanPlex
fba2a6dc68 added missed fuel up feature. 2024-01-10 05:36:42 -07:00
Hargata Softworks
a467ba08c6 Merge pull request #51 from hargata/Hargata.international.numbers
added support for international number formats.
2024-01-10 05:11:48 -07:00
DESKTOP-GENO133\IvanPlex
7bfbb49efb added support for international number formats. 2024-01-10 05:10:30 -07:00
Hargata Softworks
7e4f5807d2 Merge pull request #49 from hargata/Hargata/named.notes
added truncating function.
2024-01-09 22:16:35 -07:00
DESKTOP-GENO133\IvanPlex
d5aee08f69 added truncating function. 2024-01-09 22:15:53 -07:00
Hargata Softworks
080d50c6bc Merge pull request #48 from hargata/Hargata/named.notes
Hargata/named.notes
2024-01-09 21:49:04 -07:00
DESKTOP-GENO133\IvanPlex
3105622d63 added csv exports for gas and tax tab. 2024-01-09 21:41:58 -07:00
DESKTOP-GENO133\IvanPlex
eac181ff20 removed obsolete function. 2024-01-09 21:13:33 -07:00
DESKTOP-GENO133\IvanPlex
2a732cb343 made notes named and easier to traverse. 2024-01-09 21:10:48 -07:00
Hargata Softworks
da5e97143e Merge pull request #44 from hargata/Hargata/api
Add GET API Endpoints.
2024-01-09 19:00:12 -07:00
DESKTOP-GENO133\IvanPlex
9bdce69a2a added api documentation. 2024-01-09 18:57:55 -07:00
DESKTOP-T0O5CDB\DESK-555BD
8b1db34860 added routes for API methods. 2024-01-09 18:26:54 -07:00
DESKTOP-T0O5CDB\DESK-555BD
3c67d37e12 Added get methods to API. 2024-01-09 18:21:31 -07:00
DESKTOP-GENO133\IvanPlex
b73c2e979f draft for ze api controller. 2024-01-09 15:18:43 -07:00
DESKTOP-T0O5CDB\DESK-555BD
a52fc29067 github pages. 2024-01-09 14:44:44 -07:00
DESKTOP-T0O5CDB\DESK-555BD
c6774d4be7 Added import/export functionality for upgrades, service records, and repair records. 2024-01-09 13:51:39 -07:00
DESKTOP-T0O5CDB\DESK-555BD
1271b3517d make report container smaller to accomodate for consolidated report below. 2024-01-09 12:59:25 -07:00
Hargata Softworks
96914b3db4 Merge pull request #39 from hargata/Hargata/upgradestab
Upgrades Tab
2024-01-08 19:04:57 -07:00
DESKTOP-GENO133\IvanPlex
7ad6d3369d updated field placeholders 2024-01-08 19:00:30 -07:00
DESKTOP-GENO133\IvanPlex
0ae28d436b added upgrade tab functionality. 2024-01-08 18:33:21 -07:00
DESKTOP-T0O5CDB\DESK-555BD
4575cf338f Added upgrades section backend. 2024-01-08 17:47:11 -07:00
Hargata Softworks
c526a9f207 Merge pull request #37 from hargata/Hargata/uk.mpg
Hargata/uk.mpg
2024-01-08 16:11:39 -07:00
DESKTOP-T0O5CDB\DESK-555BD
1a3282c1ef removed unnecessary lines. 2024-01-08 16:11:02 -07:00
DESKTOP-T0O5CDB\DESK-555BD
f52e5b49c7 rehashed logic. 2024-01-08 15:31:02 -07:00
DESKTOP-T0O5CDB\DESK-555BD
507fb6daf6 refreshed logic. 2024-01-08 15:30:15 -07:00
Hargata Softworks
9d3fbd05fc Merge pull request #35 from hargata/Hargata/uk.mpg
added support for UK MPG
2024-01-08 15:14:00 -07:00
DESKTOP-T0O5CDB\DESK-555BD
2a0f884e89 fixed unit cost for gas. 2024-01-08 15:13:26 -07:00
DESKTOP-T0O5CDB\DESK-555BD
26012bf27a added support for UK MPG 2024-01-08 15:11:23 -07:00
Hargata Softworks
3fa3dbaa8c Merge pull request #32 from hargata/Hargata/ghcr.multiplatform
removing arm64 support for now until I can figure out a workaround fo…
2024-01-08 13:49:12 -07:00
DESKTOP-T0O5CDB\DESK-555BD
95cac60c71 removing arm64 support for now until I can figure out a workaround for qemu. 2024-01-08 13:48:51 -07:00
Hargata Softworks
7878ce65ca Merge pull request #31 from hargata/Hargata/ghcr.multiplatform
...
2024-01-08 13:44:30 -07:00
DESKTOP-T0O5CDB\DESK-555BD
c6c57a5de6 ... 2024-01-08 13:44:08 -07:00
Hargata Softworks
2ece3cd113 Merge pull request #30 from hargata/Hargata/ghcr.multiplatform
Hargata/ghcr.multiplatform
2024-01-08 13:37:02 -07:00
DESKTOP-T0O5CDB\DESK-555BD
9504674933 tab 2024-01-08 13:36:52 -07:00
DESKTOP-T0O5CDB\DESK-555BD
3c0bdcea0e approach 2 2024-01-08 13:35:39 -07:00
DESKTOP-T0O5CDB\DESK-555BD
a4762d5b32 lil disappointment 2024-01-08 13:26:24 -07:00
Hargata Softworks
dc18bb5b8f Merge pull request #29 from hargata/Hargata/ghcr.multiplatform
lemme try this.
2024-01-08 13:15:54 -07:00
DESKTOP-T0O5CDB\DESK-555BD
452248e681 lemme try this. 2024-01-08 13:15:32 -07:00
Hargata Softworks
a7ea03014f Merge pull request #28 from hargata/Hargata/ghcr.multiplatform
Multi Platform Docker Image
2024-01-08 12:49:16 -07:00
DESKTOP-T0O5CDB\DESK-555BD
cb89ca3426 hm 2024-01-08 12:47:00 -07:00
DESKTOP-T0O5CDB\DESK-555BD
760f7e1888 I wondre if this works 2024-01-08 12:44:07 -07:00
Hargata Softworks
ba149568a5 Merge pull request #27 from hargata/Hargata/rename.uploadedfiles
Rename Uploaded Files
2024-01-08 11:25:50 -07:00
DESKTOP-GENO133\IvanPlex
2ef281bd0a added setting to hide zero costs with dashes 2024-01-08 11:24:51 -07:00
DESKTOP-GENO133\IvanPlex
859796cfe8 fixed file deletion button not working. 2024-01-08 11:00:19 -07:00
DESKTOP-GENO133\IvanPlex
f1e0254f95 added ability to rename uploaded files on tax records. 2024-01-08 10:53:44 -07:00
DESKTOP-GENO133\IvanPlex
70d0827432 added functionality to edit uploaded file names for gas records. 2024-01-08 10:50:41 -07:00
DESKTOP-GENO133\IvanPlex
851a7af287 added functionality to rename uploaded files in collision records. 2024-01-08 10:34:45 -07:00
DESKTOP-GENO133\IvanPlex
e46be7ba0a added ability to rename files on service record page. 2024-01-08 09:05:09 -07:00
Hargata Softworks
b4258dc11a Merge pull request #24 from hargata/fuelly.import
Fuelly.import
2024-01-07 23:14:06 -07:00
DESKTOP-GENO133\IvanPlex
0f35621846 config 2024-01-07 23:08:44 -07:00
DESKTOP-GENO133\IvanPlex
374f919296 added description to mapper 2024-01-07 23:04:20 -07:00
DESKTOP-GENO133\IvanPlex
33b824b316 fuelly import but can only import one csv per car. 2024-01-07 23:00:07 -07:00
Hargata Softworks
418e468f14 Update README.md 2024-01-07 16:42:52 -07:00
Hargata Softworks
0a1f6a569d Merge pull request #22 from hargata/electric.vehicle
fixed datepicker format not parsing correctly.
2024-01-07 16:12:40 -07:00
DESKTOP-GENO133\IvanPlex
2a842d1c8c fixed datepicker format not parsing correctly. 2024-01-07 16:12:16 -07:00
83 changed files with 2412 additions and 498 deletions

View File

@@ -11,9 +11,21 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build and push the Docker image
run: |
docker login -u "hargata" -p "${{ secrets.GHCR_PAT }}" ghcr.io
docker build . --file Dockerfile --tag ghcr.io/hargata/lubelogger:latest
docker push ghcr.io/hargata/lubelogger:latest
- 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

View 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;
}
}
}

View File

@@ -45,7 +45,9 @@ namespace CarCareTracker.Controllers
UseDarkMode = bool.Parse(_config[nameof(UserConfig.UseDarkMode)]),
UseMPG = bool.Parse(_config[nameof(UserConfig.UseMPG)]),
UseDescending = bool.Parse(_config[nameof(UserConfig.UseDescending)]),
EnableAuth = bool.Parse(_config[nameof(UserConfig.EnableAuth)])
EnableAuth = bool.Parse(_config[nameof(UserConfig.EnableAuth)]),
HideZero = bool.Parse(_config[nameof(UserConfig.HideZero)]),
UseUKMPG = bool.Parse(_config[nameof(UserConfig.UseUKMPG)])
};
return PartialView("_Settings", userConfig);
}

View File

@@ -13,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()
{
@@ -37,30 +40,19 @@ namespace CarCareTracker.Controllers
//compare it against hashed credentials
try
{
var configFileContents = System.IO.File.ReadAllText(StaticHelper.UserConfigPath);
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)
AuthCookie authCookie = new AuthCookie
{
//auth success, create auth cookie
//encrypt stuff.
AuthCookie authCookie = new AuthCookie
{
Id = 1, //this is hardcoded for now
UserName = credentials.UserName,
ExpiresOn = DateTime.Now.AddDays(credentials.IsPersistent ? 30 : 1)
};
var serializedCookie = JsonSerializer.Serialize(authCookie);
var encryptedCookie = _dataProtector.Protect(serializedCookie);
Response.Cookies.Append("ACCESS_TOKEN", encryptedCookie, new CookieOptions { Expires = new DateTimeOffset(authCookie.ExpiresOn) });
return Json(true);
}
Id = 1, //this is hardcoded for now
UserName = credentials.UserName,
ExpiresOn = DateTime.Now.AddDays(credentials.IsPersistent ? 30 : 1)
};
var serializedCookie = JsonSerializer.Serialize(authCookie);
var encryptedCookie = _dataProtector.Protect(serializedCookie);
Response.Cookies.Append("ACCESS_TOKEN", encryptedCookie, new CookieOptions { Expires = new DateTimeOffset(authCookie.ExpiresOn) });
return Json(true);
}
}
catch (Exception ex)

View File

@@ -6,7 +6,7 @@ using CarCareTracker.Helper;
using CsvHelper;
using System.Globalization;
using Microsoft.AspNetCore.Authorization;
using CarCareTracker.External.Implementations;
using CarCareTracker.MapProfile;
namespace CarCareTracker.Controllers
{
@@ -21,13 +21,20 @@ namespace CarCareTracker.Controllers
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,
@@ -35,6 +42,7 @@ namespace CarCareTracker.Controllers
ICollisionRecordDataAccess collisionRecordDataAccess,
ITaxRecordDataAccess taxRecordDataAccess,
IReminderRecordDataAccess reminderRecordDataAccess,
IUpgradeRecordDataAccess upgradeRecordDataAccess,
IWebHostEnvironment webEnv,
IConfiguration config)
{
@@ -42,11 +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)]);
@@ -93,41 +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))
{
@@ -145,81 +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")
csv.Context.RegisterClassMap<FuellyMapper>();
var records = csv.GetRecords<ImportModel>().ToList();
if (records.Any())
{
var records = csv.GetRecords<GasRecordImport>().ToList();
if (records.Any())
foreach (ImportModel importModel in records)
{
foreach (GasRecordImport recordToInsert in records)
if (mode == ImportMode.GasRecord)
{
//convert to gas model.
var convertedRecord = new GasRecord()
{
VehicleId = vehicleId,
Date = recordToInsert.Date,
Mileage = recordToInsert.Odometer,
Gallons = recordToInsert.FuelConsumed,
Cost = recordToInsert.Cost
Date = DateTime.Parse(importModel.Date),
Mileage = int.Parse(importModel.Odometer, NumberStyles.Any),
Gallons = decimal.Parse(importModel.FuelConsumed, NumberStyles.Any)
};
_gasRecordDataAccess.SaveGasRecordToVehicle(convertedRecord);
if (string.IsNullOrWhiteSpace(importModel.Cost) && !string.IsNullOrWhiteSpace(importModel.Price))
{
//cost was not given but price is.
//fuelly sometimes exports CSVs without total cost.
var parsedPrice = decimal.Parse(importModel.Price, NumberStyles.Any);
convertedRecord.Cost = convertedRecord.Gallons * parsedPrice;
} else
{
convertedRecord.Cost = decimal.Parse(importModel.Cost, NumberStyles.Any);
}
if (string.IsNullOrWhiteSpace(importModel.IsFillToFull) && !string.IsNullOrWhiteSpace(importModel.PartialFuelUp))
{
var parsedBool = importModel.PartialFuelUp.Trim() == "1";
convertedRecord.IsFillToFull = !parsedBool;
} else if (!string.IsNullOrWhiteSpace(importModel.IsFillToFull))
{
var parsedBool = importModel.IsFillToFull.Trim() == "1" || importModel.IsFillToFull.Trim() == "Full";
convertedRecord.IsFillToFull = parsedBool;
}
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 == "taxrecord")
{
var records = csv.GetRecords<TaxRecordImport>().ToList();
if (records.Any())
{
foreach (TaxRecordImport recordToInsert in records)
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 == 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);
}
@@ -245,60 +361,8 @@ 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;
decimal unFactoredConsumption = 0.00M;
int unFactoredMileage = 0;
//perform computation.
for (int i = 0; i < result.Count; i++)
{
if (i > 0)
{
var currentObject = result[i];
var deltaMileage = currentObject.Mileage - previousMileage;
var gasRecordViewModel = new GasRecordViewModel()
{
Id = currentObject.Id,
VehicleId = currentObject.VehicleId,
Date = currentObject.Date.ToShortDateString(),
Mileage = currentObject.Mileage,
Gallons = currentObject.Gallons,
Cost = currentObject.Cost,
DeltaMileage = deltaMileage,
CostPerGallon = (currentObject.Cost / currentObject.Gallons)
};
if (currentObject.IsFillToFull)
{
//if user filled to full.
gasRecordViewModel.MilesPerGallon = useMPG ? ((unFactoredMileage + deltaMileage) / (unFactoredConsumption + currentObject.Gallons)) : 100 / ((unFactoredMileage + deltaMileage) / (unFactoredConsumption + currentObject.Gallons));
//reset unFactored vars
unFactoredConsumption = 0;
unFactoredMileage = 0;
} else
{
unFactoredConsumption += currentObject.Gallons;
unFactoredMileage += deltaMileage;
gasRecordViewModel.MilesPerGallon = 0;
}
computedResults.Add(gasRecordViewModel);
}
else
{
computedResults.Add(new GasRecordViewModel()
{
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();
@@ -336,7 +400,8 @@ namespace CarCareTracker.Controllers
Date = result.Date.ToShortDateString(),
Files = result.Files,
Gallons = result.Gallons,
IsFillToFull = result.IsFillToFull
IsFillToFull = result.IsFillToFull,
MissedFuelUp = result.MissedFuelUp
};
var vehicleIsElectric = _dataAccess.GetVehicleById(convertedResult.VehicleId).IsElectric;
var viewModel = new GasRecordInputContainer()
@@ -513,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)
@@ -524,35 +651,72 @@ 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 gasRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
if (year != default)
var reminders = GetRemindersAndUrgency(vehicleId, DateTime.Now.AddDays(daysToAdd));
var viewModel = new ReminderMakeUpForVehicle
{
gasRecords.RemoveAll(x => x.Date.Year != year);
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));
}
var groupedGasRecord = gasRecords.GroupBy(x => x.Date.Month).OrderBy(x => x.Key).Select(x => new GasCostForVehicleByMonth
if (selectedMetrics.Contains(ImportMode.RepairRecord))
{
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
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);
allCosts.AddRange(_reportHelper.GetGasRecordSum(gasRecords, year));
}
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"
@@ -574,98 +738,25 @@ namespace CarCareTracker.Controllers
{
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)
private List<ReminderRecordViewModel> GetRemindersAndUrgency(int vehicleId, DateTime dateCompare)
{
var currentMileage = GetMaxMileage(vehicleId);
var reminders = _reminderRecordDataAccess.GetReminderRecordsByVehicleId(vehicleId);
List<ReminderRecordViewModel> reminderViewModels = new List<ReminderRecordViewModel>();
foreach(var reminder in reminders)
{
var reminderViewModel = new ReminderRecordViewModel()
{
Id = reminder.Id,
VehicleId = reminder.VehicleId,
Date = reminder.Date,
Mileage = reminder.Mileage,
Description = reminder.Description,
Notes = reminder.Notes,
Metric = reminder.Metric
};
if (reminder.Metric == ReminderMetric.Both)
{
if (reminder.Date < DateTime.Now)
{
reminderViewModel.Urgency = ReminderUrgency.PastDue;
reminderViewModel.Metric = ReminderMetric.Date;
}
else if (reminder.Mileage < currentMileage)
{
reminderViewModel.Urgency = ReminderUrgency.PastDue;
reminderViewModel.Metric = ReminderMetric.Odometer;
}
else if (reminder.Date < DateTime.Now.AddDays(7))
{
//if less than a week from today or less than 50 miles from current mileage then very urgent.
reminderViewModel.Urgency = ReminderUrgency.VeryUrgent;
//have to specify by which metric this reminder is urgent.
reminderViewModel.Metric = ReminderMetric.Date;
}
else if (reminder.Mileage < currentMileage + 50)
{
reminderViewModel.Urgency = ReminderUrgency.VeryUrgent;
reminderViewModel.Metric = ReminderMetric.Odometer;
}
else if (reminder.Date < DateTime.Now.AddDays(30))
{
reminderViewModel.Urgency = ReminderUrgency.Urgent;
reminderViewModel.Metric = ReminderMetric.Date;
}
else if (reminder.Mileage < currentMileage + 100)
{
reminderViewModel.Urgency = ReminderUrgency.Urgent;
reminderViewModel.Metric = ReminderMetric.Odometer;
}
} else if (reminder.Metric == ReminderMetric.Date)
{
if (reminder.Date < DateTime.Now)
{
reminderViewModel.Urgency = ReminderUrgency.PastDue;
}
else if (reminder.Date < DateTime.Now.AddDays(7))
{
reminderViewModel.Urgency = ReminderUrgency.VeryUrgent;
}
else if (reminder.Date < DateTime.Now.AddDays(30))
{
reminderViewModel.Urgency = ReminderUrgency.Urgent;
}
} else if (reminder.Metric == ReminderMetric.Odometer)
{
if (reminder.Mileage < currentMileage)
{
reminderViewModel.Urgency = ReminderUrgency.PastDue;
reminderViewModel.Metric = ReminderMetric.Odometer;
}
else if (reminder.Mileage < currentMileage + 50)
{
reminderViewModel.Urgency = ReminderUrgency.VeryUrgent;
}
else if (reminder.Mileage < currentMileage + 100)
{
reminderViewModel.Urgency = ReminderUrgency.Urgent;
}
}
reminderViewModels.Add(reminderViewModel);
}
return reminderViewModels;
List<ReminderRecordViewModel> results = _reminderHelper.GetReminderRecordViewModels(reminders, currentMileage, dateCompare);
return results;
}
[HttpGet]
public IActionResult GetVehicleHaveUrgentOrPastDueReminders(int vehicleId)
{
var result = GetRemindersAndUrgency(vehicleId);
if (result.Where(x=>x.Urgency == ReminderUrgency.VeryUrgent || x.Urgency == ReminderUrgency.PastDue).Any())
var result = GetRemindersAndUrgency(vehicleId, DateTime.Now);
if (result.Where(x => x.Urgency == ReminderUrgency.VeryUrgent || x.Urgency == ReminderUrgency.PastDue).Any())
{
return Json(true);
}
@@ -674,8 +765,8 @@ namespace CarCareTracker.Controllers
[HttpGet]
public IActionResult GetReminderRecordsByVehicleId(int vehicleId)
{
var result = GetRemindersAndUrgency(vehicleId);
result = result.OrderByDescending(x=>x.Urgency).ToList();
var result = GetRemindersAndUrgency(vehicleId, DateTime.Now);
result = result.OrderByDescending(x => x.Urgency).ToList();
return PartialView("_ReminderRecords", result);
}
[HttpPost]
@@ -720,5 +811,89 @@ namespace CarCareTracker.Controllers
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
}
}

11
Enum/ImportMode.cs Normal file
View File

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

View File

@@ -9,16 +9,24 @@ namespace CarCareTracker.External.Implementations
{
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))
{
@@ -27,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;
};
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,5 +7,20 @@
{
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;
}
}
}
}

View 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"]);
}
}
}

View File

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

View File

@@ -15,6 +15,7 @@
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>();
}
}

View File

@@ -15,6 +15,7 @@
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,
@@ -24,7 +25,8 @@
Mileage = Mileage,
VehicleId = VehicleId,
Files = Files,
IsFillToFull = IsFillToFull
IsFillToFull = IsFillToFull,
MissedFuelUp = MissedFuelUp
}; }
}
}

50
Models/ImportModel.cs Normal file
View 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; }
}
}

View File

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

View File

@@ -4,6 +4,7 @@
{
public int Id { get; set; }
public int VehicleId { get; set; }
public string Description { get; set; }
public string NoteText { get; set; }
}
}

View File

@@ -1,6 +1,6 @@
namespace CarCareTracker.Models
{
public class GasCostForVehicleByMonth
public class CostForVehicleByMonth
{
public string MonthName { get; set; }
public decimal Cost { get; set; }

View File

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

View File

@@ -0,0 +1,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; }
}
}

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

View File

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

View File

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

View File

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

View File

@@ -16,7 +16,14 @@ 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"))
{

View File

@@ -13,10 +13,17 @@ Because nobody should have to deal with a homemade spreadsheet or a shoebox full
- CsvHelper
- Chart.js
## Docker Setup (Recommended)
## 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 Dockerfile, default is en_US
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

127
Views/API/Index.cshtml Normal file
View 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>

View File

@@ -18,12 +18,20 @@
<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 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">
<div class="form-check form-switch">
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="useDescending" checked="@Model.UseDescending">
<label class="form-check-label" for="useDescending">Sort lists in Descending Order(Newest to Oldest)</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="hideZero" checked="@Model.HideZero">
<label class="form-check-label" for="hideZero">Replace @(0.ToString("C")) Costs with ---</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" onChange="enableAuthCheckChanged()" type="checkbox" role="switch" id="enableAuth" checked="@Model.EnableAuth">
<label class="form-check-label" for="enableAuth">Enable Authentication</label>
@@ -76,7 +84,9 @@
useDarkMode: $("#enableDarkMode").is(':checked'),
enableCsvImports: $("#enableCsvImports").is(':checked'),
useMPG: $("#useMPG").is(':checked'),
useDescending: $("#useDescending").is(':checked')
useDescending: $("#useDescending").is(':checked'),
hideZero: $("#hideZero").is(":checked"),
useUKMpg: $("#useUKMPG").is(":checked")
}
$.post('/Home/WriteToSettings', { userConfig: userConfigObject}, function(data){
if (data) {

View File

@@ -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" />

View File

@@ -18,6 +18,9 @@
<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" />
@@ -25,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>

View File

@@ -9,6 +9,8 @@
<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">
@@ -27,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>
@@ -53,24 +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">

View File

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

View File

@@ -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,14 +30,7 @@
@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>

View File

@@ -1,6 +1,7 @@
@inject IConfiguration Configuration
@{
var enableCsvImports = bool.Parse(Configuration[nameof(UserConfig.EnableCsvImports)]);
var hideZero = bool.Parse(Configuration[nameof(UserConfig.HideZero)]);
}
@model List<CollisionRecord>
<div class="row">
@@ -18,7 +19,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>

View File

@@ -1,5 +1,5 @@
@model CostMakeUpForVehicle
@if (Model.CollisionRecordSum + Model.ServiceRecordSum + Model.GasRecordSum + Model.TaxRecordSum > 0)
@if (Model.CollisionRecordSum + Model.ServiceRecordSum + Model.GasRecordSum + Model.TaxRecordSum + Model.UpgradeRecordSum > 0)
{
<canvas id="pie-chart"></canvas>
<script>
@@ -9,17 +9,18 @@
new Chart($("#pie-chart"), {
type: 'pie',
data: {
labels: ["Planned Maintenance(Service Records)", "Unplanned Maintenance(Repairs)", "Tax", "Fuel"],
labels: ["Planned Maintenance(Service Records)", "Unplanned Maintenance(Repairs)", "Upgrades", "Tax", "Fuel"],
datasets: [
{
label: "Expenses by Category",
backgroundColor: ["#003f5c", "#58508d", "#bc5090", "#ff6361"],
backgroundColor: ["#003f5c", "#58508d", "#bc5090", "#ff6361", "#ffa600"],
data: [
@Model.ServiceRecordSum,
@Model.CollisionRecordSum,
@Model.UpgradeRecordSum,
@Model.TaxRecordSum,
@Model.GasRecordSum
]
]
}
]
},
@@ -41,7 +42,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>
}

View File

@@ -3,16 +3,30 @@
@{
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
fuelEconomyUnit = useMPG ? "mi./kWh" : "kWh/100km";
}
else if (useMPG && useUKMPG)
{
consumptionUnit = useMPG ? "gal" : "l";
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";
}
}
@@ -37,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 {
@@ -51,7 +66,7 @@
<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">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>
@@ -66,8 +81,8 @@
<td class="col-2">@gasRecord.Mileage</td>
<td class="col-2">@gasRecord.Gallons.ToString("F")</td>
<td class="col-4">@(gasRecord.MilesPerGallon == 0 ? "---" : gasRecord.MilesPerGallon.ToString("F"))</td>
<td class="col-1">@gasRecord.Cost.ToString("C3")</td>
<td class="col-1">@gasRecord.CostPerGallon.ToString("C3")</td>
<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>
@@ -76,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">

View File

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

View File

@@ -2,17 +2,30 @@
@model GasRecordInputContainer
@{
var useMPG = bool.Parse(Configuration[nameof(UserConfig.UseMPG)]);
var useUKMPG = bool.Parse(Configuration[nameof(UserConfig.UseUKMPG)]);
var useKwh = Model.UseKwh;
var isNew = Model.GasRecord.Id == 0;
string consumptionUnit;
string distanceUnit;
if (useKwh)
{
consumptionUnit = "kWh";
} else if (useUKMPG)
{
consumptionUnit = "liters";
}
else
{
consumptionUnit = useMPG ? "gallons" : "liters";
}
if (useUKMPG)
{
distanceUnit = "miles";
}
else
{
distanceUnit = useMPG ? "miles" : "kilometers";
}
}
<div class="modal-header">
<h5 class="modal-title">@(isNew ? "Add New Gas Record" : "Edit Gas Record")</h5>
@@ -29,7 +42,7 @@
<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>
<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)">
@@ -37,21 +50,18 @@
<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 refueled" value="@(isNew ? "" : Model.GasRecord.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.GasRecord.Files.Any())
{
<div>
<label>Uploaded Documents</label>
@foreach (UploadedFiles filesUploaded in Model.GasRecord.Files)
{
<div class="d-flex justify-content-between">
<a type="button" class="btn btn-link" href="@filesUploaded.Location" target="_blank">@filesUploaded.Name</a>
<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>

View 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>

View 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>

View 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>
}

View File

@@ -58,7 +58,7 @@
<td class="col-2">@reminderRecord.Metric</td>
}
<td class="col-5">@reminderRecord.Description</td>
<td class="col-3 text-truncate">@reminderRecord.Notes</td>
<td class="col-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>

View File

@@ -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>

View File

@@ -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,14 +30,7 @@
@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>

View File

@@ -1,6 +1,7 @@
@inject IConfiguration Configuration
@{
var enableCsvImports = bool.Parse(Configuration[nameof(UserConfig.EnableCsvImports)]);
var hideZero = bool.Parse(Configuration[nameof(UserConfig.HideZero)]);
}
@model List<ServiceRecord>
<div class="row">
@@ -18,7 +19,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">

View File

@@ -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,14 +28,7 @@
@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>

View File

@@ -1,6 +1,7 @@
@inject IConfiguration Configuration
@{
var enableCsvImports = bool.Parse(Configuration[nameof(UserConfig.EnableCsvImports)]);
var hideZero = bool.Parse(Configuration[nameof(UserConfig.HideZero)]);
}
@model List<TaxRecord>
<div class="row">
@@ -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>

View File

@@ -0,0 +1,84 @@
@model UpgradeRecordInput
@{
var isNew = Model.Id == 0;
}
<div class="modal-header">
<h5 class="modal-title">@(isNew ? "Add New Upgrade Record" : "Edit Upgrade Record")</h5>
<button type="button" class="btn-close" onclick="hideAddUpgradeRecordModal()" aria-label="Close"></button>
</div>
<div class="modal-body">
<form>
<div class="form-group">
<div class="row">
<div class="col-md-6 col-12">
<input type="text" id="workAroundInput" style="height:0px; width:0px; display:none;">
<label for="upgradeRecordDate">Date</label>
<div class="input-group">
<input type="text" id="upgradeRecordDate" class="form-control" placeholder="Date upgrade/mods was installed" value="@Model.Date">
<span class="input-group-text"><i class="bi bi-calendar-event"></i></span>
</div>
<label for="upgradeRecordMileage">Odometer</label>
<input type="number" id="upgradeRecordMileage" class="form-control" placeholder="Odometer reading when upgraded/modded" value="@(isNew ? "" : Model.Mileage)">
<label for="upgradeRecordDescription">Description</label>
<input type="text" id="upgradeRecordDescription" class="form-control" placeholder="Description of item(s) upgraded/modded" value="@Model.Description">
<label for="upgradeRecordCost">Cost</label>
<input type="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>

View 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>

View File

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

View File

@@ -11,6 +11,8 @@
"UseMPG": true,
"UseDescending": false,
"EnableAuth": false,
"HideZero": false,
"UseUKMPG": false,
"UserNameHash": "",
"UserPasswordHash": ""
}

View File

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

BIN
docs/fuelmileage.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

BIN
docs/garage.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

167
docs/index.html Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

BIN
docs/reminder.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
docs/servicerecord.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View File

@@ -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;
}
@@ -57,6 +85,10 @@ html {
}
}
.reportsCheckBoxContainer {
padding: 0.375rem 2.25rem 0.375rem 0.75rem;
}
.bell-shake {
animation: bellshake .5s;
backface-visibility: hidden;
@@ -99,4 +131,5 @@ html {
100% {
transform: rotate(0);
}
}
}

BIN
wwwroot/defaults/garage.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -3,10 +3,7 @@
if (data) {
$("#collisionRecordModalContent").html(data);
//initiate datepicker
$('#collisionRecordDate').datepicker({
endDate: "+0d",
format: getShortDatePattern().pattern
});
initDatePicker($('#collisionRecordDate'));
$('#collisionRecordModal').modal('show');
}
});
@@ -16,10 +13,7 @@ function showEditCollisionRecordModal(collisionRecordId) {
if (data) {
$("#collisionRecordModalContent").html(data);
//initiate datepicker
$('#collisionRecordDate').datepicker({
endDate: "+0d",
format: getShortDatePattern().pattern
});
initDatePicker($('#collisionRecordDate'));
$('#collisionRecordModal').modal('show');
}
});
@@ -103,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 {
@@ -121,8 +115,4 @@ function getAndValidateCollisionRecordValues() {
files: uploadedFiles,
addReminderRecord: addReminderRecord
}
}
function deleteCollisionRecordFile(fileLocation, event) {
event.parentElement.remove();
uploadedFiles = uploadedFiles.filter(x => x.location != fileLocation);
}

View File

@@ -3,10 +3,7 @@
if (data) {
$("#gasRecordModalContent").html(data);
//initiate datepicker
$('#gasRecordDate').datepicker({
endDate: "+0d",
format: getShortDatePattern().pattern
});
initDatePicker($('#gasRecordDate'));
$('#gasRecordModal').modal('show');
}
});
@@ -16,10 +13,7 @@ function showEditGasRecordModal(gasRecordId) {
if (data) {
$("#gasRecordModalContent").html(data);
//initiate datepicker
$('#gasRecordDate').datepicker({
endDate: "+0d",
format: getShortDatePattern().pattern
});
initDatePicker($('#gasRecordDate'));
$('#gasRecordModal').modal('show');
}
});
@@ -77,6 +71,7 @@ function getAndValidateGasRecordValues() {
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
@@ -99,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 {
@@ -114,10 +109,7 @@ function getAndValidateGasRecordValues() {
gallons: gasGallons,
cost: gasCost,
files: uploadedFiles,
isFillToFull: gasIsFillToFull
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
View 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
}
}

View File

@@ -2,9 +2,7 @@
$.get(`/Vehicle/GetReminderRecordForEditById?reminderRecordId=${reminderId}`, function (data) {
if (data) {
$("#reminderRecordModalContent").html(data);
$('#reminderDate').datepicker({
startDate: "+0d"
});
initDatePicker($('#reminderDate'), true);
$("#reminderRecordModal").modal("show");
}
});

View File

@@ -3,10 +3,7 @@
if (data) {
$("#serviceRecordModalContent").html(data);
//initiate datepicker
$('#serviceRecordDate').datepicker({
endDate: "+0d",
format: getShortDatePattern().pattern
});
initDatePicker($('#serviceRecordDate'));
$('#serviceRecordModal').modal('show');
}
});
@@ -16,10 +13,7 @@ function showEditServiceRecordModal(serviceRecordId) {
if (data) {
$("#serviceRecordModalContent").html(data);
//initiate datepicker
$('#serviceRecordDate').datepicker({
endDate: "+0d",
format: getShortDatePattern().pattern
});
initDatePicker($('#serviceRecordDate'));
$('#serviceRecordModal').modal('show');
}
});
@@ -103,7 +97,7 @@ function getAndValidateServiceRecordValues() {
} else {
$("#serviceRecordDescription").removeClass("is-invalid");
}
if (serviceCost.trim() == '') {
if (serviceCost.trim() == '' || !isValidMoney(serviceCost)) {
hasError = true;
$("#serviceRecordCost").addClass("is-invalid");
} else {
@@ -121,8 +115,4 @@ function getAndValidateServiceRecordValues() {
files: uploadedFiles,
addReminderRecord: addReminderRecord
}
}
function deleteServiceRecordFile(fileLocation, event) {
event.parentElement.remove();
uploadedFiles = uploadedFiles.filter(x => x.location != fileLocation);
}

View File

@@ -110,4 +110,24 @@ function uploadFileAsync(event) {
}
}
});
}
function isValidMoney(input) {
const euRegex = /^\$?(?=\(.*\)|[^()]*$)\(?\d{1,3}(\.?\d{3})?(,\d{1,3}?)?\)?$/;
const usRegex = /^\$?(?=\(.*\)|[^()]*$)\(?\d{1,3}(,?\d{3})?(\.\d{1,3}?)?\)?$/;
return (euRegex.test(input) || usRegex.test(input));
}
function initDatePicker(input, futureOnly) {
if (futureOnly) {
input.datepicker({
startDate: "+0d",
format: getShortDatePattern().pattern,
autoclose: true
});
} else {
input.datepicker({
endDate: "+0d",
format: getShortDatePattern().pattern,
autoclose: true
});
}
}

View File

@@ -3,10 +3,7 @@
if (data) {
$("#taxRecordModalContent").html(data);
//initiate datepicker
$('#taxRecordDate').datepicker({
endDate: "+0d",
format: getShortDatePattern().pattern
});
initDatePicker($('#taxRecordDate'));
$('#taxRecordModal').modal('show');
}
});
@@ -16,10 +13,7 @@ function showEditTaxRecordModal(taxRecordId) {
if (data) {
$("#taxRecordModalContent").html(data);
//initiate datepicker
$('#taxRecordDate').datepicker({
endDate: "+0d",
format: getShortDatePattern().pattern
});
initDatePicker($('#taxRecordDate'));
$('#taxRecordModal').modal('show');
}
});
@@ -96,7 +90,7 @@ function getAndValidateTaxRecordValues() {
} else {
$("#taxRecordDescription").removeClass("is-invalid");
}
if (taxCost.trim() == '') {
if (taxCost.trim() == '' || !isValidMoney(taxCost)) {
hasError = true;
$("#taxRecordCost").addClass("is-invalid");
} else {
@@ -113,8 +107,4 @@ function getAndValidateTaxRecordValues() {
files: uploadedFiles,
addReminderRecord: addReminderRecord
}
}
function deleteTaxRecordFile(fileLocation, event) {
event.parentElement.remove();
uploadedFiles = uploadedFiles.filter(x => x.location != fileLocation);
}

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

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

View File

@@ -1,16 +1,6 @@
function returnToGarage() {
window.location.href = '/Home';
}
function saveVehicleNote(vehicleId) {
var noteText = $("#noteTextArea").val();
$.post('/Vehicle/SaveNoteToVehicle', { vehicleId: vehicleId, noteText: noteText }, function (data) {
if (data) {
successToast("Note saved successfully.");
} else {
errorToast("An error has occurred, please try again later.");
}
})
}
$(document).ready(function () {
var vehicleId = GetVehicleId().vehicleId;
//bind tabs
@@ -20,7 +10,7 @@ $(document).ready(function () {
getVehicleServiceRecords(vehicleId);
break;
case "notes-tab":
getVehicleNote(vehicleId);
getVehicleNotes(vehicleId);
break;
case "gas-tab":
getVehicleGasRecords(vehicleId);
@@ -32,11 +22,14 @@ $(document).ready(function () {
getVehicleTaxRecords(vehicleId);
break;
case "report-tab":
getVehicleReport();
getVehicleReport(vehicleId);
break;
case "reminder-tab":
getVehicleReminders(vehicleId);
break;
case "upgrade-tab":
getVehicleUpgradeRecords(vehicleId);
break;
}
switch (e.relatedTarget.id) { //clear out previous tabs with grids in them to help with performance
case "servicerecord-tab":
@@ -57,15 +50,21 @@ $(document).ready(function () {
case "reminder-tab":
$("#reminder-tab-pane").html("");
break;
case "upgrade-tab":
$("#upgrade-tab-pane").html("");
break;
case "notes-tab":
$("#notes-tab-pane").html("");
break;
}
});
getVehicleServiceRecords(vehicleId);
});
function getVehicleNote(vehicleId) {
$.get(`/Vehicle/GetNoteByVehicleId?vehicleId=${vehicleId}`, function (data) {
function getVehicleNotes(vehicleId) {
$.get(`/Vehicle/GetNotesByVehicleId?vehicleId=${vehicleId}`, function (data) {
if (data) {
$("#noteTextArea").val(data);
$("#notes-tab-pane").html(data);
}
});
}
@@ -75,7 +74,15 @@ function getVehicleServiceRecords(vehicleId) {
$("#servicerecord-tab-pane").html(data);
getVehicleHaveImportantReminders(vehicleId);
}
})
});
}
function getVehicleUpgradeRecords(vehicleId) {
$.get(`/Vehicle/GetUpgradeRecordsByVehicleId?vehicleId=${vehicleId}`, function (data) {
if (data) {
$("#upgrade-tab-pane").html(data);
getVehicleHaveImportantReminders(vehicleId);
}
});
}
function getVehicleGasRecords(vehicleId) {
$.get(`/Vehicle/GetGasRecordsByVehicleId?vehicleId=${vehicleId}`, function (data) {
@@ -97,7 +104,6 @@ function getVehicleTaxRecords(vehicleId) {
$.get(`/Vehicle/GetTaxRecordsByVehicleId?vehicleId=${vehicleId}`, function (data) {
if (data) {
$("#tax-tab-pane").html(data);
getVehicleHaveImportantReminders(vehicleId);
}
});
}
@@ -109,8 +115,8 @@ function getVehicleReminders(vehicleId) {
}
});
}
function getVehicleReport() {
$.get(`/Vehicle/GetReportPartialView`, function (data) {
function getVehicleReport(vehicleId) {
$.get(`/Vehicle/GetReportPartialView?vehicleId=${vehicleId}`, function (data) {
if (data) {
$("#report-tab-pane").html(data);
}
@@ -127,6 +133,16 @@ function editVehicle(vehicleId) {
function hideEditVehicleModal() {
$('#editVehicleModal').modal('hide');
}
function exportVehicleData(mode) {
var vehicleId = GetVehicleId().vehicleId;
$.get('/Vehicle/ExportFromVehicleToCsv', { vehicleId: vehicleId, mode: mode }, function (data) {
if (!data) {
errorToast("An error occurred, please try again later");
} else {
window.location.href = data;
}
});
}
function showBulkImportModal(mode) {
$.get(`/Vehicle/GetBulkImportModalPartialView?mode=${mode}`, function (data) {
if (data) {
@@ -181,17 +197,13 @@ function showAddReminderModal(reminderModalInput) {
if (reminderModalInput != undefined) {
$.post('/Vehicle/GetAddReminderRecordPartialView', {reminderModel: reminderModalInput}, function (data) {
$("#reminderRecordModalContent").html(data);
$('#reminderDate').datepicker({
startDate: "+0d"
});
initDatePicker($('#reminderDate'), true);
$("#reminderRecordModal").modal("show");
});
} else {
$.post('/Vehicle/GetAddReminderRecordPartialView', function (data) {
$("#reminderRecordModalContent").html(data);
$('#reminderDate').datepicker({
startDate: "+0d"
});
initDatePicker($('#reminderDate'), true);
$("#reminderRecordModal").modal("show");
});
}
@@ -212,4 +224,33 @@ function getVehicleHaveImportantReminders(vehicleId) {
}
});
}, 500);
}
function deleteFileFromUploadedFiles(fileLocation, event) {
event.parentElement.parentElement.parentElement.remove();
uploadedFiles = uploadedFiles.filter(x => x.location != fileLocation);
}
function editFileName(fileLocation, event) {
Swal.fire({
title: 'Rename File',
html: `
<input type="text" id="newFileName" class="swal2-input" placeholder="New File Name">
`,
confirmButtonText: 'Rename',
focusConfirm: false,
preConfirm: () => {
const newFileName = $("#newFileName").val();
if (!newFileName) {
Swal.showValidationMessage(`Please enter a valid file name`)
}
return { newFileName }
},
}).then(function (result) {
if (result.isConfirmed) {
var linkDisplayObject = $(event.parentElement.parentElement).find('a')[0];
linkDisplayObject.text = result.value.newFileName;
var editFileIndex = uploadedFiles.findIndex(x => x.location == fileLocation);
uploadedFiles[editFileIndex].name = result.value.newFileName;
}
});
}

41
wwwroot/manifest.json Normal file
View File

@@ -0,0 +1,41 @@
{
"name": "LubeLogger",
"start_url": "/",
"display": "standalone",
"icons": [
{
"src": "/defaults/lubelogger_icon_72.png",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "/defaults/lubelogger_icon_128.png",
"sizes": "128x128",
"type": "image/png"
},
{
"src": "/defaults/lubelogger_icon_144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "/defaults/lubelogger_icon_192.png",
"sizes": "192x192",
"type": "image/png"
}
],
"screenshots": [
{
"src": "/defaults/garage_narrow.png",
"type": "image/png",
"sizes": "540x720",
"form_factor": "narrow"
},
{
"src": "/defaults/garage.png",
"type": "image/png",
"sizes": "720x540",
"form_factor": "wide"
}
]
}