Compare commits

..

36 Commits

Author SHA1 Message Date
DESKTOP-T0O5CDB\DESK-555BD
29680cf0e9 made vehicle title the edit button. 2025-05-17 10:39:59 -06:00
DESKTOP-T0O5CDB\DESK-555BD
723eb1a769 Added API endpoint to check for latest release. 2025-05-17 10:14:36 -06:00
DESKTOP-T0O5CDB\DESK-555BD
732a628c20 Merge branch 'Hargata/148.changes' into Hargata/updated.layout 2025-05-17 09:49:37 -06:00
DESKTOP-T0O5CDB\DESK-555BD
84d40edb0e set graph grace see #941 2025-05-16 19:58:52 -06:00
DESKTOP-T0O5CDB\DESK-555BD
9842f0e501 add report header. 2025-05-16 19:23:56 -06:00
DESKTOP-T0O5CDB\DESK-555BD
44c5f921a5 Merge branch 'Hargata/908' into Hargata/updated.layout 2025-05-16 16:54:25 -06:00
DESKTOP-T0O5CDB\DESK-555BD
fae4aa31aa show vehicle thumbnail 2025-05-16 14:45:06 -06:00
Hargata Softworks
adb505c87c Merge pull request #950 from iamdabe/vehicle-home-thumbnail
Setting to show the vehicle thumbnail in the header
2025-05-16 14:31:45 -06:00
Hargata Softworks
d21b0a9e29 Merge branch 'Hargata/updated.layout' into vehicle-home-thumbnail 2025-05-16 14:31:30 -06:00
Hargata Softworks
15e4181aff Merge pull request #949 from iamdabe/main
Updated _NoteModal.cshtml label to correctly target file input 'noteFiles'
2025-05-16 14:25:11 -06:00
DESKTOP-T0O5CDB\DESK-555BD
3a4627ef02 use regular sized logo in mobile view in garage 2025-05-16 14:12:08 -06:00
DESKTOP-T0O5CDB\DESK-555BD
d9e11273bd fix admin panel view 2025-05-16 13:49:59 -06:00
DESKTOP-T0O5CDB\DESK-555BD
8e6f40a1b1 Updated layout for post hiatus release. 2025-05-16 13:44:53 -06:00
DESKTOP-T0O5CDB\DESK-555BD
55f86ecb3f add reportheader. 2025-05-16 11:11:10 -06:00
Dave Walker
8eef1465cf First commit for new setting to show the vehicle thumbnail in the header of each vehicle page. 2025-05-15 18:22:48 +02:00
iamdabe
fd0ffba7c4 Fixed _NoteModal.cshtml label to correctly target file input 'noteFiles' 2025-05-15 17:33:43 +02:00
DESKTOP-T0O5CDB\DESK-555BD
afb710e6ad Add server API version so that we can track the current version. 2025-04-30 09:51:04 -06:00
Hargata Softworks
419b755e7a Merge pull request #937 from hargata/Hargata/update.configurator.2
Add Server Domain into configurator
2025-04-27 08:58:58 -06:00
DESKTOP-T0O5CDB\DESK-555BD
8cd5342c02 Add Server Domain into configurator 2025-04-27 08:57:48 -06:00
Hargata Softworks
fa32ecdb5a Merge pull request #916 from hargata/Hargata/oidc.userinfo
V1.4.7 Changes
2025-04-27 08:41:36 -06:00
DESKTOP-T0O5CDB\DESK-555BD
d08726bd85 Add LUBELOGGER_DOMAIN environment variable which is used to craft registration and reset password links to reduce user friction. 2025-04-27 08:37:08 -06:00
Hargata Softworks
a87861069b Merge pull request #936 from hargata/Hargata/877
Hargata/877 - API Endpoints for Basic Reminders
2025-04-26 09:35:12 -06:00
DESKTOP-T0O5CDB\DESK-555BD
b6d6a8765d update API documentation 2025-04-26 09:32:15 -06:00
DESKTOP-T0O5CDB\DESK-555BD
f307c0933a add delete endpoint 2025-04-26 09:16:38 -06:00
DESKTOP-T0O5CDB\DESK-555BD
9968ccb541 add put API 2025-04-26 09:11:40 -06:00
DESKTOP-T0O5CDB\DESK-555BD
5d3746f168 add POST api endpoint for basic reminders. 2025-04-26 08:56:05 -06:00
DESKTOP-T0O5CDB\DESK-555BD
df0c4eeca2 Merge branch 'Hargata/oidc.userinfo' into Hargata/877 2025-04-26 08:15:11 -06:00
DESKTOP-T0O5CDB\DESK-555BD
5b3c0aed72 Add Extra Field Type to multi edit modals 2025-04-26 07:53:37 -06:00
DESKTOP-T0O5CDB\DESK-555BD
040728b96d Fix 931 2025-04-24 20:54:39 -06:00
DESKTOP-T0O5CDB\DESK-555BD
e8b7b3e4ba optimized styling for planners tab 2025-04-16 11:16:32 -06:00
DESKTOP-T0O5CDB\DESK-555BD
cf1f1a884b add tooltip for real long file names 2025-04-16 10:01:27 -06:00
DESKTOP-T0O5CDB\DESK-555BD
c470db0590 minor UI fix on garage add. 2025-04-09 08:55:24 -06:00
DESKTOP-T0O5CDB\DESK-555BD
cb71650adf API hardening, allow nulls for list types in API payload, will auto convert to empty list. 2025-04-08 08:15:12 -06:00
DESKTOP-T0O5CDB\DESK-555BD
923d59af0a update configurator 2025-04-05 08:26:42 -06:00
DESKTOP-T0O5CDB\DESK-555BD
d68e1a3939 Add userinfo to retrieve email claim if not provided in id_token 2025-04-05 08:17:40 -06:00
DESKTOP-T0O5CDB\DESK-555BD
ffb126276f Add ID to reminderexportmodel. 2025-03-27 05:59:01 -06:00
46 changed files with 937 additions and 213 deletions

View File

