Compare commits
70 Commits
v1.4.5
...
Hargata/up
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29680cf0e9 | ||
|
|
723eb1a769 | ||
|
|
732a628c20 | ||
|
|
84d40edb0e | ||
|
|
9842f0e501 | ||
|
|
44c5f921a5 | ||
|
|
fae4aa31aa | ||
|
|
adb505c87c | ||
|
|
d21b0a9e29 | ||
|
|
15e4181aff | ||
|
|
3a4627ef02 | ||
|
|
d9e11273bd | ||
|
|
8e6f40a1b1 | ||
|
|
55f86ecb3f | ||
|
|
8eef1465cf | ||
|
|
fd0ffba7c4 | ||
|
|
afb710e6ad | ||
|
|
419b755e7a | ||
|
|
8cd5342c02 | ||
|
|
fa32ecdb5a | ||
|
|
d08726bd85 | ||
|
|
a87861069b | ||
|
|
b6d6a8765d | ||
|
|
f307c0933a | ||
|
|
9968ccb541 | ||
|
|
5d3746f168 | ||
|
|
df0c4eeca2 | ||
|
|
5b3c0aed72 | ||
|
|
040728b96d | ||
|
|
e8b7b3e4ba | ||
|
|
cf1f1a884b | ||
|
|
c470db0590 | ||
|
|
cb71650adf | ||
|
|
923d59af0a | ||
|
|
d68e1a3939 | ||
|
|
bf954a2946 | ||
|
|
d3f29be227 | ||
|
|
5afe88a33a | ||
|
|
6937eff576 | ||
|
|
6371cccc48 | ||
|
|
fc174160e0 | ||
|
|
876f99fd26 | ||
|
|
75c65b4681 | ||
|
|
76031d27d7 | ||
|
|
4079a93c3e | ||
|
|
d913ab2009 | ||
|
|
6bfbcc4374 | ||
|
|
73d9a7e6e9 | ||
|
|
3cbce8b584 | ||
|
|
e4fcb52b24 | ||
|
|
ae4f01ac9a | ||
|
|
46b3845b3e | ||
|
|
ffb126276f | ||
|
|
a9e4be823d | ||
|
|
56cae008a6 | ||
|
|
c2dd379ea3 | ||
|
|
baa569b323 | ||
|
|
64ce0f8c07 | ||
|
|
bd98e5a6cd | ||
|
|
83fc8b8682 | ||
|
|
89fa15bbb7 | ||
|
|
5e69be56aa | ||
|
|
b1112dc617 | ||
|
|
5a31460afe | ||
|
|
2bcedbc7d4 | ||
|
|
5047fdf1bc | ||
|
|
7c8c3fb1c8 | ||
|
|
9920bd472f | ||
|
|
d7839a8a05 | ||
|
|
7e07e73ef5 |
@@ -11,10 +11,10 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CsvHelper" Version="30.0.1" />
|
||||
<PackageReference Include="CsvHelper" Version="33.0.1" />
|
||||
<PackageReference Include="LiteDB" Version="5.0.17" />
|
||||
<PackageReference Include="MailKit" Version="4.8.0" />
|
||||
<PackageReference Include="Npgsql" Version="8.0.5" />
|
||||
<PackageReference Include="MailKit" Version="4.11.0" />
|
||||
<PackageReference Include="Npgsql" Version="9.0.3" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.3.1" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
@@ -120,7 +148,14 @@ namespace CarCareTracker.Controllers
|
||||
{
|
||||
result = _userLogic.FilterUserVehicles(result, GetUserID());
|
||||
}
|
||||
return Json(result);
|
||||
if (_config.GetInvariantApi() || Request.Headers.ContainsKey("culture-invariant"))
|
||||
{
|
||||
return Json(result, StaticHelper.GetInvariantOption());
|
||||
}
|
||||
else
|
||||
{
|
||||
return Json(result);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@@ -249,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()
|
||||
@@ -339,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
|
||||
@@ -422,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()
|
||||
@@ -502,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
|
||||
@@ -584,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()
|
||||
@@ -665,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
|
||||
@@ -748,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()
|
||||
@@ -828,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
|
||||
@@ -944,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()
|
||||
@@ -1007,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
|
||||
@@ -1106,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()
|
||||
@@ -1167,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
|
||||
@@ -1266,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()
|
||||
@@ -1345,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
|
||||
@@ -1384,6 +1532,7 @@ namespace CarCareTracker.Controllers
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
#region ReminderRecord
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpGet]
|
||||
[Route("/api/vehicle/reminders")]
|
||||
@@ -1396,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());
|
||||
@@ -1406,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()
|
||||
@@ -1419,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)
|
||||
|
||||
@@ -23,6 +23,7 @@ namespace CarCareTracker.Controllers
|
||||
private readonly IReminderRecordDataAccess _reminderRecordDataAccess;
|
||||
private readonly IReminderHelper _reminderHelper;
|
||||
private readonly ITranslationHelper _translationHelper;
|
||||
private readonly IMailHelper _mailHelper;
|
||||
public HomeController(ILogger<HomeController> logger,
|
||||
IVehicleDataAccess dataAccess,
|
||||
IUserLogic userLogic,
|
||||
@@ -33,7 +34,8 @@ namespace CarCareTracker.Controllers
|
||||
IExtraFieldDataAccess extraFieldDataAccess,
|
||||
IReminderRecordDataAccess reminderRecordDataAccess,
|
||||
IReminderHelper reminderHelper,
|
||||
ITranslationHelper translationHelper)
|
||||
ITranslationHelper translationHelper,
|
||||
IMailHelper mailHelper)
|
||||
{
|
||||
_logger = logger;
|
||||
_dataAccess = dataAccess;
|
||||
@@ -46,6 +48,7 @@ namespace CarCareTracker.Controllers
|
||||
_loginLogic = loginLogic;
|
||||
_vehicleLogic = vehicleLogic;
|
||||
_translationHelper = translationHelper;
|
||||
_mailHelper = mailHelper;
|
||||
}
|
||||
private int GetUserID()
|
||||
{
|
||||
@@ -555,6 +558,29 @@ namespace CarCareTracker.Controllers
|
||||
}
|
||||
return Json(false);
|
||||
}
|
||||
[Authorize(Roles = nameof(UserData.IsRootUser))]
|
||||
public IActionResult GetServerConfiguration()
|
||||
{
|
||||
var viewModel = new ServerSettingsViewModel
|
||||
{
|
||||
PostgresConnection = _config.GetServerPostgresConnection(),
|
||||
AllowedFileExtensions = _config.GetAllowedFileUploadExtensions(),
|
||||
CustomLogoURL = _config.GetLogoUrl(),
|
||||
MessageOfTheDay = _config.GetMOTD(),
|
||||
WebHookURL = _config.GetWebHookUrl(),
|
||||
CustomWidgetsEnabled = _config.GetCustomWidgetsEnabled(),
|
||||
InvariantAPIEnabled = _config.GetInvariantApi(),
|
||||
SMTPConfig = _config.GetMailConfig(),
|
||||
OIDCConfig = _config.GetOpenIDConfig()
|
||||
};
|
||||
return PartialView("_ServerConfig", viewModel);
|
||||
}
|
||||
[Authorize(Roles = nameof(UserData.IsRootUser))]
|
||||
public IActionResult SendTestEmail(string emailAddress)
|
||||
{
|
||||
var result = _mailHelper.SendTestEmail(emailAddress);
|
||||
return Json(result);
|
||||
}
|
||||
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
||||
public IActionResult Error()
|
||||
{
|
||||
|
||||
@@ -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,13 +140,39 @@ 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
|
||||
var tokenParser = new JwtSecurityTokenHandler();
|
||||
var parsedToken = tokenParser.ReadJwtToken(userJwt);
|
||||
var userEmailAddress = parsedToken.Claims.First(x => x.Type == "email").Value;
|
||||
var userEmailAddress = string.Empty;
|
||||
if (parsedToken.Claims.Any(x => x.Type == "email"))
|
||||
{
|
||||
userEmailAddress = parsedToken.Claims.First(x => x.Type == "email").Value;
|
||||
}
|
||||
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)}");
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(userEmailAddress))
|
||||
{
|
||||
var userData = _loginLogic.ValidateOpenIDUser(new LoginModel() { EmailAddress = userEmailAddress });
|
||||
@@ -180,6 +216,126 @@ namespace CarCareTracker.Controllers
|
||||
}
|
||||
return new RedirectResult("/Login");
|
||||
}
|
||||
public async Task<IActionResult> RemoteAuthDebug(string code, string state = "")
|
||||
{
|
||||
List<OperationResponse> results = new List<OperationResponse>();
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(code))
|
||||
{
|
||||
results.Add(OperationResponse.Succeed($"Received code from OpenID Provider: {code}"));
|
||||
//received code from OIDC provider
|
||||
//create http client to retrieve user token from OIDC
|
||||
var httpClient = new HttpClient();
|
||||
var openIdConfig = _config.GetOpenIDConfig();
|
||||
//check if validate state is enabled.
|
||||
if (openIdConfig.ValidateState)
|
||||
{
|
||||
var storedStateValue = Request.Cookies["OIDC_STATE"];
|
||||
if (!string.IsNullOrWhiteSpace(storedStateValue))
|
||||
{
|
||||
Response.Cookies.Delete("OIDC_STATE");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(storedStateValue) || string.IsNullOrWhiteSpace(state) || storedStateValue != state)
|
||||
{
|
||||
results.Add(OperationResponse.Failed($"Failed State Validation - Expected: {storedStateValue} Received: {state}"));
|
||||
} else
|
||||
{
|
||||
results.Add(OperationResponse.Succeed($"Passed State Validation - Expected: {storedStateValue} Received: {state}"));
|
||||
}
|
||||
}
|
||||
var httpParams = new List<KeyValuePair<string, string>>
|
||||
{
|
||||
new KeyValuePair<string, string>("code", code),
|
||||
new KeyValuePair<string, string>("grant_type", "authorization_code"),
|
||||
new KeyValuePair<string, string>("client_id", openIdConfig.ClientId),
|
||||
new KeyValuePair<string, string>("client_secret", openIdConfig.ClientSecret),
|
||||
new KeyValuePair<string, string>("redirect_uri", openIdConfig.RedirectURL)
|
||||
};
|
||||
if (openIdConfig.UsePKCE)
|
||||
{
|
||||
//retrieve stored challenge verifier
|
||||
var storedVerifier = Request.Cookies["OIDC_VERIFIER"];
|
||||
if (!string.IsNullOrWhiteSpace(storedVerifier))
|
||||
{
|
||||
httpParams.Add(new KeyValuePair<string, string>("code_verifier", storedVerifier));
|
||||
Response.Cookies.Delete("OIDC_VERIFIER");
|
||||
}
|
||||
}
|
||||
var httpRequest = new HttpRequestMessage(HttpMethod.Post, openIdConfig.TokenURL)
|
||||
{
|
||||
Content = new FormUrlEncodedContent(httpParams)
|
||||
};
|
||||
var tokenResult = await httpClient.SendAsync(httpRequest).Result.Content.ReadAsStringAsync();
|
||||
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}"));
|
||||
//validate JWT token
|
||||
var tokenParser = new JwtSecurityTokenHandler();
|
||||
var parsedToken = tokenParser.ReadJwtToken(userJwt);
|
||||
var userEmailAddress = string.Empty;
|
||||
if (parsedToken.Claims.Any(x => x.Type == "email"))
|
||||
{
|
||||
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);
|
||||
results.Add(OperationResponse.Failed($"Failed Claim Validation - Expected: email Received: {string.Join(",", returnedClaims)}"));
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(userEmailAddress))
|
||||
{
|
||||
var userData = _loginLogic.ValidateOpenIDUser(new LoginModel() { EmailAddress = userEmailAddress });
|
||||
if (userData.Id != default)
|
||||
{
|
||||
results.Add(OperationResponse.Succeed($"Passed User Validation - Email: {userEmailAddress} Username: {userData.UserName}"));
|
||||
}
|
||||
else
|
||||
{
|
||||
results.Add(OperationResponse.Succeed($"Passed Email Validation - Email: {userEmailAddress} User not registered"));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
results.Add(OperationResponse.Failed($"Failed Email Validation - No email received from OpenID Provider"));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
results.Add(OperationResponse.Failed($"Failed to parse JWT - Expected: id_token Received: {tokenResult}"));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
results.Add(OperationResponse.Failed("No code received from OpenID Provider"));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
results.Add(OperationResponse.Failed($"Exception: {ex.Message}"));
|
||||
}
|
||||
return View(results);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult Login(LoginModel credentials)
|
||||
{
|
||||
|
||||
@@ -64,8 +64,9 @@ namespace CarCareTracker.Controllers
|
||||
[HttpGet]
|
||||
public IActionResult GetRecurringReminderRecordsByVehicleId(int vehicleId)
|
||||
{
|
||||
var result = _reminderRecordDataAccess.GetReminderRecordsByVehicleId(vehicleId);
|
||||
var result = GetRemindersAndUrgency(vehicleId, DateTime.Now);
|
||||
result.RemoveAll(x => !x.IsRecurring);
|
||||
result = result.OrderByDescending(x => x.Urgency).ThenBy(x => x.Description).ToList();
|
||||
return PartialView("_RecurringReminderSelector", result);
|
||||
}
|
||||
[HttpPost]
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
12
Enum/ExtraFieldType.cs
Normal file
12
Enum/ExtraFieldType.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace CarCareTracker.Models
|
||||
{
|
||||
public enum ExtraFieldType
|
||||
{
|
||||
Text = 0,
|
||||
Number = 1,
|
||||
Decimal = 2,
|
||||
Date = 3,
|
||||
Time = 4,
|
||||
Location = 5
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -238,6 +250,7 @@ namespace CarCareTracker.Helper
|
||||
UserLanguage = CheckString(nameof(UserConfig.UserLanguage), "en_US"),
|
||||
HideSoldVehicles = CheckBool(CheckString(nameof(UserConfig.HideSoldVehicles))),
|
||||
EnableShopSupplies = CheckBool(CheckString(nameof(UserConfig.EnableShopSupplies))),
|
||||
ShowCalendar = CheckBool(CheckString(nameof(UserConfig.ShowCalendar))),
|
||||
EnableExtraFieldColumns = CheckBool(CheckString(nameof(UserConfig.EnableExtraFieldColumns))),
|
||||
VisibleTabs = _config.GetSection(nameof(UserConfig.VisibleTabs)).Get<List<ImportMode>>() ?? new UserConfig().VisibleTabs,
|
||||
TabOrder = _config.GetSection(nameof(UserConfig.TabOrder)).Get<List<ImportMode>>() ?? new UserConfig().TabOrder,
|
||||
@@ -245,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)
|
||||
|
||||
@@ -11,11 +11,13 @@ namespace CarCareTracker.Helper
|
||||
OperationResponse NotifyUserForPasswordReset(string emailAddress, string token);
|
||||
OperationResponse NotifyUserForAccountUpdate(string emailAddress, string token);
|
||||
OperationResponse NotifyUserForReminders(Vehicle vehicle, List<string> emailAddresses, List<ReminderRecordViewModel> reminders);
|
||||
OperationResponse SendTestEmail(string emailAddress);
|
||||
}
|
||||
public class MailHelper : IMailHelper
|
||||
{
|
||||
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;
|
||||
@@ -28,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;
|
||||
@@ -42,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)
|
||||
{
|
||||
@@ -63,7 +73,36 @@ 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)
|
||||
{
|
||||
return OperationResponse.Succeed("Email Sent!");
|
||||
}
|
||||
else
|
||||
{
|
||||
return OperationResponse.Failed();
|
||||
}
|
||||
}
|
||||
public OperationResponse SendTestEmail(string emailAddress)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(mailConfig.EmailServer))
|
||||
{
|
||||
return OperationResponse.Failed("SMTP Server Not Setup");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(emailAddress))
|
||||
{
|
||||
return OperationResponse.Failed("Email Address or Token is invalid");
|
||||
}
|
||||
string emailSubject = _translator.Translate(serverLanguage, "Test Email from LubeLogger");
|
||||
string emailBody = _translator.Translate(serverLanguage, "If you are seeing this email it means your SMTP configuration is functioning correctly");
|
||||
var result = SendEmail(new List<string> { emailAddress }, emailSubject, emailBody);
|
||||
if (result)
|
||||
{
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace CarCareTracker.Helper
|
||||
/// </summary>
|
||||
public static class StaticHelper
|
||||
{
|
||||
public const string VersionNumber = "1.4.5";
|
||||
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)
|
||||
@@ -262,7 +263,9 @@ namespace CarCareTracker.Helper
|
||||
//update isrequired setting
|
||||
foreach (ExtraField extraField in recordExtraFields)
|
||||
{
|
||||
extraField.IsRequired = templateExtraFields.Where(x => x.Name == extraField.Name).First().IsRequired;
|
||||
var firstMatchingField = templateExtraFields.First(x => x.Name == extraField.Name);
|
||||
extraField.IsRequired = firstMatchingField.IsRequired;
|
||||
extraField.FieldType = firstMatchingField.FieldType;
|
||||
}
|
||||
//append extra fields
|
||||
foreach (ExtraField extraField in templateExtraFields)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
18
Models/API/ReleaseVersion.cs
Normal file
18
Models/API/ReleaseVersion.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -8,13 +8,14 @@
|
||||
public string AuthURL { get; set; }
|
||||
public string TokenURL { get; set; }
|
||||
public string RedirectURL { get; set; }
|
||||
public string Scope { get; set; }
|
||||
public string Scope { get; set; } = "openid email";
|
||||
public string State { get; set; }
|
||||
public string CodeChallenge { get; set; }
|
||||
public bool ValidateState { get; set; } = false;
|
||||
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)
|
||||
|
||||
@@ -3,5 +3,6 @@
|
||||
public class OpenIDResult
|
||||
{
|
||||
public string id_token { get; set; }
|
||||
public string access_token { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
7
Models/OIDC/OpenIDUserInfo.cs
Normal file
7
Models/OIDC/OpenIDUserInfo.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace CarCareTracker.Models
|
||||
{
|
||||
public class OpenIDUserInfo
|
||||
{
|
||||
public string email { get; set; } = "";
|
||||
}
|
||||
}
|
||||
10
Models/Report/ReportHeader.cs
Normal file
10
Models/Report/ReportHeader.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
17
Models/Settings/ServerSettingsViewModel.cs
Normal file
17
Models/Settings/ServerSettingsViewModel.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
namespace CarCareTracker.Models
|
||||
{
|
||||
public class ServerSettingsViewModel
|
||||
{
|
||||
public string LocaleInfo { get; set; }
|
||||
public string PostgresConnection { get; set; }
|
||||
public string AllowedFileExtensions { get; set; }
|
||||
public string CustomLogoURL { get; set; }
|
||||
public string MessageOfTheDay { get; set; }
|
||||
public string WebHookURL { get; set; }
|
||||
public bool CustomWidgetsEnabled { get; set; }
|
||||
public bool InvariantAPIEnabled { get; set; }
|
||||
public MailConfig SMTPConfig { get; set; } = new MailConfig();
|
||||
public OpenIDConfig OIDCConfig { get; set; } = new OpenIDConfig();
|
||||
|
||||
}
|
||||
}
|
||||
@@ -5,5 +5,6 @@
|
||||
public string Name { get; set; }
|
||||
public string Value { get; set; }
|
||||
public bool IsRequired { get; set; }
|
||||
public ExtraFieldType FieldType { get; set; } = ExtraFieldType.Text;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
public string PreferredGasUnit { get; set; } = string.Empty;
|
||||
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; }
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace CarCareTracker.Models
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CarCareTracker.Models
|
||||
{
|
||||
public class Vehicle
|
||||
{
|
||||
@@ -8,7 +10,9 @@
|
||||
public string Make { get; set; }
|
||||
public string Model { get; set; }
|
||||
public string LicensePlate { get; set; }
|
||||
[JsonConverter(typeof(FromDateOptional))]
|
||||
public string PurchaseDate { get; set; }
|
||||
[JsonConverter(typeof(FromDateOptional))]
|
||||
public string SoldDate { get; set; }
|
||||
public decimal PurchasePrice { get; set; }
|
||||
public decimal SoldPrice { get; set; }
|
||||
@@ -22,10 +26,12 @@
|
||||
/// <summary>
|
||||
/// Primarily used for vehicles with odometer units different from user's settings.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(FromDecimalOptional))]
|
||||
public string OdometerMultiplier { get; set; } = "1";
|
||||
/// <summary>
|
||||
/// Primarily used for vehicles where the odometer does not reflect actual mileage.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(FromIntOptional))]
|
||||
public string OdometerDifference { get; set; } = "0";
|
||||
public List<DashboardMetric> DashboardMetrics { get; set; } = new List<DashboardMetric>();
|
||||
/// <summary>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
@@ -27,9 +92,11 @@
|
||||
<button class="nav-link" id="supply-tab" data-bs-toggle="tab" data-bs-target="#supply-tab-pane" type="button" role="tab"><span class="ms-2 display-3"><i class="bi bi-shop me-2"></i>@translator.Translate(userLanguage, "Supplies")</span></button>
|
||||
</li>
|
||||
}
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="calendar-tab" data-bs-toggle="tab" data-bs-target="#calendar-tab-pane" type="button" role="tab"><span class="ms-2 display-3"><i class="bi bi-calendar-week me-2"></i>@translator.Translate(userLanguage, "Calendar")</span></button>
|
||||
</li>
|
||||
@if(userConfig.ShowCalendar){
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="calendar-tab" data-bs-toggle="tab" data-bs-target="#calendar-tab-pane" type="button" role="tab"><span class="ms-2 display-3"><i class="bi bi-calendar-week me-2"></i>@translator.Translate(userLanguage, "Calendar")</span></button>
|
||||
</li>
|
||||
}
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link @(Model == "settings" ? "active" : "")" id="settings-tab" data-bs-toggle="tab" data-bs-target="#settings-tab-pane" type="button" role="tab"><span class="ms-2 display-3"><i class="bi bi-gear me-2"></i>@translator.Translate(userLanguage,"Settings")</span></button>
|
||||
</li>
|
||||
@@ -59,60 +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>
|
||||
}
|
||||
<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">
|
||||
|
||||
@@ -36,8 +36,9 @@
|
||||
<table class="table table-hover">
|
||||
<thead class="sticky-top">
|
||||
<tr class="d-flex">
|
||||
<th scope="col" class="col-8">@translator.Translate(userLanguage, "Name")</th>
|
||||
<th scope="col" class="col-5">@translator.Translate(userLanguage, "Name")</th>
|
||||
<th scope="col" class="col-2">@translator.Translate(userLanguage, "Required")</th>
|
||||
<th scope="col" class="col-3">@translator.Translate(userLanguage, "Type")</th>
|
||||
<th scope="col" class="col-2">@translator.Translate(userLanguage, "Delete")</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -47,11 +48,21 @@
|
||||
@foreach (ExtraField extraField in Model.ExtraFields)
|
||||
{
|
||||
<script>
|
||||
extraFields.push({ name: decodeHTMLEntities('@extraField.Name'), isRequired: @extraField.IsRequired.ToString().ToLower()});
|
||||
extraFields.push({ name: decodeHTMLEntities('@extraField.Name'), isRequired: @extraField.IsRequired.ToString().ToLower(), fieldType: decodeHTMLEntities('@extraField.FieldType')});
|
||||
</script>
|
||||
<tr class="d-flex">
|
||||
<td class="col-8">@extraField.Name</td>
|
||||
<td class="col-5">@extraField.Name</td>
|
||||
<td class="col-2"><input class="form-check-input" type="checkbox" onchange="updateExtraFieldIsRequired(decodeHTMLEntities('@extraField.Name'), this)" value="" @(extraField.IsRequired ? "checked" : "") /></td>
|
||||
<td class="col-3">
|
||||
<select class="form-select" onchange="updateExtraFieldType(decodeHTMLEntities('@extraField.Name'), this)">
|
||||
<!option @(extraField.FieldType == ExtraFieldType.Text ? "selected" : "") value="@((int)ExtraFieldType.Text)">@translator.Translate(userLanguage, "Text")</!option>
|
||||
<!option @(extraField.FieldType == ExtraFieldType.Number ? "selected" : "") value="@((int)ExtraFieldType.Number)">@translator.Translate(userLanguage, "Number")</!option>
|
||||
<!option @(extraField.FieldType == ExtraFieldType.Decimal ? "selected" : "") value="@((int)ExtraFieldType.Decimal)">@translator.Translate(userLanguage, "Decimal")</!option>
|
||||
<!option @(extraField.FieldType == ExtraFieldType.Date ? "selected" : "") value="@((int)ExtraFieldType.Date)">@translator.Translate(userLanguage, "Date")</!option>
|
||||
<!option @(extraField.FieldType == ExtraFieldType.Time ? "selected" : "") value="@((int)ExtraFieldType.Time)">@translator.Translate(userLanguage, "Time")</!option>
|
||||
<!option @(extraField.FieldType == ExtraFieldType.Location ? "selected" : "") value="@((int)ExtraFieldType.Location)">@translator.Translate(userLanguage, "Location")</!option>
|
||||
</select>
|
||||
</td>
|
||||
<td class="col-2"><button type="button" onclick="deleteExtraField(decodeHTMLEntities('@extraField.Name'))" class="btn btn-danger"><i class="bi bi-trash"></i></button></td>
|
||||
</tr>
|
||||
}
|
||||
@@ -99,6 +110,12 @@
|
||||
extraFieldToEdit.isRequired = $(checkbox).is(":checked");
|
||||
updateExtraFields();
|
||||
}
|
||||
function updateExtraFieldType(fieldId, dropDown){
|
||||
var indexToEdit = extraFields.findIndex(x => x.name == fieldId);
|
||||
var extraFieldToEdit = extraFields[indexToEdit];
|
||||
extraFieldToEdit.fieldType = $(dropDown).val();
|
||||
updateExtraFields();
|
||||
}
|
||||
function deleteExtraField(fieldId) {
|
||||
extraFields = extraFields.filter(x => x.name != fieldId);
|
||||
updateExtraFields();
|
||||
|
||||
@@ -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>
|
||||
227
Views/Home/_ServerConfig.cshtml
Normal file
227
Views/Home/_ServerConfig.cshtml
Normal file
@@ -0,0 +1,227 @@
|
||||
@using CarCareTracker.Helper
|
||||
@inject IConfigHelper config
|
||||
@inject ITranslationHelper translator
|
||||
@model ServerSettingsViewModel
|
||||
@{
|
||||
var userConfig = config.GetUserConfig(User);
|
||||
var userLanguage = userConfig.UserLanguage;
|
||||
}
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="serverConfigModalLabel">@translator.Translate(userLanguage, "Review Server Configurations")</h5>
|
||||
<button type="button" class="btn-close" onclick="hideServerConfigModal()" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-inline">
|
||||
<div class="form-group">
|
||||
<div class="row mb-2">
|
||||
<div class="col-md-6 col-12">
|
||||
<label for="inputPostgres">@translator.Translate(userLanguage, "Postgres Connection")</label>
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<input type="text" readonly id="inputPostgres" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@Model.PostgresConnection">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-md-6 col-12">
|
||||
<label for="inputFileExt">@translator.Translate(userLanguage, "Allowed File Extensions")</label>
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<input type="text" readonly id="inputFileExt" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@Model.AllowedFileExtensions">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-md-6 col-12">
|
||||
<label for="inputLogoURL">@translator.Translate(userLanguage, "Logo URL")</label>
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<input type="text" readonly id="inputLogoURL" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@Model.CustomLogoURL">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-md-6 col-12">
|
||||
<label for="inputMOTD">@translator.Translate(userLanguage, "Message of the Day")</label>
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<input type="text" readonly id="inputMOTD" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@Model.MessageOfTheDay">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-md-6 col-12">
|
||||
<label for="inputWebHook">@translator.Translate(userLanguage, "WebHook URL")</label>
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<input type="text" readonly id="inputWebHook" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@Model.WebHookURL">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-md-6 col-12">
|
||||
<label for="inputCustomWidget">@translator.Translate(userLanguage, "Custom Widgets")</label>
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<input type="text" readonly id="inputCustomWidget" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@(Model.CustomWidgetsEnabled ? translator.Translate(userLanguage, "Enabled") : translator.Translate(userLanguage, "Disabled"))">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-md-6 col-12">
|
||||
<label for="inputInvariantAPI">@translator.Translate(userLanguage, "Invariant API")</label>
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<input type="text" readonly id="inputInvariantAPI" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@(Model.InvariantAPIEnabled ? translator.Translate(userLanguage, "Enabled") : translator.Translate(userLanguage, "Disabled"))">
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="row mb-2">
|
||||
<div class="col-md-6 col-12">
|
||||
<label for="inputSMTPServer">@translator.Translate(userLanguage, "SMTP Server")</label>
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<div class="input-group">
|
||||
<input type="text" readonly id="inputSMTPServer" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@Model.SMTPConfig.EmailServer">
|
||||
<div class="input-group-text">
|
||||
<button type="button" @(string.IsNullOrWhiteSpace(Model.SMTPConfig.EmailServer) ? "disabled" : "") class="btn btn-sm text-secondary password-visible-button" onclick="sendTestEmail()"><i class="bi bi-send"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-md-6 col-12">
|
||||
<label for="inputSMTPPort">@translator.Translate(userLanguage, "SMTP Server Port")</label>
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<input type="text" readonly id="inputSMTPPort" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@Model.SMTPConfig.Port">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-md-6 col-12">
|
||||
<label for="inputSMTPFrom">@translator.Translate(userLanguage, "SMTP Sender Address")</label>
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<input type="text" readonly id="inputSMTPFrom" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@Model.SMTPConfig.EmailFrom">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-md-6 col-12">
|
||||
<label for="inputSMTPUsername">@translator.Translate(userLanguage, "SMTP Username")</label>
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<input type="text" readonly id="inputSMTPUsername" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@Model.SMTPConfig.Username">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-md-6 col-12">
|
||||
<label for="inputSMTPPassword">@translator.Translate(userLanguage, "SMTP Password")</label>
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<div class="input-group">
|
||||
<input type="password" readonly id="inputSMTPPassword" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@Model.SMTPConfig.Password">
|
||||
<div class="input-group-text">
|
||||
<button type="button" class="btn btn-sm text-secondary password-visible-button" onclick="togglePasswordVisibility(this)"><i class="bi bi-eye"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="row mb-2">
|
||||
<div class="col-md-6 col-12">
|
||||
<label for="inputOIDCProvider">@translator.Translate(userLanguage, "OIDC Provider")</label>
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<input type="text" readonly id="inputOIDCProvider" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@Model.OIDCConfig.Name">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-md-6 col-12">
|
||||
<label for="inputOIDCClient">@translator.Translate(userLanguage, "OIDC Client ID")</label>
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<input type="text" readonly id="inputOIDCClient" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@Model.OIDCConfig.ClientId">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-md-6 col-12">
|
||||
<label for="inputOIDCSecret">@translator.Translate(userLanguage, "OIDC Client Secret")</label>
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<div class="input-group">
|
||||
<input type="password" readonly id="inputOIDCSecret" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@Model.OIDCConfig.ClientSecret">
|
||||
<div class="input-group-text">
|
||||
<button type="button" class="btn btn-sm text-secondary password-visible-button" onclick="togglePasswordVisibility(this)"><i class="bi bi-eye"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-md-6 col-12">
|
||||
<label for="inputOIDCAuth">@translator.Translate(userLanguage, "OIDC Auth URL")</label>
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<input type="text" readonly id="inputOIDCAuth" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@Model.OIDCConfig.AuthURL">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-md-6 col-12">
|
||||
<label for="inputOIDCToken">@translator.Translate(userLanguage, "OIDC Token URL")</label>
|
||||
</div>
|
||||
<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">
|
||||
<label for="inputOIDCRedirect">@translator.Translate(userLanguage, "OIDC Redirect URL")</label>
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<input type="text" readonly id="inputOIDCRedirect" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@Model.OIDCConfig.RedirectURL">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-md-6 col-12">
|
||||
<label for="inputOIDCScope">@translator.Translate(userLanguage, "OIDC Scope")</label>
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<input type="text" readonly id="inputOIDCScope" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@Model.OIDCConfig.Scope">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-md-6 col-12">
|
||||
<label for="inputOIDCLogout">@translator.Translate(userLanguage, "OIDC Logout URL")</label>
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<input type="text" readonly id="inputOIDCLogout" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@Model.OIDCConfig.LogOutURL">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-md-6 col-12">
|
||||
<label for="inputOIDCState">@translator.Translate(userLanguage, "OIDC Validate State")</label>
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<input type="text" readonly id="inputOIDCState" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@(Model.OIDCConfig.ValidateState ? translator.Translate(userLanguage, "Enabled") : translator.Translate(userLanguage, "Disabled"))">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-md-6 col-12">
|
||||
<label for="inputOIDCPKCE">@translator.Translate(userLanguage, "OIDC Use PKCE")</label>
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<input type="text" readonly id="inputOIDCPKCE" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@(Model.OIDCConfig.UsePKCE ? translator.Translate(userLanguage, "Enabled") : translator.Translate(userLanguage, "Disabled"))">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-md-6 col-12">
|
||||
<label for="inputOIDCDisable">@translator.Translate(userLanguage, "OIDC Login Only")</label>
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<input type="text" readonly id="inputOIDCDisable" class="form-control" placeholder="@translator.Translate(userLanguage, "Not Configured")" value="@(Model.OIDCConfig.DisableRegularLogin ? translator.Translate(userLanguage, "Enabled") : translator.Translate(userLanguage, "Disabled"))">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -86,6 +86,14 @@
|
||||
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="enableShopSupplies" checked="@Model.UserConfig.EnableShopSupplies">
|
||||
<label class="form-check-label" for="enableShopSupplies">@translator.Translate(userLanguage, "Shop Supplies")</label>
|
||||
</div>
|
||||
<div class="form-check form-switch form-check-inline">
|
||||
<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)))
|
||||
{
|
||||
@@ -253,7 +261,14 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6">
|
||||
<span class="lead text-wrap">@translator.Translate(userLanguage, "Server-wide Settings")</span>
|
||||
<div class="row">
|
||||
<div class="col-10">
|
||||
<span class="lead text-wrap">@translator.Translate(userLanguage, "Server-wide Settings")</span>
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<button onclick="showServerConfigModal()" class="btn text-secondary btn-sm"><i class="bi bi-eyeglasses"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6 d-grid">
|
||||
<button onclick="showExtraFieldModal()" class="btn btn-primary btn-md text-truncate">@translator.Translate(userLanguage, "Extra Fields")</button>
|
||||
@@ -355,6 +370,12 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" data-bs-focus="false" id="serverConfigModal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content" id="serverConfigModalContent">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" data-bs-focus="false" id="tabReorderModal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content" id="tabReorderModalContent">
|
||||
|
||||
@@ -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>
|
||||
|
||||
17
Views/Login/RemoteAuthDebug.cshtml
Normal file
17
Views/Login/RemoteAuthDebug.cshtml
Normal file
@@ -0,0 +1,17 @@
|
||||
@model List<OperationResponse>
|
||||
@{
|
||||
ViewData["Title"] = "Remote Auth Debug";
|
||||
}
|
||||
<div class="mt-2">
|
||||
@foreach (OperationResponse result in Model)
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="alert text-wrap text-break @(result.Success ? "alert-success" : "alert-danger")" role="alert">
|
||||
@result.Message
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<a onclick="showRecurringReminderSelector('collisionRecordDescription')" class="btn btn-link">@translator.Translate(userLanguage, "Select Reminder")</a>
|
||||
<a onclick="showRecurringReminderSelector('collisionRecordDescription', 'collisionRecordNotes')" class="btn btn-link">@translator.Translate(userLanguage, "Select Reminder")</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -52,14 +52,7 @@
|
||||
<!option value="@tag">@tag</!option>
|
||||
}
|
||||
</select>
|
||||
@foreach (ExtraField field in Model.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.ExtraFields)
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<label for="collisionRecordNotes">@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>
|
||||
|
||||
42
Views/Vehicle/_ExtraField.cshtml
Normal file
42
Views/Vehicle/_ExtraField.cshtml
Normal file
@@ -0,0 +1,42 @@
|
||||
@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 @(field.IsRequired ? "extra-field-required" : "")" placeholder="@field.Name" value="@field.Value">
|
||||
break;
|
||||
case (ExtraFieldType.Number):
|
||||
<input type="number" inputmode="numeric" id="@elementId" class="form-control @(field.IsRequired ? "extra-field-required" : "")" placeholder="@field.Name" value="@field.Value">
|
||||
break;
|
||||
case (ExtraFieldType.Decimal):
|
||||
<input type="text" inputmode="decimal" onkeydown="interceptDecimalKeys(event)" onkeyup="fixDecimalInput(this, 2)" id="@elementId" class="form-control @(field.IsRequired ? "extra-field-required" : "")" placeholder="@field.Name" value="@field.Value">
|
||||
break;
|
||||
case (ExtraFieldType.Date):
|
||||
<div class="input-group">
|
||||
<input type="text" id="@elementId" class="form-control @(field.IsRequired ? "extra-field-required" : "")" placeholder="@field.Name" value="@field.Value">
|
||||
<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 @(field.IsRequired ? "extra-field-required" : "")" placeholder="@field.Name" value="@field.Value">
|
||||
break;
|
||||
case (ExtraFieldType.Location):
|
||||
<div class="input-group">
|
||||
<input type="text" id="@elementId" class="form-control @(field.IsRequired ? "extra-field-required" : "")" placeholder="@field.Name" value="@field.Value">
|
||||
<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 @(field.IsRequired ? "extra-field-required" : "")" placeholder="@field.Name" value="@field.Value">
|
||||
break;
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
49
Views/Vehicle/_ExtraFieldMultiple.cshtml
Normal file
49
Views/Vehicle/_ExtraFieldMultiple.cshtml
Normal 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>
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -72,14 +72,7 @@
|
||||
<!option value="@tag">@tag</!option>
|
||||
}
|
||||
</select>
|
||||
@foreach (ExtraField field in Model.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.ExtraFields)
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<label for="odometerRecordNotes">@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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -40,14 +40,7 @@
|
||||
<!option value="InProgress" @(Model.Progress == PlanProgress.InProgress ? "selected" : "")>@translator.Translate(userLanguage, "Doing")</!option>
|
||||
<!option value = "Testing" @(Model.Progress == PlanProgress.Testing ? "selected" : "")>@translator.Translate(userLanguage, "Testing")</!option>
|
||||
</select>
|
||||
@foreach (ExtraField field in Model.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.ExtraFields)
|
||||
@if (!isNew)
|
||||
{
|
||||
<label>@($"{translator.Translate(userLanguage, "Date Created")}: {Model.DateCreated}")</label>
|
||||
|
||||
@@ -40,14 +40,7 @@
|
||||
<!option value="InProgress" @(Model.Progress == PlanProgress.InProgress ? "selected" : "")>@translator.Translate(userLanguage, "Doing")</!option>
|
||||
<!option value = "Testing" @(Model.Progress == PlanProgress.Testing ? "selected" : "")>@translator.Translate(userLanguage, "Testing")</!option>
|
||||
</select>
|
||||
@foreach (ExtraField field in Model.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.ExtraFields)
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<label for="planRecordNotes">@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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
var userLanguage = userConfig.UserLanguage;
|
||||
}
|
||||
@using CarCareTracker.Helper
|
||||
@model List<ReminderRecord>
|
||||
@model List<ReminderRecordViewModel>
|
||||
@if (Model.Count() > 1)
|
||||
{
|
||||
<div class="mb-2">
|
||||
@@ -16,9 +16,25 @@
|
||||
<select class="form-select" id="recurringReminderInput">
|
||||
@if (Model.Any())
|
||||
{
|
||||
@foreach (ReminderRecord reminderRecord in Model)
|
||||
@foreach (ReminderRecordViewModel reminderRecord in Model)
|
||||
{
|
||||
<!option value="@reminderRecord.Id">@reminderRecord.Description</!option>
|
||||
@switch(reminderRecord.UserMetric){
|
||||
case (ReminderMetric.Both):
|
||||
<!option value="@reminderRecord.Id" data-description="@reminderRecord.Description" class="@StaticHelper.GetReminderUrgencyColor(reminderRecord.Urgency)">
|
||||
@($"{reminderRecord.Description} | {reminderRecord.Date.ToShortDateString()} | {reminderRecord.Mileage}")
|
||||
</!option>
|
||||
break;
|
||||
case (ReminderMetric.Odometer):
|
||||
<!option value="@reminderRecord.Id" data-description="@reminderRecord.Description" class="@StaticHelper.GetReminderUrgencyColor(reminderRecord.Urgency)">
|
||||
@($"{reminderRecord.Description} | {reminderRecord.Mileage}")
|
||||
</!option>
|
||||
break;
|
||||
case (ReminderMetric.Date):
|
||||
<!option value="@reminderRecord.Id" data-description="@reminderRecord.Description" class="@StaticHelper.GetReminderUrgencyColor(reminderRecord.Urgency)">
|
||||
@($"{reminderRecord.Description} | {reminderRecord.Date.ToShortDateString()}")
|
||||
</!option>
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else
|
||||
{
|
||||
@@ -27,11 +43,26 @@
|
||||
</select>
|
||||
<div id="recurringMultipleReminders" style="display:none;">
|
||||
<ul class="list-group">
|
||||
@foreach (ReminderRecord reminderRecord in Model)
|
||||
@foreach (ReminderRecordViewModel reminderRecord in Model)
|
||||
{
|
||||
<li class="list-group-item text-start">
|
||||
<input class="form-check-input" type="checkbox" value="@reminderRecord.Id" id="recurringReminder_@reminderRecord.Id">
|
||||
<label class="form-check-label stretched-link" for="recurringReminder_@reminderRecord.Id">@reminderRecord.Description</label>
|
||||
<input class="form-check-input" type="checkbox" value="@reminderRecord.Id" data-description="@reminderRecord.Description" id="recurringReminder_@reminderRecord.Id">
|
||||
<label class="form-check-label stretched-link" for="recurringReminder_@reminderRecord.Id">
|
||||
@reminderRecord.Description
|
||||
<br /><small class="badge @StaticHelper.GetReminderUrgencyColor(reminderRecord.Urgency)">
|
||||
@switch (reminderRecord.UserMetric){
|
||||
case (ReminderMetric.Both):
|
||||
<i class='bi bi-calendar-event me-2'></i>@reminderRecord.Date.ToShortDateString()<i class='bi bi-speedometer ms-2 me-2'></i>@reminderRecord.Mileage
|
||||
break;
|
||||
case (ReminderMetric.Odometer):
|
||||
<i class='bi bi-speedometer me-2'></i>@reminderRecord.Mileage
|
||||
break;
|
||||
case (ReminderMetric.Date):
|
||||
<i class='bi bi-calendar-event me-2'></i>@reminderRecord.Date.ToShortDateString()
|
||||
break;
|
||||
}
|
||||
</small>
|
||||
</label>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
|
||||
@@ -85,6 +85,20 @@
|
||||
<label class="form-check-label stretched-link" for="chkCol_Notes">@translator.Translate(userLanguage, "Notes")</label>
|
||||
</div>
|
||||
</li>
|
||||
@if(hasRefresh){
|
||||
<li class="dropdown-item">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='done' onChange="showTableColumns(this, 'ReminderRecord')" type="checkbox" id="chkCol_Done" checked>
|
||||
<label class="form-check-label stretched-link" for="chkCol_Done">@translator.Translate(userLanguage, "Done")</label>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
<li class="dropdown-item">
|
||||
<div class="list-group-item">
|
||||
<input class="form-check-input col-visible-toggle" data-column-toggle='delete' onChange="showTableColumns(this, 'ReminderRecord')" type="checkbox" id="chkCol_Delete" checked>
|
||||
<label class="form-check-label stretched-link" for="chkCol_Delete">@translator.Translate(userLanguage, "Delete")</label>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
@@ -113,9 +127,9 @@
|
||||
<th scope="col" data-column="notes" class="flex-grow-1 flex-shrink-1 col-2 text-truncate">@translator.Translate(userLanguage, "Notes")</th>
|
||||
@if (hasRefresh)
|
||||
{
|
||||
<th scope="col" class="flex-grow-1 flex-shrink-1 col-2 text-truncate hideOnPrint">@translator.Translate(userLanguage, "Done")</th>
|
||||
<th scope="col" data-column="done" class="flex-grow-1 flex-shrink-1 col-2 text-truncate hideOnPrint">@translator.Translate(userLanguage, "Done")</th>
|
||||
}
|
||||
<th scope="col" class="flex-grow-1 flex-shrink-1 col-2 text-truncate hideOnPrint">@translator.Translate(userLanguage, "Delete")</th>
|
||||
<th scope="col" data-column="delete" class="flex-grow-1 flex-shrink-1 col-2 text-truncate hideOnPrint">@translator.Translate(userLanguage, "Delete")</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -164,14 +178,14 @@
|
||||
<td data-column="notes" class="flex-grow-1 flex-shrink-1 col-2 text-truncate">@StaticHelper.TruncateStrings(reminderRecord.Notes)</td>
|
||||
@if (hasRefresh)
|
||||
{
|
||||
<td class="flex-grow-1 flex-shrink-1 col-2 text-truncate hideOnPrint">
|
||||
<td class="flex-grow-1 flex-shrink-1 col-2 text-truncate hideOnPrint" data-column="done">
|
||||
@if((reminderRecord.Urgency == ReminderUrgency.VeryUrgent || reminderRecord.Urgency == ReminderUrgency.PastDue) && reminderRecord.IsRecurring)
|
||||
{
|
||||
<button type="button" class="btn btn-secondary" onclick="markDoneReminderRecord(@reminderRecord.Id, this)"><i class="bi bi-check-lg"></i></button>
|
||||
}
|
||||
</td>
|
||||
}
|
||||
<td class="flex-grow-1 flex-shrink-1 col-2 text-truncate hideOnPrint">
|
||||
<td class="flex-grow-1 flex-shrink-1 col-2 text-truncate hideOnPrint" data-column="delete">
|
||||
<button type="button" class="btn btn-danger" onclick="deleteReminderRecord(@reminderRecord.Id, this)"><i class="bi bi-trash"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -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">
|
||||
|
||||
24
Views/Vehicle/_ReportHeader.cshtml
Normal file
24
Views/Vehicle/_ReportHeader.cshtml
Normal 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>
|
||||
@@ -38,7 +38,7 @@
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<a onclick="showRecurringReminderSelector('serviceRecordDescription')" class="btn btn-link">@translator.Translate(userLanguage, "Select Reminder")</a>
|
||||
<a onclick="showRecurringReminderSelector('serviceRecordDescription', 'serviceRecordNotes')" class="btn btn-link">@translator.Translate(userLanguage, "Select Reminder")</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -52,14 +52,7 @@
|
||||
<!option value="@tag">@tag</!option>
|
||||
}
|
||||
</select>
|
||||
@foreach (ExtraField field in Model.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.ExtraFields)
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<label for="serviceRecordNotes">@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>
|
||||
|
||||
@@ -50,14 +50,7 @@
|
||||
<!option value="@tag">@tag</!option>
|
||||
}
|
||||
</select>
|
||||
@foreach (ExtraField field in Model.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.ExtraFields)
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<label for="supplyRecordNotes">@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>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<a onclick="showRecurringReminderSelector('taxRecordDescription')" class="btn btn-link">@translator.Translate(userLanguage, "Select Reminder")</a>
|
||||
<a onclick="showRecurringReminderSelector('taxRecordDescription', 'taxRecordNotes')" class="btn btn-link">@translator.Translate(userLanguage, "Select Reminder")</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -41,14 +41,7 @@
|
||||
<!option value="@tag">@tag</!option>
|
||||
}
|
||||
</select>
|
||||
@foreach (ExtraField field in Model.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.ExtraFields)
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<label for="taxRecordNotes">@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>
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<a onclick="showRecurringReminderSelector('upgradeRecordDescription')" class="btn btn-link">@translator.Translate(userLanguage, "Select Reminder")</a>
|
||||
<a onclick="showRecurringReminderSelector('upgradeRecordDescription', 'upgradeRecordNotes')" class="btn btn-link">@translator.Translate(userLanguage, "Select Reminder")</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -52,14 +52,7 @@
|
||||
<!option value="@tag">@tag</!option>
|
||||
}
|
||||
</select>
|
||||
@foreach (ExtraField field in Model.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.ExtraFields)
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<label for="upgradeRecordNotes">@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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -36,14 +36,7 @@
|
||||
<input type="text" id="inputModel" class="form-control" placeholder="@translator.Translate(userLanguage, "Model")" value="@Model.Model">
|
||||
<label for="inputLicensePlate">@translator.Translate(userLanguage, "License Plate")</label>
|
||||
<input type="text" id="inputLicensePlate" class="form-control" placeholder="@translator.Translate(userLanguage, "License Plate")" value="@Model.LicensePlate">
|
||||
@foreach (ExtraField field in Model.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.ExtraFields)
|
||||
<label for="inputIdentifier" class="@(!Model.ExtraFields.Any() ? "d-none" : "")">@translator.Translate(userLanguage, "Identifier")</label>
|
||||
<select class="form-select @(!Model.ExtraFields.Any() ? "d-none" : "")" id="inputIdentifier" )>
|
||||
<!option value="LicensePlate" @(Model.VehicleIdentifier == "LicensePlate" ? "selected" : "")>@translator.Translate(userLanguage, "License Plate")</!option>
|
||||
@@ -89,9 +82,15 @@
|
||||
</div>
|
||||
<div id="collapsePurchaseInfo" class="accordion-collapse collapse" data-bs-parent="#vehicleModalAccordion">
|
||||
<label for="inputPurchaseDate">@translator.Translate(userLanguage, "Purchased Date(optional)")</label>
|
||||
<input type="text" id="inputPurchaseDate" class="form-control" placeholder="@translator.Translate(userLanguage, "Purchased Date")" value="@Model.PurchaseDate">
|
||||
<div class="input-group">
|
||||
<input type="text" id="inputPurchaseDate" class="form-control" placeholder="@translator.Translate(userLanguage, "Purchased Date")" value="@Model.PurchaseDate">
|
||||
<span class="input-group-text"><i class="bi bi-calendar-event"></i></span>
|
||||
</div>
|
||||
<label for="inputSoldDate">@translator.Translate(userLanguage, "Sold Date(optional)")</label>
|
||||
<input type="text" id="inputSoldDate" class="form-control" placeholder="@translator.Translate(userLanguage, "Sold Date")" value="@Model.SoldDate">
|
||||
<div class="input-group">
|
||||
<input type="text" id="inputSoldDate" class="form-control" placeholder="@translator.Translate(userLanguage, "Sold Date")" value="@Model.SoldDate">
|
||||
<span class="input-group-text"><i class="bi bi-calendar-event"></i></span>
|
||||
</div>
|
||||
<label for="inputPurchasePrice">@translator.Translate(userLanguage, "Purchased Price(optional)")</label>
|
||||
<input type="text" inputmode="decimal" onkeydown="interceptDecimalKeys(event)" onkeyup="fixDecimalInput(this, 2)" id="inputPurchasePrice" class="form-control" placeholder="@translator.Translate(userLanguage, "Purchased Price")" value="@(Model.PurchasePrice == default ? "" : Model.PurchasePrice)">
|
||||
<label for="inputSoldPrice">@translator.Translate(userLanguage, "Sold Price(optional)")</label>
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
"EnableAutoReminderRefresh": false,
|
||||
"EnableAutoOdometerInsert": false,
|
||||
"EnableShopSupplies": false,
|
||||
"ShowCalendar": true,
|
||||
"ShowVehicleThumbnail": true,
|
||||
"EnableExtraFieldColumns": false,
|
||||
"UseUKMPG": false,
|
||||
"UseThreeDecimalGasCost": true,
|
||||
|
||||
@@ -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')}`);
|
||||
|
||||
@@ -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
BIN
wwwroot/defaults/lubelogger_logo_small.png
Normal file
BIN
wwwroot/defaults/lubelogger_logo_small.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
@@ -323,10 +323,11 @@ function updateMPGLabels() {
|
||||
var maxMPG = rowMPG.length > 0 ? rowMPG.reduce((a, b) => a > b ? a : b) : 0;
|
||||
var minMPG = rowMPG.length > 0 && rowNonZeroMPG.length > 0 ? rowNonZeroMPG.reduce((a, b) => a < b ? a : b) : 0;
|
||||
var totalMilesTraveled = rowMPG.length > 0 ? rowsToAggregate.children('[data-gas-type="mileage"]').toArray().map(x => globalParseFloat($(x).attr("data-gas-aggregate"))).reduce((a, b) => a + b) : 0;
|
||||
var totalGasConsumed = rowMPG.length > 0 ? rowsToAggregate.children('[data-gas-type="consumption"]').toArray().map(x => globalParseFloat(x.textContent)).reduce((a, b) => a + b) : 0;
|
||||
var totalUnaggregatedGasConsumed = rowsUnaggregated.length > 0 ? rowsUnaggregated.children('[data-gas-type="consumption"]').toArray().map(x => globalParseFloat(x.textContent)).reduce((a, b) => a + b) : 0;
|
||||
var totalGasConsumed = rowMPG.length > 0 ? rowsToAggregate.children('[data-gas-type="consumption"]').toArray().map(x => globalParseFloat($(x).attr("data-gas-aggregate"))).reduce((a, b) => a + b) : 0;
|
||||
var totalGasConsumedFV = rowMPG.length > 0 ? rowsToAggregate.children('[data-gas-type="consumption"]').toArray().map(x => globalParseFloat(x.textContent)).reduce((a, b) => a + b) : 0;
|
||||
var totalUnaggregatedGasConsumedFV = rowsUnaggregated.length > 0 ? rowsUnaggregated.children('[data-gas-type="consumption"]').toArray().map(x => globalParseFloat(x.textContent)).reduce((a, b) => a + b) : 0;
|
||||
var totalMilesTraveledUnaggregated = rowsUnaggregated.length > 0 ? rowsUnaggregated.children('[data-gas-type="mileage"]').toArray().map(x => globalParseFloat($(x).attr("data-gas-aggregate"))).reduce((a, b) => a + b) : 0;
|
||||
var fullGasConsumed = totalGasConsumed + totalUnaggregatedGasConsumed;
|
||||
var fullGasConsumed = totalGasConsumedFV + totalUnaggregatedGasConsumedFV;
|
||||
var fullDistanceTraveled = totalMilesTraveled + totalMilesTraveledUnaggregated;
|
||||
if (totalGasConsumed > 0 && rowNonZeroMPG.length > 0) {
|
||||
var averageMPG = totalMilesTraveled / totalGasConsumed;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -4,6 +4,15 @@
|
||||
$("#extraFieldModal").modal('show');
|
||||
});
|
||||
}
|
||||
function showServerConfigModal() {
|
||||
$.get(`/Home/GetServerConfiguration`, function (data) {
|
||||
$("#serverConfigModalContent").html(data);
|
||||
$("#serverConfigModal").modal('show');
|
||||
});
|
||||
}
|
||||
function hideServerConfigModal() {
|
||||
$("#serverConfigModal").modal('hide');
|
||||
}
|
||||
function hideExtraFieldModal() {
|
||||
$("#extraFieldModal").modal('hide');
|
||||
}
|
||||
@@ -62,6 +71,8 @@ function updateSettings() {
|
||||
enableAutoReminderRefresh: $("#enableAutoReminderRefresh").is(":checked"),
|
||||
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(),
|
||||
@@ -85,6 +96,33 @@ function updateSettings() {
|
||||
}
|
||||
})
|
||||
}
|
||||
function sendTestEmail() {
|
||||
Swal.fire({
|
||||
title: 'Send Test Email',
|
||||
html: `
|
||||
<input type="text" id="testEmailRecipient" class="swal2-input" placeholder="Email Address" onkeydown="handleSwalEnter(event)">
|
||||
`,
|
||||
confirmButtonText: 'Send',
|
||||
focusConfirm: false,
|
||||
preConfirm: () => {
|
||||
const emailRecipient = $("#testEmailRecipient").val();
|
||||
if (!emailRecipient || emailRecipient.trim() == '') {
|
||||
Swal.showValidationMessage(`Please enter a valid email address`);
|
||||
}
|
||||
return { emailRecipient }
|
||||
},
|
||||
}).then(function (result) {
|
||||
if (result.isConfirmed) {
|
||||
$.post('/Home/SendTestEmail', { emailAddress: result.value.emailRecipient }, function (data) {
|
||||
if (data.success) {
|
||||
successToast(data.message);
|
||||
} else {
|
||||
errorToast(data.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
function makeBackup() {
|
||||
$.get('/Files/MakeBackup', function (data) {
|
||||
window.location.href = data;
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
function successToast(message) {
|
||||
function returnToGarage() {
|
||||
window.location.href = '/Home';
|
||||
}
|
||||
function successToast(message) {
|
||||
Swal.fire({
|
||||
toast: true,
|
||||
position: "top-end",
|
||||
@@ -336,6 +339,16 @@ function isValidMoney(input) {
|
||||
const usRegex = /^\$?(?=\(.*\)|[^()]*$)\(?\d{1,3}((,\d{3}){0,8}|(\d{3}){0,8})(\.\d{1,3}?)?\)?$/;
|
||||
return (euRegex.test(input) || usRegex.test(input));
|
||||
}
|
||||
function initExtraFieldDatePicker(fieldName) {
|
||||
let inputField = $(`#${fieldName}`);
|
||||
if (inputField.length > 0) {
|
||||
inputField.datepicker({
|
||||
format: getShortDatePattern().pattern,
|
||||
autoclose: true,
|
||||
weekStart: getGlobalConfig().firstDayOfWeek
|
||||
});
|
||||
}
|
||||
}
|
||||
function initDatePicker(input, futureOnly) {
|
||||
if (futureOnly) {
|
||||
input.datepicker({
|
||||
@@ -702,7 +715,7 @@ function getAndValidateExtraFields() {
|
||||
var extraFieldsVisible = $(".modal.fade.show").find(".extra-field");
|
||||
extraFieldsVisible.map((index, elem) => {
|
||||
var extraFieldName = $(elem).children("label").text();
|
||||
var extraFieldInput = $(elem).children("input");
|
||||
var extraFieldInput = $(elem).find("input");
|
||||
var extraFieldValue = extraFieldInput.val();
|
||||
var extraFieldIsRequired = extraFieldInput.hasClass('extra-field-required');
|
||||
if (extraFieldIsRequired && extraFieldValue.trim() == '') {
|
||||
@@ -1541,4 +1554,29 @@ function callBackOnEnter(event, callBack) {
|
||||
if (event.keyCode == 13) {
|
||||
callBack();
|
||||
}
|
||||
}
|
||||
|
||||
function populateLocationField(fieldName) {
|
||||
let populateLocationFieldCallBack = (position) => {
|
||||
$(`#${fieldName}`).val(`${position.coords.latitude},${position.coords.longitude}`)
|
||||
};
|
||||
let populateLocationFieldErrorCallBack = (errMsg) => {
|
||||
if (errMsg && errMsg.code) {
|
||||
switch (errMsg.code) {
|
||||
case 1:
|
||||
errorToast(errMsg.message);
|
||||
break;
|
||||
case 2:
|
||||
errorToast("Location Unavailable");
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
if (navigator.geolocation) {
|
||||
try {
|
||||
navigator.geolocation.getCurrentPosition(populateLocationFieldCallBack, populateLocationFieldErrorCallBack, { maximumAge: 1000, timeout: 4000, enableHighAccuracy: true });
|
||||
} catch (err) {
|
||||
errorToast('Location Services not Enabled');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
@@ -336,7 +333,7 @@ function moveRecord(recordId, source, dest) {
|
||||
}
|
||||
});
|
||||
}
|
||||
function showRecurringReminderSelector(descriptionFieldName) {
|
||||
function showRecurringReminderSelector(descriptionFieldName, noteFieldName) {
|
||||
$.get(`/Vehicle/GetRecurringReminderRecordsByVehicleId?vehicleId=${GetVehicleId().vehicleId}`, function (data) {
|
||||
if (data) {
|
||||
//prompt user to select a recurring reminder
|
||||
@@ -356,9 +353,16 @@ function showRecurringReminderSelector(descriptionFieldName) {
|
||||
}).then(function (result) {
|
||||
if (result.isConfirmed) {
|
||||
recurringReminderRecordId = result.value.selectedRecurringReminderData.ids;
|
||||
var descriptionField = $(`#${descriptionFieldName}`);
|
||||
let descriptionField = $(`#${descriptionFieldName}`);
|
||||
let noteField = $(`#${noteFieldName}`);
|
||||
if (descriptionField.length > 0) {
|
||||
descriptionField.val(result.value.selectedRecurringReminderData.text);
|
||||
let descriptionFieldText = result.value.selectedRecurringReminderData.text.join(', ');
|
||||
descriptionField.val(descriptionFieldText);
|
||||
}
|
||||
if (noteField.length > 0 && result.value.selectedRecurringReminderData.text.length > 1) {
|
||||
result.value.selectedRecurringReminderData.text.map(x => {
|
||||
noteField.append(`- ${x}\r\n`);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -583,7 +587,7 @@ function getAndValidateSelectedRecurringReminder() {
|
||||
$("#recurringMultipleReminders :checked").map(function () {
|
||||
selectedRecurringRemindersArray.push({
|
||||
value: this.value,
|
||||
text: $(this).parent().find('.form-check-label').text()
|
||||
text: $(this).attr("data-description")
|
||||
});
|
||||
});
|
||||
if (selectedRecurringRemindersArray.length == 0) {
|
||||
@@ -596,13 +600,13 @@ function getAndValidateSelectedRecurringReminder() {
|
||||
return {
|
||||
hasError: false,
|
||||
ids: selectedRecurringRemindersArray.map(x=>x.value),
|
||||
text: selectedRecurringRemindersArray.map(x=>x.text).join(', ')
|
||||
text: selectedRecurringRemindersArray.map(x=>x.text)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//validate single reminder
|
||||
var selectedRecurringReminder = $("#recurringReminderInput").val();
|
||||
var selectedRecurringReminderText = $("#recurringReminderInput option:selected").text();
|
||||
var selectedRecurringReminderText = $("#recurringReminderInput option:selected").attr("data-description");
|
||||
if (!selectedRecurringReminder || parseInt(selectedRecurringReminder) == 0) {
|
||||
return {
|
||||
hasError: true,
|
||||
@@ -613,42 +617,43 @@ function getAndValidateSelectedRecurringReminder() {
|
||||
return {
|
||||
hasError: false,
|
||||
ids: [selectedRecurringReminder],
|
||||
text: selectedRecurringReminderText
|
||||
text: [selectedRecurringReminderText]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
function getLastOdometerReadingAndIncrement(odometerFieldName) {
|
||||
Swal.fire({
|
||||
title: 'Increment Last Reported Odometer Reading',
|
||||
html: `
|
||||
$.get(`/Vehicle/GetMaxMileage?vehicleId=${GetVehicleId().vehicleId}`, function (currentOdometer) {
|
||||
let additionalHtml = isNaN(currentOdometer) || currentOdometer == 0 ? '' : `<span>Current Odometer: ${currentOdometer}</span><br/>`;
|
||||
Swal.fire({
|
||||
title: 'Increment Last Reported Odometer Reading',
|
||||
html: `${additionalHtml}
|
||||
<input type="text" inputmode="decimal" id="inputOdometerIncrement" class="swal2-input" placeholder="Increment" onkeydown="handleSwalEnter(event)">
|
||||
`,
|
||||
confirmButtonText: 'Add',
|
||||
focusConfirm: false,
|
||||
preConfirm: () => {
|
||||
const odometerIncrement = parseInt(globalParseFloat($("#inputOdometerIncrement").val()));
|
||||
if (isNaN(odometerIncrement) || odometerIncrement <= 0) {
|
||||
Swal.showValidationMessage(`Please enter a non-zero amount to increment`);
|
||||
}
|
||||
return { odometerIncrement }
|
||||
},
|
||||
}).then(function (result) {
|
||||
if (result.isConfirmed) {
|
||||
var amountToIncrement = result.value.odometerIncrement;
|
||||
$.get(`/Vehicle/GetMaxMileage?vehicleId=${GetVehicleId().vehicleId}`, function (data) {
|
||||
var newAmount = data + amountToIncrement;
|
||||
if (!isNaN(newAmount)) {
|
||||
var odometerField = $(`#${odometerFieldName}`);
|
||||
if (odometerField.length > 0) {
|
||||
odometerField.val(newAmount);
|
||||
confirmButtonText: 'Add',
|
||||
focusConfirm: false,
|
||||
preConfirm: () => {
|
||||
const odometerIncrement = parseInt(globalParseFloat($("#inputOdometerIncrement").val()));
|
||||
if (isNaN(odometerIncrement) || odometerIncrement < 0) {
|
||||
Swal.showValidationMessage(`Please enter a positive amount to increment or 0 to use current odometer`);
|
||||
}
|
||||
return { odometerIncrement }
|
||||
},
|
||||
}).then(function (result) {
|
||||
if (result.isConfirmed) {
|
||||
var amountToIncrement = result.value.odometerIncrement;
|
||||
var newAmount = currentOdometer + amountToIncrement;
|
||||
if (!isNaN(newAmount)) {
|
||||
var odometerField = $(`#${odometerFieldName}`);
|
||||
if (odometerField.length > 0) {
|
||||
odometerField.val(newAmount);
|
||||
} else {
|
||||
errorToast(genericErrorMessage());
|
||||
}
|
||||
} else {
|
||||
errorToast(genericErrorMessage());
|
||||
}
|
||||
} else {
|
||||
errorToast(genericErrorMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user