Compare commits

...

52 Commits

Author SHA1 Message Date
Hargata Softworks
b69749b073 Merge pull request #635 from hargata/Hargata/update.demo
Update demo defaults.
2024-09-24 22:35:47 -06:00
DESKTOP-T0O5CDB\DESK-555BD
951eea844f Updated demo default 2024-09-24 22:35:23 -06:00
DESKTOP-T0O5CDB\DESK-555BD
f3e5aff0c9 Updated demo file 2024-09-24 22:33:00 -06:00
Hargata Softworks
ef3e450e4f Merge pull request #634 from hargata/Hargata/api.enhancement.dos
Added documentation and new API endpoint to calculate adjusted odometer.
2024-09-24 22:05:02 -06:00
DESKTOP-T0O5CDB\DESK-555BD
8272fa20da Added documentation and new API endpoint to calculate adjusted odometer. 2024-09-24 22:04:31 -06:00
Hargata Softworks
9ef0a748a3 Merge pull request #633 from hargata/Hargata/563.619
added flag for optional odometer.
2024-09-24 19:20:30 -06:00
DESKTOP-T0O5CDB\DESK-555BD
6de928aef4 added flag for optional odometer. 2024-09-24 19:18:13 -06:00
Hargata Softworks
34ff874949 Merge pull request #632 from hargata/Hargata/563.619
make odometer optional
2024-09-24 18:43:11 -06:00
DESKTOP-T0O5CDB\DESK-555BD
a653f3ac7f make odometer optional 2024-09-24 18:36:59 -06:00
Hargata Softworks
dc96084406 Merge pull request #631 from hargata/Hargata/630
Fix light mode bug.
2024-09-24 18:30:40 -06:00
DESKTOP-T0O5CDB\DESK-555BD
e056aaa7dc Fix light mode bug. 2024-09-24 18:11:50 -06:00
Hargata Softworks
73bb3c11b4 Merge pull request #628 from hargata/Hargata/adaptive.color.mode
fix label.
2024-09-23 08:37:37 -06:00
DESKTOP-T0O5CDB\DESK-555BD
c88a040728 fix label. 2024-09-23 08:36:58 -06:00
Hargata Softworks
dfa56c2b12 Merge pull request #627 from hargata/Hargata/adaptive.color.mode
Added switch for adaptive color mode.
2024-09-23 08:34:01 -06:00
DESKTOP-T0O5CDB\DESK-555BD
104d4bab52 Added switch for adaptive color mode. 2024-09-23 08:31:54 -06:00
Hargata Softworks
6916161e87 Merge pull request #626 from hargata/Hargata/info.endpoint.update
#482 - Allow Root Users to Login via OIDC
2024-09-22 14:36:23 -06:00
DESKTOP-T0O5CDB\DESK-555BD
512852d217 added flag to enable root user to login via OIDC 2024-09-22 14:32:51 -06:00
DESKTOP-T0O5CDB\DESK-555BD
a61c699417 Added PlanRecords to info endpoint 2024-09-22 13:48:01 -06:00
Hargata Softworks
1e832f014f Merge pull request #625 from hargata/Hargata/update.links
Updated documentation links and removed static documentation.
2024-09-22 10:42:10 -06:00
DESKTOP-T0O5CDB\DESK-555BD
e9a463e2e5 Updated documentation links and removed static documentation. 2024-09-22 10:41:36 -06:00
Hargata Softworks
d0c19d0436 Merge pull request #624 from hargata/Hargata/618
#618
2024-09-22 10:24:01 -06:00
DESKTOP-T0O5CDB\DESK-555BD
3428abf358 code clean up for default reminder email and added flag to disable registration on front end. 2024-09-22 10:20:25 -06:00
Hargata Softworks
e6857331e2 Merge pull request #617 from hargata/Hargata/603
adaptive color mode if dark mode is not enabled.
2024-09-13 14:24:59 -06:00
DESKTOP-T0O5CDB\DESK-555BD
1fd93d2efe update variable name 2024-09-13 14:24:11 -06:00
DESKTOP-T0O5CDB\DESK-555BD
84e44acbfe adaptive color mode if dark mode is not enabled. 2024-09-13 07:51:32 -06:00
Hargata Softworks
77014c71f2 Merge pull request #614 from hargata/Hargata/517
Add Default Reminder Email Recipient
2024-09-12 11:15:55 -06:00
DESKTOP-T0O5CDB\DESK-555BD
2db01aa4ad reworded response message 2024-09-12 10:00:39 -06:00
DESKTOP-T0O5CDB\DESK-555BD
6c3fa21cc5 Added environment variable to configure default reminder email recipient. Fixed bug with due date in email body and improved logging and error catching when sending emails. 2024-09-12 09:43:52 -06:00
Hargata Softworks
26b7d101ab Merge pull request #610 from hargata/Hargata/609
Added custom thresholds for reminder.
2024-09-07 13:37:52 -06:00
DESKTOP-T0O5CDB\DESK-555BD
f3c3cdf0cb Added custom thresholds for reminder. 2024-09-07 08:39:26 -06:00
Hargata Softworks
e62fa31485 Merge pull request #608 from hargata/Hargata/606
See #606
2024-09-05 13:19:27 -06:00
DESKTOP-T0O5CDB\DESK-555BD
5f283c1558 uh 2024-09-05 13:11:24 -06:00
DESKTOP-T0O5CDB\DESK-555BD
52163098d2 See #606 2024-09-05 13:04:16 -06:00
Hargata Softworks
dfe27f0577 Merge pull request #605 from hargata/Hargata/598
Added colored icons for Planner items.
2024-09-04 09:23:29 -06:00
DESKTOP-T0O5CDB\DESK-555BD
e415a3468f Added colored icons for Planner items. 2024-09-03 20:13:07 -06:00
Hargata Softworks
71302a52e4 Merge pull request #602 from hargata/Hargata/light.mode.menu.fix
Fix menu color when in light mode.
2024-09-02 11:41:14 -06:00
DESKTOP-T0O5CDB\DESK-555BD
762730eb0f Fix menu color when in light mode. 2024-09-02 11:40:11 -06:00
Hargata Softworks
3999a8a54d Merge pull request #601 from hargata/Hargata/div.zero.hotfix
Fix divide by zero error in Cost Per Mile metric
2024-09-01 11:02:42 -06:00
DESKTOP-T0O5CDB\DESK-555BD
065291e146 Fix divide by zero error in Cost Per Mile metric 2024-09-01 11:02:04 -06:00
Hargata Softworks
99376aba1c Merge pull request #600 from hargata/Hargata/537
Add user-configurable dashboard metric
2024-09-01 10:48:30 -06:00
DESKTOP-T0O5CDB\DESK-555BD
698e8a0a95 fix default placeholder label for data table. 2024-08-31 23:50:17 -06:00
DESKTOP-T0O5CDB\DESK-555BD
b1b3a127dc fix bug with data table. 2024-08-31 20:28:51 -06:00
DESKTOP-T0O5CDB\DESK-555BD
ec55c95c71 Allow users to set which dashboard metric they want to see in the garage. 2024-08-31 20:28:32 -06:00
Hargata Softworks
e6eb2b473c Merge pull request #599 from hargata/Hargata/551
Added data table
2024-08-31 17:36:06 -06:00
DESKTOP-T0O5CDB\DESK-555BD
a4b39820e4 updated label. 2024-08-31 10:39:36 -06:00
DESKTOP-T0O5CDB\DESK-555BD
4a20c81047 convert to number of days and replace zero values with dashes 2024-08-31 10:33:45 -06:00
DESKTOP-T0O5CDB\DESK-555BD
b72ab461e5 revert and enhance. 2024-08-31 09:46:08 -06:00
DESKTOP-T0O5CDB\DESK-555BD
0be498d9bd Added data table 2024-08-30 15:29:09 -06:00
Hargata Softworks
00bad986e5 Merge pull request #596 from hargata/Hargata/reminder.tooltip.fix
Stats API Endpoint
2024-08-29 08:19:52 -06:00
DESKTOP-T0O5CDB\DESK-555BD
4710e84dcf more stats. 2024-08-27 12:20:50 -06:00
DESKTOP-T0O5CDB\DESK-555BD
ee55f8c884 added vehicle info API endpoint for dashboards and whatnot. 2024-08-27 12:04:57 -06:00
DESKTOP-T0O5CDB\DESK-555BD
3a74116e70 Do not load tooltips on touch screen only device. 2024-08-27 11:07:59 -06:00
78 changed files with 1033 additions and 938 deletions

View File