@@ -112,6 +112,34 @@ namespace CarCareTracker.Controllers
}
}
[HttpGet]
[Route("/api/version")]
public async Task<IActionResult> ServerVersion(bool checkForUpdate = false)
{
var viewModel = new ReleaseVersion
{
CurrentVersion = StaticHelper.VersionNumber,
LatestVersion = StaticHelper.VersionNumber
};
if (checkForUpdate)
{
try
{
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.UserAgent.TryParseAdd("request");
var releaseResponse = await httpClient.GetFromJsonAsync<ReleaseResponse>(StaticHelper.ReleasePath) ?? new ReleaseResponse();
if (!string.IsNullOrWhiteSpace(releaseResponse.tag_name))
{
viewModel.LatestVersion = releaseResponse.tag_name;
}
}
catch (Exception ex)
{
return Json(OperationResponse.Failed($"Unable to retrieve latest version from GitHub API: {ex.Message}"));
}
}
return Json(viewModel);
}
[HttpGet]
[Route("/api/vehicles")]
public IActionResult Vehicles()
{
@@ -256,6 +284,15 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Progress cannot be set to Done."));
}
//hardening - turns null values for List types into empty lists.
if (input.Files == null)
{
input.Files = new List<UploadedFiles>();
}
if (input.ExtraFields == null)
{
input.ExtraFields = new List<ExtraField>();
}
try
{
var planRecord = new PlanRecord()
@@ -346,6 +383,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Progress cannot be set to Done."));
}
if (input.Files == null)
{
input.Files = new List<UploadedFiles>();
}
if (input.ExtraFields == null)
{
input.ExtraFields = new List<ExtraField>();
}
try
{
//retrieve existing record
@@ -429,6 +474,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Date, Description, Odometer, and Cost cannot be empty."));
}
if (input.Files == null)
{
input.Files = new List<UploadedFiles>();
}
if (input.ExtraFields == null)
{
input.ExtraFields = new List<ExtraField>();
}
try
{
var serviceRecord = new ServiceRecord()
@@ -509,6 +562,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Id, Date, Description, Odometer, and Cost cannot be empty."));
}
if (input.Files == null)
{
input.Files = new List<UploadedFiles>();
}
if (input.ExtraFields == null)
{
input.ExtraFields = new List<ExtraField>();
}
try
{
//retrieve existing record
@@ -591,6 +652,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Date, Description, Odometer, and Cost cannot be empty."));
}
if (input.Files == null)
{
input.Files = new List<UploadedFiles>();
}
if (input.ExtraFields == null)
{
input.ExtraFields = new List<ExtraField>();
}
try
{
var repairRecord = new CollisionRecord()
@@ -672,6 +741,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Id, Date, Description, Odometer, and Cost cannot be empty."));
}
if (input.Files == null)
{
input.Files = new List<UploadedFiles>();
}
if (input.ExtraFields == null)
{
input.ExtraFields = new List<ExtraField>();
}
try
{
//retrieve existing record
@@ -755,6 +832,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Date, Description, Odometer, and Cost cannot be empty."));
}
if (input.Files == null)
{
input.Files = new List<UploadedFiles>();
}
if (input.ExtraFields == null)
{
input.ExtraFields = new List<ExtraField>();
}
try
{
var upgradeRecord = new UpgradeRecord()
@@ -835,6 +920,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Id, Date, Description, Odometer, and Cost cannot be empty."));
}
if (input.Files == null)
{
input.Files = new List<UploadedFiles>();
}
if (input.ExtraFields == null)
{
input.ExtraFields = new List<ExtraField>();
}
try
{
//retrieve existing record
@@ -951,6 +1044,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Date, Description, and Cost cannot be empty."));
}
if (input.Files == null)
{
input.Files = new List<UploadedFiles>();
}
if (input.ExtraFields == null)
{
input.ExtraFields = new List<ExtraField>();
}
try
{
var taxRecord = new TaxRecord()
@@ -1014,6 +1115,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Id, Date, Description, and Cost cannot be empty."));
}
if (input.Files == null)
{
input.Files = new List<UploadedFiles>();
}
if (input.ExtraFields == null)
{
input.ExtraFields = new List<ExtraField>();
}
try
{
//retrieve existing record
@@ -1113,6 +1222,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Date, and Odometer cannot be empty."));
}
if (input.Files == null)
{
input.Files = new List<UploadedFiles>();
}
if (input.ExtraFields == null)
{
input.ExtraFields = new List<ExtraField>();
}
try
{
var odometerRecord = new OdometerRecord()
@@ -1174,6 +1291,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Id, Date, Initial Odometer, and Odometer cannot be empty."));
}
if (input.Files == null)
{
input.Files = new List<UploadedFiles>();
}
if (input.ExtraFields == null)
{
input.ExtraFields = new List<ExtraField>();
}
try
{
//retrieve existing record
@@ -1273,6 +1398,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Date, Odometer, FuelConsumed, IsFillToFull, MissedFuelUp, and Cost cannot be empty."));
}
if (input.Files == null)
{
input.Files = new List<UploadedFiles>();
}
if (input.ExtraFields == null)
{
input.ExtraFields = new List<ExtraField>();
}
try
{
var gasRecord = new GasRecord()
@@ -1352,6 +1485,14 @@ namespace CarCareTracker.Controllers
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Id, Date, Odometer, FuelConsumed, IsFillToFull, MissedFuelUp, and Cost cannot be empty."));
}
if (input.Files == null)
{
input.Files = new List<UploadedFiles>();
}
if (input.ExtraFields == null)
{
input.ExtraFields = new List<ExtraField>();
}
try
{
//retrieve existing record
@@ -1391,6 +1532,7 @@ namespace CarCareTracker.Controllers
}
}
#endregion
#region ReminderRecord
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
[Route("/api/vehicle/reminders")]
@@ -1403,7 +1545,7 @@ namespace CarCareTracker.Controllers
}
var currentMileage = _vehicleLogic.GetMaxMileage(vehicleId);
var reminders = _reminderRecordDataAccess.GetReminderRecordsByVehicleId(vehicleId);
var results = _reminderHelper.GetReminderRecordViewModels(reminders, currentMileage, DateTime.Now).Select(x=> new ReminderExportModel { Description = x.Description, Urgency = x.Urgency.ToString(), Metric = x.Metric.ToString(), Notes = x.Notes, DueDate = x.Date.ToShortDateString(), DueOdometer = x.Mileage.ToString()});
var results = _reminderHelper.GetReminderRecordViewModels(reminders, currentMileage, DateTime.Now).Select(x=> new ReminderExportModel { Id = x.Id.ToString(), Description = x.Description, Urgency = x.Urgency.ToString(), Metric = x.Metric.ToString(), Notes = x.Notes, DueDate = x.Date.ToShortDateString(), DueOdometer = x.Mileage.ToString(), Tags = string.Join(' ', x.Tags) });
if (_config.GetInvariantApi() || Request.Headers.ContainsKey("culture-invariant"))
{
return Json(results, StaticHelper.GetInvariantOption());
@@ -1413,6 +1555,183 @@ namespace CarCareTracker.Controllers
return Json(results);
}
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpPost]
[Route("/api/vehicle/reminders/add")]
[Consumes("application/json")]
public IActionResult AddReminderRecordJson(int vehicleId, [FromBody] ReminderExportModel input) => AddReminderRecord(vehicleId, input);
[TypeFilter(typeof(CollaboratorFilter))]
[HttpPost]
[Route("/api/vehicle/reminders/add")]
public IActionResult AddReminderRecord(int vehicleId, ReminderExportModel input)
{
if (vehicleId == default)
{
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Must provide a valid vehicle id"));
}
if (string.IsNullOrWhiteSpace(input.Description) ||
string.IsNullOrWhiteSpace(input.Metric))
{
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Description and Metric cannot be empty."));
}
bool validMetric = Enum.TryParse(input.Metric, out ReminderMetric parsedMetric);
bool validDate = DateTime.TryParse(input.DueDate, out DateTime parsedDate);
bool validOdometer = int.TryParse(input.DueOdometer, out int parsedOdometer);
if (!validMetric)
{
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, values for Metric(Date, Odometer, Both) is invalid."));
}
//validate metrics
switch (parsedMetric)
{
case ReminderMetric.Both:
//validate due date and odometer
if (!validDate || !validOdometer)
{
return Json(OperationResponse.Failed("Input object invalid, DueDate and DueOdometer must be valid if Metric is Both"));
}
break;
case ReminderMetric.Date:
if (!validDate)
{
return Json(OperationResponse.Failed("Input object invalid, DueDate must be valid if Metric is Date"));
}
break;
case ReminderMetric.Odometer:
if (!validOdometer)
{
return Json(OperationResponse.Failed("Input object invalid, DueOdometer must be valid if Metric is Odometer"));
}
break;
}
try
{
var reminderRecord = new ReminderRecord()
{
VehicleId = vehicleId,
Description = input.Description,
Mileage = parsedOdometer,
Date = parsedDate,
Metric = parsedMetric,
Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes,
Tags = string.IsNullOrWhiteSpace(input.Tags) ? new List<string>() : input.Tags.Split(' ').Distinct().ToList()
};
_reminderRecordDataAccess.SaveReminderRecordToVehicle(reminderRecord);
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromReminderRecord(reminderRecord, "reminderrecord.add.api", User.Identity.Name));
return Json(OperationResponse.Succeed("Reminder Record Added"));
}
catch (Exception ex)
{
Response.StatusCode = 500;
return Json(OperationResponse.Failed(ex.Message));
}
}
[HttpPut]
[Route("/api/vehicle/reminders/update")]
[Consumes("application/json")]
public IActionResult UpdateReminderRecordJson([FromBody] ReminderExportModel input) => UpdateReminderRecord(input);
[HttpPut]
[Route("/api/vehicle/reminders/update")]
public IActionResult UpdateReminderRecord(ReminderExportModel input)
{
if (string.IsNullOrWhiteSpace(input.Id) ||
string.IsNullOrWhiteSpace(input.Description) ||
string.IsNullOrWhiteSpace(input.Metric))
{
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, Id, Description and Metric cannot be empty."));
}
bool validMetric = Enum.TryParse(input.Metric, out ReminderMetric parsedMetric);
bool validDate = DateTime.TryParse(input.DueDate, out DateTime parsedDate);
bool validOdometer = int.TryParse(input.DueOdometer, out int parsedOdometer);
if (!validMetric)
{
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Input object invalid, values for Metric(Date, Odometer, Both) is invalid."));
}
//validate metrics
switch (parsedMetric)
{
case ReminderMetric.Both:
//validate due date and odometer
if (!validDate || !validOdometer)
{
return Json(OperationResponse.Failed("Input object invalid, DueDate and DueOdometer must be valid if Metric is Both"));
}
break;
case ReminderMetric.Date:
if (!validDate)
{
return Json(OperationResponse.Failed("Input object invalid, DueDate must be valid if Metric is Date"));
}
break;
case ReminderMetric.Odometer:
if (!validOdometer)
{
return Json(OperationResponse.Failed("Input object invalid, DueOdometer must be valid if Metric is Odometer"));
}
break;
}
try
{
//retrieve existing record
var existingRecord = _reminderRecordDataAccess.GetReminderRecordById(int.Parse(input.Id));
if (existingRecord != null && existingRecord.Id == int.Parse(input.Id))
{
//check if user has access to the vehicleId
if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId))
{
Response.StatusCode = 401;
return Json(OperationResponse.Failed("Access Denied, you don't have access to this vehicle."));
}
existingRecord.Date = parsedDate;
existingRecord.Mileage = parsedOdometer;
existingRecord.Description = input.Description;
existingRecord.Metric = parsedMetric;
existingRecord.Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes;
existingRecord.Tags = string.IsNullOrWhiteSpace(input.Tags) ? new List<string>() : input.Tags.Split(' ').Distinct().ToList();
_reminderRecordDataAccess.SaveReminderRecordToVehicle(existingRecord);
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromReminderRecord(existingRecord, "reminderrecord.update.api", User.Identity.Name));
}
else
{
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Invalid Record Id"));
}
return Json(OperationResponse.Succeed("Reminder Record Updated"));
}
catch (Exception ex)
{
Response.StatusCode = 500;
return Json(OperationResponse.Failed(ex.Message));
}
}
[HttpDelete]
[Route("/api/vehicle/reminders/delete")]
public IActionResult DeleteReminderRecord(int id)
{
var existingRecord = _reminderRecordDataAccess.GetReminderRecordById(id);
if (existingRecord == null || existingRecord.Id == default)
{
Response.StatusCode = 400;
return Json(OperationResponse.Failed("Invalid Record Id"));
}
//security check.
if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId))
{
Response.StatusCode = 401;
return Json(OperationResponse.Failed("Access Denied, you don't have access to this vehicle."));
}
var result = _reminderRecordDataAccess.DeleteReminderRecordById(existingRecord.Id);
if (result)
{
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromReminderRecord(existingRecord, "reminderrecord.delete.api", User.Identity.Name));
}
return Json(OperationResponse.Conditional(result, "Reminder Record Deleted"));
}
[HttpGet]
[Route("/api/calendar")]
public IActionResult Calendar()
@@ -1426,6 +1745,7 @@ namespace CarCareTracker.Controllers
var calendarContent = StaticHelper.RemindersToCalendar(reminders);
return File(calendarContent, "text/calendar");
}
#endregion
[HttpPost]
[Route("/api/documents/upload")]
public IActionResult UploadDocument(List<IFormFile> documents)

View File

