diff --git a/Controllers/APIController.cs b/Controllers/APIController.cs index 5ca76c0..80f05f3 100644 --- a/Controllers/APIController.cs +++ b/Controllers/APIController.cs @@ -1,4 +1,5 @@ -using CarCareTracker.External.Interfaces; +using CarCareTracker.External.Implementations; +using CarCareTracker.External.Interfaces; using CarCareTracker.Filter; using CarCareTracker.Helper; using CarCareTracker.Logic; @@ -20,6 +21,7 @@ namespace CarCareTracker.Controllers private readonly ITaxRecordDataAccess _taxRecordDataAccess; private readonly IReminderRecordDataAccess _reminderRecordDataAccess; private readonly IUpgradeRecordDataAccess _upgradeRecordDataAccess; + private readonly IOdometerRecordDataAccess _odometerRecordDataAccess; private readonly IReminderHelper _reminderHelper; private readonly IGasHelper _gasHelper; private readonly IUserLogic _userLogic; @@ -34,6 +36,7 @@ namespace CarCareTracker.Controllers ITaxRecordDataAccess taxRecordDataAccess, IReminderRecordDataAccess reminderRecordDataAccess, IUpgradeRecordDataAccess upgradeRecordDataAccess, + IOdometerRecordDataAccess odometerRecordDataAccess, IFileHelper fileHelper, IUserLogic userLogic) { @@ -45,6 +48,7 @@ namespace CarCareTracker.Controllers _taxRecordDataAccess = taxRecordDataAccess; _reminderRecordDataAccess = reminderRecordDataAccess; _upgradeRecordDataAccess = upgradeRecordDataAccess; + _odometerRecordDataAccess = odometerRecordDataAccess; _gasHelper = gasHelper; _reminderHelper = reminderHelper; _userLogic = userLogic; @@ -106,6 +110,47 @@ namespace CarCareTracker.Controllers } [TypeFilter(typeof(CollaboratorFilter))] [HttpGet] + [Route("/api/vehicle/odometerrecords")] + public IActionResult OdometerRecords(int vehicleId) + { + var vehicleRecords = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId); + var result = vehicleRecords.Select(x => new OdometerRecordExportModel { Date = x.Date.ToShortDateString(), Odometer = x.Mileage.ToString(), Notes = x.Notes }); + return Json(result); + } + [TypeFilter(typeof(CollaboratorFilter))] + [HttpPost] + [Route("/api/vehicle/odometerrecords/add")] + public IActionResult AddOdometerRecord(int vehicleId, OdometerRecordExportModel input) + { + var response = new OperationResponse(); + if (vehicleId == default) + { + response.Success = false; + response.Message = "Must provide a valid vehicle id"; + return Json(response); + } + try + { + var odometerRecord = new OdometerRecord() + { + VehicleId = vehicleId, + Date = DateTime.Parse(input.Date), + Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes, + Mileage = int.Parse(input.Odometer) + }; + _odometerRecordDataAccess.SaveOdometerRecordToVehicle(odometerRecord); + response.Success = true; + response.Message = "Odometer Record Added"; + return Json(response); + } catch (Exception ex) + { + response.Success = false; + response.Message = StaticHelper.GenericErrorMessage; + return Json(response); + } + } + [TypeFilter(typeof(CollaboratorFilter))] + [HttpGet] [Route("/api/vehicle/gasrecords")] public IActionResult GasRecords(int vehicleId, bool useMPG, bool useUKMPG) { @@ -164,6 +209,11 @@ namespace CarCareTracker.Controllers { numbersArray.Add(upgradeRecords.Max(x => x.Mileage)); } + var odometerRecords = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId); + if (odometerRecords.Any()) + { + numbersArray.Add(odometerRecords.Max(x => x.Mileage)); + } return numbersArray.Any() ? numbersArray.Max() : 0; } } diff --git a/Controllers/VehicleController.cs b/Controllers/VehicleController.cs index c072e6c..0352d11 100644 --- a/Controllers/VehicleController.cs +++ b/Controllers/VehicleController.cs @@ -27,6 +27,7 @@ namespace CarCareTracker.Controllers private readonly IUpgradeRecordDataAccess _upgradeRecordDataAccess; private readonly ISupplyRecordDataAccess _supplyRecordDataAccess; private readonly IPlanRecordDataAccess _planRecordDataAccess; + private readonly IOdometerRecordDataAccess _odometerRecordDataAccess; private readonly IWebHostEnvironment _webEnv; private readonly IConfigHelper _config; private readonly IFileHelper _fileHelper; @@ -50,6 +51,7 @@ namespace CarCareTracker.Controllers IUpgradeRecordDataAccess upgradeRecordDataAccess, ISupplyRecordDataAccess supplyRecordDataAccess, IPlanRecordDataAccess planRecordDataAccess, + IOdometerRecordDataAccess odometerRecordDataAccess, IUserLogic userLogic, IWebHostEnvironment webEnv, IConfigHelper config) @@ -69,6 +71,7 @@ namespace CarCareTracker.Controllers _upgradeRecordDataAccess = upgradeRecordDataAccess; _supplyRecordDataAccess = supplyRecordDataAccess; _planRecordDataAccess = planRecordDataAccess; + _odometerRecordDataAccess = odometerRecordDataAccess; _userLogic = userLogic; _webEnv = webEnv; _config = config; @@ -143,7 +146,7 @@ namespace CarCareTracker.Controllers _dataAccess.DeleteVehicle(vehicleId); return Json(result); } - #region "Bulk Imports" + #region "Bulk Imports and Exports" [HttpGet] public IActionResult GetBulkImportModalPartialView(ImportMode mode) { @@ -215,6 +218,24 @@ namespace CarCareTracker.Controllers return Json($"/{fileNameToExport}"); } } + else if (mode == ImportMode.OdometerRecord) + { + var fileNameToExport = $"temp/{Guid.NewGuid()}.csv"; + var fullExportFilePath = _fileHelper.GetFullFilePath(fileNameToExport, false); + var vehicleRecords = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId); + if (vehicleRecords.Any()) + { + var exportData = vehicleRecords.Select(x => new OdometerRecordExportModel { Date = x.Date.ToShortDateString(), 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.SupplyRecord) { var fileNameToExport = $"temp/{Guid.NewGuid()}.csv"; @@ -406,6 +427,17 @@ namespace CarCareTracker.Controllers }; _serviceRecordDataAccess.SaveServiceRecordToVehicle(convertedRecord); } + else if (mode == ImportMode.OdometerRecord) + { + var convertedRecord = new OdometerRecord() + { + VehicleId = vehicleId, + Date = DateTime.Parse(importModel.Date), + Mileage = int.Parse(importModel.Odometer, NumberStyles.Any), + Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes + }; + _odometerRecordDataAccess.SaveOdometerRecordToVehicle(convertedRecord); + } else if (mode == ImportMode.PlanRecord) { var progressIsEnum = Enum.TryParse(importModel.Progress, out PlanProgress parsedProgress); @@ -1012,6 +1044,11 @@ namespace CarCareTracker.Controllers { numbersArray.Add(upgradeRecords.Max(x => x.Mileage)); } + var odometerRecords = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId); + if (odometerRecords.Any()) + { + numbersArray.Add(odometerRecords.Max(x => x.Mileage)); + } return numbersArray.Any() ? numbersArray.Max() : 0; } private List GetRemindersAndUrgency(int vehicleId, DateTime dateCompare) @@ -1369,5 +1406,58 @@ namespace CarCareTracker.Controllers return Json(result); } #endregion + #region "Odometer Records" + [TypeFilter(typeof(CollaboratorFilter))] + [HttpGet] + public IActionResult GetOdometerRecordsByVehicleId(int vehicleId) + { + var result = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId); + bool _useDescending = _config.GetUserConfig(User).UseDescending; + 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("_OdometerRecords", result); + } + [HttpPost] + public IActionResult SaveOdometerRecordToVehicleId(OdometerRecordInput odometerRecord) + { + //move files from temp. + odometerRecord.Files = odometerRecord.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList(); + var result = _odometerRecordDataAccess.SaveOdometerRecordToVehicle(odometerRecord.ToOdometerRecord()); + return Json(result); + } + [HttpGet] + public IActionResult GetAddOdometerRecordPartialView() + { + return PartialView("_OdometerRecordModal", new OdometerRecordInput()); + } + [HttpGet] + public IActionResult GetOdometerRecordForEditById(int odometerRecordId) + { + var result = _odometerRecordDataAccess.GetOdometerRecordById(odometerRecordId); + //convert to Input object. + var convertedResult = new OdometerRecordInput + { + Id = result.Id, + Date = result.Date.ToShortDateString(), + Mileage = result.Mileage, + Notes = result.Notes, + VehicleId = result.VehicleId, + Files = result.Files + }; + return PartialView("_OdometerRecordModal", convertedResult); + } + [HttpPost] + public IActionResult DeleteOdometerRecordById(int odometerRecordId) + { + var result = _odometerRecordDataAccess.DeleteOdometerRecordById(odometerRecordId); + return Json(result); + } + #endregion } } diff --git a/External/Implementations/OdometerRecordDataAccess.cs b/External/Implementations/OdometerRecordDataAccess.cs new file mode 100644 index 0000000..8695078 --- /dev/null +++ b/External/Implementations/OdometerRecordDataAccess.cs @@ -0,0 +1,57 @@ +using CarCareTracker.External.Interfaces; +using CarCareTracker.Helper; +using CarCareTracker.Models; +using LiteDB; + +namespace CarCareTracker.External.Implementations +{ + public class OdometerRecordDataAccess : IOdometerRecordDataAccess + { + private static string dbName = StaticHelper.DbName; + private static string tableName = "odometerrecords"; + public List GetOdometerRecordsByVehicleId(int vehicleId) + { + using (var db = new LiteDatabase(dbName)) + { + var table = db.GetCollection(tableName); + var odometerRecords = table.Find(Query.EQ(nameof(OdometerRecord.VehicleId), vehicleId)); + return odometerRecords.ToList() ?? new List(); + }; + } + public OdometerRecord GetOdometerRecordById(int odometerRecordId) + { + using (var db = new LiteDatabase(dbName)) + { + var table = db.GetCollection(tableName); + return table.FindById(odometerRecordId); + }; + } + public bool DeleteOdometerRecordById(int odometerRecordId) + { + using (var db = new LiteDatabase(dbName)) + { + var table = db.GetCollection(tableName); + table.Delete(odometerRecordId); + return true; + }; + } + public bool SaveOdometerRecordToVehicle(OdometerRecord odometerRecord) + { + using (var db = new LiteDatabase(dbName)) + { + var table = db.GetCollection(tableName); + table.Upsert(odometerRecord); + return true; + }; + } + public bool DeleteAllOdometerRecordsByVehicleId(int vehicleId) + { + using (var db = new LiteDatabase(dbName)) + { + var table = db.GetCollection(tableName); + var odometerRecords = table.DeleteMany(Query.EQ(nameof(OdometerRecord.VehicleId), vehicleId)); + return true; + }; + } + } +} diff --git a/External/Interfaces/IOdometerRecordDataAccess.cs b/External/Interfaces/IOdometerRecordDataAccess.cs new file mode 100644 index 0000000..2c64bcf --- /dev/null +++ b/External/Interfaces/IOdometerRecordDataAccess.cs @@ -0,0 +1,13 @@ +using CarCareTracker.Models; + +namespace CarCareTracker.External.Interfaces +{ + public interface IOdometerRecordDataAccess + { + public List GetOdometerRecordsByVehicleId(int vehicleId); + public OdometerRecord GetOdometerRecordById(int odometerRecordId); + public bool DeleteOdometerRecordById(int odometerRecordId); + public bool SaveOdometerRecordToVehicle(OdometerRecord odometerRecord); + public bool DeleteAllOdometerRecordsByVehicleId(int vehicleId); + } +} diff --git a/Models/ImportModel.cs b/Models/ImportModel.cs index 0406846..c03b6aa 100644 --- a/Models/ImportModel.cs +++ b/Models/ImportModel.cs @@ -44,6 +44,12 @@ public string Notes { get; set; } public string Cost { get; set; } } + public class OdometerRecordExportModel + { + public string Date { get; set; } + public string Odometer { get; set; } + public string Notes { get; set; } + } public class TaxRecordExportModel { public string Date { get; set; } diff --git a/Models/OdometerRecord/OdometerRecord.cs b/Models/OdometerRecord/OdometerRecord.cs new file mode 100644 index 0000000..3901789 --- /dev/null +++ b/Models/OdometerRecord/OdometerRecord.cs @@ -0,0 +1,12 @@ +namespace CarCareTracker.Models +{ + public class OdometerRecord + { + public int Id { get; set; } + public int VehicleId { get; set; } + public DateTime Date { get; set; } + public int Mileage { get; set; } + public string Notes { get; set; } + public List Files { get; set; } = new List(); + } +} diff --git a/Models/OdometerRecord/OdometerRecordInput.cs b/Models/OdometerRecord/OdometerRecordInput.cs new file mode 100644 index 0000000..27179a3 --- /dev/null +++ b/Models/OdometerRecord/OdometerRecordInput.cs @@ -0,0 +1,13 @@ +namespace CarCareTracker.Models +{ + public class OdometerRecordInput + { + public int Id { get; set; } + public int VehicleId { get; set; } + public string Date { get; set; } = DateTime.Now.ToShortDateString(); + public int Mileage { get; set; } + public string Notes { get; set; } + public List Files { get; set; } = new List(); + public OdometerRecord ToOdometerRecord() { return new OdometerRecord { Id = Id, VehicleId = VehicleId, Date = DateTime.Parse(Date), Mileage = Mileage, Notes = Notes, Files = Files }; } + } +} diff --git a/Program.cs b/Program.cs index 12a4140..c0be2b6 100644 --- a/Program.cs +++ b/Program.cs @@ -24,6 +24,7 @@ builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); //configure helpers builder.Services.AddSingleton(); diff --git a/Views/API/Index.cshtml b/Views/API/Index.cshtml index 2f60ced..158caad 100644 --- a/Views/API/Index.cshtml +++ b/Views/API/Index.cshtml @@ -141,4 +141,38 @@ No Params(must be root user) -} \ No newline at end of file +} +
+
+ GET +
+
+ /api/vehicle/odometerrecords +
+
+ Returns a list of odometer records for the vehicle +
+
+ vehicleId - Id of Vehicle +
+
+
+
+ POST +
+
+ /api/vehicle/odometerrecords/add +
+
+ Returns a list of odometer records for the vehicle +
+
+ vehicleId - Id of Vehicle +
+ Body(form-data): {
+ date - Date to be entered
+ odometer - Odometer reading
+ notes - notes(optional)
+ } +
+
\ No newline at end of file diff --git a/Views/Vehicle/Index.cshtml b/Views/Vehicle/Index.cshtml index 6440912..0d7f3a5 100644 --- a/Views/Vehicle/Index.cshtml +++ b/Views/Vehicle/Index.cshtml @@ -19,6 +19,7 @@ + }
@@ -35,6 +36,9 @@ + @@ -83,6 +87,9 @@ + @@ -125,6 +132,7 @@
+
@@ -72,6 +75,8 @@ getVehicleSupplyRecords(vehicleId); } else if (mode == "PlanRecord"){ getVehiclePlanRecords(vehicleId); + } else if (mode == "OdometerRecord") { + getVehicleOdometerRecords(vehicleId); } } else { errorToast("An error has occurred, please double check the data and try again."); diff --git a/Views/Vehicle/_OdometerRecordModal.cshtml b/Views/Vehicle/_OdometerRecordModal.cshtml new file mode 100644 index 0000000..eae9515 --- /dev/null +++ b/Views/Vehicle/_OdometerRecordModal.cshtml @@ -0,0 +1,71 @@ +@model OdometerRecordInput +@{ + var isNew = Model.Id == 0; +} + + + + \ No newline at end of file diff --git a/Views/Vehicle/_OdometerRecords.cshtml b/Views/Vehicle/_OdometerRecords.cshtml new file mode 100644 index 0000000..4c37fa0 --- /dev/null +++ b/Views/Vehicle/_OdometerRecords.cshtml @@ -0,0 +1,69 @@ +@using CarCareTracker.Helper +@inject IConfigHelper config +@{ + var enableCsvImports = config.GetUserConfig(User).EnableCsvImports; + var hideZero = config.GetUserConfig(User).HideZero; +} +@model List +
+
+
+ @($"# of Odometer Records: {Model.Count()}") +
+
+ @if (enableCsvImports) + { +
+ + + +
+ } + else + { + + } +
+
+
+
+
+
+
+ +
+
+ + + + + + + + + + @foreach (OdometerRecord odometerRecord in Model) + { + + + + + + } + +
DateOdometerNotes
@odometerRecord.Date.ToShortDateString()@odometerRecord.Mileage@CarCareTracker.Helper.StaticHelper.TruncateStrings(odometerRecord.Notes)
+
+
+ + + \ No newline at end of file diff --git a/wwwroot/defaults/odometersample.csv b/wwwroot/defaults/odometersample.csv new file mode 100644 index 0000000..14f0b80 --- /dev/null +++ b/wwwroot/defaults/odometersample.csv @@ -0,0 +1,2 @@ +Date,Odometer,Notes +1/1/2024,260001,test test diff --git a/wwwroot/js/odometerrecord.js b/wwwroot/js/odometerrecord.js new file mode 100644 index 0000000..78b6d2f --- /dev/null +++ b/wwwroot/js/odometerrecord.js @@ -0,0 +1,101 @@ +function showAddOdometerRecordModal() { + $.get('/Vehicle/GetAddOdometerRecordPartialView', function (data) { + if (data) { + $("#odometerRecordModalContent").html(data); + //initiate datepicker + initDatePicker($('#odometerRecordDate')); + $('#odometerRecordModal').modal('show'); + } + }); +} +function showEditOdometerRecordModal(odometerRecordId) { + $.get(`/Vehicle/GetOdometerRecordForEditById?odometerRecordId=${odometerRecordId}`, function (data) { + if (data) { + $("#odometerRecordModalContent").html(data); + //initiate datepicker + initDatePicker($('#odometerRecordDate')); + $('#odometerRecordModal').modal('show'); + } + }); +} +function hideAddOdometerRecordModal() { + $('#odometerRecordModal').modal('hide'); +} +function deleteOdometerRecord(odometerRecordId) { + $("#workAroundInput").show(); + Swal.fire({ + title: "Confirm Deletion?", + text: "Deleted Odometer Records cannot be restored.", + showCancelButton: true, + confirmButtonText: "Delete", + confirmButtonColor: "#dc3545" + }).then((result) => { + if (result.isConfirmed) { + $.post(`/Vehicle/DeleteOdometerRecordById?odometerRecordId=${odometerRecordId}`, function (data) { + if (data) { + hideAddOdometerRecordModal(); + successToast("Odometer Record Deleted"); + var vehicleId = GetVehicleId().vehicleId; + getVehicleOdometerRecords(vehicleId); + } else { + errorToast("An error has occurred, please try again later."); + } + }); + } else { + $("#workAroundInput").hide(); + } + }); +} +function saveOdometerRecordToVehicle(isEdit) { + //get values + var formValues = getAndValidateOdometerRecordValues(); + //validate + if (formValues.hasError) { + errorToast("Please check the form data"); + return; + } + //save to db. + $.post('/Vehicle/SaveOdometerRecordToVehicleId', { odometerRecord: formValues }, function (data) { + if (data) { + successToast(isEdit ? "Odometer Record Updated" : "Odometer Record Added."); + hideAddOdometerRecordModal(); + saveScrollPosition(); + getVehicleOdometerRecords(formValues.vehicleId); + if (formValues.addReminderRecord) { + setTimeout(function () { showAddReminderModal(formValues); }, 500); + } + } else { + errorToast("An error has occurred, please try again later."); + } + }) +} +function getAndValidateOdometerRecordValues() { + var serviceDate = $("#odometerRecordDate").val(); + var serviceMileage = $("#odometerRecordMileage").val(); + var serviceNotes = $("#odometerRecordNotes").val(); + var vehicleId = GetVehicleId().vehicleId; + var odometerRecordId = getOdometerRecordModelData().id; + //validation + var hasError = false; + if (serviceDate.trim() == '') { //eliminates whitespace. + hasError = true; + $("#odometerRecordDate").addClass("is-invalid"); + } else { + $("#odometerRecordDate").removeClass("is-invalid"); + } + if (serviceMileage.trim() == '' || parseInt(serviceMileage) < 0) { + hasError = true; + $("#odometerRecordMileage").addClass("is-invalid"); + } else { + $("#odometerRecordMileage").removeClass("is-invalid"); + } + return { + id: odometerRecordId, + hasError: hasError, + vehicleId: vehicleId, + date: serviceDate, + mileage: serviceMileage, + notes: serviceNotes, + files: uploadedFiles + } +} \ No newline at end of file diff --git a/wwwroot/js/vehicle.js b/wwwroot/js/vehicle.js index e4b69b4..3dc2ab8 100644 --- a/wwwroot/js/vehicle.js +++ b/wwwroot/js/vehicle.js @@ -36,6 +36,9 @@ $(document).ready(function () { case "plan-tab": getVehiclePlanRecords(vehicleId); break; + case "odometer-tab": + getVehicleOdometerRecords(vehicleId); + break; } switch (e.relatedTarget.id) { //clear out previous tabs with grids in them to help with performance case "servicerecord-tab": @@ -68,6 +71,9 @@ $(document).ready(function () { case "plan-tab": $("#plan-tab-pane").html(""); break; + case "odometer-tab": + $("odometer-tab-pane").html(""); + break; } }); var defaultTab = GetDefaultTab().tab; @@ -102,6 +108,9 @@ $(document).ready(function () { case "PlanRecord": getVehiclePlanRecords(vehicleId); break; + case "OdometerRecord": + getVehicleOdometerRecords(vehicleId); + break; } }); @@ -131,6 +140,15 @@ function getVehiclePlanRecords(vehicleId) { } }); } +function getVehicleOdometerRecords(vehicleId) { + $.get(`/Vehicle/GetOdometerRecordsByVehicleId?vehicleId=${vehicleId}`, function (data) { + if (data) { + $("#odometer-tab-pane").html(data); + restoreScrollPosition(); + getVehicleHaveImportantReminders(vehicleId); + } + }); +} function getVehicleSupplyRecords(vehicleId) { $.get(`/Vehicle/GetSupplyRecordsByVehicleId?vehicleId=${vehicleId}`, function (data) { if (data) {