@@ -10,7 +10,7 @@ assignees: ''
**Checklist**
Please make sure you have performed the following steps before opening a new bug ticket, change `[ ]` to `[x]` to mark it as done
- [ ] I have read and tried the steps outlined in the [Troubleshooting Guide](https://docs.lubelogger.com/Troubleshooting)
- [ ] I have read and tried the steps outlined in the [Troubleshooting Guide](https://docs.lubelogger.com/Installation/Troubleshooting)
- [ ] I have searched through existing issues.
**Description**

View File

@@ -99,6 +99,98 @@ namespace CarCareTracker.Controllers
}
return Json(result);
}
[HttpGet]
[Route("/api/vehicle/info")]
public IActionResult VehicleInfo(int vehicleId)
{
//stats for a specific or all vehicles
List<Vehicle> vehicles = new List<Vehicle>();
if (vehicleId != default)
{
if (_userLogic.UserCanEditVehicle(GetUserID(), vehicleId))
{
vehicles.Add(_dataAccess.GetVehicleById(vehicleId));
} else
{
return new RedirectResult("/Error/Unauthorized");
}
} else
{
var result = _dataAccess.GetVehicles();
if (!User.IsInRole(nameof(UserData.IsRootUser)))
{
result = _userLogic.FilterUserVehicles(result, GetUserID());
}
vehicles.AddRange(result);
}
List<VehicleInfo> apiResult = new List<VehicleInfo>();
foreach(Vehicle vehicle in vehicles)
{
var currentMileage = _vehicleLogic.GetMaxMileage(vehicle.Id);
var reminders = _reminderRecordDataAccess.GetReminderRecordsByVehicleId(vehicle.Id);
var results = _reminderHelper.GetReminderRecordViewModels(reminders, currentMileage, DateTime.Now);
var serviceRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicle.Id);
var repairRecords = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicle.Id);
var upgradeRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicle.Id);
var gasRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicle.Id);
var taxRecords = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicle.Id);
var planRecords = _planRecordDataAccess.GetPlanRecordsByVehicleId(vehicle.Id);
var resultToAdd = new VehicleInfo()
{
VehicleData = vehicle,
LastReportedOdometer = currentMileage,
ServiceRecordCount = serviceRecords.Count(),
ServiceRecordCost = serviceRecords.Sum(x=>x.Cost),
RepairRecordCount = repairRecords.Count(),
RepairRecordCost = repairRecords.Sum(x=>x.Cost),
UpgradeRecordCount = upgradeRecords.Count(),
UpgradeRecordCost = upgradeRecords.Sum(x=>x.Cost),
GasRecordCount = gasRecords.Count(),
GasRecordCost = gasRecords.Sum(x=>x.Cost),
TaxRecordCount = taxRecords.Count(),
TaxRecordCost = taxRecords.Sum(x=> x.Cost),
VeryUrgentReminderCount = results.Count(x => x.Urgency == ReminderUrgency.VeryUrgent),
PastDueReminderCount = results.Count(x => x.Urgency == ReminderUrgency.PastDue),
UrgentReminderCount = results.Count(x => x.Urgency == ReminderUrgency.Urgent),
NotUrgentReminderCount = results.Count(x => x.Urgency == ReminderUrgency.NotUrgent),
PlanRecordBackLogCount = planRecords.Count(x=>x.Progress == PlanProgress.Backlog),
PlanRecordInProgressCount = planRecords.Count(x=>x.Progress == PlanProgress.InProgress),
PlanRecordTestingCount = planRecords.Count(x=>x.Progress == PlanProgress.Testing),
PlanRecordDoneCount = planRecords.Count(x=>x.Progress == PlanProgress.Done)
};
//set next reminder
if (results.Any(x => (x.Metric == ReminderMetric.Date || x.Metric == ReminderMetric.Both) && x.Date >= DateTime.Now.Date))
{
resultToAdd.NextReminder = results.Where(x => x.Date >= DateTime.Now.Date).OrderBy(x => x.Date).Select(x => new ReminderExportModel { Description = x.Description, Urgency = x.Urgency.ToString(), Metric = x.Metric.ToString(), Notes = x.Notes, DueDate = x.Date.ToShortDateString(), DueOdometer = x.Mileage.ToString() }).First();
}
else if (results.Any(x => (x.Metric == ReminderMetric.Odometer || x.Metric == ReminderMetric.Both) && x.Mileage >= currentMileage))
{
resultToAdd.NextReminder = results.Where(x => x.Mileage >= currentMileage).OrderBy(x => x.Mileage).Select(x => new ReminderExportModel { Description = x.Description, Urgency = x.Urgency.ToString(), Metric = x.Metric.ToString(), Notes = x.Notes, DueDate = x.Date.ToShortDateString(), DueOdometer = x.Mileage.ToString() }).First();
}
apiResult.Add(resultToAdd);
}
return Json(apiResult);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
[Route("/api/vehicle/adjustedodometer")]
public IActionResult AdjustedOdometer(int vehicleId, int odometer)
{
var vehicle = _dataAccess.GetVehicleById(vehicleId);
if (vehicle == null || !vehicle.HasOdometerAdjustment)
{
return Json(odometer);
} else
{
var convertedOdometer = (odometer + int.Parse(vehicle.OdometerDifference)) * int.Parse(vehicle.OdometerMultiplier);
return Json(convertedOdometer);
}
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
[Route("/api/vehicle/servicerecords")]
@@ -594,6 +686,7 @@ namespace CarCareTracker.Controllers
{
var vehicles = _dataAccess.GetVehicles();
List<OperationResponse> operationResponses = new List<OperationResponse>();
var defaultEmailAddress = _config.GetUserConfig(User).DefaultReminderEmail;
foreach(Vehicle vehicle in vehicles)
{
var vehicleId = vehicle.Id;
@@ -609,6 +702,10 @@ namespace CarCareTracker.Controllers
//get list of recipients.
var userIds = _userAccessDataAccess.GetUserAccessByVehicleId(vehicleId).Select(x => x.Id.UserId);
List<string> emailRecipients = new List<string>();
if (!string.IsNullOrWhiteSpace(defaultEmailAddress))
{
emailRecipients.Add(defaultEmailAddress);
}
foreach (int userId in userIds)
{
var userData = _userRecordDataAccess.GetUserRecordById(userId);
@@ -621,15 +718,19 @@ namespace CarCareTracker.Controllers
var result = _mailHelper.NotifyUserForReminders(vehicle, emailRecipients, results);
operationResponses.Add(result);
}
if (operationResponses.All(x => x.Success))
if (!operationResponses.Any())
{
return Json(new OperationResponse { Success = true, Message = "Emails sent" });
return Json(new OperationResponse { Success = false, Message = "No Emails Sent, No Vehicles Available or No Recipients Configured" });
}
else if (operationResponses.All(x => x.Success))
{
return Json(new OperationResponse { Success = true, Message = $"Emails Sent({operationResponses.Count()})" });
} else if (operationResponses.All(x => !x.Success))
{
return Json(new OperationResponse { Success = false, Message = "All emails failed, check SMTP settings" });
return Json(new OperationResponse { Success = false, Message = $"All Emails Failed({operationResponses.Count()}), Check SMTP Settings" });
} else
{
return Json(new OperationResponse { Success = true, Message = "Some emails sent, some failed, check recipient settings" });
return Json(new OperationResponse { Success = true, Message = $"Emails Sent({operationResponses.Count(x => x.Success)}), Emails Failed({operationResponses.Count(x => !x.Success)}), Check Recipient Settings" });
}
}
[Authorize(Roles = nameof(UserData.IsRootUser))]

View File

@@ -59,22 +59,50 @@ namespace CarCareTracker.Controllers
{
vehiclesStored = _userLogic.FilterUserVehicles(vehiclesStored, GetUserID());
}
var vehicleViewModels = vehiclesStored.Select(x => new VehicleViewModel
{
Id = x.Id,
ImageLocation = x.ImageLocation,
Year = x.Year,
Make = x.Make,
Model = x.Model,
LicensePlate = x.LicensePlate,
SoldDate = x.SoldDate,
IsElectric = x.IsElectric,
IsDiesel = x.IsDiesel,
UseHours = x.UseHours,
ExtraFields = x.ExtraFields,
Tags = x.Tags,
LastReportedMileage = _vehicleLogic.GetMaxMileage(x.Id),
HasReminders = _vehicleLogic.GetVehicleHasUrgentOrPastDueReminders(x.Id)
var vehicleViewModels = vehiclesStored.Select(x => {
var vehicleVM = new VehicleViewModel
{
Id = x.Id,
ImageLocation = x.ImageLocation,
Year = x.Year,
Make = x.Make,
Model = x.Model,
LicensePlate = x.LicensePlate,
SoldDate = x.SoldDate,
IsElectric = x.IsElectric,
IsDiesel = x.IsDiesel,
UseHours = x.UseHours,
OdometerOptional = x.OdometerOptional,
ExtraFields = x.ExtraFields,
Tags = x.Tags,
DashboardMetrics = x.DashboardMetrics
};
//dashboard metrics
if (x.DashboardMetrics.Any())
{
var vehicleRecords = _vehicleLogic.GetVehicleRecords(x.Id);
var userConfig = _config.GetUserConfig(User);
var distanceUnit = x.UseHours ? "h" : userConfig.UseMPG ? "mi." : "km";
if (vehicleVM.DashboardMetrics.Contains(DashboardMetric.Default))
{
vehicleVM.LastReportedMileage = _vehicleLogic.GetMaxMileage(vehicleRecords);
vehicleVM.HasReminders = _vehicleLogic.GetVehicleHasUrgentOrPastDueReminders(x.Id, vehicleVM.LastReportedMileage);
}
if (vehicleVM.DashboardMetrics.Contains(DashboardMetric.CostPerMile))
{
var vehicleTotalCost = _vehicleLogic.GetVehicleTotalCost(vehicleRecords);
var maxMileage = _vehicleLogic.GetMaxMileage(vehicleRecords);
var minMileage = _vehicleLogic.GetMinMileage(vehicleRecords);
var totalDistance = maxMileage - minMileage;
vehicleVM.CostPerMile = totalDistance != default ? vehicleTotalCost / totalDistance : 0.00M;
vehicleVM.DistanceUnit = distanceUnit;
}
if (vehicleVM.DashboardMetrics.Contains(DashboardMetric.TotalCost))
{
vehicleVM.TotalCost = _vehicleLogic.GetVehicleTotalCost(vehicleRecords);
}
}
return vehicleVM;
}).ToList();
return PartialView("_GarageDisplay", vehicleViewModels);
}

View File

@@ -51,6 +51,10 @@ namespace CarCareTracker.Controllers
}
public IActionResult Registration()
{
if (_config.GetServerDisabledRegistration())
{
return RedirectToAction("Index");
}
return View();
}
public IActionResult ForgotPassword()

View File

@@ -1140,6 +1140,7 @@ namespace CarCareTracker.Controllers
var taxRecords = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId);
var upgradeRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId);
var odometerRecords = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId);
var userConfig = _config.GetUserConfig(User);
var viewModel = new ReportViewModel();
//get totalCostMakeUp
viewModel.CostMakeUpForVehicle = new CostMakeUpForVehicle
@@ -1205,7 +1206,6 @@ namespace CarCareTracker.Controllers
var collaborators = _userLogic.GetCollaboratorsForVehicle(vehicleId);
viewModel.Collaborators = collaborators;
//get MPG per month.
var userConfig = _config.GetUserConfig(User);
var mileageData = _gasHelper.GetGasRecordViewModels(gasRecords, userConfig.UseMPG, userConfig.UseUKMPG);
mileageData.RemoveAll(x => x.MilesPerGallon == default);
var monthlyMileageData = StaticHelper.GetBaseLineCostsNoMonthName();
@@ -1272,6 +1272,45 @@ namespace CarCareTracker.Controllers
return PartialView("_CostMakeUpReport", viewModel);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
public IActionResult GetCostTableForVehicle(int vehicleId, int year = 0)
{
var vehicleRecords = _vehicleLogic.GetVehicleRecords(vehicleId);
var serviceRecords = vehicleRecords.ServiceRecords;
var gasRecords = vehicleRecords.GasRecords;
var collisionRecords = vehicleRecords.CollisionRecords;
var taxRecords = vehicleRecords.TaxRecords;
var upgradeRecords = vehicleRecords.UpgradeRecords;
var odometerRecords = vehicleRecords.OdometerRecords;
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);
odometerRecords.RemoveAll(x => x.Date.Year != year);
}
var maxMileage = _vehicleLogic.GetMaxMileage(vehicleRecords);
var minMileage = _vehicleLogic.GetMinMileage(vehicleRecords);
var vehicleData = _dataAccess.GetVehicleById(vehicleId);
var userConfig = _config.GetUserConfig(User);
var totalDistanceTraveled = maxMileage - minMileage;
var totalDays = _vehicleLogic.GetOwnershipDays(vehicleData.PurchaseDate, vehicleData.SoldDate, serviceRecords, collisionRecords, gasRecords, upgradeRecords, odometerRecords, taxRecords);
var viewModel = new CostTableForVehicle
{
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),
TotalDistance = totalDistanceTraveled,
DistanceUnit = vehicleData.UseHours ? "Cost Per Hour" : userConfig.UseMPG ? "Cost Per Mile" : "Cost Per Kilometer",
NumberOfDays = totalDays
};
return PartialView("_CostTableReport", viewModel);
}
[TypeFilter(typeof(CollaboratorFilter))]
public IActionResult GetReminderMakeUpByVehicle(int vehicleId, int daysToAdd)
{
var reminders = GetRemindersAndUrgency(vehicleId, DateTime.Now.AddDays(daysToAdd));
@@ -1694,6 +1733,8 @@ namespace CarCareTracker.Controllers
Mileage = result.Mileage,
Metric = result.Metric,
IsRecurring = result.IsRecurring,
UseCustomThresholds = result.UseCustomThresholds,
CustomThresholds = result.CustomThresholds,
ReminderMileageInterval = result.ReminderMileageInterval,
ReminderMonthInterval = result.ReminderMonthInterval,
CustomMileageInterval = result.CustomMileageInterval,

9
Enum/DashboardMetric.cs Normal file
View File

@@ -0,0 +1,9 @@
namespace CarCareTracker.Models
{
public enum DashboardMetric
{
Default = 0,
TotalCost = 1,
CostPerMile = 2
}
}

View File

@@ -12,10 +12,12 @@ namespace CarCareTracker.Helper
UserConfig GetUserConfig(ClaimsPrincipal user);
bool SaveUserConfig(ClaimsPrincipal user, UserConfig configData);
bool AuthenticateRootUser(string username, string password);
bool AuthenticateRootUserOIDC(string email);
string GetWebHookUrl();
string GetMOTD();
string GetLogoUrl();
string GetServerLanguage();
bool GetServerDisabledRegistration();
bool GetServerEnableShopSupplies();
string GetServerPostgresConnection();
string GetAllowedFileUploadExtensions();
@@ -89,11 +91,26 @@ namespace CarCareTracker.Helper
}
return username == rootUsername && password == rootPassword;
}
public bool AuthenticateRootUserOIDC(string email)
{
var rootEmail = _config[nameof(UserConfig.DefaultReminderEmail)] ?? string.Empty;
var rootUserOIDC = bool.Parse(_config[nameof(UserConfig.EnableRootUserOIDC)]);
if (!rootUserOIDC || string.IsNullOrWhiteSpace(rootEmail))
{
return false;
}
return email == rootEmail;
}
public string GetServerLanguage()
{
var serverLanguage = _config[nameof(UserConfig.UserLanguage)] ?? "en_US";
return serverLanguage;
}
public bool GetServerDisabledRegistration()
{
var registrationDisabled = bool.Parse(_config[nameof(UserConfig.DisableRegistration)]);
return registrationDisabled;
}
public string GetServerPostgresConnection()
{
if (!string.IsNullOrWhiteSpace(_config["POSTGRES_CONNECTION"]))
@@ -162,9 +179,11 @@ namespace CarCareTracker.Helper
{
EnableCsvImports = bool.Parse(_config[nameof(UserConfig.EnableCsvImports)]),
UseDarkMode = bool.Parse(_config[nameof(UserConfig.UseDarkMode)]),
UseSystemColorMode = bool.Parse(_config[nameof(UserConfig.UseSystemColorMode)]),
UseMPG = bool.Parse(_config[nameof(UserConfig.UseMPG)]),
UseDescending = bool.Parse(_config[nameof(UserConfig.UseDescending)]),
EnableAuth = bool.Parse(_config[nameof(UserConfig.EnableAuth)]),
EnableRootUserOIDC = bool.Parse(_config[nameof(UserConfig.EnableRootUserOIDC)]),
HideZero = bool.Parse(_config[nameof(UserConfig.HideZero)]),
UseUKMPG = bool.Parse(_config[nameof(UserConfig.UseUKMPG)]),
UseMarkDownOnSavedNotes = bool.Parse(_config[nameof(UserConfig.UseMarkDownOnSavedNotes)]),
@@ -180,7 +199,9 @@ namespace CarCareTracker.Helper
VisibleTabs = _config.GetSection(nameof(UserConfig.VisibleTabs)).Get<List<ImportMode>>(),
UserColumnPreferences = _config.GetSection(nameof(UserConfig.UserColumnPreferences)).Get<List<UserColumnPreference>>() ?? new List<UserColumnPreference>(),
ReminderUrgencyConfig = _config.GetSection(nameof(UserConfig.ReminderUrgencyConfig)).Get<ReminderUrgencyConfig>() ?? new ReminderUrgencyConfig(),
DefaultTab = (ImportMode)int.Parse(_config[nameof(UserConfig.DefaultTab)])
DefaultTab = (ImportMode)int.Parse(_config[nameof(UserConfig.DefaultTab)]),
DefaultReminderEmail = _config[nameof(UserConfig.DefaultReminderEmail)],
DisableRegistration = bool.Parse(_config[nameof(UserConfig.DisableRegistration)])
};
int userId = 0;
if (user != null)

View File

@@ -57,6 +57,10 @@ namespace CarCareTracker.Helper
if (i > 0)
{
var deltaMileage = currentObject.Mileage - previousMileage;
if (deltaMileage < 0)
{
deltaMileage = 0;
}
var gasRecordViewModel = new GasRecordViewModel()
{
Id = currentObject.Id,

View File

@@ -113,14 +113,20 @@ namespace CarCareTracker.Helper
string tableBody = "";
foreach(ReminderRecordViewModel reminder in reminders)
{
var dueOn = reminder.Metric == ReminderMetric.Both ? $"{reminder.Date} or {reminder.Mileage}" : reminder.Metric == ReminderMetric.Date ? $"{reminder.Date.ToShortDateString()}" : $"{reminder.Mileage}";
var dueOn = reminder.Metric == ReminderMetric.Both ? $"{reminder.Date.ToShortDateString()} or {reminder.Mileage}" : reminder.Metric == ReminderMetric.Date ? $"{reminder.Date.ToShortDateString()}" : $"{reminder.Mileage}";
tableBody += $"<tr class='{reminder.Urgency}'><td>{StaticHelper.GetTitleCaseReminderUrgency(reminder.Urgency)}</td><td>{reminder.Description}</td><td>{dueOn}</td></tr>";
}
emailBody = emailBody.Replace("{TableBody}", tableBody);
try
{
SendEmail(emailAddresses, emailSubject, emailBody);
return new OperationResponse { Success = true, Message = "Email Sent!" };
var result = SendEmail(emailAddresses, emailSubject, emailBody);
if (result)
{
return new OperationResponse { Success = true, Message = "Email Sent!" };
} else
{
return new OperationResponse { Success = false, Message = StaticHelper.GenericErrorMessage };
}
} catch (Exception ex)
{
return new OperationResponse { Success = false, Message = ex.Message };

View File

@@ -66,6 +66,10 @@ namespace CarCareTracker.Helper
var reminderUrgencyConfig = _config.GetReminderUrgencyConfig();
foreach (var reminder in reminders)
{
if (reminder.UseCustomThresholds)
{
reminderUrgencyConfig = reminder.CustomThresholds;
}
var reminderViewModel = new ReminderRecordViewModel()
{
Id = reminder.Id,

View File

@@ -9,7 +9,7 @@ namespace CarCareTracker.Helper
/// </summary>
public static class StaticHelper
{
public static string VersionNumber = "1.3.6";
public static string VersionNumber = "1.3.8";
public static string DbName = "data/cartracker.db";
public static string UserConfigPath = "config/userConfig.json";
public static string GenericErrorMessage = "An error occurred, please try again later";

View File

@@ -245,14 +245,7 @@ namespace CarCareTracker.Logic
{
if (UserIsRoot(credentials))
{
return new UserData()
{
Id = -1,
UserName = credentials.UserName,
IsAdmin = true,
IsRootUser = true,
EmailAddress = string.Empty
};
return GetRootUserData(credentials.UserName);
}
else
{
@@ -271,6 +264,13 @@ namespace CarCareTracker.Logic
}
public UserData ValidateOpenIDUser(LoginModel credentials)
{
//validate for root user
var isRootUser = _configHelper.AuthenticateRootUserOIDC(credentials.EmailAddress);
if (isRootUser)
{
return GetRootUserData(credentials.EmailAddress);
}
var result = _userData.GetUserRecordByEmailAddress(credentials.EmailAddress);
if (result.Id != default)
{
@@ -420,6 +420,17 @@ namespace CarCareTracker.Logic
var hashedPassword = GetHash(credentials.Password);
return _configHelper.AuthenticateRootUser(hashedUserName, hashedPassword);
}
private UserData GetRootUserData(string username)
{
return new UserData()
{
Id = -1,
UserName = username,
IsAdmin = true,
IsRootUser = true,
EmailAddress = string.Empty
};
}
#endregion
private static string GetHash(string value)
{

View File

@@ -33,6 +33,10 @@ namespace CarCareTracker.Logic
}
public bool AutoInsertOdometerRecord(OdometerRecord odometer)
{
if (odometer.Mileage == default)
{
return false;
}
var lastReportedMileage = GetLastOdometerRecordMileage(odometer.VehicleId, new List<OdometerRecord>());
odometer.InitialMileage = lastReportedMileage != default ? lastReportedMileage : odometer.Mileage;

View File

@@ -6,9 +6,14 @@ namespace CarCareTracker.Logic
{
public interface IVehicleLogic
{
VehicleRecords GetVehicleRecords(int vehicleId);
decimal GetVehicleTotalCost(VehicleRecords vehicleRecords);
int GetMaxMileage(int vehicleId);
int GetMaxMileage(VehicleRecords vehicleRecords);
int GetMinMileage(int vehicleId);
bool GetVehicleHasUrgentOrPastDueReminders(int vehicleId);
int GetMinMileage(VehicleRecords vehicleRecords);
int GetOwnershipDays(string purchaseDate, string soldDate, List<ServiceRecord> serviceRecords, List<CollisionRecord> repairRecords, List<GasRecord> gasRecords, List<UpgradeRecord> upgradeRecords, List<OdometerRecord> odometerRecords, List<TaxRecord> taxRecords);
bool GetVehicleHasUrgentOrPastDueReminders(int vehicleId, int currentMileage);
}
public class VehicleLogic: IVehicleLogic
{
@@ -16,6 +21,7 @@ namespace CarCareTracker.Logic
private readonly IGasRecordDataAccess _gasRecordDataAccess;
private readonly ICollisionRecordDataAccess _collisionRecordDataAccess;
private readonly IUpgradeRecordDataAccess _upgradeRecordDataAccess;
private readonly ITaxRecordDataAccess _taxRecordDataAccess;
private readonly IOdometerRecordDataAccess _odometerRecordDataAccess;
private readonly IReminderRecordDataAccess _reminderRecordDataAccess;
private readonly IReminderHelper _reminderHelper;
@@ -24,6 +30,7 @@ namespace CarCareTracker.Logic
IGasRecordDataAccess gasRecordDataAccess,
ICollisionRecordDataAccess collisionRecordDataAccess,
IUpgradeRecordDataAccess upgradeRecordDataAccess,
ITaxRecordDataAccess taxRecordDataAccess,
IOdometerRecordDataAccess odometerRecordDataAccess,
IReminderRecordDataAccess reminderRecordDataAccess,
IReminderHelper reminderHelper
@@ -32,10 +39,32 @@ namespace CarCareTracker.Logic
_gasRecordDataAccess = gasRecordDataAccess;
_collisionRecordDataAccess = collisionRecordDataAccess;
_upgradeRecordDataAccess = upgradeRecordDataAccess;
_taxRecordDataAccess = taxRecordDataAccess;
_odometerRecordDataAccess = odometerRecordDataAccess;
_reminderRecordDataAccess = reminderRecordDataAccess;
_reminderHelper = reminderHelper;
}
public VehicleRecords GetVehicleRecords(int vehicleId)
{
return new VehicleRecords
{
ServiceRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId),
GasRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId),
CollisionRecords = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId),
TaxRecords = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId),
UpgradeRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId),
OdometerRecords = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId),
};
}
public decimal GetVehicleTotalCost(VehicleRecords vehicleRecords)
{
var serviceRecordSum = vehicleRecords.ServiceRecords.Sum(x => x.Cost);
var repairRecordSum = vehicleRecords.CollisionRecords.Sum(x => x.Cost);
var upgradeRecordSum = vehicleRecords.UpgradeRecords.Sum(x => x.Cost);
var taxRecordSum = vehicleRecords.TaxRecords.Sum(x => x.Cost);
var gasRecordSum = vehicleRecords.GasRecords.Sum(x => x.Cost);
return serviceRecordSum + repairRecordSum + upgradeRecordSum + taxRecordSum + gasRecordSum;
}
public int GetMaxMileage(int vehicleId)
{
var numbersArray = new List<int>();
@@ -66,6 +95,31 @@ namespace CarCareTracker.Logic
}
return numbersArray.Any() ? numbersArray.Max() : 0;
}
public int GetMaxMileage(VehicleRecords vehicleRecords)
{
var numbersArray = new List<int>();
if (vehicleRecords.ServiceRecords.Any())
{
numbersArray.Add(vehicleRecords.ServiceRecords.Max(x => x.Mileage));
}
if (vehicleRecords.CollisionRecords.Any())
{
numbersArray.Add(vehicleRecords.CollisionRecords.Max(x => x.Mileage));
}
if (vehicleRecords.GasRecords.Any())
{
numbersArray.Add(vehicleRecords.GasRecords.Max(x => x.Mileage));
}
if (vehicleRecords.UpgradeRecords.Any())
{
numbersArray.Add(vehicleRecords.UpgradeRecords.Max(x => x.Mileage));
}
if (vehicleRecords.OdometerRecords.Any())
{
numbersArray.Add(vehicleRecords.OdometerRecords.Max(x => x.Mileage));
}
return numbersArray.Any() ? numbersArray.Max() : 0;
}
public int GetMinMileage(int vehicleId)
{
var numbersArray = new List<int>();
@@ -96,9 +150,70 @@ namespace CarCareTracker.Logic
}
return numbersArray.Any() ? numbersArray.Min() : 0;
}
public bool GetVehicleHasUrgentOrPastDueReminders(int vehicleId)
public int GetMinMileage(VehicleRecords vehicleRecords)
{
var numbersArray = new List<int>();
var _serviceRecords = vehicleRecords.ServiceRecords.Where(x => x.Mileage != default).ToList();
if (_serviceRecords.Any())
{
numbersArray.Add(_serviceRecords.Min(x => x.Mileage));
}
var _repairRecords = vehicleRecords.CollisionRecords.Where(x => x.Mileage != default).ToList();
if (_repairRecords.Any())
{
numbersArray.Add(_repairRecords.Min(x => x.Mileage));
}
var _gasRecords = vehicleRecords.GasRecords.Where(x => x.Mileage != default).ToList();
if (_gasRecords.Any())
{
numbersArray.Add(_gasRecords.Min(x => x.Mileage));
}
var _upgradeRecords = vehicleRecords.UpgradeRecords.Where(x => x.Mileage != default).ToList();
if (_upgradeRecords.Any())
{
numbersArray.Add(_upgradeRecords.Min(x => x.Mileage));
}
var _odometerRecords = vehicleRecords.OdometerRecords.Where(x => x.Mileage != default).ToList();
if (_odometerRecords.Any())
{
numbersArray.Add(_odometerRecords.Min(x => x.Mileage));
}
return numbersArray.Any() ? numbersArray.Min() : 0;
}
public int GetOwnershipDays(string purchaseDate, string soldDate, List<ServiceRecord> serviceRecords, List<CollisionRecord> repairRecords, List<GasRecord> gasRecords, List<UpgradeRecord> upgradeRecords, List<OdometerRecord> odometerRecords, List<TaxRecord> taxRecords)
{
var startDate = DateTime.Now;
var endDate = DateTime.Now;
if (!string.IsNullOrWhiteSpace(soldDate))
{
endDate = DateTime.Parse(soldDate);
}
if (!string.IsNullOrWhiteSpace(purchaseDate))
{
//if purchase date is provided, then we just have to subtract the begin date to end date and return number of months
startDate = DateTime.Parse(purchaseDate);
var timeElapsed = (int)Math.Floor((endDate - startDate).TotalDays);
return timeElapsed;
}
var dateArray = new List<DateTime>();
dateArray.AddRange(serviceRecords.Select(x => x.Date));
dateArray.AddRange(repairRecords.Select(x => x.Date));
dateArray.AddRange(gasRecords.Select(x => x.Date));
dateArray.AddRange(upgradeRecords.Select(x => x.Date));
dateArray.AddRange(odometerRecords.Select(x => x.Date));
dateArray.AddRange(taxRecords.Select(x => x.Date));
if (dateArray.Any())
{
startDate = dateArray.Min();
var timeElapsed = (int)Math.Floor((endDate - startDate).TotalDays);
return timeElapsed;
} else
{
return 1;
}
}
public bool GetVehicleHasUrgentOrPastDueReminders(int vehicleId, int currentMileage)
{
var currentMileage = GetMaxMileage(vehicleId);
var reminders = _reminderRecordDataAccess.GetReminderRecordsByVehicleId(vehicleId);
var results = _reminderHelper.GetReminderRecordViewModels(reminders, currentMileage, DateTime.Now);
return results.Any(x => x.Urgency == ReminderUrgency.VeryUrgent || x.Urgency == ReminderUrgency.PastDue);

27
Models/API/VehicleInfo.cs Normal file
View File

@@ -0,0 +1,27 @@
namespace CarCareTracker.Models
{
public class VehicleInfo
{
public Vehicle VehicleData { get; set; } = new Vehicle();
public int VeryUrgentReminderCount { get; set; }
public int UrgentReminderCount { get; set;}
public int NotUrgentReminderCount { get; set; }
public int PastDueReminderCount { get; set; }
public ReminderExportModel NextReminder { get; set; }
public int ServiceRecordCount { get; set; }
public decimal ServiceRecordCost { get; set; }
public int RepairRecordCount { get; set; }
public decimal RepairRecordCost { get; set; }
public int UpgradeRecordCount { get; set; }
public decimal UpgradeRecordCost { get; set; }
public int TaxRecordCount { get; set; }
public decimal TaxRecordCost { get; set; }
public int GasRecordCount { get; set; }
public decimal GasRecordCost { get; set; }
public int LastReportedOdometer { get; set; }
public int PlanRecordBackLogCount { get; set; }
public int PlanRecordInProgressCount { get; set; }
public int PlanRecordTestingCount { get; set; }
public int PlanRecordDoneCount { get; set; }
}
}

View File

@@ -9,6 +9,8 @@
public string Description { get; set; }
public string Notes { get; set; }
public bool IsRecurring { get; set; } = false;
public bool UseCustomThresholds { get; set; } = false;
public ReminderUrgencyConfig CustomThresholds { get; set; } = new ReminderUrgencyConfig();
public int CustomMileageInterval { get; set; } = 0;
public int CustomMonthInterval { get; set; } = 0;
public ReminderMileageInterval ReminderMileageInterval { get; set; } = ReminderMileageInterval.FiveThousandMiles;

View File

@@ -9,6 +9,8 @@
public string Description { get; set; }
public string Notes { get; set; }
public bool IsRecurring { get; set; } = false;
public bool UseCustomThresholds { get; set; } = false;
public ReminderUrgencyConfig CustomThresholds { get; set; } = new ReminderUrgencyConfig();
public int CustomMileageInterval { get; set; } = 0;
public int CustomMonthInterval { get; set; } = 0;
public ReminderMileageInterval ReminderMileageInterval { get; set; } = ReminderMileageInterval.FiveThousandMiles;
@@ -26,6 +28,8 @@
Description = Description,
Metric = Metric,
IsRecurring = IsRecurring,
UseCustomThresholds = UseCustomThresholds,
CustomThresholds = CustomThresholds,
ReminderMileageInterval = ReminderMileageInterval,
ReminderMonthInterval = ReminderMonthInterval,
CustomMileageInterval = CustomMileageInterval,

View File

@@ -0,0 +1,27 @@
namespace CarCareTracker.Models
{
public class CostTableForVehicle
{
public string DistanceUnit { get; set; } = "Cost Per Mile";
public int TotalDistance { get; set; }
public int NumberOfDays { get; set; }
public decimal ServiceRecordSum { get; set; }
public decimal GasRecordSum { get; set; }
public decimal TaxRecordSum { get; set; }
public decimal CollisionRecordSum { get; set; }
public decimal UpgradeRecordSum { get; set; }
public decimal ServiceRecordPerMile { get { return TotalDistance != default ? ServiceRecordSum / TotalDistance : 0; } }
public decimal GasRecordPerMile { get { return TotalDistance != default ? GasRecordSum / TotalDistance : 0; } }
public decimal CollisionRecordPerMile { get { return TotalDistance != default ? CollisionRecordSum / TotalDistance : 0; } }
public decimal UpgradeRecordPerMile { get { return TotalDistance != default ? UpgradeRecordSum / TotalDistance : 0; } }
public decimal TaxRecordPerMile { get { return TotalDistance != default ? TaxRecordSum / TotalDistance : 0; } }
public decimal ServiceRecordPerDay { get { return NumberOfDays != default ? ServiceRecordSum / NumberOfDays : 0; } }
public decimal GasRecordPerDay { get { return NumberOfDays != default ? GasRecordSum / NumberOfDays : 0; } }
public decimal CollisionRecordPerDay { get { return NumberOfDays != default ? CollisionRecordSum / NumberOfDays : 0; } }
public decimal UpgradeRecordPerDay { get { return NumberOfDays != default ? UpgradeRecordSum / NumberOfDays : 0; } }
public decimal TaxRecordPerDay { get { return NumberOfDays != default ? TaxRecordSum / NumberOfDays : 0; } }
public decimal TotalPerDay { get { return ServiceRecordPerDay + CollisionRecordPerDay + UpgradeRecordPerDay + GasRecordPerDay + TaxRecordPerDay; } }
public decimal TotalPerMile { get { return ServiceRecordPerMile + CollisionRecordPerMile + UpgradeRecordPerMile + GasRecordPerMile + TaxRecordPerMile; } }
public decimal TotalCost { get { return ServiceRecordSum + CollisionRecordSum + UpgradeRecordSum + GasRecordSum + TaxRecordSum; } }
}
}

View File

@@ -0,0 +1,12 @@
namespace CarCareTracker.Models
{
public class VehicleRecords
{
public List<ServiceRecord> ServiceRecords { get; set; } = new List<ServiceRecord>();
public List<CollisionRecord> CollisionRecords { get; set; } = new List<CollisionRecord>();
public List<UpgradeRecord> UpgradeRecords { get; set; } = new List<UpgradeRecord>();
public List<GasRecord> GasRecords { get; set; } = new List<GasRecord>();
public List<TaxRecord> TaxRecords { get; set; } = new List<TaxRecord>();
public List<OdometerRecord> OdometerRecords { get; set; } = new List<OdometerRecord>();
}
}

View File

@@ -3,10 +3,13 @@
public class UserConfig
{
public bool UseDarkMode { get; set; }
public bool UseSystemColorMode { get; set; }
public bool EnableCsvImports { get; set; }
public bool UseMPG { get; set; }
public bool UseDescending { get; set; }
public bool EnableAuth { get; set; }
public bool DisableRegistration { get; set; }
public bool EnableRootUserOIDC { get; set; }
public bool HideZero { get; set; }
public bool UseUKMPG {get;set;}
public bool UseThreeDecimalGasCost { get; set; }
@@ -22,6 +25,7 @@
public ReminderUrgencyConfig ReminderUrgencyConfig { get; set; } = new ReminderUrgencyConfig();
public string UserNameHash { get; set; }
public string UserPasswordHash { get; set;}
public string DefaultReminderEmail { get; set; } = string.Empty;
public string UserLanguage { get; set; } = "en_US";
public List<ImportMode> VisibleTabs { get; set; } = new List<ImportMode>() {
ImportMode.Dashboard,

View File

@@ -15,6 +15,7 @@
public bool IsElectric { get; set; } = false;
public bool IsDiesel { get; set; } = false;
public bool UseHours { get; set; } = false;
public bool OdometerOptional { get; set; } = false;
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
public List<string> Tags { get; set; } = new List<string>();
public bool HasOdometerAdjustment { get; set; } = false;
@@ -26,5 +27,6 @@
/// Primarily used for vehicles where the odometer does not reflect actual mileage.
/// </summary>
public string OdometerDifference { get; set; } = "0";
public List<DashboardMetric> DashboardMetrics { get; set; } = new List<DashboardMetric>();
}
}

View File

@@ -12,9 +12,15 @@
public bool IsElectric { get; set; } = false;
public bool IsDiesel { get; set; } = false;
public bool UseHours { get; set; } = false;
public bool OdometerOptional { get; set; } = false;
public List<ExtraField> ExtraFields { get; set; } = new List<ExtraField>();
public List<string> Tags { get; set; } = new List<string>();
public int LastReportedMileage;
public bool HasReminders = false;
//Dashboard Metric Attributes
public List<DashboardMetric> DashboardMetrics { get; set; } = new List<DashboardMetric>();
public int LastReportedMileage { get; set; }
public bool HasReminders { get; set; } = false;
public decimal CostPerMile { get; set; }
public decimal TotalCost { get; set; }
public string DistanceUnit { get; set; }
}
}

View File

@@ -20,7 +20,7 @@ Try it out before you download it! The live demo resets every 20 minutes.
## Download
LubeLogger is available as both a Docker Image and a Windows Standalone Executable.
Read this [Getting Started Guide](https://docs.lubelogger.com/Getting%20Started) on how to download either of them
Read this [Getting Started Guide](https://docs.lubelogger.com/Installation/Getting%20Started) on how to download either of them
### Kubernetes Deployment
[Helm Chart](https://artifacthub.io/packages/helm/anza-labs/lubelogger) provided by [Anza-Labs](https://github.com/anza-labs)
@@ -28,7 +28,7 @@ Read this [Getting Started Guide](https://docs.lubelogger.com/Getting%20Started)
### Need Help?
[Documentation](https://docs.lubelogger.com/)
[Troubleshooting Guide](https://docs.lubelogger.com/Troubleshooting)
[Troubleshooting Guide](https://docs.lubelogger.com/Installation/Troubleshooting)
[Search Existing Issues](https://github.com/hargata/lubelog/issues)

View File

@@ -40,6 +40,36 @@
No Params
</div>
</div>
<div class="row">
<div class="col-1">
GET
</div>
<div class="col-5 copyable">
<code>/api/vehicle/info</code>
</div>
<div class="col-3">
Returns details for list of vehicles or specific vehicle
</div>
<div class="col-3">
VehicleId - Id of Vehicle(optional)
</div>
</div>
<div class="row">
<div class="col-1">
GET
</div>
<div class="col-5 copyable">
<code>/api/vehicle/adjustedodometer</code>
</div>
<div class="col-3">
Returns odometer reading with adjustments applied
</div>
<div class="col-3">
vehicleId - Id of Vehicle
<br />
odometer - Unadjusted odometer
</div>
</div>
<div class="row">
<div class="col-1">
GET
@@ -86,6 +116,7 @@
initialOdometer - Initial Odometer reading(optional)<br />
odometer - Odometer reading<br />
notes - notes(optional)<br />
extrafields - <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover reminder-calendar-item" onclick="showExtraFieldsInfo()">extrafields(optional)</a><br />
}
</div>
</div>
@@ -122,6 +153,7 @@
description - Description<br/>
cost - Cost<br />
notes - notes(optional)<br />
extrafields - <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover reminder-calendar-item" onclick="showExtraFieldsInfo()">extrafields(optional)</a><br />
}
</div>
</div>
@@ -158,6 +190,7 @@
description - Description<br />
cost - Cost<br />
notes - notes(optional)<br />
extrafields - <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover reminder-calendar-item" onclick="showExtraFieldsInfo()">extrafields(optional)</a><br />
}
</div>
</div>
@@ -194,6 +227,7 @@
description - Description<br />
cost - Cost<br />
notes - notes(optional)<br />
extrafields - <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover reminder-calendar-item" onclick="showExtraFieldsInfo()">extrafields(optional)</a><br />
}
</div>
</div>
@@ -229,6 +263,7 @@
description - Description<br />
cost - Cost<br />
notes - notes(optional)<br />
extrafields - <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover reminder-calendar-item" onclick="showExtraFieldsInfo()">extrafields(optional)</a><br />
}
</div>
</div>
@@ -271,6 +306,7 @@
isFillToFull(bool) - Filled To Full<br />
missedFuelUp(bool) - Missed Fuel Up<br />
notes - notes(optional)<br />
extrafields - <a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover reminder-calendar-item" onclick="showExtraFieldsInfo()">extrafields(optional)</a><br />
}
</div>
</div>
@@ -339,4 +375,11 @@
$('.copyable').on('click', function (e) {
copyToClipboard(e.currentTarget);
})
function showExtraFieldsInfo(){
Swal.fire({
title: "Format for Extra Fields",
html: "extrafields[i][name] - Name of Extra Field<br/>extrafields[i][value] - Value of Extra Field<br/>i is an integer that starts at 0 and increments with each extrafield",
icon: "info"
});
}
</script>

View File

@@ -13,6 +13,7 @@
}
@section Scripts {
<script src="~/js/garage.js?v=@StaticHelper.VersionNumber"></script>
<script src="~/js/settings.js?v=@StaticHelper.VersionNumber"></script>
<script src="~/js/supplyrecord.js?v=@StaticHelper.VersionNumber"></script>
<script src="~/lib/drawdown/drawdown.js"></script>
}
@@ -63,7 +64,7 @@
<div class="d-flex lubelogger-navbar">
<img src="@logoUrl" />
<div class="lubelogger-navbar-button">
<button type="button" class="btn btn-dark" onclick="showMobileNav()"><i class="bi bi-list lubelogger-menu-icon"></i></button>
<button type="button" class="btn btn-adaptive" onclick="showMobileNav()"><i class="bi bi-list lubelogger-menu-icon"></i></button>
</div>
</div>
</div>

View File

@@ -36,21 +36,40 @@
@if (!string.IsNullOrWhiteSpace(vehicle.SoldDate))
{
<div class="vehicle-sold-banner"><p class='display-6 mb-0'>@translator.Translate(userLanguage, "SOLD")</p></div>
} else if (vehicle.LastReportedMileage != default)
} else if (vehicle.DashboardMetrics.Any())
{
<div class="vehicle-sold-banner">
<div class="d-flex justify-content-between">
<div>
<span class="ms-2"><i class="bi bi-speedometer me-2"></i>@vehicle.LastReportedMileage.ToString("N0")</span>
</div>
@if (vehicle.HasReminders)
{
@if (vehicle.DashboardMetrics.Contains(DashboardMetric.Default) && vehicle.LastReportedMileage != default)
{
<div class="d-flex justify-content-between">
<div>
<span class="me-2"><i class="bi bi bi-bell-fill text-warning"></i></span>
<span class="ms-2"><i class="bi bi-speedometer me-2"></i>@vehicle.LastReportedMileage.ToString("N0")</span>
</div>
}
</div>
<span></span>
@if (vehicle.HasReminders)
{
<div>
<span class="me-2"><i class="bi bi bi-bell-fill text-warning"></i></span>
</div>
}
</div>
}
@if (vehicle.DashboardMetrics.Contains(DashboardMetric.CostPerMile) && vehicle.CostPerMile != default)
{
<div class="d-flex justify-content-between">
<div>
<span class="ms-2"><i class="bi bi-cash-coin me-2"></i>@($"{vehicle.CostPerMile.ToString("C2")}/{vehicle.DistanceUnit}")</span>
</div>
</div>
}
@if (vehicle.DashboardMetrics.Contains(DashboardMetric.TotalCost) && vehicle.TotalCost != default)
{
<div class="d-flex justify-content-between">
<div>
<span class="ms-2"><i class="bi bi-cash-coin me-2"></i>@($"{vehicle.TotalCost.ToString("C2")}")</span>
</div>
</div>
}
</div>
}
<div class="card-body">

View File

@@ -13,10 +13,14 @@
<div class="col-12 col-md-6">
<input id="preferredGasUnit" style="display:none;" value="@Model.UserConfig.PreferredGasUnit" />
<input id="preferredFuelMileageUnit" style="display:none;" value="@Model.UserConfig.PreferredGasMileageUnit" />
<div class="form-check form-switch">
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="enableDarkMode" checked="@Model.UserConfig.UseDarkMode">
<div class="form-check form-switch form-check-inline">
<input class="form-check-input" onChange="updateColorModeSettings(this)" type="checkbox" role="switch" id="enableDarkMode" checked="@Model.UserConfig.UseDarkMode">
<label class="form-check-label" for="enableDarkMode">@translator.Translate(userLanguage, "Dark Mode")</label>
</div>
<div class="form-check form-switch form-check-inline">
<input class="form-check-input" onChange="updateColorModeSettings(this)" type="checkbox" role="switch" id="useSystemColorMode" checked="@Model.UserConfig.UseSystemColorMode">
<label class="form-check-label" for="useSystemColorMode">@translator.Translate(userLanguage, "Adaptive Color Mode")</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="enableCsvImports" checked="@Model.UserConfig.EnableCsvImports">
<label class="form-check-label" for="enableCsvImports">@translator.Translate(userLanguage, "Enable CSV Imports")</label>
@@ -71,6 +75,17 @@
<input class="form-check-input" onChange="enableAuthCheckChanged()" type="checkbox" role="switch" id="enableAuth" checked="@Model.UserConfig.EnableAuth">
<label class="form-check-label" for="enableAuth">@translator.Translate(userLanguage, "Enable Authentication")</label>
</div>
@if (Model.UserConfig.EnableAuth)
{
<div class="form-check form-switch">
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="disableRegistration" checked="@Model.UserConfig.DisableRegistration">
<label class="form-check-label" for="disableRegistration">@translator.Translate(userLanguage, "Disable Registration")</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="enableRootUserOIDC" checked="@Model.UserConfig.EnableRootUserOIDC">
<label class="form-check-label" for="enableRootUserOIDC">@translator.Translate(userLanguage, "Enable OIDC for Root User")<br /><small class="text-body-secondary">@translator.Translate(userLanguage, "Uses the Default Reminder Email for OIDC Auth")</small></label>
</div>
}
}
</div>
<div class="col-12 col-md-6">
@@ -198,6 +213,19 @@
</div>
</div>
</div>
<div class="col-12 col-md-6">
<span class="lead">@translator.Translate(userLanguage, "Default Reminder Email")</span>
<div class="row">
<div class="col-12 d-grid">
<div class="input-group">
<input id="inputDefaultEmail" class="form-control" placeholder="@translator.Translate(userLanguage,"Default Email for Reminder")" value="@Model.UserConfig.DefaultReminderEmail">
<div class="input-group-text">
<button type="button" class="btn btn-sm btn-primary zero-y-padding" onclick="updateSettings()"><i class="bi bi-floppy"></i></button>
</div>
</div>
</div>
</div>
</div>
</div>
}
</div>
@@ -306,137 +334,6 @@
}
});
}
function showExtraFieldModal() {
$.get(`/Home/GetExtraFieldsModal?importMode=0`, function (data) {
$("#extraFieldModalContent").html(data);
$("#extraFieldModal").modal('show');
});
}
function hideExtraFieldModal() {
$("#extraFieldModal").modal('hide');
}
function getCheckedTabs() {
var visibleTabs = $("#visibleTabs :checked").map(function () {
return this.value;
});
return visibleTabs.toArray();
}
function deleteLanguage() {
var languageFileLocation = `/translations/${$("#defaultLanguage").val()}.json`;
$.post('/Files/DeleteFiles', { fileLocation: languageFileLocation }, function (data) {
//reset user language back to en_US
$("#defaultLanguage").val('en_US');
updateSettings();
});
}
function updateSettings() {
var visibleTabs = getCheckedTabs();
var defaultTab = $("#defaultTab").val();
if (!visibleTabs.includes(defaultTab)) {
defaultTab = "Dashboard"; //default to dashboard.
}
var userConfigObject = {
useDarkMode: $("#enableDarkMode").is(':checked'),
enableCsvImports: $("#enableCsvImports").is(':checked'),
useMPG: $("#useMPG").is(':checked'),
useDescending: $("#useDescending").is(':checked'),
hideZero: $("#hideZero").is(":checked"),
useUKMpg: $("#useUKMPG").is(":checked"),
useThreeDecimalGasCost: $("#useThreeDecimal").is(":checked"),
useMarkDownOnSavedNotes: $("#useMarkDownOnSavedNotes").is(":checked"),
enableAutoReminderRefresh: $("#enableAutoReminderRefresh").is(":checked"),
enableAutoOdometerInsert: $("#enableAutoOdometerInsert").is(":checked"),
enableShopSupplies: $("#enableShopSupplies").is(":checked"),
enableExtraFieldColumns: $("#enableExtraFieldColumns").is(":checked"),
hideSoldVehicles: $("#hideSoldVehicles").is(":checked"),
preferredGasUnit: $("#preferredGasUnit").val(),
preferredGasMileageUnit: $("#preferredFuelMileageUnit").val(),
userLanguage: $("#defaultLanguage").val(),
visibleTabs: visibleTabs,
defaultTab: defaultTab
}
sloader.show();
$.post('/Home/WriteToSettings', { userConfig: userConfigObject }, function (data) {
sloader.hide();
if (data) {
setTimeout(function () { window.location.href = '/Home/Index?tab=settings' }, 500);
} else {
errorToast(genericErrorMessage());
}
})
}
function makeBackup() {
$.get('/Files/MakeBackup', function (data) {
window.location.href = data;
});
}
function openUploadLanguage() {
$("#inputLanguage").click();
}
function openRestoreBackup() {
$("#inputBackup").click();
}
function uploadLanguage(event) {
let formData = new FormData();
formData.append("file", event.files[0]);
sloader.show();
$.ajax({
url: "/Files/HandleTranslationFileUpload",
data: formData,
cache: false,
processData: false,
contentType: false,
type: 'POST',
success: function (response) {
sloader.hide();
if (response.success) {
setTimeout(function () { window.location.href = '/Home/Index?tab=settings' }, 500);
} else {
errorToast(response.message);
}
},
error: function () {
sloader.hide();
errorToast("An error has occurred, please check the file size and try again later.");
}
});
}
function restoreBackup(event) {
let formData = new FormData();
formData.append("file", event.files[0]);
console.log('LubeLogger - DB Restoration Started');
sloader.show();
$.ajax({
url: "/Files/HandleFileUpload",
data: formData,
cache: false,
processData: false,
contentType: false,
type: 'POST',
success: function (response) {
if (response.trim() != '') {
$.post('/Files/RestoreBackup', { fileName: response }, function (data) {
sloader.hide();
if (data) {
console.log('LubeLogger - DB Restoration Completed');
successToast("Backup Restored");
setTimeout(function () { window.location.href = '/Home/Index' }, 500);
} else {
errorToast(genericErrorMessage());
console.log('LubeLogger - DB Restoration Failed - Failed to process backup file.');
}
});
} else {
console.log('LubeLogger - DB Restoration Failed - Failed to upload backup file.');
}
},
error: function () {
sloader.hide();
console.log('LubeLogger - DB Restoration Failed - Request failed to reach backend, please check file size.');
errorToast("An error has occurred, please check the file size and try again later.");
}
});
}
function enableAuthCheckChanged() {
var enableAuth = $("#enableAuth").is(":checked");
if (enableAuth) {

View File

@@ -14,7 +14,7 @@
<hr />
<div class="col-12">
<div class="d-flex justify-content-center">
<p><a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover" href="https://docs.lubelogger.com/Funding" target="_blank">Become a Sponsor</a></p>
<p><a class="link-body-emphasis link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover" href="https://docs.lubelogger.com/Misc/Funding" target="_blank">Become a Sponsor</a></p>
</div>
</div>
@if (Model.LifeTime.Any())

View File

@@ -5,6 +5,7 @@
@{
var logoUrl = config.GetLogoUrl();
var userLanguage = config.GetServerLanguage();
var registrationDisabled = config.GetServerDisabledRegistration();
var openIdConfigName = config.GetOpenIDConfig().Name;
}
@{
@@ -46,9 +47,12 @@
<div class="d-grid">
<a href="/Login/ForgotPassword" class="btn btn-link mt-2">@translator.Translate(userLanguage, "Forgot Password")</a>
</div>
<div class="d-grid">
<a href="/Login/Registration" class="btn btn-link mt-2">@translator.Translate(userLanguage, "Register")</a>
</div>
@if (!registrationDisabled)
{
<div class="d-grid">
<a href="/Login/Registration" class="btn btn-link mt-2">@translator.Translate(userLanguage, "Register")</a>
</div>
}
</div>
</div>
</div>

View File

@@ -5,6 +5,7 @@
@{
var userConfig = config.GetUserConfig(User);
var useDarkMode = userConfig.UseDarkMode;
var useSystemColorMode = userConfig.UseSystemColorMode;
var enableCsvImports = userConfig.EnableCsvImports;
var useMPG = userConfig.UseMPG;
var useMarkDown = userConfig.UseMarkDownOnSavedNotes;
@@ -54,7 +55,7 @@
<script>
function getGlobalConfig() {
return {
useDarkMode : "@useDarkMode" == "True",
useDarkMode: "@useDarkMode" == "True" || ("@useSystemColorMode" == "True" && window.matchMedia('(prefers-color-scheme: dark)').matches),
enableCsvImport : "@enableCsvImports" == "True",
useMarkDown: "@useMarkDown" == "True",
currencySymbol: decodeHTMLEntities("@numberFormat.CurrencySymbol"),
@@ -111,6 +112,12 @@
break;
}
}
function setThemeBasedOnDevice() {
var systemPrefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (systemPrefersDarkMode) {
$(document.documentElement).attr("data-bs-theme", "dark");
}
}
</script>
@await RenderSectionAsync("Scripts", required: false)
</head>
@@ -122,3 +129,9 @@
</div>
</body>
</html>
@if (useSystemColorMode)
{
<script>
setThemeBasedOnDevice();
</script>
}

View File

@@ -78,7 +78,7 @@
<h1 class="text-truncate display-4">@($"{Model.Year} {Model.Make} {Model.Model}")<small class="text-body-secondary">@($"(#{Model.LicensePlate})")</small></h1>
<button onclick="editVehicle(@Model.Id)" class="lubelogger-tab btn btn-warning btn-md mt-1 mb-1"><i class="bi bi-pencil-square"></i></button>
<div class="lubelogger-navbar-button">
<button type="button" class="btn btn-dark" onclick="showMobileNav()"><i class="bi bi-list lubelogger-menu-icon"></i></button>
<button type="button" class="btn btn-adaptive" onclick="showMobileNav()"><i class="bi bi-list lubelogger-menu-icon"></i></button>
</div>
</div>
</div>
@@ -171,6 +171,7 @@
function GetVehicleId() {
return {
vehicleId: @Model.Id,
odometerOptional: @Model.OdometerOptional.ToString().ToLower(),
hasOdometerAdjustment: @Model.HasOdometerAdjustment.ToString().ToLower(),
odometerDifference: decodeHTMLEntities('@Model.OdometerDifference'),
odometerMultiplier: decodeHTMLEntities('@Model.OdometerMultiplier')

View File

@@ -24,7 +24,7 @@
</div>
<label for="collisionRecordMileage">@translator.Translate(userLanguage, "Odometer")</label>
<div class="input-group">
<input type="number" inputmode="numeric" id="collisionRecordMileage" class="form-control" placeholder="@translator.Translate(userLanguage,"Odometer reading when repaired")" value="@(isNew ? "" : Model.Mileage)">
<input type="number" inputmode="numeric" id="collisionRecordMileage" class="form-control" placeholder="@translator.Translate(userLanguage,"Odometer reading when repaired")" value="@(isNew || Model.Mileage == default ? "" : Model.Mileage)">
@if (isNew)
{
<div class="input-group-text">

View File

@@ -123,7 +123,7 @@
{
<tr class="d-flex user-select-none" style="cursor:pointer;" onmouseup="stopEvent()" ontouchstart="detectRowLongTouch(this)" ontouchend="detectRowTouchEndPremature(this)" data-rowId="@collisionRecord.Id" oncontextmenu="showTableContextMenu(this)" onmousemove="rangeMouseMove(this)" onclick="handleTableRowClick(this, showEditCollisionRecordModal,@collisionRecord.Id)" data-tags='@string.Join(" ", collisionRecord.Tags)'>
<td class="col-2 col-xl-1 flex-grow-1" data-column="date" data-date="@StaticHelper.GetEpochFromDateTime(collisionRecord.Date)">@collisionRecord.Date.ToShortDateString()</td>
<td class="col-2 flex-grow-1 flex-shrink-1" data-column="odometer">@collisionRecord.Mileage</td>
<td class="col-2 flex-grow-1 flex-shrink-1" data-column="odometer">@(collisionRecord.Mileage == default ? "---" : collisionRecord.Mileage.ToString())</td>
<td class="col-3 col-xl-4 flex-grow-1 flex-shrink-1" data-column="description">@collisionRecord.Description</td>
<td class="col-2 flex-grow-1 flex-shrink-1" data-column="cost" data-record-type="cost">@((hideZero && collisionRecord.Cost == default) ? "---" : collisionRecord.Cost.ToString("C"))</td>
<td class="col-3 flex-grow-1 flex-shrink-1 text-truncate" data-column="notes">@CarCareTracker.Helper.StaticHelper.TruncateStrings(collisionRecord.Notes)</td>

View File

@@ -36,6 +36,9 @@
]
},
options: {
onClick: (e) => {
showDataTable();
},
plugins: {
legend: {
position: "bottom",

View File

@@ -0,0 +1,78 @@
@using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@{
var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
var hideZero = userConfig.HideZero;
}
@model CostTableForVehicle
@if (Model.CollisionRecordSum + Model.ServiceRecordSum + Model.GasRecordSum + Model.TaxRecordSum + Model.UpgradeRecordSum > 0)
{
<div>
<div class="modal-header">
<h5 class="modal-title">@(translator.Translate(userLanguage, "Vehicle Cost Breakdown"))</h5>
<button type="button" class="btn-close" onclick="hideDataTable()" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-12">
<table class="table table-hover">
<thead class="sticky-top">
<tr class="d-flex">
<th scope="col" class="col-3 flex-grow-1">@translator.Translate(userLanguage, "Type")</th>
<th scope="col" class="col-3 flex-grow-1">@translator.Translate(userLanguage, "Cost Per Day")</th>
<th scope="col" class="col-3 flex-grow-1">@translator.Translate(userLanguage, Model.DistanceUnit)</th>
<th scope="col" class="col-3 flex-grow-1">@translator.Translate(userLanguage, "Total")</th>
</tr>
</thead>
<tbody>
<tr class="d-flex">
<td class="col-3 flex-grow-1">@translator.Translate(userLanguage, "Service Records")</td>
<td class="col-3 flex-grow-1">@(hideZero && Model.ServiceRecordPerDay == default ? "---" : Model.ServiceRecordPerDay.ToString("C2"))</td>
<td class="col-3 flex-grow-1">@(hideZero && Model.ServiceRecordPerMile == default ? "---" :Model.ServiceRecordPerMile.ToString("C2"))</td>
<td class="col-3 flex-grow-1">@(hideZero && Model.ServiceRecordSum == default ? "---" :Model.ServiceRecordSum.ToString("C2"))</td>
</tr>
<tr class="d-flex">
<td class="col-3 flex-grow-1">@translator.Translate(userLanguage, "Repairs")</td>
<td class="col-3 flex-grow-1">@(hideZero && Model.CollisionRecordPerDay == default ? "---" :Model.CollisionRecordPerDay.ToString("C2"))</td>
<td class="col-3 flex-grow-1">@(hideZero && Model.CollisionRecordPerMile == default ? "---" :Model.CollisionRecordPerMile.ToString("C2"))</td>
<td class="col-3 flex-grow-1">@(hideZero && Model.CollisionRecordSum == default ? "---" :Model.CollisionRecordSum.ToString("C2"))</td>
</tr>
<tr class="d-flex">
<td class="col-3 flex-grow-1">@translator.Translate(userLanguage, "Upgrades")</td>
<td class="col-3 flex-grow-1">@(hideZero && Model.UpgradeRecordPerDay == default ? "---" :Model.UpgradeRecordPerDay.ToString("C2"))</td>
<td class="col-3 flex-grow-1">@(hideZero && Model.UpgradeRecordPerMile == default ? "---" :Model.UpgradeRecordPerMile.ToString("C2"))</td>
<td class="col-3 flex-grow-1">@(hideZero && Model.UpgradeRecordSum == default ? "---" :Model.UpgradeRecordSum.ToString("C2"))</td>
</tr>
<tr class="d-flex">
<td class="col-3 flex-grow-1">@translator.Translate(userLanguage, "Fuel")</td>
<td class="col-3 flex-grow-1">@(hideZero && Model.GasRecordPerDay == default ? "---" :Model.GasRecordPerDay.ToString("C2"))</td>
<td class="col-3 flex-grow-1">@(hideZero && Model.GasRecordPerMile == default ? "---" :Model.GasRecordPerMile.ToString("C2"))</td>
<td class="col-3 flex-grow-1">@(hideZero && Model.GasRecordSum == default ? "---" :Model.GasRecordSum.ToString("C2"))</td>
</tr>
<tr class="d-flex">
<td class="col-3 flex-grow-1">@translator.Translate(userLanguage, "Taxes")</td>
<td class="col-3 flex-grow-1">@(hideZero && Model.TaxRecordPerDay == default ? "---" :Model.TaxRecordPerDay.ToString("C2"))</td>
<td class="col-3 flex-grow-1">@(hideZero && Model.TaxRecordPerMile == default ? "---" : Model.TaxRecordPerMile.ToString("C2"))</td>
<td class="col-3 flex-grow-1">@(hideZero && Model.TaxRecordSum == default ? "---" :Model.TaxRecordSum.ToString("C2"))</td>
</tr>
<tr class="d-flex">
<td class="col-3 flex-grow-1">@translator.Translate(userLanguage, "Total")</td>
<td class="col-3 flex-grow-1">@(hideZero && Model.TotalPerDay == default ? "---" : Model.TotalPerDay.ToString("C2"))</td>
<td class="col-3 flex-grow-1">@(hideZero && Model.TotalPerMile == default ? "---" : Model.TotalPerMile.ToString("C2"))</td>
<td class="col-3 flex-grow-1">@(hideZero && Model.TotalCost == default ? "---" : Model.TotalCost.ToString("C2"))</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
}
else
{
<div class="text-center">
<h4>@translator.Translate(userLanguage, "No data found or all records have zero sums, insert records with non-zero sums to see visualizations here.")</h4>
</div>
}

View File

@@ -190,7 +190,7 @@
{
<tr class="d-flex user-select-none" style="cursor:pointer;" onmouseup="stopEvent()" ontouchstart="detectRowLongTouch(this)" ontouchend="detectRowTouchEndPremature(this)" data-rowId="@gasRecord.Id" oncontextmenu="showTableContextMenu(this)" onmousemove="rangeMouseMove(this)" onclick="handleTableRowClick(this, showEditGasRecordModal,@gasRecord.Id)" data-tags='@string.Join(" ", gasRecord.Tags)'>
<td class="col-2 flex-grow-1" data-column="daterefueled">@gasRecord.Date</td>
<td class="col-2 flex-grow-1 flex-shrink-1" data-column="odometer" data-gas-type="mileage" data-gas-aggregate="@gasRecord.DeltaMileage" data-gas-original="@gasRecord.Mileage">@gasRecord.Mileage</td>
<td class="col-2 flex-grow-1 flex-shrink-1" data-column="odometer" data-gas-type="mileage" data-gas-aggregate="@gasRecord.DeltaMileage" data-gas-original="@gasRecord.Mileage">@(gasRecord.Mileage == default ? "---" : gasRecord.Mileage.ToString())</td>
<td class="col-1 flex-grow-1 flex-shrink-1" data-column="delta">@(gasRecord.DeltaMileage == default ? "---" : gasRecord.DeltaMileage)</td>
<td class="col-2 flex-grow-1 flex-shrink-1" data-column="consumption" data-gas-type="consumption" data-gas-aggregate="@gasRecord.Gallons">@gasRecord.Gallons.ToString("F")</td>
<td class="col-3 flex-grow-1 flex-shrink-1" data-column="fueleconomy" data-gas-type="fueleconomy" data-aggregated='@(gasRecord.IncludeInAverage.ToString().ToLower())'>@(gasRecord.MilesPerGallon == 0 ? "---" : gasRecord.MilesPerGallon.ToString("F"))</td>

View File

@@ -53,7 +53,7 @@
</div>
<label for="gasRecordMileage">@($"{translator.Translate(userLanguage,"Odometer Reading")}({distanceUnit})")</label>
<div class="input-group">
<input type="number" inputmode="numeric" id="gasRecordMileage" class="form-control" placeholder="@translator.Translate(userLanguage,"Odometer reading when refueled")" value="@(isNew ? "" : Model.GasRecord.Mileage)">
<input type="number" inputmode="numeric" id="gasRecordMileage" class="form-control" placeholder="@translator.Translate(userLanguage,"Odometer reading when refueled")" value="@(isNew || Model.GasRecord.Mileage == default ? "" : Model.GasRecord.Mileage)">
@if (isNew)
{
<div class="input-group-text">

View File

@@ -18,36 +18,36 @@
<div class="row">
@if (Model.ReminderRecordId != default)
{
<div class="col-4 col-md-1">
<i class="bi bi-bell"></i>
<div class="col-3 col-md-1">
<span class="badge text-bg-light planner-indicator"><i class="bi bi-bell-fill text-warning"></i></span>
</div>
}
<div class="@(Model.ReminderRecordId != default ? "col-4" : "col-6") col-md-1">
<div class="@(Model.ReminderRecordId != default ? "col-3" : "col-6") col-md-1">
@if (Model.ImportMode == ImportMode.ServiceRecord)
{
<i class="bi bi-card-checklist"></i>
<span class="badge text-bg-primary planner-indicator"><i class="bi bi-card-checklist text-white"></i></span>
}
else if (Model.ImportMode == ImportMode.UpgradeRecord)
{
<i class="bi bi-wrench-adjustable"></i>
<span class="badge text-bg-success planner-indicator"><i class="bi bi-wrench-adjustable text-white"></i></span>
}
else if (Model.ImportMode == ImportMode.RepairRecord)
{
<i class="bi bi-exclamation-octagon"></i>
<span class="badge text-bg-warning planner-indicator"><i class="bi bi-exclamation-octagon text-white"></i></span>
}
</div>
<div class="@(Model.ReminderRecordId != default ? "col-4" : "col-6") col-md-1">
<div class="@(Model.ReminderRecordId != default ? "col-3" : "col-6") col-md-1">
@if (Model.Priority == PlanPriority.Critical)
{
<i class="bi bi-fire"></i>
<span class="badge text-bg-danger planner-indicator"><i class="bi bi-fire text-white"></i></span>
}
else if (Model.Priority == PlanPriority.Normal)
{
<i class="bi bi-water"></i>
<span class="badge text-bg-primary planner-indicator"><i class="bi bi-water text-white"></i></span>
}
else if (Model.Priority == PlanPriority.Low)
{
<i class="bi bi-snow"></i>
<span class="badge text-bg-info planner-indicator"><i class="bi bi-snow text-white"></i></span>
}
</div>
</div>

View File

@@ -93,6 +93,22 @@
<!option value="ThreeYears" @(Model.ReminderMonthInterval == ReminderMonthInterval.ThreeYears ? "selected" : "")>@translator.Translate(userLanguage, "3 Years")</!option>
<!option value="FiveYears" @(Model.ReminderMonthInterval == ReminderMonthInterval.FiveYears ? "selected" : "")>@translator.Translate(userLanguage, "5 Years")</!option>
</select>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" onchange="toggleCustomThresholds()" id="reminderUseCustomThresholds" checked="@Model.UseCustomThresholds">
<label class="form-check-label" for="reminderUseCustomThresholds">@translator.Translate(userLanguage, "Use Custom Thresholds")</label>
</div>
<div class="collapse @(Model.UseCustomThresholds ? "show" : "")" id="reminderCustomThresholds">
<div>
<label for="reminderUrgentDays">@translator.Translate(userLanguage, "Urgent(Days)")</label>
<input type="text" id="reminderUrgentDays" class="form-control" placeholder="@translator.Translate(userLanguage, "Urgent")" value="@Model.CustomThresholds.UrgentDays">
<label for="reminderVeryUrgentDays">@translator.Translate(userLanguage, "Very Urgent(Days)")</label>
<input type="text" id="reminderVeryUrgentDays" class="form-control" placeholder="@translator.Translate(userLanguage, "Very Urgent")" value="@Model.CustomThresholds.VeryUrgentDays">
<label for="reminderUrgentDistance">@translator.Translate(userLanguage, "Urgent(Distance)")</label>
<input type="text" id="reminderUrgentDistance" class="form-control" placeholder="@translator.Translate(userLanguage, "Urgent")" value="@Model.CustomThresholds.UrgentDistance">
<label for="reminderVeryUrgentDistance">@translator.Translate(userLanguage, "Very Urgent(Distance)")</label>
<input type="text" id="reminderVeryUrgentDistance" class="form-control" placeholder="@translator.Translate(userLanguage, "Very Urgent")" value="@Model.CustomThresholds.VeryUrgentDistance">
</div>
</div>
</div>
</div>
</div>

View File

@@ -125,5 +125,7 @@
</ul>
<script>
$("[data-column='metric']").map((x, y) => new bootstrap.Tooltip(y));
if (!getDeviceIsTouchOnly()) {
$("[data-column='metric']").map((x, y) => new bootstrap.Tooltip(y));
}
</script>

View File

@@ -146,6 +146,13 @@
</div>
</div>
</div>
<div class="modal fade" data-bs-focus="false" id="vehicleDataTableModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content" id="vehicleDataTableModalContent">
</div>
</div>
</div>
<div id="vehicleHistoryReport" class="showOnPrint"></div>
<script>

View File

@@ -24,7 +24,7 @@
</div>
<label for="serviceRecordMileage">@translator.Translate(userLanguage, "Odometer")</label>
<div class="input-group">
<input type="number" inputmode="numeric" id="serviceRecordMileage" class="form-control" placeholder="@translator.Translate(userLanguage,"Odometer reading when serviced")" value="@(isNew ? "" : Model.Mileage)">
<input type="number" inputmode="numeric" id="serviceRecordMileage" class="form-control" placeholder="@translator.Translate(userLanguage,"Odometer reading when serviced")" value="@(isNew || Model.Mileage == default ? "" : Model.Mileage)">
@if (isNew)
{
<div class="input-group-text">

View File

@@ -123,7 +123,7 @@
{
<tr class="d-flex user-select-none" style="cursor:pointer;" onmouseup="stopEvent()" ontouchstart="detectRowLongTouch(this)" ontouchend="detectRowTouchEndPremature(this)" data-rowId="@serviceRecord.Id" oncontextmenu="showTableContextMenu(this)" onmousemove="rangeMouseMove(this)" onclick="handleTableRowClick(this, showEditServiceRecordModal,@serviceRecord.Id)" data-tags='@string.Join(" ", serviceRecord.Tags)'>
<td class="col-2 col-xl-1 flex-grow-1" data-column="date" data-date="@StaticHelper.GetEpochFromDateTime(serviceRecord.Date)">@serviceRecord.Date.ToShortDateString()</td>
<td class="col-2 flex-grow-1 flex-shrink-1" data-column="odometer">@serviceRecord.Mileage</td>
<td class="col-2 flex-grow-1 flex-shrink-1" data-column="odometer">@(serviceRecord.Mileage == default ? "---" : serviceRecord.Mileage.ToString())</td>
<td class="col-3 col-xl-4 flex-grow-1 flex-shrink-1" data-column="description">@serviceRecord.Description</td>
<td class="col-2 flex-grow-1 flex-shrink-1" data-column="cost" data-record-type="cost">@((hideZero && serviceRecord.Cost == default) ? "---" : serviceRecord.Cost.ToString("C"))</td>
<td class="col-3 text-truncate flex-grow-1 flex-shrink-1" data-column="notes">@CarCareTracker.Helper.StaticHelper.TruncateStrings(serviceRecord.Notes)</td>

View File

@@ -24,7 +24,7 @@
</div>
<label for="upgradeRecordMileage">@translator.Translate(userLanguage, "Odometer")</label>
<div class="input-group">
<input type="number" inputmode="numeric" id="upgradeRecordMileage" class="form-control" placeholder="@translator.Translate(userLanguage,"Odometer reading when upgraded/modded")" value="@(isNew ? "" : Model.Mileage)">
<input type="number" inputmode="numeric" id="upgradeRecordMileage" class="form-control" placeholder="@translator.Translate(userLanguage,"Odometer reading when upgraded/modded")" value="@(isNew || Model.Mileage == default ? "" : Model.Mileage)">
@if (isNew)
{
<div class="input-group-text">

View File

@@ -123,7 +123,7 @@
{
<tr class="d-flex user-select-none" style="cursor:pointer;" onmouseup="stopEvent()" ontouchstart="detectRowLongTouch(this)" ontouchend="detectRowTouchEndPremature(this)" data-rowId="@upgradeRecord.Id" oncontextmenu="showTableContextMenu(this)" onmousemove="rangeMouseMove(this)" onclick="handleTableRowClick(this, showEditUpgradeRecordModal,@upgradeRecord.Id)" data-tags='@string.Join(" ", upgradeRecord.Tags)'>
<td class="col-2 flex-grow-1 col-xl-1" data-column="date" data-date="@StaticHelper.GetEpochFromDateTime(upgradeRecord.Date)">@upgradeRecord.Date.ToShortDateString()</td>
<td class="col-2 flex-grow-1 flex-shrink-1" data-column="odometer">@upgradeRecord.Mileage</td>
<td class="col-2 flex-grow-1 flex-shrink-1" data-column="odometer">@(upgradeRecord.Mileage == default ? "---" : upgradeRecord.Mileage.ToString())</td>
<td class="col-3 flex-grow-1 flex-shrink-1 col-xl-4" data-column="description">@upgradeRecord.Description</td>
<td class="col-2 flex-grow-1 flex-shrink-1" data-column="cost" data-record-type="cost">@((hideZero && upgradeRecord.Cost == default) ? "---" : upgradeRecord.Cost.ToString("C"))</td>
<td class="col-3 flex-grow-1 flex-shrink-1 text-truncate" data-column="notes">@CarCareTracker.Helper.StaticHelper.TruncateStrings(upgradeRecord.Notes)</td>

View File

@@ -56,6 +56,10 @@
<input class="form-check-input" type="checkbox" role="switch" id="inputUseHours" checked="@Model.UseHours">
<label class="form-check-label" for="inputUseHours">@translator.Translate(userLanguage, "Use Engine Hours")</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="inputOdometerOptional" checked="@Model.OdometerOptional">
<label class="form-check-label" for="inputOdometerOptional">@translator.Translate(userLanguage, "Odometer Optional")</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" onchange="toggleOdometerAdjustment()" id="inputHasOdometerAdjustment" checked="@Model.HasOdometerAdjustment">
<label class="form-check-label" for="inputHasOdometerAdjustment">@translator.Translate(userLanguage, "Odometer Adjustments")</label>
@@ -87,6 +91,29 @@
</div>
</div>
</div>
<div class="accordion accordion-flush" id="vehicleModalMetricAccordion">
<div class="accordion-item">
<div class="accordion-header">
<button class="accordion-button skinny collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseMetricInfo">
@translator.Translate(userLanguage, "Dashboard Metrics")
</button>
</div>
<div id="collapseMetricInfo" class="accordion-collapse collapse" data-bs-parent="#vehicleModalMetricAccordion">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="inputMetricDefault" value="@DashboardMetric.Default" checked="@Model.DashboardMetrics.Contains(DashboardMetric.Default)">
<label class="form-check-label" for="inputMetricDefault">@translator.Translate(userLanguage, "Last Odometer")</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="inputMetricCostPerMile" value="@DashboardMetric.CostPerMile" checked="@Model.DashboardMetrics.Contains(DashboardMetric.CostPerMile)">
<label class="form-check-label" for="inputMetricCostPerMile">@translator.Translate(userLanguage, "Total Cost / Total Distance Driven")</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="inputMetricTotalCost" value="@DashboardMetric.TotalCost" checked="@Model.DashboardMetrics.Contains(DashboardMetric.TotalCost)">
<label class="form-check-label" for="inputMetricTotalCost">@translator.Translate(userLanguage, "Total Cost")</label>
</div>
</div>
</div>
</div>
<label for="inputTag">@translator.Translate(userLanguage, "Tags(optional)")</label>
<select multiple class="form-select" id="inputTag">
@foreach (string tag in Model.Tags)

View File

@@ -7,10 +7,13 @@
},
"AllowedHosts": "*",
"UseDarkMode": false,
"UseSystemColorMode": false,
"EnableCsvImports": true,
"UseMPG": true,
"UseDescending": false,
"EnableAuth": false,
"DisableRegistration": false,
"EnableRootUserOIDC": false,
"HideZero": false,
"EnableAutoReminderRefresh": false,
"EnableAutoOdometerInsert": false,
@@ -27,5 +30,6 @@
"VisibleTabs": [ 0, 1, 4, 2, 3, 6, 5, 8 ],
"DefaultTab": 8,
"UserNameHash": "",
"UserPasswordHash": ""
"UserPasswordHash": "",
"DefaultReminderEmail": ""
}

View File

@@ -1,14 +0,0 @@
# API
LubeLogger provides API endpoints to retrieve and add records, full documentation of these endpoints can be found at `/api`.
## Authentication
If authentication is enabled, it implements Basic Auth based on RFC2617, which stipulates that the "token" is passed in as a Base64-encoded string comprising of a username and password separated by a colon(":"). Because of this, neither the username nor password can contain a colon(":") character.
### Testing
You can utilize any REST API testing tool to test your use-case.
## Example Use Cases
- Send Email Reminders, see [[Reminders|Records/Reminders#reminder-emails]]
- Insert Odometer Records, see [[Odometer|Records/Odometer#api-integration]]
- Create DB Backups

View File

@@ -1,45 +0,0 @@
# Authentication
LubeLogger does not require authentication by default; however, it is highly recommend that you set up authentication if your LubeLogger instance is accessible vvia the Internet or if you wish to invite other users to your instance.
## Enabling Authentication
To enable authentication, all you have to do is navigate to the "Settings" tab and check "Enable Authentication".
A dialog will then prompt you to enter a username and password. These are the credentials for the Root/Super User.
Once you have entered the credentials, click the "Setup" button and you will be redirected to a login screen, enter the credentials of the Root/Super User here to login.
## Creating / Inviting New Users
LubeLogger relies on an invitation-only model for creating/registering new users. It is highly recommended that LubeLogger is configured with SMTP in order to make the user registration process as smooth as possible, see [[Getting Started]] for more information regarding SMTP configuration.
To Create/Invite New Users, you first need to enable authentication and set up the Root User credentials. Once that is done, upon login, you will see that there is now a dropdown to the right of the "Settings" tab that has your root username on it. Click on that dropdown and select "Admin Panel"
![](/Authentication/a/image-1706398957700.png)
You will now be taken to a new page. There are two sections in this page, Tokens and Users.
![](/Authentication/a/image-1706398972637.png)
Tokens are used for invitees to register their user account or existing users to reset their password. These tokens are single use and are validated against the email address they are issued for.
Users, as the name suggests, is a list of users in the system. You can also mark or unmark existing users as Admins here, see below to understand what permissions Admin users have.
To invite a user, simply click on the "Generate User Token" button and type in their email address, note that this is case-sensitive.
![](/Authentication/a/image-1706398926360.png)
If SMTP is configured and the Auto Notify(via Email) switch is checked, the user will receive an email that looks like this:
![](/Authentication/a/image-1706398865186.png)
## Root/Super User
You might be tempted to use your root credentials as your main credentials, and there is nothing wrong that, but you should know that there are a few caveats associated with the root user.
1. Root/Super Users can view and edit all vehicles, this might seem like a great advantage initially, but it can be problematic if there are sufficient users and you have to sift through dozens of vehicles to get to yours.
2. Any setting you enable/disable will become the default setting inherited by new users.
For the reasons above, it is highly recommended that you create a second user for personal use and mark it as an Admin. See below for a breakdown of permissions across user tiers.
| Permissions | User | Admin | Root/Super User |
| ---------------------- | ----------------------------------------- | ----------------------------------------- | ---------------------- |
| View/Edit Vehicles | Only vehicles which they are collaborator | Only vehicles which they are collaborator | View/Edit All Vehicles |
| Access API | Yes | Yes | Yes |
| Personalized Settings | Yes | Yes | No, settings will be set as server default |
| Add/Remove Users | No | Yes | Yes |
| Make/Restore Backups | No | No | Yes |
| Disable Authentication | No | No | Yes |

View File

@@ -1,32 +0,0 @@
# Dashboard
The Dashboard is where you get an overview of your vehicle. It is the default tab that the user will see when they click into a vehicle and it is also the only tab that cannot be hidden.
![](/Dashboard/a/image-1706633228249.png)
The Dashboard includes the following data:
- Expenses By Type By Year/All Time(Top-Left)
- Total Expenses By Month By Year/All Time(Top-Center)
- Reminders by current date or future date(Top-Right)
- Collaborators(Bottom-Left), see [[Adding Collaborators|Vehicle Management#adding-a-collaborator]]
- Fuel Mileage By Month By Year/All Time(Bottom-Center)
- Vehicle Maintenance Report Generator(Bottom-Right)
## Filtering Data by Year
The year dropdown above the pie chart(top-left) allows the user to filter the aggregated data by year. The year selections are populated by retrieving all years between the year of the oldest record and the current year. If the oldest record is less than 5 years old, the selections will still be populated by the last 5 years.
Note: Changing the selected year will automatically refresh the Expenses by Type Pie Chart, the Total Expenses by Month and the Fuel Mileage By Month Bar Charts.
![](/Dashboard/a/image-1706477893556.png)
## Vehicle Maintenance Report
The Vehicle Maintainence Report Generator is a button that will generate a consolidated report of all work performed on the vehicle. For performance reasons, this report is designed to be printed as soon as it is generated. You can either choose to print it to paper or PDF.
![](/Dashboard/a/image-1706478028103.png)
## Export Attachments
The Export Attachments button provies a convenient feature to export all attachments into a chronologically-ordered zip file. Upon clicking the button, you will be prompted to select which tabs you want attachments to be exported from.
![](/Dashboard/a/image-1706574853389.png)
Once you have made your selection, a zip file will be created and downloaded onto your computer. This zip file contains all of the attachments and are named in chronological order(i.e.: the oldest attachment will be named 0 and the second oldest attachment 1, 2...)

View File

@@ -1,62 +0,0 @@
# Fuel Records
The Fuel tab keeps track of the fuel mileage for your vehicle.
![](/Records/Fuel%20Records/a/image-1707455181666.png)
LubeLogger supports fuel mileage calculation in the following formats:
- American Imperial (MPG)
- European/Asian Metric (L/100Km)
- British(Purchase Gas in Liters and calculate fuel mileage as Miles per Imperial UK Gallons)
- Electric Vehicles (mi./kWh or kWh/100Km)
## Initial Fuel Up
In order to calculate fuel mileage, you must first have an initial fuel entry with the current odometer reading. An odometer reading is needed so that the app can calculate the distance traveled between fuel ups. It is recommended that you fill it up to full for the first entry.
## Imperfect Fuel Ups
For the most accurate results it is recommended that you always fill your vehicle up to full and not miss any fuel ups, but sometimes things happen, which is why we provided the following:
### Partial Fuel Ups
On the occassions that you cannot fill your vehicle up to full, you can defer the fuel mileage calculation by unchecking the "Is Filled To Full" switch. Doing this tells the app to defer fuel mileage calculation until the next Full Fill Up.
![](/Records/Fuel%20Records/a/image-1706406318412.png)
### Missed Fuel Ups
Check this if you have missed a fuel up record prior to adding this fuel record. This effectively resets the fuel mileage calculation and will show up as $0 or "---" in the fuel records. Checking this ensures that the average fuel mileage calculation isn't skewed due to missed fuel ups.
## Calculation of Average Fuel Mileage
Average MPG is calculated by excluding the initial and missed fuel ups, then taking the difference between the min and max odometer reading and dividing it by total amount of gas consumed(if Metric then it is further divided by 1/100). This method will include all of the gas consumed by full and partial fuel ups.
## Fuel Units
The consumption, fuel mileage, and odometer units are determined by two settings: "Use Imperial Calculation" and "Use UK MPG Calculation"
| Setting | Use UK MPG Calculation Checked | Use UK MPG Calculation Unchecked |
| -------- | -------- | -------- |
| Use Imperial Calculation Checked | Distance: Miles, Consumption: Liters, Fuel Mileage: Miles per UK Gallons | Distance: Miles, Consumption: Gallons, Fuel Mileage: MPG |
| Use Imperial Calculation Unchecked | Distance: Miles, Consumption: Liters, Fuel Mileage: l/100mi. | Distance: Km, Consumption: Liters, Fuel Mileage: l/100km |
### Alternate Fuel Units
If you wish to see alternate units which converts the calculated units within the Gas Tab, you can right click on the table headers for Consumption and Fuel Economy. These settings persists for the user, so the next time you login to LubeLogger it will automatically perform the conversion for you.
![](/Records/Fuel%20Records/a/image-1706797201107.gif)
#### Consumption
For consumption, the units are cycled between US gal, Liters, and Imp Gal(UK). Changing the consumption unit will also change the unit cost.
#### Fuel Economy
The units can only toggle between l/100km and km/l, which means that this unit cannot be converted if your fuel economy unit is not l/100km. Changing the fuel economy unit will also update the values in the Average, Min, and Max Fuel Economy labels as well as the Fuel Economy Unit in the Consolidated Report.
## Importing from CSV/Fuelly/SpiritMonitor.de
LubeLogger supports importing CSV exports from other apps, below lists the column names that are acceptable/mapped to our data points:
| LubeLogger Data Field | Imported CSV |
| -------------------------------------- | -------------------------------------------------------------- |
| date | date, fuelup_date |
| odometer | odometer |
| fuelconsumed | gallons, liters, litres, consumption, quantity, fuelconsumed |
| cost | cost, total cost, totalcost, total price |
| notes | notes, note |
| partialfuelup(inverse of isfilltofull) | partial_fuelup |
| isfilltofull | isfilltofull, filled up |
| missedfuelup | missedfuelup, missed_fuelup |
| tags | tags |

View File

@@ -1,73 +0,0 @@
# Getting Started
## Docker
The Docker Container Repository is the most reliable and up-to-date distribution channel for LubeLogger.
You need to have Docker Windows installed and Virtualization enabled(typically a BIOS setting).
You will then clone the following files onto your computer from the repository _.env_ and _docker-compose.yml_ or _docker-compose-traefik.yml_ if you're using Traefik.
In the .env file you will find the following and here are the explanations for the variables.
```
LC_ALL=en_US.UTF-8 <- Locale and Language Settings, this will affect how numbers, currencies, and dates are formatted.
LANG=en_US.UTF-8 <- Same as above. Note that some languages don't have UTF-8 encodings.
MailConfig__EmailServer="" <- Email SMTP settings used only for configuring multiple users(to send their registration token and forgot password tokens)
MailConfig__EmailFrom="" <- Same as above.
MailConfig__UseSSL="false" <- Same as above.
MailConfig__Port=587 <- Same as above.
MailConfig__Username="" <- Same as above.
MailConfig__Password="" <- Same as above.
```
Once you're happy with the configuration, run the following commands to pull down the image and run container.
```
docker pull ghcr.io/hargata/lubelogger:latest
docker-compose up
```
By default the app will start listening at localhost:8080, this port can be configured in the docker-compose file.
## Windows Standalone Executable
Windows Standalone executables are provided on a request basis, and will usually be included with every other release.
To run the server, you just have to download the zip archive attached to the release, usually named LubeLogger_vNNN_win_x64.zip, extract the archive and double click on CarCareTracker.exe
Occassionally you might run into an issue regarding a missing folder, to fix that, just create a "config" folder where CarCareTracker.exe is located.
If you wish to set up SMTP when using this approach, you will have to configure the environment settings in appsettings.json located in the same folder as CarCareTracker.exe
You just have to add the MailConfig section into it, but I provided the full appsettings.json anyways as an example.
```
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"UseDarkMode": false,
"EnableCsvImports": true,
"UseMPG": true,
"UseDescending": false,
"EnableAuth": false,
"HideZero": false,
"EnableAutoReminderRefresh": false,
"EnableAutoOdometerInsert": false,
"UseUKMPG": false,
"UseThreeDecimalGasCost": true,
"VisibleTabs": [ 0, 1, 4, 2, 3, 6, 5, 8 ],
"DefaultTab": 8,
"UserNameHash": "",
"UserPasswordHash": "",
"MailConfig": {
"EmailServer": "",
"EmailFrom": "",
"UseSSL": true,
"Port": 587,
"Username": "",
"Password": ""
}
}
```
When using this approach, the default port the app will be listening on is 5000, so you will navigate to localhost:5000
## Test that It Works
Whichever path you choose, once you get the app up and running, just navigate to the IP address and port the server is listening to and you should be able to see the app

View File

@@ -1,46 +0,0 @@
# Set Up HTTPS
LubeLogger runs on Kestrel, which is a cross-platform standalone web server provided by .NET
If you're running LubeLogger behind a reverse proxy(i.e. NGINX), then this walkthrough does not apply to you since the SSL certs will be served up by NGINX instead of Kestrel.
This article covers the step-by-step process to set up HTTPS for a LubeLogger instance.
## Docker
If you're running LubeLogger on a Docker instance, first read [this article by Microsoft](https://learn.microsoft.com/en-us/aspnet/core/security/docker-compose-https?view=aspnetcore-8.0)
1. Convert the .PEM / .CRT files into .PFX, read [this StackOverflow post](https://stackoverflow.com/questions/808669/convert-a-cert-pem-certificate-to-a-pfx-certificate)
2. Open and modify the .env file and add the following lines(note that in this example I used bob as the password for the cert)
```
ASPNETCORE_Kestrel__Certificates__Default__Password=bob
ASPNETCORE_Kestrel__Certificates__Default__Path=/https/<yourPFXCertificateName>.pfx
ASPNETCORE_URLS=https://+:443;http://+:80
```
3. Open and modify docker-compose.yml. You will need to bind a new volume to the Docker container so that Kestrel can access the certificate file.
```
volumes:
- ~/https/:/https:ro
```
4. Run `docker-compose up -d` to start up the container and `https://localhost` will now have a valid cert.
## Windows
If you're running LubeLogger as the standalone Windows executable, first read [this article by Microsoft](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel/endpoints?view=aspnetcore-8.0#configure-https-in-appsettingsjson)
1. Convert the .PEM / .CRT files into .PFX, read [this StackOverflow post](https://stackoverflow.com/questions/808669/convert-a-cert-pem-certificate-to-a-pfx-certificate)
2. Open and modify appsettings.json located in the same directory as the CarCareTracker executable and add the following lines(note that in this example I used bob as the password for the cert)
```
"Kestrel": {
"Endpoints": {
"Http": {
"Url": "http://localhost:80"
},
"HttpsInlineCertFile": {
"Url": "https://localhost:443",
"Certificate": {
"Path": "<path to .pfx file>",
"Password": "bob"
}
}
}
```
3. Restart the app and `https://localhost` will now have a valid cert.

View File

@@ -1,25 +0,0 @@
# Notes
The Notes tab contains important notes about your vehicle.
## Markdown Parsing
Markdown formatting is supported across all Notes fields in the app. The underlying markdown parser is [Drawdown](https://github.com/adamvleggett/drawdown). To toggle between edit and preview mode, simply click on the markdown icon next to the "Notes" label.
![](/Records/Notes/a/image-1706634016273.png)
![](/Records/Notes/a/image-1706634022811.png)
There is also a setting, that when enabled, will automatically load all notes in Markdown form when viewing existing records. The only exception is when an existing record has an empty notes field.
## Pinned Notes
Notes can be pinned by checking the "Pinned" switch when creating or editing a note.
![](/Records/Notes/a/image-1706633376978.png)
Pinned Notes will always show up at the very top of the list.
![](/Records/Notes/a/image-1706455413890.png)
On non-touchscreen devices, pinned notes can be viewed when the user hovers over the vehicle tile in the garage. On mobile, the user have to hold down on the garage tile in order for the pinned notes to show up.
![](/Records/Notes/a/image-1706455460700.png)

View File

@@ -1,14 +0,0 @@
# Odometer
The Odometer tab is where you can log your current odometer reading without having to insert any Service/Repair/Upgrade/Fuel records. This odometer readings entered in this tab allows Reminder urgencies to be calculated as accurately as possible since it uses the maximum mileage reported in each of the tabs to determine the last reported mileage.
The Odometer tab is hidden by default and must be enabled by checking the "Odometer" switch under "Visible Tabs" in the Settings tab.
## API Integration
As with the other tabs, odometer readings can be retrieved via a GET endpoint and inserted via a POST API endpoint.
Example use cases:
- An app to integrate with OBDII and insert odometer reading from the vehicle's computer onto LubeLogger.
- An app to keep track of distance traveled via GPS and incrementing the last reported odometer reading.
These are not functionalities provided out of the box by LubeLogger, and are just examples of the possibilities achievable via the API endpoints.

View File

@@ -1,62 +0,0 @@
# Planner
The Planner tab is where you can plan and track progress of future or current plans for your vehicle. The planner tab consists of 4 swimlanes(vertical panels) where plans can be moved to different stages via drag and drop.
![](/Records/Planner/a/image-1706459391188.png)
The records stored in Service/Repair/Upgrade tabs tend to be inserted retroactively(after the work is done), hence the Planner tab was developed to keep track of To-Do's.
## Adding a new Plan
The Planner tab is hidden by default, you must enable it by checking the "Planner" switch under "Visible Tabs" within the Settings tab.
To add a new plan, simply click on the "Add Plan Record" button and fill in the details of the plan.
![](/Records/Planner/a/image-1707453045755.png)
## Plan Type
You are required to select at least one Plan Type - Service, Repair, or Upgrade. When the Plan Record is moved to Done, the Plan Record will be automatically converted into the selected type and it will then show up in its respective tab. The type is indicated by an icon corresponding to their respective tabs.
## Plan Priority
Within the swimlanes, plans are sorted from Critical priority to Low priority. They are also indicated by an icon - Fire for Critical, Waves for Normal, and Snowflake for Low.
![](/Records/Planner/a/image-1706459640917.png)
## Plan Stages
**Planned** - This is considered the backlog, where no work has been performed yet. i.e.: Shopping for a lift kit.
**Doing** - This is when work has been started on the task. i.e.: Installing the lift kit.
**Testing** - This is where the work is being reviewed / tested. i.e.: Going for a test drive with the lift kit.
**Done** - This is where the work is marked as completed.
### Updating Plan Stages
There are two ways you can update the stage a plan is currently in: drag and drop or updating it via the dropdown when editing a plan. You can backtrack a plan's stages within Planned, Doing, and Testing. i.e.: You can move the plan back from Testing to Doing if you find out there is more work to be done.
### Moving to Done
when a Plan is being moved to Done, it will prompt you to enter the current odometer reading. This is to ensure that the Service/Repair/Upgrade record that will be created from it contains the most up-to-date odometer reading.
Once a Plan has been marked as Done, it can never be taken out of it. The only action that can be performed on it is Delete. You can delete Plans in the Done stage by clicking on them.
## Plan Templates
For records that have a fixed interval(i.e. oil changes), it is recommended that you create a Plan Template instead.
### Creating a Template
To create a template, simply fill in the details / select the supplies to requisition for the plan, but instead of clicking the "Add New Plan Record" button, you will click on the dropdown next to it and select "Save As Template"
![](/Records/Planner/a/image-1707453250314.png)
The template will now be created and named after the description of the plan record.
### Using Templates
To create a plan record from a template, click on the "View Templates button" and a dialog will show up with the saved templates for the vehicle
![](/Records/Planner/a/image-1707453421824.png)
![](/Records/Planner/a/image-1707453431731.png)
The shop icon indicates if the template has supplies requisition and the paper clip icon(not shown) indicates if the template has file attachments.
To use the template, simply click on the "+" button. The app will then perform a check to ensure that there are sufficient quantities of the supplies used by the template. If there are insufficient quantities, an error message will pop up telling you which supply is short.
![](/Records/Planner/a/image-1707453639902.png)

View File

@@ -1,53 +0,0 @@
# Postgres
For users that desire additional scalability for their backend, LubeLogger now supports a PostgreSQL backend.
## Configuration
To configure LubeLogger to use PostgreSQL, you must first create a database with a schema named "app" in it, in the screenshot below we created a DB named "lubelogger" and then a schema named "app".
![](/Postgres/a/image-1707454502212.png)
Once that is done, simply inject the environment variable `POSTGRES_CONNECTION` with your connection string, example:
```
Host=<yourserveraddress:port>;Username=<yourusername>;Password=<yourpassword>;Database=<databasename>;
```
LubeLogger will then automatically create the tables it needs, all records will then be saved and loaded from Postgres tables from now on.
## Backups
Once you have switched over to Postgres, LubeLogger's built in and backup function will only back up images, documents, and the server config. You are responsible for maintaining backups of the DB records.
## Database Migration
A tool is provided to ease the migration process between LiteDB and Postgres. This tool can be found at the `/migration` endpoint and is only accessible when a Postgres connection is provided.
![](/Postgres/a/image-1707516092170.png)
### Importing to Postgres
To transfer all your existing data from LiteDB to Postgres:
1. Create a backup using the "Make Backup" feature in the Settings tab.
2. Extract the zip file.
3. You should see a folder named "data" in the extracted folder
4. Inside the data folder will be a .db file named `cartracker.db`
5. Navigate to the Database Migration tool
6. Click "Import to Postgres"
7. Select the `cartracker.db` file
8. Your data will be imported into your Postgres DB, and you may double check that the DB has been imported successfully using a Postgres DB Administration Tool.
### Exporting from Postgres
In the event that you need to transfer all your data back onto a LiteDB database file from Postgres, you may do so using the Database Migration tool:
1. Navigate to the Database Migration Tool
2. Click "Export from Postgres"
3. Extract the downloaded zip file and you should find `cartracker.db` in it.
4. Create a backup using the "Make Backup" feature in the Settings tab.
5. Extract the zip file.
6. You should see a folder named "data" in the extracted folder, if not, create it.
7. Place `cartracker.db` inside the "data" folder
8. Re-zip the extracted folder
9. Restore the backup using the "Restore Backup" feature in the Settings tab.
10. Make sure you remove the PostgreSQL connection from the environment variables so that all future changes will be saved in LiteDB.

View File

@@ -1,49 +0,0 @@
# Reminders
Reminders are future tasks where the urgency are based on how close the user is to the due date or odometer reading.
![](/Records/Reminders/a/image-1706403372050.png)
## Adding Reminders
Reminders can either be added directly on the Reminders tab by clicking the "Add Reminder" button or via adding a new [[Service/Repair/Upgrade Record|Records/Service Records#adding-reminders]].
## Reminder Metrics
Similar to the maintenance schedule outlined in your vehicle's user manual, the urgency of a Reminder can be set either via a due date, a future odometer reading, or whichever comes first.
![](/Records/Reminders/a/image-1706403594197.png)
## Reminder Urgency
Depending on the metric selected, reminder urgency is calculated either via the server's current date, the max odometer reading across the Odometer/Service/Repair/Upgrade/Fuel tabs, or whichever comes first.
| Urgency | Due Date | Future Odometer Reading |
| ---------- | ------------- | ----------------------- |
| Not Urgent | > 30 days out | > 100 miles out |
| Urgent | < 30 days out | < 100 miles out |
| Very Urgent | < 7 days out | < 50 miles out |
| Past Due | > 0 days past | > 0 miles past |
## Recurring Reminders
Reminders can be set to become recurring so that you don't have to create a new reminder for recurring maintenance such as oil changes. When you have completed the task set by the reminder, you can either have it automatically refresh when it lapses or by manually refreshing it. Refreshing a reminder effectively pushes out the due date or the odometer reading based on the recurring interval, i.e.: if a Reminder is due at 10000 miles and the interval is set at every 5000 miles, refreshing the Reminder will push the future odometer reading out to 15000 miles.
### Automatically Refresh Past Due Reminders
There is a setting within the Settings tab that allows users to automatically refresh past due reminders. Note that with this setting enabled, any reminder that becomes Past Due will be automatically refreshed, this requires a lot of diligence from the user to heed their reminders and stay on top of it.
![](/Records/Reminders/a/image-1706404019404.png)
### Manually Refresh Reminders
When a recurring reminder falls into Very Urgent or Past Due status, there will be a button on the Reminders page that will allow the user to manually refresh the reminder.
![](/Records/Reminders/a/image-1706404336137.png)
This reminder is set to be recurring every 1 year, so when the "Done" button is clicked, it will push the due date of this reminder to 1/31/2025.
![](/Records/Reminders/a/image-1706404394320.png)
![](/Reminders/a/image-1706404403748.png)
## Reminder Emails
If SMTP is configured within LubeLogger, the Root User can set up a cron / scheduled task that runs at an interval to send out emails to collaborators of vehicles with reminders. The API endpoint allows the user to specify what level of urgencies should the user be notified of.
Sample bash script: https://github.com/hargata/lubelog_scripts/blob/main/bash/sendreminders.sh
The sample provided above will send email reminders out for reminders of all urgencies.

View File

@@ -1,19 +0,0 @@
# Replacing The LubeLogger Logo
You can overwrite the LubeLogger Logo that is displayed in the Login and Home/Garage page.
To do so, simply inject an environment variable with the key `LUBELOGGER_LOGO_URL` into your lubelogger instance either via the .env file or the appsettings.json file.
## .env
```
LUBELOGGER_LOGO_URL=<URL to your Logo>
```
## appsettings.json
```
LUBELOGGER_LOGO_URL:<URL to your Logo>
```
## Non-replaceable Locations
- Logo in the About section in the Settings tab
- Logo that shows up in the top left of the Vehicle Maintenance History Report

View File

@@ -1,36 +0,0 @@
# Service/Repair/Upgrade Records
These three are perhaps the most important tabs in LubeLogger. They are functionally identically to one another, except for the type of records stored in them.
Service Records: These are planned/scheduled maintenance performed on the vehicle, usually on a fixed interval. Examples include oil changes, brakes, tires, spark plugs, air filter.
Repair Records: These are unplanned/unscheduled work performed on the vehicle whether due to an accident, a component breaking unexpectedly, or a broken component with no fixed maintenance interval. Examples include replacing the alternators, starters, radiators, power steering pump, bumpers.
Upgrade Records: These are work performed on the vehicle that enhances the functionality or aesthetics of the vehicle. Examples include: roof racks, lift kits, aftermarket wheels, stereos.
To add a new record, simply navigate to the tab and click the "Add New Service/Repair/Upgrade Record" button and you will be prompted to input the details of the record.
![](/Records/Service%20Records/a/image-1706797383329.png)
## Moving Records
To move existing records between the three tabs, simply click on the dropdown button to the right of the Delete button and select the tab to move the record to.
![](/Records/Service%20Records/a/image-1706797399768.png)
## Supplies Requisition
If you have supplies set up, you can click the "Choose Supplies" link under the Cost field, and a dialog will prompt you to select the supplies and quantity of each supplies you wish to requisition for this record.
![](/Records/Service%20Records/a/image-1706402198461.png)
Once you have selected the supplies, the Cost field will automatically update to reflect the costs of the supplies you have selected based on the quantity of each supply. Note that at this point, before the record is created, the supply is not requisitioned yet and you can still edit the selected supplies/quantities.
Once the record has been created, the supplies will be requisitioned and the quantity / cost of the supplies will be deducted according to the usage. This cannot be reversed(i.e.: you cannot restore the quantities by editing an existing service/repair/upgrade record), you have to go to the Supplies tab to correct the quantity/cost of the supply.
### Supplies Unit Cost Calculation
LubeLogger is not an inventory management system. Unit costs are calculated as an average of total spent / quantity, which means that everytime you replenish your supplies, it will average out the cost even if the latest batch of supplies you purchased is significantly costlier than the last batch. There is no LIFO/FIFO/FAFO inventory valuation methods.
For more information on Supplies, see [[Supplies|Records/Supplies]]
## Adding Reminders
You are given the option to set a reminder upon creating a record. This is helpful for recurring services such as Oil Changes. To do so, simply check the "Add Reminder" switch before clicking the "Add New Service Record" button. A new dialog will show up after the record has been created and all the fields will be pre-populated.
For more information on Reminders, see [[Reminders|Records/Reminders]]

View File

@@ -1,27 +0,0 @@
# Supplies
The Supplies tab is where you can keep track of supplies or parts purchased for your vehicle, either as spare parts or for future use.
![](/Records/Supplies/a/image-1706402930133.png)
The Supplies tab is hidden by default and requires the user to enable it via the Settings tab under the "Visible Tabs" section.
![](/Records/Supplies/a/image-1706403044005.png)
To add a new Supply Record, simply click the "Add New Supply Record" button and you will be prompted for the details of the Supply.
![](/Records/Supplies/a/image-1706403006743.png)
Supplies that are in the system that have a quantity greater than zero are available for [[Requisitioning|Records/Service Records#supplies-requisition]]
## Shop Supplies
Shop supplies are for supplies that are at a garage level and is available for requisitioning across all vehicles. This is useful for supplies that are shared across multiple vehicles such as motor oil, washer fluid, tires, etc.
![](/Records/Supplies/a/image-1707454148363.png)
This tab is disabled by default, but can be enabled by the Root User in the Settings tab.
![](/Records/Supplies/a/image-1707454169597.png)
Note that shop supplies are available for all users and all vehicles, do not enable this if you don't wish to share supplies with other users. When enabled, any user can add / edit / delete any shop supplies.

View File

@@ -1,22 +0,0 @@
# Taxes
The Taxes tab keeps track of fees and expenses that are incurred by your vehicle that are unrelated to odometer readings.
![](/Records/Taxes/a/image-1706496894832.png)
Examples of records that can be recorded in the Taxes tab include:
- Registration Fee/Road Tax
- Insurance
- Fines/Citations
- Roadside Assistance
- Car Washes
- and more!
## Recurring Taxes
Taxes can be set to be recurring so that a new record is created at every interval. The newly-created tax record is duplicated from the last recurrence.
![](/Records/Taxes/a/image-1706496915044.png)
The way this functionality works is that when the user clicks into a vehicle detail, a check is performed if there are any recurring tax records that are past due. If there are, those tax records are then duplicated with the due date pushed out by the set interval. The newly duplicated will be set to recur at the set interval while the tax record that was past due will have recurrance disabled.
### Notes on Recurring Taxes
Since recurring taxes are duplicated from the previous occurrence, it is not recommended for fees with differing values be set up as recurring, i.e.: Registration Fees do not stay constant year to year in most if not all parts of the United States, even if most people renew their vehicle's registration at the same time every year.

View File

@@ -1,35 +0,0 @@
# Translations
LubeLogger supports UI Translations for ~95% of UI elements.
The following are not covered by translations:
- Toasts(messages that pop up on the top right)
- Sweetalert prompts(confirm delete dialogs, etc)
- About section
## Where to get translations
Translations can be found at [this repository](https://github.com/hargata/lubelog_translations/)
1. To upload a translation file, login as the root user.
2. Navigate to "Settings"
3. Click "Upload" under the "Manage Languages" section
![](/Translations/a/image-1707068703210.png)
4. Select the language file you wish to upload
5. The page should refresh
6. Select the language file from the dropdown to set it as your default language.
## Creating your own translation
1. Download the [latest en_US.json](https://github.com/hargata/lubelog/blob/main/wwwroot/defaults/en_US.json) file from the GitHub Repository for LubeLogger.
2. Rename this file, en_US is a reserved name.
3. Use a JSON pretty-printer to make it human-readable
![](/Translations/a/image-1707068227706.png)
3. The objects to the left of the ":" are the translation keys, DO NOT modify these.
4. The objects to the right of the ":" are the translation values(shown in green), these are what you want to translate.
5. To test out your translation, simply upload it to your LubeLogger instance and test it out.
## Contribute
Follow the instructions outlined in the [official repository](https://github.com/hargata/lubelog_translations/)
Translation efforts are coordinated via [this thread](https://github.com/hargata/lubelog/discussions/240)

View File

@@ -1,56 +0,0 @@
# Troubleshooting
Common issues and steps you can take to fix them.
## General Issues
### Button doesn't work / feature stopped working.
Your browser might have cached an older version of a JavaScript(JS) file which is no longer compatible with the current version of LubeLogger. Clear your browser's cache and retry.
### Can't Send Email via SMTP
Note that for most email providers, you can no longer use your account password to authenticate and must instead generate an app password for LubeLogger to be able to authenticate on your behalf to your email provider's SMTP server.
If you've downloaded the .env file from the GitHub repo, there is an issue with how the file gets formatted when it is downloaded, you will have to copy the contents and re-create one manually on your machine.
### Console shows Authentication Errors
Those are purely informational, add a line in your environment variables to prevent information logs from showing up in the console.
## Locale Issues
### Can't input values in "," format / shows up as 0.
Ensure that your locale environment variables are configured correctly, note that if running via docker, both environment variables LANG and LC_ALL have to be identical.
### Can't change locale.
Environment variables are injected on deployment. You will need to re-deploy.
## Server Issues
### NGINX / Cloudflare
LubeLogger is a web app that runs on Kestrel, it literally doesn't matter if it's deployed behind a reverse proxy or Cloudflare tunnel. As long as the app can receive traffic on the port it's configured on, it will run.
Here's a sample Nginx reverse proxy configuration courtesy of [thehijacker](https://github.com/thehijacker)
```
server
{
listen 443 ssl http2;
server_name lubelogger.domain.com;
ssl_certificate /etc/nginx/ssl/acme/domain.com/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/acme/domain.com/key.pem;
ssl_dhparam /etc/nginx/ssl/acme/domain.com/dhparams.pem;
ssl_trusted_certificate /etc/nginx/ssl/acme/domain.com/fullchain.pem;
location /
{
proxy_pass http://192.168.28.53:8289;
client_max_body_size 50000M;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_redirect off;
}
}
```

View File

@@ -1,50 +0,0 @@
# Vehicle Management
## Adding a Vehicle
To add a vehicle, simply click on the green "+" button in the "Garage" tab. A dialog will then prompt you for the following details of the vehicle you wish to add: Year, Make, Model, License Plate, and optionally, a picture of the vehicle. If it's an Electric Vehicle, you should check the "Electric Vehicle" switch. This ensures that "fuel" economy is measured in kWh instead of gallons or liters.
![](/Vehicle%20Management/a/image-1706759765922.png)
Once you're done, click "Add New Vehicle" and the vehicle will now be visible in the Garage Tab.
![](/Vehicle%20Management/a/image-1706759782021.png)
### Sorting Vehicles
To sort vehicles by Year, simply right click on the Garage tab while the tab is selected.
On mobile, make sure the Garage tab is selected and long hold on the garage menu button.
## Editing a Vehicle
To edit an existing vehicle, go into the vehicle details by clicking on the vehicle tile in the Garage. If you're on a computer/tablet, there will be a yellow button on the top right of the screen, click it to edit details regarding the vehicle.
![](/Vehicle%20Management/a/image-1706759823490.png)
On mobile devices, the "Edit Vehicle" button is available within the menu
![](/Vehicle%20Management/a/image-1706400015891.png)
## Deleting a Vehicle.
To delete an existing vehicle, click on the "Manage Vehicle" dropdown and select "Delete Vehicle". You will then be prompted for confirmation before the vehicle is deleted. Once the vehicle is deleted, you will be redirected back to the Garage.
![](/Vehicle%20Management/a/image-1706400064717.png)
On mobile devices, the "Delete Vehicle" button is available within the menu, located at the very bottom.
![](/Vehicle%20Management/a/image-1706400198451.png)
## Adding a Collaborator
If more than one individual logs records to your vehicle(e.g.: spouse, employee, etc) and they have their own user account with LubeLogger, you can add them as a collaborator.
To add new collaborator, simply navigate to the Dashboard tab in the vehicle details view and look to the bottom right:
![](/Vehicle%20Management/a/image-1706400316910.png)
Click on the blue Add User icon on the top left of the Collaborators panel and you will be prompted to type in their username, note that a user with that username must exist in the system or you will get an error.
![](/Vehicle%20Management/a/image-1706400382568.png)
Once you have added them as a collaborator, their name will now show up in the Collaborators list, and you will also be given the option to remove them.
![](/Vehicle%20Management/a/image-1706400425454.png)
Once this is done, you should have the new collaborator refresh their browser and they should be able to see the vehicle in their Garage.

View File

@@ -163,7 +163,7 @@
</div>
<div class="col-12 d-flex justify-content-center">
<p class="lead">
Read this <a class='link-light link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover' href='https://docs.lubelogger.com/Getting%20Started'>Getting Started Guide</a> on how to download either of them
Read this <a class='link-light link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover' href='https://docs.lubelogger.com/Installation/Getting%20Started'>Getting Started Guide</a> on how to download either of them
</p>
</div>
</div>

View File

@@ -377,4 +377,40 @@ input[type="file"] {
.accordion-button.skinny {
padding: 0.438rem 0rem !important;
background-color: inherit !important;
}
.planner-indicator{
font-size:1em;
padding:0.2em;
}
html[data-bs-theme="dark"] .btn-adaptive {
--bs-btn-color: #fff;
--bs-btn-bg: #212529;
--bs-btn-border-color: #212529;
--bs-btn-hover-color: #fff;
--bs-btn-hover-bg: #424649;
--bs-btn-hover-border-color: #373b3e;
--bs-btn-focus-shadow-rgb: 66, 70, 73;
--bs-btn-active-color: #fff;
--bs-btn-active-bg: #4d5154;
--bs-btn-active-border-color: #373b3e;
--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--bs-btn-disabled-color: #fff;
--bs-btn-disabled-bg: #212529;
--bs-btn-disabled-border-color: #212529;
}
html[data-bs-theme="light"] .btn-adaptive {
--bs-btn-color: #000;
--bs-btn-bg: #f8f9fa;
--bs-btn-border-color: #f8f9fa;
--bs-btn-hover-color: #000;
--bs-btn-hover-bg: #d3d4d5;
--bs-btn-hover-border-color: #c6c7c8;
--bs-btn-focus-shadow-rgb: 211, 212, 213;
--bs-btn-active-color: #000;
--bs-btn-active-bg: #c6c7c8;
--bs-btn-active-border-color: #babbbc;
--bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
--bs-btn-disabled-color: #000;
--bs-btn-disabled-bg: #f8f9fa;
--bs-btn-disabled-border-color: #f8f9fa;
}

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -100,6 +100,9 @@ function getAndValidateCollisionRecordValues() {
var collisionRecordId = getCollisionRecordModelData().id;
var addReminderRecord = $("#addReminderCheck").is(":checked");
//Odometer Adjustments
if (isNaN(collisionMileage) && GetVehicleId().odometerOptional) {
collisionMileage = '0';
}
collisionMileage = GetAdjustedOdometer(collisionRecordId, collisionMileage);
//validation
var hasError = false;

View File

@@ -99,6 +99,9 @@ function getAndValidateGasRecordValues() {
var vehicleId = GetVehicleId().vehicleId;
var gasRecordId = getGasRecordModelData().id;
//Odometer Adjustments
if (isNaN(gasMileage) && GetVehicleId().odometerOptional) {
gasMileage = '0';
}
gasMileage = GetAdjustedOdometer(gasRecordId, gasMileage);
//validation
var hasError = false;

View File

@@ -287,7 +287,10 @@ function updatePlanRecordProgress(newProgress) {
showCancelButton: true,
focusConfirm: false,
preConfirm: () => {
const odometer = $("#inputOdometer").val();
var odometer = $("#inputOdometer").val();
if (odometer.trim() == '' && GetVehicleId().odometerOptional) {
odometer = '0';
}
if (!odometer || isNaN(odometer)) {
Swal.showValidationMessage(`Please enter an odometer reading`)
}

View File

@@ -102,6 +102,14 @@ function deleteReminderRecord(reminderRecordId, e) {
}
});
}
function toggleCustomThresholds() {
var isChecked = $("#reminderUseCustomThresholds").is(':checked');
if (isChecked) {
$("#reminderCustomThresholds").collapse('show');
} else {
$("#reminderCustomThresholds").collapse('hide');
}
}
function saveReminderRecordToVehicle(isEdit) {
//get values
var formValues = getAndValidateReminderRecordValues();
@@ -180,9 +188,13 @@ function getAndValidateReminderRecordValues() {
var reminderRecurringMonth = $("#reminderRecurringMonth").val();
var reminderRecurringMileage = $("#reminderRecurringMileage").val();
var reminderTags = $("#reminderRecordTag").val();
var reminderCustomMileageInterval = customMileageInterval;
var vehicleId = GetVehicleId().vehicleId;
var reminderId = getReminderRecordModelData().id;
var reminderUseCustomThresholds = $("#reminderUseCustomThresholds").is(":checked");
var reminderUrgentDays = $("#reminderUrgentDays").val();
var reminderVeryUrgentDays = $("#reminderVeryUrgentDays").val();
var reminderUrgentDistance = $("#reminderUrgentDistance").val();
var reminderVeryUrgentDistance = $("#reminderVeryUrgentDistance").val();
//validation
var hasError = false;
var reminderDateIsInvalid = reminderDate.trim() == ''; //eliminates whitespace.
@@ -205,6 +217,33 @@ function getAndValidateReminderRecordValues() {
} else {
$("#reminderDescription").removeClass("is-invalid");
}
if (reminderUseCustomThresholds) {
//validate custom threshold values
if (reminderUrgentDays.trim() == '' || isNaN(reminderUrgentDays) || parseInt(reminderUrgentDays) < 0) {
hasError = true;
$("#reminderUrgentDays").addClass("is-invalid");
} else {
$("#reminderUrgentDays").removeClass("is-invalid");
}
if (reminderVeryUrgentDays.trim() == '' || isNaN(reminderVeryUrgentDays) || parseInt(reminderVeryUrgentDays) < 0) {
hasError = true;
$("#reminderVeryUrgentDays").addClass("is-invalid");
} else {
$("#reminderVeryUrgentDays").removeClass("is-invalid");
}
if (reminderUrgentDistance.trim() == '' || isNaN(reminderUrgentDistance) || parseInt(reminderUrgentDistance) < 0) {
hasError = true;
$("#reminderUrgentDistance").addClass("is-invalid");
} else {
$("#reminderUrgentDistance").removeClass("is-invalid");
}
if (reminderVeryUrgentDistance.trim() == '' || isNaN(reminderVeryUrgentDistance) || parseInt(reminderVeryUrgentDistance) < 0) {
hasError = true;
$("#reminderVeryUrgentDistance").addClass("is-invalid");
} else {
$("#reminderVeryUrgentDistance").removeClass("is-invalid");
}
}
if (reminderOption == undefined) {
hasError = true;
$("#reminderMetricDate").addClass("is-invalid");
@@ -226,6 +265,13 @@ function getAndValidateReminderRecordValues() {
notes: reminderNotes,
metric: reminderOption,
isRecurring: reminderIsRecurring,
useCustomThresholds: reminderUseCustomThresholds,
customThresholds: {
urgentDays: reminderUrgentDays,
veryUrgentDays: reminderVeryUrgentDays,
urgentDistance: reminderUrgentDistance,
veryUrgentDistance: reminderVeryUrgentDistance
},
reminderMileageInterval: reminderRecurringMileage,
reminderMonthInterval: reminderRecurringMonth,
customMileageInterval: customMileageInterval,

View File

@@ -180,6 +180,17 @@ function exportAttachments() {
}
});
}
function showDataTable() {
var vehicleId = GetVehicleId().vehicleId;
var year = getYear();
$.get(`/Vehicle/GetCostTableForVehicle?vehicleId=${vehicleId}`, { year: year }, function (data) {
$("#vehicleDataTableModalContent").html(data);
$("#vehicleDataTableModal").modal('show');
});
}
function hideDataTable() {
$("#vehicleDataTableModal").modal('hide');
}
function showGlobalSearch() {
$('#globalSearchModal').modal('show');
}

View File

@@ -100,6 +100,9 @@ function getAndValidateServiceRecordValues() {
var serviceRecordId = getServiceRecordModelData().id;
var addReminderRecord = $("#addReminderCheck").is(":checked");
//Odometer Adjustments
if (isNaN(serviceMileage) && GetVehicleId().odometerOptional) {
serviceMileage = '0';
}
serviceMileage = GetAdjustedOdometer(serviceRecordId, serviceMileage);
//validation
var hasError = false;

154
wwwroot/js/settings.js Normal file
View File

@@ -0,0 +1,154 @@
function showExtraFieldModal() {
$.get(`/Home/GetExtraFieldsModal?importMode=0`, function (data) {
$("#extraFieldModalContent").html(data);
$("#extraFieldModal").modal('show');
});
}
function hideExtraFieldModal() {
$("#extraFieldModal").modal('hide');
}
function getCheckedTabs() {
var visibleTabs = $("#visibleTabs :checked").map(function () {
return this.value;
});
return visibleTabs.toArray();
}
function deleteLanguage() {
var languageFileLocation = `/translations/${$("#defaultLanguage").val()}.json`;
$.post('/Files/DeleteFiles', { fileLocation: languageFileLocation }, function (data) {
//reset user language back to en_US
$("#defaultLanguage").val('en_US');
updateSettings();
});
}
function updateColorModeSettings(e) {
var colorMode = $(e).prop("id");
switch (colorMode) {
case "enableDarkMode":
//uncheck system prefernce
$("#useSystemColorMode").prop('checked', false);
updateSettings();
break;
case "useSystemColorMode":
$("#enableDarkMode").prop('checked', false);
updateSettings();
break;
}
}
function updateSettings() {
var visibleTabs = getCheckedTabs();
var defaultTab = $("#defaultTab").val();
if (!visibleTabs.includes(defaultTab)) {
defaultTab = "Dashboard"; //default to dashboard.
}
//Root User Only Settings that aren't rendered:
var defaultReminderEmail = $("#inputDefaultEmail").length > 0 ? $("#inputDefaultEmail").val() : "";
var disableRegistration = $("#disableRegistration").length > 0 ? $("#disableRegistration").is(":checked") : false;
var enableRootUserOIDC = $("#enableRootUserOIDC").length > 0 ? $("#enableRootUserOIDC").is(":checked") : false;
var userConfigObject = {
useDarkMode: $("#enableDarkMode").is(':checked'),
useSystemColorMode: $("#useSystemColorMode").is(':checked'),
enableCsvImports: $("#enableCsvImports").is(':checked'),
useMPG: $("#useMPG").is(':checked'),
useDescending: $("#useDescending").is(':checked'),
hideZero: $("#hideZero").is(":checked"),
useUKMpg: $("#useUKMPG").is(":checked"),
useThreeDecimalGasCost: $("#useThreeDecimal").is(":checked"),
useMarkDownOnSavedNotes: $("#useMarkDownOnSavedNotes").is(":checked"),
enableAutoReminderRefresh: $("#enableAutoReminderRefresh").is(":checked"),
enableAutoOdometerInsert: $("#enableAutoOdometerInsert").is(":checked"),
enableShopSupplies: $("#enableShopSupplies").is(":checked"),
enableExtraFieldColumns: $("#enableExtraFieldColumns").is(":checked"),
hideSoldVehicles: $("#hideSoldVehicles").is(":checked"),
preferredGasUnit: $("#preferredGasUnit").val(),
preferredGasMileageUnit: $("#preferredFuelMileageUnit").val(),
userLanguage: $("#defaultLanguage").val(),
visibleTabs: visibleTabs,
defaultTab: defaultTab,
disableRegistration: disableRegistration,
defaultReminderEmail: defaultReminderEmail,
enableRootUserOIDC: enableRootUserOIDC
}
sloader.show();
$.post('/Home/WriteToSettings', { userConfig: userConfigObject }, function (data) {
sloader.hide();
if (data) {
setTimeout(function () { window.location.href = '/Home/Index?tab=settings' }, 500);
} else {
errorToast(genericErrorMessage());
}
})
}
function makeBackup() {
$.get('/Files/MakeBackup', function (data) {
window.location.href = data;
});
}
function openUploadLanguage() {
$("#inputLanguage").click();
}
function openRestoreBackup() {
$("#inputBackup").click();
}
function uploadLanguage(event) {
let formData = new FormData();
formData.append("file", event.files[0]);
sloader.show();
$.ajax({
url: "/Files/HandleTranslationFileUpload",
data: formData,
cache: false,
processData: false,
contentType: false,
type: 'POST',
success: function (response) {
sloader.hide();
if (response.success) {
setTimeout(function () { window.location.href = '/Home/Index?tab=settings' }, 500);
} else {
errorToast(response.message);
}
},
error: function () {
sloader.hide();
errorToast("An error has occurred, please check the file size and try again later.");
}
});
}
function restoreBackup(event) {
let formData = new FormData();
formData.append("file", event.files[0]);
console.log('LubeLogger - DB Restoration Started');
sloader.show();
$.ajax({
url: "/Files/HandleFileUpload",
data: formData,
cache: false,
processData: false,
contentType: false,
type: 'POST',
success: function (response) {
if (response.trim() != '') {
$.post('/Files/RestoreBackup', { fileName: response }, function (data) {
sloader.hide();
if (data) {
console.log('LubeLogger - DB Restoration Completed');
successToast("Backup Restored");
setTimeout(function () { window.location.href = '/Home/Index' }, 500);
} else {
errorToast(genericErrorMessage());
console.log('LubeLogger - DB Restoration Failed - Failed to process backup file.');
}
});
} else {
console.log('LubeLogger - DB Restoration Failed - Failed to upload backup file.');
}
},
error: function () {
sloader.hide();
console.log('LubeLogger - DB Restoration Failed - Request failed to reach backend, please check file size.');
errorToast("An error has occurred, please check the file size and try again later.");
}
});
}

View File

@@ -43,11 +43,15 @@ function saveVehicle(isEdit) {
var vehicleIsElectric = $("#inputFuelType").val() == 'Electric';
var vehicleIsDiesel = $("#inputFuelType").val() == 'Diesel';
var vehicleUseHours = $("#inputUseHours").is(":checked");
var vehicleOdometerOptional = $("#inputOdometerOptional").is(":checked");
var vehicleHasOdometerAdjustment = $("#inputHasOdometerAdjustment").is(':checked');
var vehicleOdometerMultiplier = $("#inputOdometerMultiplier").val();
var vehicleOdometerDifference = parseInt(globalParseFloat($("#inputOdometerDifference").val())).toString();
var vehiclePurchasePrice = $("#inputPurchasePrice").val();
var vehicleSoldPrice = $("#inputSoldPrice").val();
var vehicleDashboardMetrics = $("#collapseMetricInfo :checked").map(function () {
return this.value;
}).toArray();
var extraFields = getAndValidateExtraFields(true);
//validate
var hasError = false;
@@ -126,11 +130,13 @@ function saveVehicle(isEdit) {
extraFields: extraFields.extraFields,
purchaseDate: vehiclePurchaseDate,
soldDate: vehicleSoldDate,
odometerOptional: vehicleOdometerOptional,
hasOdometerAdjustment: vehicleHasOdometerAdjustment,
odometerMultiplier: vehicleOdometerMultiplier,
odometerDifference: vehicleOdometerDifference,
purchasePrice: vehiclePurchasePrice,
soldPrice: vehicleSoldPrice
soldPrice: vehicleSoldPrice,
dashboardMetrics: vehicleDashboardMetrics
}, function (data) {
if (data) {
if (!isEdit) {

View File

@@ -100,6 +100,9 @@ function getAndValidateUpgradeRecordValues() {
var upgradeRecordId = getUpgradeRecordModelData().id;
var addReminderRecord = $("#addReminderCheck").is(":checked");
//Odometer Adjustments
if (isNaN(upgradeMileage) && GetVehicleId().odometerOptional) {
upgradeMileage = '0';
}
upgradeMileage = GetAdjustedOdometer(upgradeRecordId, upgradeMileage);
//validation
var hasError = false;