@@ -49,21 +49,31 @@ namespace CarCareTracker.Controllers
}
return View(model: redirectURL);
}
public IActionResult Registration()
public IActionResult Registration(string token = "", string email = "")
{
if (_config.GetServerDisabledRegistration())
{
return RedirectToAction("Index");
}
return View();
var viewModel = new LoginModel
{
EmailAddress = string.IsNullOrWhiteSpace(email) ? string.Empty : email,
Token = string.IsNullOrWhiteSpace(token) ? string.Empty : token
};
return View(viewModel);
}
public IActionResult ForgotPassword()
{
return View();
}
public IActionResult ResetPassword()
public IActionResult ResetPassword(string token = "", string email = "")
{
return View();
var viewModel = new LoginModel
{
EmailAddress = string.IsNullOrWhiteSpace(email) ? string.Empty : email,
Token = string.IsNullOrWhiteSpace(token) ? string.Empty : token
};
return View(viewModel);
}
public IActionResult GetRemoteLoginLink()
{
@@ -130,7 +140,9 @@ namespace CarCareTracker.Controllers
Content = new FormUrlEncodedContent(httpParams)
};
var tokenResult = await httpClient.SendAsync(httpRequest).Result.Content.ReadAsStringAsync();
var userJwt = JsonSerializer.Deserialize<OpenIDResult>(tokenResult)?.id_token ?? string.Empty;
var decodedToken = JsonSerializer.Deserialize<OpenIDResult>(tokenResult);
var userJwt = decodedToken?.id_token ?? string.Empty;
var userAccessToken = decodedToken?.access_token ?? string.Empty;
if (!string.IsNullOrWhiteSpace(userJwt))
{
//validate JWT token
@@ -140,7 +152,23 @@ namespace CarCareTracker.Controllers
if (parsedToken.Claims.Any(x => x.Type == "email"))
{
userEmailAddress = parsedToken.Claims.First(x => x.Type == "email").Value;
} else
}
else if (!string.IsNullOrWhiteSpace(openIdConfig.UserInfoURL) && !string.IsNullOrWhiteSpace(userAccessToken))
{
//retrieve claims from userinfo endpoint if no email claims are returned within id_token
var userInfoHttpRequest = new HttpRequestMessage(HttpMethod.Get, openIdConfig.UserInfoURL);
userInfoHttpRequest.Headers.Add("Authorization", $"Bearer {userAccessToken}");
var userInfoResult = await httpClient.SendAsync(userInfoHttpRequest).Result.Content.ReadAsStringAsync();
var userInfo = JsonSerializer.Deserialize<OpenIDUserInfo>(userInfoResult);
if (!string.IsNullOrWhiteSpace(userInfo?.email ?? string.Empty))
{
userEmailAddress = userInfo?.email ?? string.Empty;
} else
{
_logger.LogError($"OpenID Provider did not provide an email claim via UserInfo endpoint");
}
}
else
{
var returnedClaims = parsedToken.Claims.Select(x => x.Type);
_logger.LogError($"OpenID Provider did not provide an email claim, claims returned: {string.Join(",", returnedClaims)}");
@@ -239,7 +267,9 @@ namespace CarCareTracker.Controllers
Content = new FormUrlEncodedContent(httpParams)
};
var tokenResult = await httpClient.SendAsync(httpRequest).Result.Content.ReadAsStringAsync();
var userJwt = JsonSerializer.Deserialize<OpenIDResult>(tokenResult)?.id_token ?? string.Empty;
var decodedToken = JsonSerializer.Deserialize<OpenIDResult>(tokenResult);
var userJwt = decodedToken?.id_token ?? string.Empty;
var userAccessToken = decodedToken?.access_token ?? string.Empty;
if (!string.IsNullOrWhiteSpace(userJwt))
{
results.Add(OperationResponse.Succeed($"Passed JWT Parsing - id_token: {userJwt}"));
@@ -252,6 +282,22 @@ namespace CarCareTracker.Controllers
userEmailAddress = parsedToken.Claims.First(x => x.Type == "email").Value;
results.Add(OperationResponse.Succeed($"Passed Claim Validation - email"));
}
else if (!string.IsNullOrWhiteSpace(openIdConfig.UserInfoURL) && !string.IsNullOrWhiteSpace(userAccessToken))
{
//retrieve claims from userinfo endpoint if no email claims are returned within id_token
var userInfoHttpRequest = new HttpRequestMessage(HttpMethod.Get, openIdConfig.UserInfoURL);
userInfoHttpRequest.Headers.Add("Authorization", $"Bearer {userAccessToken}");
var userInfoResult = await httpClient.SendAsync(userInfoHttpRequest).Result.Content.ReadAsStringAsync();
var userInfo = JsonSerializer.Deserialize<OpenIDUserInfo>(userInfoResult);
if (!string.IsNullOrWhiteSpace(userInfo?.email ?? string.Empty))
{
userEmailAddress = userInfo?.email ?? string.Empty;
results.Add(OperationResponse.Succeed($"Passed Claim Validation - Retrieved email via UserInfo endpoint"));
} else
{
results.Add(OperationResponse.Failed($"Failed Claim Validation - Unable to retrieve email via UserInfo endpoint: {openIdConfig.UserInfoURL} using access_token: {userAccessToken} - Received {userInfoResult}"));
}
}
else
{
var returnedClaims = parsedToken.Claims.Select(x => x.Type);

View File

@@ -14,14 +14,15 @@ namespace CarCareTracker.Controllers
{
//get records
var vehicleData = _dataAccess.GetVehicleById(vehicleId);
var serviceRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId);
var gasRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
var collisionRecords = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId);
var taxRecords = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId);
var upgradeRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId);
var odometerRecords = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId);
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;
var userConfig = _config.GetUserConfig(User);
var viewModel = new ReportViewModel();
var viewModel = new ReportViewModel() { ReportHeaderForVehicle = new ReportHeader() };
//check if custom widgets are configured
viewModel.CustomWidgetsConfigured = _fileHelper.WidgetsExist();
//get totalCostMakeUp
@@ -91,6 +92,7 @@ namespace CarCareTracker.Controllers
var mileageData = _gasHelper.GetGasRecordViewModels(gasRecords, userConfig.UseMPG, userConfig.UseUKMPG);
string preferredFuelMileageUnit = _config.GetUserConfig(User).PreferredGasMileageUnit;
var fuelEconomyMileageUnit = StaticHelper.GetFuelEconomyUnit(vehicleData.IsElectric, vehicleData.UseHours, userConfig.UseMPG, userConfig.UseUKMPG);
var averageMPG = _gasHelper.GetAverageGasMileage(mileageData, userConfig.UseMPG);
mileageData.RemoveAll(x => x.MilesPerGallon == default);
bool invertedFuelMileageUnit = fuelEconomyMileageUnit == "l/100km" && preferredFuelMileageUnit == "km/l";
var monthlyMileageData = StaticHelper.GetBaseLineCostsNoMonthName();
@@ -114,6 +116,12 @@ namespace CarCareTracker.Controllers
monthMileage.Cost = 100 / monthMileage.Cost;
}
}
var newAverageMPG = decimal.Parse(averageMPG, NumberStyles.Any);
if (newAverageMPG != 0)
{
newAverageMPG = 100 / newAverageMPG;
}
averageMPG = newAverageMPG.ToString("F");
}
var mpgViewModel = new MPGForVehicleByMonth {
CostData = monthlyMileageData,
@@ -121,6 +129,15 @@ namespace CarCareTracker.Controllers
SortedCostData = (userConfig.UseMPG || invertedFuelMileageUnit) ? monthlyMileageData.OrderByDescending(x => x.Cost).ToList() : monthlyMileageData.OrderBy(x => x.Cost).ToList()
};
viewModel.FuelMileageForVehicleByMonth = mpgViewModel;
//report header
var maxMileage = _vehicleLogic.GetMaxMileage(vehicleRecords);
var minMileage = _vehicleLogic.GetMinMileage(vehicleRecords);
viewModel.ReportHeaderForVehicle.TotalCost = _vehicleLogic.GetVehicleTotalCost(vehicleRecords);
viewModel.ReportHeaderForVehicle.AverageMPG = $"{averageMPG} {mpgViewModel.Unit}";
viewModel.ReportHeaderForVehicle.MaxOdometer = maxMileage;
viewModel.ReportHeaderForVehicle.DistanceTraveled = maxMileage - minMileage;
return PartialView("_Report", viewModel);
}
[TypeFilter(typeof(CollaboratorFilter))]
@@ -145,6 +162,63 @@ namespace CarCareTracker.Controllers
return Json(result);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpPost]
public IActionResult GetSummaryForVehicle(int vehicleId, int year = 0)
{
var vehicleData = _dataAccess.GetVehicleById(vehicleId);
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 userConfig = _config.GetUserConfig(User);
var mileageData = _gasHelper.GetGasRecordViewModels(gasRecords, userConfig.UseMPG, userConfig.UseUKMPG);
string preferredFuelMileageUnit = _config.GetUserConfig(User).PreferredGasMileageUnit;
var fuelEconomyMileageUnit = StaticHelper.GetFuelEconomyUnit(vehicleData.IsElectric, vehicleData.UseHours, userConfig.UseMPG, userConfig.UseUKMPG);
var averageMPG = _gasHelper.GetAverageGasMileage(mileageData, userConfig.UseMPG);
bool invertedFuelMileageUnit = fuelEconomyMileageUnit == "l/100km" && preferredFuelMileageUnit == "km/l";
if (invertedFuelMileageUnit)
{
var newAverageMPG = decimal.Parse(averageMPG, NumberStyles.Any);
if (newAverageMPG != 0)
{
newAverageMPG = 100 / newAverageMPG;
}
averageMPG = newAverageMPG.ToString("F");
}
var mpgUnit = invertedFuelMileageUnit ? preferredFuelMileageUnit : fuelEconomyMileageUnit;
var maxMileage = _vehicleLogic.GetMaxMileage(vehicleRecords);
var minMileage = _vehicleLogic.GetMinMileage(vehicleRecords);
var viewModel = new ReportHeader()
{
TotalCost = _vehicleLogic.GetVehicleTotalCost(vehicleRecords),
AverageMPG = $"{averageMPG} {mpgUnit}",
MaxOdometer = maxMileage,
DistanceTraveled = maxMileage - minMileage
};
return PartialView("_ReportHeader", viewModel);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
public IActionResult GetCostMakeUpForVehicle(int vehicleId, int year = 0)
{

View File

@@ -19,11 +19,13 @@ namespace CarCareTracker.Helper
bool GetCustomWidgetsEnabled();
string GetMOTD();
string GetLogoUrl();
string GetSmallLogoUrl();
string GetServerLanguage();
bool GetServerDisabledRegistration();
bool GetServerEnableShopSupplies();
string GetServerPostgresConnection();
string GetAllowedFileUploadExtensions();
string GetServerDomain();
bool DeleteUserConfig(int userId);
bool GetInvariantApi();
bool GetServerOpenRegistration();
@@ -62,6 +64,11 @@ namespace CarCareTracker.Helper
var motd = CheckString("LUBELOGGER_MOTD");
return motd;
}
public string GetServerDomain()
{
var domain = CheckString("LUBELOGGER_DOMAIN");
return domain;
}
public bool GetServerOpenRegistration()
{
return CheckBool(CheckString("LUBELOGGER_OPEN_REGISTRATION"));
@@ -86,6 +93,11 @@ namespace CarCareTracker.Helper
var logoUrl = CheckString("LUBELOGGER_LOGO_URL", "/defaults/lubelogger_logo.png");
return logoUrl;
}
public string GetSmallLogoUrl()
{
var logoUrl = CheckString("LUBELOGGER_LOGO_SMALL_URL", "/defaults/lubelogger_logo_small.png");
return logoUrl;
}
public string GetAllowedFileUploadExtensions()
{
var allowedFileExtensions = CheckString("LUBELOGGER_ALLOWED_FILE_EXTENSIONS", StaticHelper.DefaultAllowedFileExtensions);
@@ -246,7 +258,8 @@ namespace CarCareTracker.Helper
ReminderUrgencyConfig = _config.GetSection(nameof(UserConfig.ReminderUrgencyConfig)).Get<ReminderUrgencyConfig>() ?? new ReminderUrgencyConfig(),
DefaultTab = (ImportMode)int.Parse(CheckString(nameof(UserConfig.DefaultTab), "8")),
DefaultReminderEmail = CheckString(nameof(UserConfig.DefaultReminderEmail)),
DisableRegistration = CheckBool(CheckString(nameof(UserConfig.DisableRegistration)))
DisableRegistration = CheckBool(CheckString(nameof(UserConfig.DisableRegistration))),
ShowVehicleThumbnail = CheckBool(CheckString(nameof(UserConfig.ShowVehicleThumbnail)))
};
int userId = 0;
if (user != null)

View File

@@ -17,6 +17,7 @@ namespace CarCareTracker.Helper
{
private readonly MailConfig mailConfig;
private readonly string serverLanguage;
private readonly string serverDomain;
private readonly IFileHelper _fileHelper;
private readonly ITranslationHelper _translator;
private readonly ILogger<MailHelper> _logger;
@@ -29,6 +30,7 @@ namespace CarCareTracker.Helper
//load mailConfig from Configuration
mailConfig = config.GetMailConfig();
serverLanguage = config.GetServerLanguage();
serverDomain = config.GetServerDomain();
_fileHelper = fileHelper;
_translator = translationHelper;
_logger = logger;
@@ -43,7 +45,14 @@ namespace CarCareTracker.Helper
return OperationResponse.Failed("Email Address or Token is invalid");
}
string emailSubject = _translator.Translate(serverLanguage, "Your Registration Token for LubeLogger");
string emailBody = $"{_translator.Translate(serverLanguage, "A token has been generated on your behalf, please complete your registration for LubeLogger using the token")}: {token}";
string tokenHtml = token;
if (!string.IsNullOrWhiteSpace(serverDomain))
{
string cleanedURL = serverDomain.EndsWith('/') ? serverDomain.TrimEnd('/') : serverDomain;
//construct registration URL.
tokenHtml = $"<a href='{cleanedURL}/Login/Registration?email={emailAddress}&token={token}' target='_blank'>{token}</a>";
}
string emailBody = $"<span>{_translator.Translate(serverLanguage, "A token has been generated on your behalf, please complete your registration for LubeLogger using the token")}: {tokenHtml}</span>";
var result = SendEmail(new List<string> { emailAddress }, emailSubject, emailBody);
if (result)
{
@@ -64,7 +73,14 @@ namespace CarCareTracker.Helper
return OperationResponse.Failed("Email Address or Token is invalid");
}
string emailSubject = _translator.Translate(serverLanguage, "Your Password Reset Token for LubeLogger");
string emailBody = $"{_translator.Translate(serverLanguage, "A token has been generated on your behalf, please reset your password for LubeLogger using the token")}: {token}";
string tokenHtml = token;
if (!string.IsNullOrWhiteSpace(serverDomain))
{
string cleanedURL = serverDomain.EndsWith('/') ? serverDomain.TrimEnd('/') : serverDomain;
//construct registration URL.
tokenHtml = $"<a href='{cleanedURL}/Login/ResetPassword?email={emailAddress}&token={token}' target='_blank'>{token}</a>";
}
string emailBody = $"<span>{_translator.Translate(serverLanguage, "A token has been generated on your behalf, please reset your password for LubeLogger using the token")}: {tokenHtml}</span>";
var result = SendEmail(new List<string> { emailAddress }, emailSubject, emailBody);
if (result)
{

View File

@@ -12,7 +12,7 @@ namespace CarCareTracker.Helper
/// </summary>
public static class StaticHelper
{
public const string VersionNumber = "1.4.6";
public const string VersionNumber = "1.4.8";
public const string DbName = "data/cartracker.db";
public const string UserConfigPath = "data/config/userConfig.json";
public const string LegacyUserConfigPath = "config/userConfig.json";
@@ -22,6 +22,7 @@ namespace CarCareTracker.Helper
public const string DefaultAllowedFileExtensions = ".png,.jpg,.jpeg,.pdf,.xls,.xlsx,.docx";
public const string SponsorsPath = "https://hargata.github.io/hargata/sponsors.json";
public const string TranslationPath = "https://hargata.github.io/lubelog_translations";
public const string ReleasePath = "https://api.github.com/repos/hargata/lubelog/releases/latest";
public const string TranslationDirectoryPath = $"{TranslationPath}/directory.json";
public const string ReportNote = "Report generated by LubeLogger, a Free and Open Source Vehicle Maintenance Tracker - LubeLogger.com";
public static string GetTitleCaseReminderUrgency(ReminderUrgency input)

View File

@@ -303,11 +303,11 @@ namespace CarCareTracker.Logic
//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();
resultToAdd.NextReminder = results.Where(x => x.Date >= DateTime.Now.Date).OrderBy(x => x.Date).Select(x => new ReminderExportModel { Id = x.Id.ToString(), Description = x.Description, Urgency = x.Urgency.ToString(), Metric = x.Metric.ToString(), Notes = x.Notes, DueDate = x.Date.ToShortDateString(), DueOdometer = x.Mileage.ToString(), Tags = string.Join(' ', x.Tags) }).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();
resultToAdd.NextReminder = results.Where(x => x.Mileage >= currentMileage).OrderBy(x => x.Mileage).Select(x => new ReminderExportModel { Id = x.Id.ToString(), Description = x.Description, Urgency = x.Urgency.ToString(), Metric = x.Metric.ToString(), Notes = x.Notes, DueDate = x.Date.ToShortDateString(), DueOdometer = x.Mileage.ToString(), Tags = string.Join(' ', x.Tags) }).First();
}
apiResult.Add(resultToAdd);
}

View File

@@ -0,0 +1,18 @@
namespace CarCareTracker.Models
{
/// <summary>
/// For deserializing GitHub response for latest version
/// </summary>
public class ReleaseResponse
{
public string tag_name { get; set; }
}
/// <summary>
/// For returning the version numbers via API.
/// </summary>
public class ReleaseVersion
{
public string CurrentVersion { get; set; }
public string LatestVersion { get; set; }
}
}

View File

@@ -15,6 +15,7 @@
public bool DisableRegularLogin { get; set; } = false;
public bool UsePKCE { get; set; } = false;
public string LogOutURL { get; set; } = "";
public string UserInfoURL { get; set; } = "";
public string RemoteAuthURL { get {
var redirectUrl = $"{AuthURL}?client_id={ClientId}&response_type=code&redirect_uri={RedirectURL}&scope={Scope}&state={State}";
if (UsePKCE)

View File

@@ -3,5 +3,6 @@
public class OpenIDResult
{
public string id_token { get; set; }
public string access_token { get; set; }
}
}

View File

@@ -0,0 +1,7 @@
namespace CarCareTracker.Models
{
public class OpenIDUserInfo
{
public string email { get; set; } = "";
}
}

View File

@@ -0,0 +1,10 @@
namespace CarCareTracker.Models
{
public class ReportHeader
{
public int MaxOdometer { get; set; }
public int DistanceTraveled { get; set; }
public decimal TotalCost { get; set; }
public string AverageMPG { get; set; }
}
}

View File

@@ -2,6 +2,7 @@
{
public class ReportViewModel
{
public ReportHeader ReportHeaderForVehicle { get; set; } = new ReportHeader();
public List<CostForVehicleByMonth> CostForVehicleByMonth { get; set; } = new List<CostForVehicleByMonth>();
public MPGForVehicleByMonth FuelMileageForVehicleByMonth { get; set; } = new MPGForVehicleByMonth();
public CostMakeUpForVehicle CostMakeUpForVehicle { get; set; } = new CostMakeUpForVehicle();

View File

@@ -115,6 +115,8 @@ namespace CarCareTracker.Models
}
public class ReminderExportModel
{
[JsonConverter(typeof(FromIntOptional))]
public string Id { get; set; }
public string Description { get; set; }
public string Urgency { get; set; }
public string Metric { get; set; }
@@ -123,6 +125,7 @@ namespace CarCareTracker.Models
public string DueDate { get; set; }
[JsonConverter(typeof(FromIntOptional))]
public string DueOdometer { get; set; }
public string Tags { get; set; }
}
public class PlanRecordExportModel
{

View File

@@ -25,6 +25,7 @@
public string PreferredGasMileageUnit { get; set; } = string.Empty;
public bool UseUnitForFuelCost { get; set; }
public bool ShowCalendar { get; set; }
public bool ShowVehicleThumbnail { get; set; }
public List<UserColumnPreference> UserColumnPreferences { get; set; } = new List<UserColumnPreference>();
public ReminderUrgencyConfig ReminderUrgencyConfig { get; set; } = new ReminderUrgencyConfig();
public string UserNameHash { get; set; }

View File

@@ -40,6 +40,20 @@
No Params
</div>
</div>
<div class="row api-method">
<div class="col-1">
<span class="badge bg-success">GET</span>
</div>
<div class="col-5 copyable testable">
<code>/api/version</code>
</div>
<div class="col-3">
Returns current version of LubeLogger and checks for updates
</div>
<div class="col-3">
CheckForUpdate(bool) - checks for update(optional)
</div>
</div>
<div class="row api-method">
<div class="col-1">
<span class="badge bg-success">GET</span>
@@ -669,6 +683,65 @@
vehicleId - Id of Vehicle
</div>
</div>
<div class="row api-method">
<div class="col-1">
<span class="badge bg-primary">POST</span>
</div>
<div class="col-5 copyable">
<code>/api/vehicle/reminders/add</code>
</div>
<div class="col-3">
Adds Reminder Record to the vehicle
</div>
<div class="col-3">
vehicleId - Id of Vehicle
<br />
Body(form-data): {<br />
description - Description<br />
dueDate - Due Date<br />
dueOdometer - Due Odometer reading<br />
metric - Date/Odometer/Both<br />
notes - notes(optional)<br />
tags - tags separated by space(optional)<br />
}
</div>
</div>
<div class="row api-method">
<div class="col-1">
<span class="badge text-bg-warning">PUT</span>
</div>
<div class="col-5 copyable">
<code>/api/vehicle/reminders/update</code>
</div>
<div class="col-3">
Updates Reminder Record
</div>
<div class="col-3">
Body(form-data): {<br />
Id - Id of Reminder Record<br />
description - Description<br />
dueDate - Due Date<br />
dueOdometer - Due Odometer reading<br />
metric - Date/Odometer/Both<br />
notes - notes(optional)<br />
tags - tags separated by space(optional)<br />
}
</div>
</div>
<div class="row api-method">
<div class="col-1">
<span class="badge text-bg-danger">DELETE</span>
</div>
<div class="col-5 copyable">
<code>/api/vehicle/reminders/delete</code>
</div>
<div class="col-3">
Deletes Reminder Record
</div>
<div class="col-3">
Id - Id of Reminder Record
</div>
</div>
<div class="row api-method">
<div class="col-1">
<span class="badge bg-success">GET</span>

View File

@@ -13,17 +13,21 @@
emailServerIsSetup = false;
}
}
@section Nav {
<div class="container-fluid">
<div class="row mt-2">
<div class="d-flex lubelogger-navbar">
<div class="me-2" style="cursor:pointer;" onclick="returnToGarage()">
<img src="@config.GetSmallLogoUrl()" class="lubelogger-logo" />
</div>
<span class="text-truncate lead">@translator.Translate(userLanguage, "Admin Panel")</span>
</div>
</div>
<hr />
</div>
}
@model AdminViewModel
<div class="container">
<div class="row d-flex align-items-center justify-content-between justify-content-md-start">
<div class="col-2 col-md-1">
<a href="/Home" class="btn btn-secondary btn-md mt-1 mb-1"><i class="bi bi-arrow-left-square"></i></a>
</div>
<div class="col-6 col-md-7 text-end text-md-start">
<span class="display-6">@translator.Translate(userLanguage, "Admin Panel")</span>
</div>
</div>
<hr />
<div class="row">
<div class="col-12">
<div class="row">
@@ -56,23 +60,23 @@
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<div class="d-flex align-items-center">
<span class="lead">@translator.Translate(userLanguage, "Tokens")</span>
<div class="d-flex align-items-center">
<span class="lead">@translator.Translate(userLanguage, "Tokens")</span>
</div>
<div class="d-flex align-items-center ms-auto">
<div class="btn-group">
<button onclick="generateNewToken()" class="btn btn-primary btn-md mt-1 mb-1">
<i class="bi bi-pencil-square me-2"></i>@translator.Translate(userLanguage, "Generate")
</button>
<button class="btn btn-outline-primary btn-md mt-1 mb-1" @(emailServerIsSetup ? "" : "disabled") onclick="toggleAutoNotify(event)">
<div class="form-check">
<input class="form-check-input" type="checkbox" role="switch" id="enableAutoNotify" @(emailServerIsSetup ? "checked" : "disabled")>
<label class="form-check-label" for="enableAutoNotify">@translator.Translate(userLanguage, "Notify")</label>
</div>
</button>
</div>
<div class="d-flex align-items-center ms-auto">
<div class="btn-group">
<button onclick="generateNewToken()" class="btn btn-primary btn-md mt-1 mb-1">
<i class="bi bi-pencil-square me-2"></i>@translator.Translate(userLanguage, "Generate")
</button>
<button class="btn btn-outline-primary btn-md mt-1 mb-1" @(emailServerIsSetup ? "" : "disabled") onclick="toggleAutoNotify(event)">
<div class="form-check">
<input class="form-check-input" type="checkbox" role="switch" id="enableAutoNotify" @(emailServerIsSetup ? "checked" : "disabled")>
<label class="form-check-label" for="enableAutoNotify">@translator.Translate(userLanguage, "Notify")</label>
</div>
</button>
</div>
<button class="btn btn-close btn-md mt-1 mb-1 ms-2" onclick="hideTokenModal()" aria-label="Close"></button>
</div>
</div>
</div>
<div class="modal-body">
<table class="table table-hover">

View File

@@ -16,6 +16,71 @@
<script src="~/js/supplyrecord.js?v=@StaticHelper.VersionNumber"></script>
<script src="~/lib/drawdown/drawdown.js"></script>
}
@section Nav {
<div class="container-fluid">
<div class="row mt-2">
<div class="d-flex lubelogger-navbar">
<div class="me-2" style="cursor:pointer;" onclick="returnToGarage()">
<img src="@config.GetSmallLogoUrl()" class="lubelogger-logo lubelogger-tab" />
<img src="@config.GetLogoUrl()" class="lubelogger-logo lubelogger-mobile-nav-show" />
</div>
<ul class="nav nav-tabs lubelogger-tab flex-grow-1" id="homeTab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link resizable-nav-link @(Model == "garage" ? "active" : "")" oncontextmenu="sortGarage(this)" id="garage-tab" data-bs-toggle="tab" data-bs-target="#garage-tab-pane" type="button" role="tab"><i class="bi bi-car-front"></i><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Garage")</span></button>
</li>
@if (config.GetServerEnableShopSupplies())
{
<li class="nav-item" role="presentation">
<button class="nav-link resizable-nav-link" id="supply-tab" data-bs-toggle="tab" data-bs-target="#supply-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-shop"></i><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Supplies")</span></button>
</li>
}
@if (userConfig.ShowCalendar)
{
<li class="nav-item" role="presentation">
<button class="nav-link resizable-nav-link" id="calendar-tab" data-bs-toggle="tab" data-bs-target="#calendar-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-calendar-week"></i><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Calendar")</span></button>
</li>
}
<li class="nav-item ms-auto" role="presentation">
<button class="nav-link resizable-nav-link @(Model == "settings" ? "active" : "")" id="settings-tab" data-bs-toggle="tab" data-bs-target="#settings-tab-pane" type="button" role="tab"><i class="bi bi-gear"></i><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Settings")</span></button>
</li>
@if (User.IsInRole("CookieAuth") || User.IsInRole("APIAuth"))
{
<li class="nav-item dropdown" role="presentation">
<a class="nav-link resizable-nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button" aria-expanded="false"><i class="bi bi-person"></i><span class="ms-2 d-sm-none d-md-inline">@User.Identity.Name</span></a>
<ul class="dropdown-menu">
@if (User.IsInRole(nameof(UserData.IsAdmin)))
{
<li>
<a class="dropdown-item" href="/Admin"><i class="bi bi-people me-2"></i>@translator.Translate(userLanguage, "Admin Panel")</a>
</li>
}
@if (User.IsInRole(nameof(UserData.IsRootUser)))
{
<li>
<button class="dropdown-item" onclick="showRootAccountInformationModal()"><i class="bi bi-person-gear me-2"></i>@translator.Translate(userLanguage, "Profile")</button>
</li>
}
else
{
<li>
<button class="dropdown-item" onclick="showAccountInformationModal()"><i class="bi bi-person-gear me-2"></i>@translator.Translate(userLanguage, "Profile")</button>
</li>
}
<li>
<button class="dropdown-item" onclick="performLogOut()"><i class="bi bi-box-arrow-right me-2"></i>@translator.Translate(userLanguage, "Logout")</button>
</li>
</ul>
</li>
}
</ul>
<div class="lubelogger-navbar-button">
<button type="button" class="btn btn-adaptive" onclick="showMobileNav()"><i class="bi bi-list lubelogger-menu-icon"></i></button>
</div>
</div>
</div>
<hr />
</div>
}
<div class="lubelogger-mobile-nav" onclick="hideMobileNav()">
<ul class="navbar-nav" id="homeTab" role="tablist">
<li class="nav-item" role="presentation">
@@ -61,62 +126,6 @@
</ul>
</div>
<div class="container">
<div class="row mt-2">
<div class="d-flex lubelogger-navbar">
<img src="@config.GetLogoUrl()" class="lubelogger-logo" />
<div class="lubelogger-navbar-button">
<button type="button" class="btn btn-adaptive" onclick="showMobileNav()"><i class="bi bi-list lubelogger-menu-icon"></i></button>
</div>
</div>
</div>
<hr />
<ul class="nav nav-tabs lubelogger-tab" id="homeTab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link resizable-nav-link @(Model == "garage" ? "active" : "")" oncontextmenu="sortGarage(this)" id="garage-tab" data-bs-toggle="tab" data-bs-target="#garage-tab-pane" type="button" role="tab"><i class="bi bi-car-front"></i><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Garage")</span></button>
</li>
@if (config.GetServerEnableShopSupplies())
{
<li class="nav-item" role="presentation">
<button class="nav-link resizable-nav-link" id="supply-tab" data-bs-toggle="tab" data-bs-target="#supply-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-shop"></i><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Supplies")</span></button>
</li>
}
@if (userConfig.ShowCalendar){
<li class="nav-item" role="presentation">
<button class="nav-link resizable-nav-link" id="calendar-tab" data-bs-toggle="tab" data-bs-target="#calendar-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-calendar-week"></i><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Calendar")</span></button>
</li>
}
<li class="nav-item ms-auto" role="presentation">
<button class="nav-link resizable-nav-link @(Model == "settings" ? "active" : "")" id="settings-tab" data-bs-toggle="tab" data-bs-target="#settings-tab-pane" type="button" role="tab"><i class="bi bi-gear"></i><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Settings")</span></button>
</li>
@if (User.IsInRole("CookieAuth") || User.IsInRole("APIAuth"))
{
<li class="nav-item dropdown" role="presentation">
<a class="nav-link resizable-nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button" aria-expanded="false"><i class="bi bi-person"></i><span class="ms-2 d-sm-none d-md-inline">@User.Identity.Name</span></a>
<ul class="dropdown-menu">
@if (User.IsInRole(nameof(UserData.IsAdmin)))
{
<li>
<a class="dropdown-item" href="/Admin"><i class="bi bi-people me-2"></i>@translator.Translate(userLanguage,"Admin Panel")</a>
</li>
}
@if (User.IsInRole(nameof(UserData.IsRootUser)))
{
<li>
<button class="dropdown-item" onclick="showRootAccountInformationModal()"><i class="bi bi-person-gear me-2"></i>@translator.Translate(userLanguage, "Profile")</button>
</li>
} else
{
<li>
<button class="dropdown-item" onclick="showAccountInformationModal()"><i class="bi bi-person-gear me-2"></i>@translator.Translate(userLanguage, "Profile")</button>
</li>
}
<li>
<button class="dropdown-item" onclick="performLogOut()"><i class="bi bi-box-arrow-right me-2"></i>@translator.Translate(userLanguage,"Logout")</button>
</li>
</ul>
</li>
}
</ul>
<div class="tab-content" id="homeTab">
<div class="tab-pane fade @(Model == "garage" ? "show active" : "")" id="garage-tab-pane" role="tabpanel" tabindex="0">
<div id="garageContainer">

View File

@@ -81,9 +81,9 @@
</div>
}
}
<div class="col-xl-2 col-lg-3 col-md-4 col-sm-4 col-6 garage-item-add">
<div class="col-xl-2 col-lg-3 col-md-4 col-sm-4 col-6 garage-item-add user-select-none">
<div class="card" onclick="showAddVehicleModal()" style="height:100%;">
<img src="/defaults/addnew_vehicle.png" style="object-fit:scale-down;height:100%;" />
<img src="/defaults/addnew_vehicle.png" style="object-fit:scale-down;height:100%;pointer-events:none;" />
</div>
</div>
</div>

View File

@@ -165,6 +165,14 @@
<div class="col-md-6 col-12">
<input type="text" readonly id="inputOIDCToken" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@Model.OIDCConfig.TokenURL">
</div>
</div>
<div class="row mb-2">
<div class="col-md-6 col-12">
<label for="inputOIDCUserInfo">@translator.Translate(userLanguage, "OIDC UserInfo URL")</label>
</div>
<div class="col-md-6 col-12">
<input type="text" readonly id="inputOIDCUserInfo" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@Model.OIDCConfig.UserInfoURL">
</div>
</div>
<div class="row mb-2">
<div class="col-md-6 col-12">

View File

@@ -90,6 +90,10 @@
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="showCalendar" checked="@Model.UserConfig.ShowCalendar">
<label class="form-check-label" for="showCalendar">@translator.Translate(userLanguage, "Show Calendar")</label>
</div>
<div class="form-check form-switch form-check-inline">
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="showVehicleThumbnail" checked="@Model.UserConfig.ShowVehicleThumbnail">
<label class="form-check-label" for="showCalendar">@translator.Translate(userLanguage, "Show Vehicle Thumbnail in Header")</label>
</div>
</div>
@if (User.IsInRole(nameof(UserData.IsRootUser)))
{

View File

@@ -5,6 +5,7 @@
var userLanguage = config.GetServerLanguage();
var openRegistrationEnabled = config.GetServerOpenRegistration();
}
@model LoginModel
@{
ViewData["Title"] = "Register";
}
@@ -19,18 +20,18 @@
<label for="inputToken">@translator.Translate(userLanguage, "Token")</label>
@if (openRegistrationEnabled) {
<div class="input-group">
<input type="text" id="inputToken" class="form-control">
<input type="text" id="inputToken" class="form-control" value="@Model.Token">
<div class="input-group-text">
<button type="button" class="btn btn-sm text-secondary password-visible-button" onclick="sendRegistrationToken()"><i class="bi bi-send"></i></button>
</div>
</div>
} else {
<input type="text" id="inputToken" class="form-control">
<input type="text" id="inputToken" class="form-control" value="@Model.Token">
}
</div>
<div class="form-group">
<label for="inputEmail">@translator.Translate(userLanguage, "Email Address")</label>
<input type="text" id="inputEmail" class="form-control">
<input type="text" id="inputEmail" class="form-control" value="@Model.EmailAddress">
</div>
<div class="form-group">
<label for="inputUserName">@translator.Translate(userLanguage, "Username")</label>

View File

@@ -4,6 +4,7 @@
@{
var userLanguage = config.GetServerLanguage();
}
@model LoginModel
@{
ViewData["Title"] = "Reset Password";
}
@@ -16,11 +17,11 @@
<img src="@config.GetLogoUrl()" class="lubelogger-logo" />
<div class="form-group">
<label for="inputToken">@translator.Translate(userLanguage, "Token")</label>
<input type="text" id="inputToken" class="form-control">
<input type="text" id="inputToken" class="form-control" value="@Model.Token">
</div>
<div class="form-group">
<label for="inputUserName">@translator.Translate(userLanguage, "Email Address")</label>
<input type="text" id="inputEmail" class="form-control">
<input type="text" id="inputEmail" class="form-control" value="@Model.EmailAddress">
</div>
<div class="form-group">
<label for="inputUserPassword">@translator.Translate(userLanguage, "New Password")</label>

View File

@@ -161,6 +161,7 @@
@await RenderSectionAsync("Scripts", required: false)
</head>
<body>
@await RenderSectionAsync("Nav", required: false)
<div class="container" style="height:85vh;">
<main role="main">
@RenderBody()

View File

@@ -25,6 +25,61 @@
<script src="~/lib/chart-js/chart.umd.js"></script>
<script src="~/lib/drawdown/drawdown.js"></script>
}
@section Nav{
<div class="container-fluid">
<div class="row mt-2">
<div class="d-flex lubelogger-navbar">
<div class="me-2" style="cursor:pointer;" onclick="returnToGarage()">
@if(userConfig.ShowVehicleThumbnail) {
<img src="@Model.ImageLocation" class="lubelogger-vehicle-logo @(string.IsNullOrWhiteSpace(Model.SoldDate) ? "" : "sold")" />
} else {
<img src="@config.GetSmallLogoUrl()" class="lubelogger-logo" />
}
</div>
<ul class="nav nav-tabs lubelogger-tab flex-grow-1" id="vehicleTab" role="tablist">
<li class="nav-item" role="presentation" style="order: @userConfig.TabOrder.FindIndex(x=>x == ImportMode.Dashboard)">
<button class="nav-link resizable-nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.Dashboard)" id="report-tab" data-bs-toggle="tab" data-bs-target="#report-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-file-bar-graph"></i><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Dashboard")</span></button>
</li>
<li class="nav-item" role="presentation" style="order: @userConfig.TabOrder.FindIndex(x=>x == ImportMode.PlanRecord)">
<button class="nav-link resizable-nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.PlanRecord)" id="plan-tab" data-bs-toggle="tab" data-bs-target="#plan-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-bar-chart-steps"></i><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Planner")</span></button>
</li>
<li class="nav-item" role="presentation" style="order: @userConfig.TabOrder.FindIndex(x=>x == ImportMode.OdometerRecord)">
<button class="nav-link resizable-nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.OdometerRecord)" id="odometer-tab" data-bs-toggle="tab" data-bs-target="#odometer-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-speedometer"></i><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Odometer")</span></button>
</li>
<li class="nav-item" role="presentation" style="order: @userConfig.TabOrder.FindIndex(x=>x == ImportMode.ServiceRecord)">
<button class="nav-link resizable-nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.ServiceRecord)" id="servicerecord-tab" data-bs-toggle="tab" data-bs-target="#servicerecord-tab-pane" type="button" role="tab" aria-selected="true"><i class="bi bi-card-checklist"></i><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Service Records")</span></button>
</li>
<li class="nav-item" role="presentation" style="order: @userConfig.TabOrder.FindIndex(x=>x == ImportMode.RepairRecord)">
<button class="nav-link resizable-nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.RepairRecord)" id="accident-tab" data-bs-toggle="tab" data-bs-target="#accident-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-exclamation-octagon"></i><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Repairs")</span></button>
</li>
<li class="nav-item" role="presentation" style="order: @userConfig.TabOrder.FindIndex(x=>x == ImportMode.UpgradeRecord)">
<button class="nav-link resizable-nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.UpgradeRecord)" id="upgrade-tab" data-bs-toggle="tab" data-bs-target="#upgrade-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-wrench-adjustable"></i><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Upgrades")</span></button>
</li>
<li class="nav-item" role="presentation" style="order: @userConfig.TabOrder.FindIndex(x=>x == ImportMode.GasRecord)">
<button class="nav-link resizable-nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.GasRecord)" id="gas-tab" data-bs-toggle="tab" data-bs-target="#gas-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-fuel-pump"></i><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Fuel")</span></button>
</li>
<li class="nav-item" role="presentation" style="order: @userConfig.TabOrder.FindIndex(x=>x == ImportMode.SupplyRecord)">
<button class="nav-link resizable-nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.SupplyRecord)" id="supply-tab" data-bs-toggle="tab" data-bs-target="#supply-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-shop"></i><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Supplies")</span></button>
</li>
<li class="nav-item" role="presentation" style="order: @userConfig.TabOrder.FindIndex(x=>x == ImportMode.TaxRecord)">
<button class="nav-link resizable-nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.TaxRecord)" id="tax-tab" data-bs-toggle="tab" data-bs-target="#tax-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-currency-dollar"></i><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Taxes")</span></button>
</li>
<li class="nav-item" role="presentation" style="order: @userConfig.TabOrder.FindIndex(x=>x == ImportMode.NoteRecord)">
<button class="nav-link resizable-nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.NoteRecord)" id="notes-tab" data-bs-toggle="tab" data-bs-target="#notes-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-journal-bookmark"></i><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Notes")</span></button>
</li>
<li class="nav-item" role="presentation" style="order: @userConfig.TabOrder.FindIndex(x=>x == ImportMode.ReminderRecord)">
<button class="nav-link resizable-nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.ReminderRecord)" id="reminder-tab" data-bs-toggle="tab" data-bs-target="#reminder-tab-pane" type="button" role="tab" aria-selected="false"><div class="reminderBellDiv" style="display:inline-flex;"><i class="reminderBell bi bi-bell"></i></div><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Reminders")</span></button>
</li>
</ul>
<span style="cursor:pointer;" onclick="editVehicle(@Model.Id)" class="text-truncate"><span class="lead">@($"{Model.Year} {Model.Make} {Model.Model}")<small class="text-body-secondary">@($"(#{StaticHelper.GetVehicleIdentifier(Model)})")</small></span><span class="ms-2 lubelogger-tab"><i class="bi bi-pencil-square"></i></span></span>
<div class="lubelogger-navbar-button">
<button type="button" class="btn btn-adaptive" onclick="showMobileNav()"><i class="bi bi-list lubelogger-menu-icon"></i></button>
</div>
</div>
</div>
<hr />
</div>
}
<div class="lubelogger-mobile-nav" onclick="hideMobileNav()">
<ul class="nav navbar-nav" id="vehicleTab" role="tablist">
<li class="nav-item" role="presentation" style="order: -2">
@@ -69,52 +124,6 @@
</ul>
</div>
<div class="container">
<div class="row">
<div class="d-flex justify-content-between">
<button onclick="returnToGarage()" class="lubelogger-tab btn btn-secondary btn-md mt-1 mb-1"><i class="bi bi-arrow-left-square"></i></button>
<h1 class="text-truncate display-4">@($"{Model.Year} {Model.Make} {Model.Model}")<small class="text-body-secondary">@($"(#{StaticHelper.GetVehicleIdentifier(Model)})")</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-adaptive" onclick="showMobileNav()"><i class="bi bi-list lubelogger-menu-icon"></i></button>
</div>
</div>
</div>
<hr />
<ul class="nav nav-tabs lubelogger-tab" id="vehicleTab" role="tablist">
<li class="nav-item" role="presentation" style="order: @userConfig.TabOrder.FindIndex(x=>x == ImportMode.Dashboard)">
<button class="nav-link resizable-nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.Dashboard)" id="report-tab" data-bs-toggle="tab" data-bs-target="#report-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-file-bar-graph"></i><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Dashboard")</span></button>
</li>
<li class="nav-item" role="presentation" style="order: @userConfig.TabOrder.FindIndex(x=>x == ImportMode.PlanRecord)">
<button class="nav-link resizable-nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.PlanRecord)" id="plan-tab" data-bs-toggle="tab" data-bs-target="#plan-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-bar-chart-steps"></i><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Planner")</span></button>
</li>
<li class="nav-item" role="presentation" style="order: @userConfig.TabOrder.FindIndex(x=>x == ImportMode.OdometerRecord)">
<button class="nav-link resizable-nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.OdometerRecord)" id="odometer-tab" data-bs-toggle="tab" data-bs-target="#odometer-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-speedometer"></i><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Odometer")</span></button>
</li>
<li class="nav-item" role="presentation" style="order: @userConfig.TabOrder.FindIndex(x=>x == ImportMode.ServiceRecord)">
<button class="nav-link resizable-nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.ServiceRecord)" id="servicerecord-tab" data-bs-toggle="tab" data-bs-target="#servicerecord-tab-pane" type="button" role="tab" aria-selected="true"><i class="bi bi-card-checklist"></i><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Service Records")</span></button>
</li>
<li class="nav-item" role="presentation" style="order: @userConfig.TabOrder.FindIndex(x=>x == ImportMode.RepairRecord)">
<button class="nav-link resizable-nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.RepairRecord)" id="accident-tab" data-bs-toggle="tab" data-bs-target="#accident-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-exclamation-octagon"></i><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Repairs")</span></button>
</li>
<li class="nav-item" role="presentation" style="order: @userConfig.TabOrder.FindIndex(x=>x == ImportMode.UpgradeRecord)">
<button class="nav-link resizable-nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.UpgradeRecord)" id="upgrade-tab" data-bs-toggle="tab" data-bs-target="#upgrade-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-wrench-adjustable"></i><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Upgrades")</span></button>
</li>
<li class="nav-item" role="presentation" style="order: @userConfig.TabOrder.FindIndex(x=>x == ImportMode.GasRecord)">
<button class="nav-link resizable-nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.GasRecord)" id="gas-tab" data-bs-toggle="tab" data-bs-target="#gas-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-fuel-pump"></i><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Fuel")</span></button>
</li>
<li class="nav-item" role="presentation" style="order: @userConfig.TabOrder.FindIndex(x=>x == ImportMode.SupplyRecord)">
<button class="nav-link resizable-nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.SupplyRecord)" id="supply-tab" data-bs-toggle="tab" data-bs-target="#supply-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-shop"></i><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Supplies")</span></button>
</li>
<li class="nav-item" role="presentation" style="order: @userConfig.TabOrder.FindIndex(x=>x == ImportMode.TaxRecord)">
<button class="nav-link resizable-nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.TaxRecord)" id="tax-tab" data-bs-toggle="tab" data-bs-target="#tax-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-currency-dollar"></i><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Taxes")</span></button>
</li>
<li class="nav-item" role="presentation" style="order: @userConfig.TabOrder.FindIndex(x=>x == ImportMode.NoteRecord)">
<button class="nav-link resizable-nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.NoteRecord)" id="notes-tab" data-bs-toggle="tab" data-bs-target="#notes-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-journal-bookmark"></i><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Notes")</span></button>
</li>
<li class="nav-item" role="presentation" style="order: @userConfig.TabOrder.FindIndex(x=>x == ImportMode.ReminderRecord)">
<button class="nav-link resizable-nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.ReminderRecord)" id="reminder-tab" data-bs-toggle="tab" data-bs-target="#reminder-tab-pane" type="button" role="tab" aria-selected="false"><div class="reminderBellDiv" style="display:inline-flex;"><i class="reminderBell bi bi-bell"></i></div><span class="ms-2 d-sm-none d-md-inline">@translator.Translate(userLanguage, "Reminders")</span></button>
</li>
</ul>
<div class="tab-content" id="vehicleTabContent">
<div class="tab-pane fade @StaticHelper.DefaultActiveTabContent(userConfig, ImportMode.ServiceRecord)" id="servicerecord-tab-pane" role="tabpanel" tabindex="0"></div>
<div class="tab-pane fade @StaticHelper.DefaultActiveTabContent(userConfig, ImportMode.GasRecord)" id="gas-tab-pane" role="tabpanel" tabindex="0"></div>

View File

@@ -0,0 +1,49 @@
@using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@{
var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
}
@model List<ExtraField>
@if (Model.Any()){
@foreach (ExtraField field in Model)
{
var elementId = Guid.NewGuid();
<div class="extra-field">
<label for="@elementId">@field.Name</label>
@switch(field.FieldType){
case (ExtraFieldType.Text):
<input type="text" id="@elementId" class="form-control" placeholder="@translator.Translate(userLanguage,"(multiple)")">
break;
case (ExtraFieldType.Number):
<input type="number" inputmode="numeric" id="@elementId" class="form-control" placeholder="@translator.Translate(userLanguage,"(multiple)")">
break;
case (ExtraFieldType.Decimal):
<input type="text" inputmode="decimal" onkeydown="interceptDecimalKeys(event)" onkeyup="fixDecimalInput(this, 2)" id="@elementId" class="form-control" placeholder="@translator.Translate(userLanguage,"(multiple)")">
break;
case (ExtraFieldType.Date):
<div class="input-group">
<input type="text" id="@elementId" class="form-control" placeholder="@translator.Translate(userLanguage,"(multiple)")">
<span class="input-group-text"><i class="bi bi-calendar-event"></i></span>
</div>
<script>initExtraFieldDatePicker('@elementId')</script>
break;
case (ExtraFieldType.Time):
<input type="time" id="@elementId" class="form-control" placeholder="@translator.Translate(userLanguage,"(multiple)")">
break;
case (ExtraFieldType.Location):
<div class="input-group">
<input type="text" id="@elementId" class="form-control" placeholder="@translator.Translate(userLanguage,"(multiple)")">
<div class="input-group-text">
<button type="button" class="btn btn-sm btn-primary zero-y-padding" onclick="populateLocationField('@elementId')"><i class="bi bi-geo-alt"></i></button>
</div>
</div>
break;
default:
<input type="text" id="@elementId" class="form-control" placeholder="@translator.Translate(userLanguage,"(multiple)")">
break;
}
</div>
}
}

View File

@@ -12,7 +12,7 @@
{
<li class="list-group-item">
<div class="d-flex justify-content-between">
<a type="button" class="btn btn-link text-truncate uploadedFileName" href="@filesUploaded.Location" target="_blank">@filesUploaded.Name</a>
<a type="button" class="btn btn-link text-truncate uploadedFileName" href="@filesUploaded.Location" title="@filesUploaded.Name" target="_blank">@filesUploaded.Name</a>
<div class="d-flex align-items-center">
<button type="button" class="btn btn-sm btn-outline-secondary me-2" onclick="editFileName('@filesUploaded.Location', this)"><i class="bi bi-pencil"></i></button>
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteFileFromUploadedFiles('@filesUploaded.Location', this)"><i class="bi bi-trash"></i></button>

View File

@@ -97,14 +97,7 @@
<!option value="@tag">@tag</!option>
}
</select>
@foreach (ExtraField field in Model.GasRecord.ExtraFields)
{
var elementId = Guid.NewGuid();
<div class="extra-field">
<label for="@elementId">@field.Name</label>
<input type="text" id="@elementId" class="form-control @(field.IsRequired ? "extra-field-required" : "")" placeholder="@field.Name" value="@field.Value">
</div>
}
@await Html.PartialAsync("_ExtraField", Model.GasRecord.ExtraFields)
</div>
<div class="col-md-6 col-12">
<label for="gasRecordNotes">@translator.Translate(userLanguage,"Notes(optional)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>

View File

@@ -30,14 +30,7 @@
<input type="text" inputmode="decimal" onkeydown="interceptDecimalKeys(event)" onkeyup="@(useThreeDecimals ? "fixDecimalInput(this, 3)" : "fixDecimalInput(this, 2)")" id="gasRecordCost" class="form-control" placeholder="@translator.Translate(userLanguage,"(multiple)")">
<label for="gasRecordTag">@translator.Translate(userLanguage, "Tags(use --- to clear all existing tags)")</label>
<select multiple class="form-select" id="gasRecordTag"></select>
@foreach (ExtraField field in Model.EditRecord.ExtraFields)
{
var elementId = Guid.NewGuid();
<div class="extra-field">
<label for="@elementId">@field.Name</label>
<input type="text" id="@elementId" class="form-control" placeholder="@translator.Translate(userLanguage,"(multiple)")">
</div>
}
@await Html.PartialAsync("_ExtraFieldMultiple", Model.EditRecord.ExtraFields)
</div>
<div class="col-md-6 col-12">
<label for="gasRecordNotes">@translator.Translate(userLanguage, "Notes(use --- to clear all existing notes)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>

View File

@@ -28,14 +28,7 @@
<input type="text" inputmode="decimal" onkeydown="interceptDecimalKeys(event)" onkeyup="fixDecimalInput(this, 2)" id="genericRecordCost" class="form-control" placeholder="@translator.Translate(userLanguage,"(multiple)")">
<label for="genericRecordTag">@translator.Translate(userLanguage, "Tags(use --- to clear all existing tags)")</label>
<select multiple class="form-select" id="genericRecordTag"></select>
@foreach (ExtraField field in Model.EditRecord.ExtraFields)
{
var elementId = Guid.NewGuid();
<div class="extra-field">
<label for="@elementId">@field.Name</label>
<input type="text" id="@elementId" class="form-control" placeholder="@translator.Translate(userLanguage,"(multiple)")">
</div>
}
@await Html.PartialAsync("_ExtraFieldMultiple", Model.EditRecord.ExtraFields)
</div>
<div class="col-md-6 col-12">
<label for="genericRecordNotes">@translator.Translate(userLanguage, "Notes(use --- to clear all existing notes)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>

View File

@@ -6,10 +6,15 @@
var barGraphColors = StaticHelper.GetBarChartColors();
var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
var graphGrace = Decimal.ToInt32(Model.CostData.Max(x => x.Cost) - Model.CostData.Min(x => x.Cost));
if (graphGrace < 0 || Model.CostData.Min(x=>x.Cost) - graphGrace < 0)
{
graphGrace = 0;
}
}
@if (Model.CostData.Any(x => x.Cost > 0))
{
<canvas id="bar-chart-mpg"></canvas>
<script>
renderChart();
@@ -54,7 +59,8 @@
},
scales: {
y: {
beginAtZero: true,
beginAtZero: false,
grace: decodeHTMLEntities('@(graphGrace)'),
ticks: {
color: useDarkMode ? "#fff" : "#000"
}

View File

@@ -33,14 +33,14 @@
{
<div>
@await Html.PartialAsync("_UploadedFiles", Model.Files)
<label for="serviceRecordFiles">@translator.Translate(userLanguage, "Upload more documents")</label>
<label for="noteFiles">@translator.Translate(userLanguage, "Upload more documents")</label>
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept="@config.GetAllowedFileUploadExtensions()" class="form-control-file" id="noteFiles">
<br /><small class="text-body-secondary">@translator.Translate(userLanguage, "Max File Size: 28.6MB")</small>
</div>
}
else
{
<label for="serviceRecordFiles">@translator.Translate(userLanguage, "Upload documents(optional)")</label>
<label for="noteFiles">@translator.Translate(userLanguage, "Upload documents(optional)")</label>
<input onChange="uploadVehicleFilesAsync(this)" type="file" multiple accept="@config.GetAllowedFileUploadExtensions()" class="form-control-file" id="noteFiles">
<br />
<small class="text-body-secondary">@translator.Translate(userLanguage, "Max File Size: 28.6MB")</small>
@@ -87,4 +87,4 @@
function getNoteModelData(){
return { id: @Model.Id}
}
</script>
</script>

View File

@@ -26,14 +26,7 @@
<input type="number" inputmode="numeric" id="odometerRecordMileage" class="form-control" placeholder="@translator.Translate(userLanguage,"(multiple)")">
<label for="odometerRecordTag">@translator.Translate(userLanguage, "Tags(use --- to clear all existing tags)")</label>
<select multiple class="form-select" id="odometerRecordTag"></select>
@foreach (ExtraField field in Model.EditRecord.ExtraFields)
{
var elementId = Guid.NewGuid();
<div class="extra-field">
<label for="@elementId">@field.Name</label>
<input type="text" id="@elementId" class="form-control" placeholder="@translator.Translate(userLanguage,"(multiple)")">
</div>
}
@await Html.PartialAsync("_ExtraFieldMultiple", Model.EditRecord.ExtraFields)
</div>
<div class="col-md-6 col-12">
<label for="odometerRecordNotes">@translator.Translate(userLanguage, "Notes(use --- to clear all existing notes)")<a class="link-underline link-underline-opacity-0" onclick="showLinks(this)"><i class="bi bi-markdown ms-2"></i></a></label>

View File

@@ -48,7 +48,7 @@
</div>
</div>
<div class="row swimlane">
<div class="col-3 d-flex flex-column swimlane mid" ondragover="dragOver(event)" ondrop="dropBox(event, 'Backlog')">
<div class="col-3 d-flex flex-column swimlane" ondragover="dragOver(event)" ondrop="dropBox(event, 'Backlog')">
<div class="row">
<div class="col-12 d-flex justify-content-center" style="height:5vh;">
<span class="display-7">@translator.Translate(userLanguage,"Planned")</span>
@@ -59,7 +59,7 @@
@await Html.PartialAsync("_PlanRecordItem", planRecord)
}
</div>
<div class="col-3 d-flex flex-column swimlane mid" ondragover="dragOver(event)" ondrop="dropBox(event, 'InProgress')">
<div class="col-3 d-flex flex-column swimlane" ondragover="dragOver(event)" ondrop="dropBox(event, 'InProgress')">
<div class="row">
<div class="col-12 d-flex justify-content-center" style="height:5vh;">
<span class="display-7">@translator.Translate(userLanguage,"Doing")</span>
@@ -81,7 +81,7 @@
@await Html.PartialAsync("_PlanRecordItem", planRecord)
}
</div>
<div class="col-3 d-flex flex-column swimlane end" ondragover="dragOver(event)" ondrop="dropBox(event, 'Done')">
<div class="col-3 d-flex flex-column swimlane" ondragover="dragOver(event)" ondrop="dropBox(event, 'Done')">
<div class="row">
<div class="col-12 d-flex justify-content-center" style="height:5vh;">
<span class="display-7">@translator.Translate(userLanguage,"Done")</span>

View File

@@ -7,6 +7,10 @@
}
@model ReportViewModel
<div class="container reportTabContainer">
<div class="row hideOnPrint" id="reportHeaderContent">
@await Html.PartialAsync("_ReportHeader", Model.ReportHeaderForVehicle)
</div>
<hr />
<div class="row hideOnPrint">
<div class="col-md-3 col-12 mt-2">
<div class="row">

View File

@@ -0,0 +1,24 @@
@using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@{
var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
}
@model ReportHeader
<div class="col-md-3 col-12 mt-2 text-center">
<span class="lead">@Model.MaxOdometer.ToString("N0")</span><br />
<span class="text-secondary">@translator.Translate(userLanguage, "Last Reported Odometer Reading")</span>
</div>
<div class="col-md-3 col-12 mt-2 text-center">
<span class="lead">@Model.DistanceTraveled.ToString("N0")</span><br />
<span class="text-secondary">@translator.Translate(userLanguage, "Distance Traveled")</span>
</div>
<div class="col-md-3 col-12 mt-2 text-center">
<span class="lead">@StaticHelper.HideZeroCost(Model.TotalCost.ToString("C2"), true)</span><br />
<span class="text-secondary">@translator.Translate(userLanguage, "Total Cost")</span>
</div>
<div class="col-md-3 col-12 mt-2 text-center">
<span class="lead">@Model.AverageMPG</span><br />
<span class="text-secondary">@translator.Translate(userLanguage, "Average Fuel Economy")</span>
</div>

View File

@@ -12,7 +12,7 @@
{
<li class="list-group-item">
<div class="d-flex justify-content-between">
<a type="button" class="btn btn-link text-truncate uploadedFileName" href="@filesUploaded.Location" target="_blank">@filesUploaded.Name</a>
<a type="button" class="btn btn-link text-truncate uploadedFileName" href="@filesUploaded.Location" title="@filesUploaded.Name" target="_blank">@filesUploaded.Name</a>
<div class="d-flex align-items-center">
<button type="button" class="btn btn-sm btn-outline-secondary me-2" onclick="editFileName('@filesUploaded.Location', this)"><i class="bi bi-pencil"></i></button>
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteFileFromUploadedFiles('@filesUploaded.Location', this)"><i class="bi bi-trash"></i></button>

View File

@@ -19,7 +19,8 @@
"EnableAutoReminderRefresh": false,
"EnableAutoOdometerInsert": false,
"EnableShopSupplies": false,
"ShowCalendar": true,
"ShowCalendar": true,
"ShowVehicleThumbnail": true,
"EnableExtraFieldColumns": false,
"UseUKMPG": false,
"UseThreeDecimalGasCost": true,

View File

@@ -112,6 +112,11 @@
<input type="text" id="inputSmtpFrom" class="form-control">
<small class="text-body-secondary">Sender Email Address</small>
</div>
<div class="form-group">
<label for="inputServerDomain">LubeLogger Domain</label>
<input type="text" id="inputServerDomain" class="form-control">
<small class="text-body-secondary">For Links in Emails</small>
</div>
</div>
</div>
</div>
@@ -150,6 +155,11 @@
<input type="text" id="inputOIDCTokenURL" class="form-control">
<small class="text-body-secondary">Token URL from Provider</small>
</div>
<div class="form-group">
<label for="inputOIDCUserInfoURL">User Info URL</label>
<input type="text" id="inputOIDCUserInfoURL" class="form-control">
<small class="text-body-secondary">Required by some Providers</small>
</div>
<div class="form-group">
<label for="inputOIDCRedirectURL">LubeLogger URL</label>
<input type="text" id="inputOIDCRedirectURL" class="form-control">
@@ -316,6 +326,9 @@ function generateConfig(){
if ($("#inputInvariantAPI").is(":checked")){
windowConfig["LUBELOGGER_INVARIANT_API"]=$('#inputInvariantAPI').is(':checked');
}
if ($("#inputServerDomain").val().trim() != ''){
windowConfig["LUBELOGGER_DOMAIN"] = $('#inputServerDomain').val();
}
if ($('#inputSmtpServer').val().trim() != ''){
windowConfig["MailConfig"] = {
EmailServer: $("#inputSmtpServer").val(),
@@ -332,6 +345,7 @@ function generateConfig(){
ClientSecret: $("#inputOIDCClientSecret").val(),
AuthURL: $("#inputOIDCAuthURL").val(),
TokenURL: $("#inputOIDCTokenURL").val(),
UserInfoURL: $("#inputOIDCUserInfoURL").val(),
RedirectURL: redirectUrl,
Scope: $("#inputOIDCScope").val(),
ValidateState: $("#inputOIDCValidateState").is(":checked"),
@@ -386,6 +400,9 @@ function generateConfig(){
if ($("#inputPostgres").val().trim() != ''){
dockerConfig.push(`POSTGRES_CONNECTION="${$('#inputPostgres').val()}"`);
}
if ($("#inputServerDomain").val().trim() != ''){
dockerConfig.push(`LUBELOGGER_DOMAIN="${$('#inputServerDomain').val()}"`);
}
if ($("#inputCustomWidgets").is(":checked")){
dockerConfig.push(`LUBELOGGER_CUSTOM_WIDGETS="${$('#inputCustomWidgets').is(':checked')}"`);
}
@@ -405,6 +422,7 @@ function generateConfig(){
dockerConfig.push(`OpenIDConfig__ClientSecret="${$('#inputOIDCClientSecret').val()}"`);
dockerConfig.push(`OpenIDConfig__AuthURL="${$('#inputOIDCAuthURL').val()}"`);
dockerConfig.push(`OpenIDConfig__TokenURL="${$('#inputOIDCTokenURL').val()}"`);
dockerConfig.push(`OpenIDConfig__UserInfoURL="${$('#inputOIDCUserInfoURL').val()}"`);
dockerConfig.push(`OpenIDConfig__RedirectURL="${redirectUrl}"`);
dockerConfig.push(`OpenIDConfig__Scope="${$('#inputOIDCScope').val()}"`);
dockerConfig.push(`OpenIDConfig__ValidateState=${$('#inputOIDCValidateState').is(':checked')}`);

View File

@@ -48,12 +48,10 @@ html {
.swimlane{
height:100%;
}
.swimlane.mid {
.swimlane:not(:last-child) {
border-right-style: solid;
}
.swimlane.end {
border-left-style: solid;
}
.showOnPrint {
display: none;
@@ -244,6 +242,18 @@ html {
background-color: #0d6efd;
}
.lubelogger-navbar {
align-items: center;
}
.lubelogger-tab {
border:none;
}
.lubelogger-tab .nav-link {
border: none;
}
/*Media Queries*/
@media (max-width: 576px) {
.lubelogger-tab {
@@ -300,10 +310,6 @@ html {
display: none;
}
.lubelogger-navbar {
justify-content: center;
}
.lubelogger-navbar-button {
display: none;
}
@@ -502,7 +508,8 @@ html[data-bs-theme="light"] .api-method:hover {
.lubelogger-logo {
height: 48px;
width: 204px;
min-width: 48px;
max-width: 204px;
object-fit: scale-down;
pointer-events: none;
}
@@ -528,4 +535,16 @@ html[data-bs-theme="light"] .api-method:hover {
font-size: 0.6em;
font-weight: 500;
top: 15%
}
}
.lubelogger-vehicle-logo {
height: 48px;
width: 48px;
border-radius: 50%;
object-fit: cover;
pointer-events: none;
}
.lubelogger-vehicle-logo.sold {
filter: grayscale(100%);
}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@@ -143,6 +143,14 @@ function refreshMPGChart() {
var year = getYear();
$.post('/Vehicle/GetMonthMPGByVehicle', {vehicleId: vehicleId, year: year}, function (data) {
$("#monthFuelMileageReportContent").html(data);
refreshReportHeader();
})
}
function refreshReportHeader() {
var vehicleId = GetVehicleId().vehicleId;
var year = getYear();
$.post('/Vehicle/GetSummaryForVehicle', { vehicleId: vehicleId, year: year }, function (data) {
$("#reportHeaderContent").html(data);
})
}
function setSelectedMetrics() {

View File

@@ -72,6 +72,7 @@ function updateSettings() {
enableAutoOdometerInsert: $("#enableAutoOdometerInsert").is(":checked"),
enableShopSupplies: $("#enableShopSupplies").is(":checked"),
showCalendar: $("#showCalendar").is(":checked"),
showVehicleThumbnail: $("#showVehicleThumbnail").is(":checked"),
enableExtraFieldColumns: $("#enableExtraFieldColumns").is(":checked"),
hideSoldVehicles: $("#hideSoldVehicles").is(":checked"),
preferredGasUnit: $("#preferredGasUnit").val(),

View File

@@ -1,4 +1,7 @@
function successToast(message) {
function returnToGarage() {
window.location.href = '/Home';
}
function successToast(message) {
Swal.fire({
toast: true,
position: "top-end",

View File

@@ -1,7 +1,4 @@
function returnToGarage() {
window.location.href = '/Home';
}
$(document).ready(function () {
$(document).ready(function () {
var vehicleId = GetVehicleId().vehicleId;
//bind tabs
$('button[data-bs-toggle="tab"]').on('show.bs.tab', function (e) {